diff options
Diffstat (limited to 'modules')
609 files changed, 23707 insertions, 7326 deletions
diff --git a/modules/SCsub b/modules/SCsub index fcc01e2c1b..7c9946170f 100644 --- a/modules/SCsub +++ b/modules/SCsub @@ -7,6 +7,9 @@ Import("env") env_modules = env.Clone() +# Allow modules to detect if they are being built as a module. +env_modules.Append(CPPDEFINES=["GODOT_MODULE"]) + Export("env_modules") # Header with MODULE_*_ENABLED defines. diff --git a/modules/astcenc/image_compress_astcenc.cpp b/modules/astcenc/image_compress_astcenc.cpp index 1c643d780d..31df83efae 100644 --- a/modules/astcenc/image_compress_astcenc.cpp +++ b/modules/astcenc/image_compress_astcenc.cpp @@ -169,7 +169,7 @@ void _compress_astc(Image *r_img, Image::ASTCFormat p_format) { r_img->set_data(width, height, mipmaps, target_format, dest_data); - print_verbose(vformat("astcenc: Encoding took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time))); + print_verbose(vformat("astcenc: Encoding took %d ms.", OS::get_singleton()->get_ticks_msec() - start_time)); } void _decompress_astc(Image *r_img) { @@ -286,5 +286,5 @@ void _decompress_astc(Image *r_img) { r_img->set_data(width, height, mipmaps, target_format, dest_data); - print_verbose(vformat("astcenc: Decompression took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time))); + print_verbose(vformat("astcenc: Decompression took %d ms.", OS::get_singleton()->get_ticks_msec() - start_time)); } diff --git a/modules/basis_universal/SCsub b/modules/basis_universal/SCsub index 14669847bc..80bfd7e858 100644 --- a/modules/basis_universal/SCsub +++ b/modules/basis_universal/SCsub @@ -28,7 +28,6 @@ encoder_sources = [ "basisu_resample_filters.cpp", "basisu_ssim.cpp", "basisu_uastc_enc.cpp", - "jpgd.cpp", "pvpngreader.cpp", ] encoder_sources = [thirdparty_dir + "encoder/" + file for file in encoder_sources] @@ -36,9 +35,11 @@ transcoder_sources = [thirdparty_dir + "transcoder/basisu_transcoder.cpp"] # Treat Basis headers as system headers to avoid raising warnings. Not supported on MSVC. if not env.msvc: - env_basisu.Append(CPPFLAGS=["-isystem", Dir(thirdparty_dir).path]) + env_basisu.Append( + CPPFLAGS=["-isystem", Dir(thirdparty_dir).path, "-isystem", Dir("#thirdparty/jpeg-compressor").path] + ) else: - env_basisu.Prepend(CPPPATH=[thirdparty_dir]) + env_basisu.Prepend(CPPPATH=[thirdparty_dir, "#thirdparty/jpeg-compressor"]) if env["builtin_zstd"]: env_basisu.Prepend(CPPPATH=["#thirdparty/zstd"]) diff --git a/modules/basis_universal/config.py b/modules/basis_universal/config.py index d22f9454ed..6a67f2c77c 100644 --- a/modules/basis_universal/config.py +++ b/modules/basis_universal/config.py @@ -1,4 +1,5 @@ def can_build(env, platform): + env.module_add_dependencies("basis_universal", ["jpg"]) return True diff --git a/modules/basis_universal/image_compress_basisu.cpp b/modules/basis_universal/image_compress_basisu.cpp new file mode 100644 index 0000000000..72e7977eef --- /dev/null +++ b/modules/basis_universal/image_compress_basisu.cpp @@ -0,0 +1,269 @@ +/**************************************************************************/ +/* image_compress_basisu.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "image_compress_basisu.h" + +#include "servers/rendering_server.h" + +#include <transcoder/basisu_transcoder.h> +#ifdef TOOLS_ENABLED +#include <encoder/basisu_comp.h> +#endif + +void basis_universal_init() { +#ifdef TOOLS_ENABLED + basisu::basisu_encoder_init(); +#endif + + basist::basisu_transcoder_init(); +} + +#ifdef TOOLS_ENABLED +Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels) { + Ref<Image> image = p_image->duplicate(); + image->convert(Image::FORMAT_RGBA8); + + basisu::basis_compressor_params params; + + params.m_uastc = true; + params.m_quality_level = basisu::BASISU_QUALITY_MIN; + params.m_pack_uastc_flags &= ~basisu::cPackUASTCLevelMask; + params.m_pack_uastc_flags |= basisu::cPackUASTCLevelFastest; + + params.m_rdo_uastc = 0.0f; + params.m_rdo_uastc_quality_scalar = 0.0f; + params.m_rdo_uastc_dict_size = 1024; + + params.m_mip_fast = true; + params.m_multithreading = true; + params.m_check_for_alpha = false; + + basisu::job_pool job_pool(OS::get_singleton()->get_processor_count()); + params.m_pJob_pool = &job_pool; + + BasisDecompressFormat decompress_format = BASIS_DECOMPRESS_RG; + switch (p_channels) { + case Image::USED_CHANNELS_L: { + decompress_format = BASIS_DECOMPRESS_RGB; + } break; + case Image::USED_CHANNELS_LA: { + params.m_force_alpha = true; + decompress_format = BASIS_DECOMPRESS_RGBA; + } break; + case Image::USED_CHANNELS_R: { + decompress_format = BASIS_DECOMPRESS_RGB; + } break; + case Image::USED_CHANNELS_RG: { + // Currently RG textures are compressed as DXT5/ETC2_RGBA8 with a RA -> RG swizzle, + // as BasisUniversal didn't use to support ETC2_RG11 transcoding. + params.m_force_alpha = true; + image->convert_rg_to_ra_rgba8(); + decompress_format = BASIS_DECOMPRESS_RG_AS_RA; + } break; + case Image::USED_CHANNELS_RGB: { + decompress_format = BASIS_DECOMPRESS_RGB; + } break; + case Image::USED_CHANNELS_RGBA: { + params.m_force_alpha = true; + decompress_format = BASIS_DECOMPRESS_RGBA; + } break; + } + + { + // Encode the image with mipmaps. + Vector<uint8_t> image_data = image->get_data(); + basisu::vector<basisu::image> basisu_mipmaps; + + for (int32_t i = 0; i <= image->get_mipmap_count(); i++) { + int ofs, size, width, height; + image->get_mipmap_offset_size_and_dimensions(i, ofs, size, width, height); + + basisu::image basisu_image(width, height); + memcpy(basisu_image.get_ptr(), image_data.ptr() + ofs, size); + + if (i == 0) { + params.m_source_images.push_back(basisu_image); + } else { + basisu_mipmaps.push_back(basisu_image); + } + } + + params.m_source_mipmap_images.push_back(basisu_mipmaps); + } + + // Encode the image data. + Vector<uint8_t> basisu_data; + + basisu::basis_compressor compressor; + compressor.init(params); + + int basisu_err = compressor.process(); + ERR_FAIL_COND_V(basisu_err != basisu::basis_compressor::cECSuccess, basisu_data); + + const basisu::uint8_vec &basisu_out = compressor.get_output_basis_file(); + basisu_data.resize(basisu_out.size() + 4); + + // Copy the encoded data to the buffer. + { + uint8_t *w = basisu_data.ptrw(); + *(uint32_t *)w = decompress_format; + + memcpy(w + 4, basisu_out.get_ptr(), basisu_out.size()); + } + + return basisu_data; +} +#endif // TOOLS_ENABLED + +Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) { + Ref<Image> image; + ERR_FAIL_NULL_V_MSG(p_data, image, "Cannot unpack invalid BasisUniversal data."); + + const uint8_t *src_ptr = p_data; + int src_size = p_size; + + basist::transcoder_texture_format basisu_format = basist::transcoder_texture_format::cTFTotalTextureFormats; + Image::Format image_format = Image::FORMAT_MAX; + + // Get supported compression formats. + bool bptc_supported = RS::get_singleton()->has_os_feature("bptc"); + bool astc_supported = RS::get_singleton()->has_os_feature("astc"); + bool s3tc_supported = RS::get_singleton()->has_os_feature("s3tc"); + bool etc2_supported = RS::get_singleton()->has_os_feature("etc2"); + + bool needs_ra_rg_swap = false; + + switch (*(uint32_t *)(src_ptr)) { + case BASIS_DECOMPRESS_RG: { + // RGTC transcoding is currently performed with RG_AS_RA, fail. + ERR_FAIL_V(image); + } break; + case BASIS_DECOMPRESS_RGB: { + if (bptc_supported) { + basisu_format = basist::transcoder_texture_format::cTFBC7_M6_OPAQUE_ONLY; + image_format = Image::FORMAT_BPTC_RGBA; + } else if (astc_supported) { + basisu_format = basist::transcoder_texture_format::cTFASTC_4x4_RGBA; + image_format = Image::FORMAT_ASTC_4x4; + } else if (s3tc_supported) { + basisu_format = basist::transcoder_texture_format::cTFBC1; + image_format = Image::FORMAT_DXT1; + } else if (etc2_supported) { + basisu_format = basist::transcoder_texture_format::cTFETC1; + image_format = Image::FORMAT_ETC2_RGB8; + } else { + // No supported VRAM compression formats, decompress. + basisu_format = basist::transcoder_texture_format::cTFRGBA32; + image_format = Image::FORMAT_RGBA8; + } + + } break; + case BASIS_DECOMPRESS_RGBA: { + if (bptc_supported) { + basisu_format = basist::transcoder_texture_format::cTFBC7_M5; + image_format = Image::FORMAT_BPTC_RGBA; + } else if (astc_supported) { + basisu_format = basist::transcoder_texture_format::cTFASTC_4x4_RGBA; + image_format = Image::FORMAT_ASTC_4x4; + } else if (s3tc_supported) { + basisu_format = basist::transcoder_texture_format::cTFBC3; + image_format = Image::FORMAT_DXT5; + } else if (etc2_supported) { + basisu_format = basist::transcoder_texture_format::cTFETC2; + image_format = Image::FORMAT_ETC2_RGBA8; + } else { + // No supported VRAM compression formats, decompress. + basisu_format = basist::transcoder_texture_format::cTFRGBA32; + image_format = Image::FORMAT_RGBA8; + } + } break; + case BASIS_DECOMPRESS_RG_AS_RA: { + if (s3tc_supported) { + basisu_format = basist::transcoder_texture_format::cTFBC3; + image_format = Image::FORMAT_DXT5_RA_AS_RG; + } else if (etc2_supported) { + basisu_format = basist::transcoder_texture_format::cTFETC2; + image_format = Image::FORMAT_ETC2_RA_AS_RG; + } else { + // No supported VRAM compression formats, decompress. + basisu_format = basist::transcoder_texture_format::cTFRGBA32; + image_format = Image::FORMAT_RGBA8; + needs_ra_rg_swap = true; + } + } break; + } + + src_ptr += 4; + src_size -= 4; + + basist::basisu_transcoder transcoder; + ERR_FAIL_COND_V(!transcoder.validate_header(src_ptr, src_size), image); + + transcoder.start_transcoding(src_ptr, src_size); + + basist::basisu_image_info basisu_info; + transcoder.get_image_info(src_ptr, src_size, basisu_info, 0); + + // Create the buffer for transcoded/decompressed data. + Vector<uint8_t> out_data; + out_data.resize(Image::get_image_data_size(basisu_info.m_width, basisu_info.m_height, image_format, basisu_info.m_total_levels > 1)); + + uint8_t *dst = out_data.ptrw(); + memset(dst, 0, out_data.size()); + + uint32_t mip_count = Image::get_image_required_mipmaps(basisu_info.m_orig_width, basisu_info.m_orig_height, image_format); + for (uint32_t i = 0; i <= mip_count; i++) { + basist::basisu_image_level_info basisu_level; + transcoder.get_image_level_info(src_ptr, src_size, basisu_level, 0, i); + + uint32_t mip_block_or_pixel_count = image_format >= Image::FORMAT_DXT1 ? basisu_level.m_total_blocks : basisu_level.m_orig_width * basisu_level.m_orig_height; + int ofs = Image::get_image_mipmap_offset(basisu_info.m_width, basisu_info.m_height, image_format, i); + + bool result = transcoder.transcode_image_level(src_ptr, src_size, 0, i, dst + ofs, mip_block_or_pixel_count, basisu_format); + + if (!result) { + print_line(vformat("BasisUniversal cannot unpack level %d.", i)); + break; + } + } + + image = Image::create_from_data(basisu_info.m_width, basisu_info.m_height, basisu_info.m_total_levels > 1, image_format, out_data); + + if (needs_ra_rg_swap) { + // Swap uncompressed RA-as-RG texture's color channels. + image->convert_ra_rgba8_to_rg(); + } + + return image; +} + +Ref<Image> basis_universal_unpacker(const Vector<uint8_t> &p_buffer) { + return basis_universal_unpacker_ptr(p_buffer.ptr(), p_buffer.size()); +} diff --git a/modules/basis_universal/image_compress_basisu.h b/modules/basis_universal/image_compress_basisu.h new file mode 100644 index 0000000000..ac5d62ae73 --- /dev/null +++ b/modules/basis_universal/image_compress_basisu.h @@ -0,0 +1,52 @@ +/**************************************************************************/ +/* image_compress_basisu.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef IMAGE_COMPRESS_BASISU_H +#define IMAGE_COMPRESS_BASISU_H + +#include "core/io/image.h" + +enum BasisDecompressFormat { + BASIS_DECOMPRESS_RG, + BASIS_DECOMPRESS_RGB, + BASIS_DECOMPRESS_RGBA, + BASIS_DECOMPRESS_RG_AS_RA, +}; + +void basis_universal_init(); + +#ifdef TOOLS_ENABLED +Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels); +#endif + +Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size); +Ref<Image> basis_universal_unpacker(const Vector<uint8_t> &p_buffer); + +#endif // IMAGE_COMPRESS_BASISU_H diff --git a/modules/basis_universal/patches/external-jpgd.patch b/modules/basis_universal/patches/external-jpgd.patch new file mode 100644 index 0000000000..7a805d00cb --- /dev/null +++ b/modules/basis_universal/patches/external-jpgd.patch @@ -0,0 +1,13 @@ +diff --git a/thirdparty/basis_universal/encoder/basisu_enc.cpp b/thirdparty/basis_universal/encoder/basisu_enc.cpp +index c431ceaf12..e87dd636a2 100644 +--- a/thirdparty/basis_universal/encoder/basisu_enc.cpp ++++ b/thirdparty/basis_universal/encoder/basisu_enc.cpp +@@ -409,7 +409,7 @@ namespace basisu + bool load_jpg(const char *pFilename, image& img) + { + int width = 0, height = 0, actual_comps = 0; +- uint8_t *pImage_data = jpgd::decompress_jpeg_image_from_file(pFilename, &width, &height, &actual_comps, 4, jpgd::jpeg_decoder::cFlagLinearChromaFiltering); ++ uint8_t *pImage_data = jpgd::decompress_jpeg_image_from_file(pFilename, &width, &height, &actual_comps, 4, jpgd::jpeg_decoder::cFlagBoxChromaFiltering); + if (!pImage_data) + return false; + diff --git a/modules/basis_universal/register_types.cpp b/modules/basis_universal/register_types.cpp index 7c0bc4ac82..06a3fb76cb 100644 --- a/modules/basis_universal/register_types.cpp +++ b/modules/basis_universal/register_types.cpp @@ -30,262 +30,19 @@ #include "register_types.h" -#include "core/os/os.h" -#include "servers/rendering_server.h" - -#include <transcoder/basisu_transcoder.h> - -#ifdef TOOLS_ENABLED -#include <encoder/basisu_comp.h> -#endif - -enum BasisDecompressFormat { - BASIS_DECOMPRESS_RG, - BASIS_DECOMPRESS_RGB, - BASIS_DECOMPRESS_RGBA, - BASIS_DECOMPRESS_RG_AS_RA -}; - -//workaround for lack of ETC2 RG -#define USE_RG_AS_RGBA - -#ifdef TOOLS_ENABLED -static Vector<uint8_t> basis_universal_packer(const Ref<Image> &p_image, Image::UsedChannels p_channels) { - Vector<uint8_t> budata; - { - basisu::basis_compressor_params params; - Ref<Image> image = p_image->duplicate(); - if (image->get_format() != Image::FORMAT_RGBA8) { - image->convert(Image::FORMAT_RGBA8); - } - - params.m_uastc = true; - params.m_quality_level = basisu::BASISU_QUALITY_MIN; - - params.m_pack_uastc_flags &= ~basisu::cPackUASTCLevelMask; - - static const uint32_t s_level_flags[basisu::TOTAL_PACK_UASTC_LEVELS] = { basisu::cPackUASTCLevelFastest, basisu::cPackUASTCLevelFaster, basisu::cPackUASTCLevelDefault, basisu::cPackUASTCLevelSlower, basisu::cPackUASTCLevelVerySlow }; - params.m_pack_uastc_flags |= s_level_flags[0]; - params.m_rdo_uastc = 0.0f; - params.m_rdo_uastc_quality_scalar = 0.0f; - params.m_rdo_uastc_dict_size = 1024; - - params.m_mip_fast = true; - params.m_multithreading = true; - - basisu::job_pool jpool(OS::get_singleton()->get_processor_count()); - params.m_pJob_pool = &jpool; - - BasisDecompressFormat decompress_format = BASIS_DECOMPRESS_RG; - params.m_check_for_alpha = false; - - switch (p_channels) { - case Image::USED_CHANNELS_L: { - decompress_format = BASIS_DECOMPRESS_RGB; - } break; - case Image::USED_CHANNELS_LA: { - params.m_force_alpha = true; - decompress_format = BASIS_DECOMPRESS_RGBA; - } break; - case Image::USED_CHANNELS_R: { - decompress_format = BASIS_DECOMPRESS_RGB; - } break; - case Image::USED_CHANNELS_RG: { -#ifdef USE_RG_AS_RGBA - params.m_force_alpha = true; - image->convert_rg_to_ra_rgba8(); - decompress_format = BASIS_DECOMPRESS_RG_AS_RA; -#else - params.m_seperate_rg_to_color_alpha = true; - decompress_format = BASIS_DECOMPRESS_RG; -#endif - } break; - case Image::USED_CHANNELS_RGB: { - decompress_format = BASIS_DECOMPRESS_RGB; - } break; - case Image::USED_CHANNELS_RGBA: { - params.m_force_alpha = true; - decompress_format = BASIS_DECOMPRESS_RGBA; - } break; - } - - if (!image->has_mipmaps()) { - basisu::image buimg(image->get_width(), image->get_height()); - Vector<uint8_t> vec = image->get_data(); - const uint8_t *r = vec.ptr(); - memcpy(buimg.get_ptr(), r, vec.size()); - params.m_source_images.push_back(buimg); - } else { - { - Ref<Image> base_image = image->get_image_from_mipmap(0); - Vector<uint8_t> image_vec = base_image->get_data(); - basisu::image buimg_image(base_image->get_width(), base_image->get_height()); - const uint8_t *r = image_vec.ptr(); - memcpy(buimg_image.get_ptr(), r, image_vec.size()); - params.m_source_images.push_back(buimg_image); - } - basisu::vector<basisu::image> images; - for (int32_t mip_map_i = 1; mip_map_i <= image->get_mipmap_count(); mip_map_i++) { - Ref<Image> mip_map = image->get_image_from_mipmap(mip_map_i); - Vector<uint8_t> mip_map_vec = mip_map->get_data(); - basisu::image buimg_mipmap(mip_map->get_width(), mip_map->get_height()); - const uint8_t *r = mip_map_vec.ptr(); - memcpy(buimg_mipmap.get_ptr(), r, mip_map_vec.size()); - images.push_back(buimg_mipmap); - } - params.m_source_mipmap_images.push_back(images); - } - - basisu::basis_compressor c; - c.init(params); - - int buerr = c.process(); - ERR_FAIL_COND_V(buerr != basisu::basis_compressor::cECSuccess, budata); - - const basisu::uint8_vec &buvec = c.get_output_basis_file(); - budata.resize(buvec.size() + 4); - - { - uint8_t *w = budata.ptrw(); - uint32_t *decf = (uint32_t *)w; - *decf = decompress_format; - memcpy(w + 4, &buvec[0], buvec.size()); - } - } - - return budata; -} -#endif // TOOLS_ENABLED - -static Ref<Image> basis_universal_unpacker_ptr(const uint8_t *p_data, int p_size) { - Ref<Image> image; - - const uint8_t *ptr = p_data; - int size = p_size; - ERR_FAIL_NULL_V_MSG(p_data, image, "Cannot unpack invalid basis universal data."); - - basist::transcoder_texture_format format = basist::transcoder_texture_format::cTFTotalTextureFormats; - Image::Format imgfmt = Image::FORMAT_MAX; - - switch (*(uint32_t *)(ptr)) { - case BASIS_DECOMPRESS_RG: { - if (RS::get_singleton()->has_os_feature("rgtc")) { - format = basist::transcoder_texture_format::cTFBC5; // get this from renderer - imgfmt = Image::FORMAT_RGTC_RG; - } else if (RS::get_singleton()->has_os_feature("etc2")) { - //unfortunately, basis universal does not support - // - ERR_FAIL_V(image); //unimplemented here - //format = basist::transcoder_texture_format::cTFETC1; // get this from renderer - //imgfmt = Image::FORMAT_RGTC_RG; - } else { - // FIXME: There wasn't anything here, but then imgformat is used uninitialized. - ERR_FAIL_V(image); - } - } break; - case BASIS_DECOMPRESS_RGB: { - if (RS::get_singleton()->has_os_feature("bptc")) { - format = basist::transcoder_texture_format::cTFBC7_M6_OPAQUE_ONLY; // get this from renderer - imgfmt = Image::FORMAT_BPTC_RGBA; - } else if (RS::get_singleton()->has_os_feature("s3tc")) { - format = basist::transcoder_texture_format::cTFBC1; // get this from renderer - imgfmt = Image::FORMAT_DXT1; - } else if (RS::get_singleton()->has_os_feature("etc")) { - format = basist::transcoder_texture_format::cTFETC1; // get this from renderer - imgfmt = Image::FORMAT_ETC; - } else { - format = basist::transcoder_texture_format::cTFBGR565; // get this from renderer - imgfmt = Image::FORMAT_RGB565; - } - - } break; - case BASIS_DECOMPRESS_RGBA: { - if (RS::get_singleton()->has_os_feature("bptc")) { - format = basist::transcoder_texture_format::cTFBC7_M5; // get this from renderer - imgfmt = Image::FORMAT_BPTC_RGBA; - } else if (RS::get_singleton()->has_os_feature("s3tc")) { - format = basist::transcoder_texture_format::cTFBC3; // get this from renderer - imgfmt = Image::FORMAT_DXT5; - } else if (RS::get_singleton()->has_os_feature("etc2")) { - format = basist::transcoder_texture_format::cTFETC2; // get this from renderer - imgfmt = Image::FORMAT_ETC2_RGBA8; - } else { - //opengl most likely - format = basist::transcoder_texture_format::cTFRGBA4444; // get this from renderer - imgfmt = Image::FORMAT_RGBA4444; - } - } break; - case BASIS_DECOMPRESS_RG_AS_RA: { - if (RS::get_singleton()->has_os_feature("s3tc")) { - format = basist::transcoder_texture_format::cTFBC3; // get this from renderer - imgfmt = Image::FORMAT_DXT5_RA_AS_RG; - } else if (RS::get_singleton()->has_os_feature("etc2")) { - format = basist::transcoder_texture_format::cTFETC2; // get this from renderer - imgfmt = Image::FORMAT_ETC2_RGBA8; - } else { - //opengl most likely, bad for normal maps, nothing to do about this. - format = basist::transcoder_texture_format::cTFRGBA32; - imgfmt = Image::FORMAT_RGBA8; - } - } break; - } - - ptr += 4; - size -= 4; - - basist::basisu_transcoder tr; - - ERR_FAIL_COND_V(!tr.validate_header(ptr, size), image); - - tr.start_transcoding(ptr, size); - - basist::basisu_image_info info; - tr.get_image_info(ptr, size, info, 0); - Vector<uint8_t> gpudata; - gpudata.resize(Image::get_image_data_size(info.m_width, info.m_height, imgfmt, info.m_total_levels > 1)); - - uint8_t *w = gpudata.ptrw(); - uint8_t *dst = w; - for (int i = 0; i < gpudata.size(); i++) { - dst[i] = 0x00; - } - uint32_t mip_count = Image::get_image_required_mipmaps(info.m_orig_width, info.m_orig_height, imgfmt); - for (uint32_t level_i = 0; level_i <= mip_count; level_i++) { - basist::basisu_image_level_info level; - tr.get_image_level_info(ptr, size, level, 0, level_i); - int ofs = Image::get_image_mipmap_offset(info.m_width, info.m_height, imgfmt, level_i); - bool ret = tr.transcode_image_level(ptr, size, 0, level_i, dst + ofs, level.m_total_blocks, format); - if (!ret) { - print_line(vformat("Basis universal cannot unpack level %d.", level_i)); - break; - }; - } - - image = Image::create_from_data(info.m_width, info.m_height, info.m_total_levels > 1, imgfmt, gpudata); - - return image; -} - -static Ref<Image> basis_universal_unpacker(const Vector<uint8_t> &p_buffer) { - Ref<Image> image; - - const uint8_t *r = p_buffer.ptr(); - int size = p_buffer.size(); - return basis_universal_unpacker_ptr(r, size); -} +#include "image_compress_basisu.h" void initialize_basis_universal_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } + basis_universal_init(); + #ifdef TOOLS_ENABLED - using namespace basisu; - using namespace basist; - basisu_encoder_init(); Image::basis_universal_packer = basis_universal_packer; #endif - basist::basisu_transcoder_init(); + Image::basis_universal_unpacker = basis_universal_unpacker; Image::basis_universal_unpacker_ptr = basis_universal_unpacker_ptr; } @@ -298,6 +55,7 @@ void uninitialize_basis_universal_module(ModuleInitializationLevel p_level) { #ifdef TOOLS_ENABLED Image::basis_universal_packer = nullptr; #endif + Image::basis_universal_unpacker = nullptr; Image::basis_universal_unpacker_ptr = nullptr; } diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index 0656f8224c..7c93fbf081 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -150,13 +150,13 @@ float CSGShape3D::get_snap() const { void CSGShape3D::_make_dirty(bool p_parent_removing) { if ((p_parent_removing || is_root_shape()) && !dirty) { - call_deferred(SNAME("_update_shape")); // Must be deferred; otherwise, is_root_shape() will use the previous parent + callable_mp(this, &CSGShape3D::_update_shape).call_deferred(); // Must be deferred; otherwise, is_root_shape() will use the previous parent. } if (!is_root_shape()) { parent_shape->_make_dirty(); } else if (!dirty) { - call_deferred(SNAME("_update_shape")); + callable_mp(this, &CSGShape3D::_update_shape).call_deferred(); } dirty = true; @@ -488,7 +488,9 @@ bool CSGShape3D::_is_debug_collision_shape_visible() { } void CSGShape3D::_update_debug_collision_shape() { - // NOTE: This is called only for the root shape with collision, when root_collision_shape is valid. + if (!use_collision || !is_root_shape() || !root_collision_shape.is_valid() || !_is_debug_collision_shape_visible()) { + return; + } ERR_FAIL_NULL(RenderingServer::get_singleton()); @@ -573,6 +575,11 @@ void CSGShape3D::_notification(int p_what) { // Update this node's parent only if its own visibility has changed, not the visibility of parent nodes parent_shape->_make_dirty(); } + if (is_visible()) { + _update_debug_collision_shape(); + } else { + _clear_debug_collision_shape(); + } last_visible = is_visible(); } break; @@ -793,7 +800,7 @@ CSGBrush *CSGMesh3D::_build_brush() { if (arrays.size() == 0) { _make_dirty(); - ERR_FAIL_COND_V(arrays.size() == 0, memnew(CSGBrush)); + ERR_FAIL_COND_V(arrays.is_empty(), memnew(CSGBrush)); } Vector<Vector3> avertices = arrays[Mesh::ARRAY_VERTEX]; @@ -1819,11 +1826,13 @@ CSGBrush *CSGPolygon3D::_build_brush() { if (path) { path->disconnect("tree_exited", callable_mp(this, &CSGPolygon3D::_path_exited)); path->disconnect("curve_changed", callable_mp(this, &CSGPolygon3D::_path_changed)); + path->set_update_callback(Callable()); } path = current_path; if (path) { path->connect("tree_exited", callable_mp(this, &CSGPolygon3D::_path_exited)); path->connect("curve_changed", callable_mp(this, &CSGPolygon3D::_path_changed)); + path->set_update_callback(callable_mp(this, &CSGPolygon3D::_path_changed)); } } @@ -2219,7 +2228,7 @@ void CSGPolygon3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "path_node", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Path3D"), "set_path_node", "get_path_node"); ADD_PROPERTY(PropertyInfo(Variant::INT, "path_interval_type", PROPERTY_HINT_ENUM, "Distance,Subdivide"), "set_path_interval_type", "get_path_interval_type"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "path_interval", PROPERTY_HINT_RANGE, "0.01,1.0,0.01,exp,or_greater"), "set_path_interval", "get_path_interval"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "path_simplify_angle", PROPERTY_HINT_RANGE, "0.0,180.0,0.1,exp"), "set_path_simplify_angle", "get_path_simplify_angle"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "path_simplify_angle", PROPERTY_HINT_RANGE, "0.0,180.0,0.1"), "set_path_simplify_angle", "get_path_simplify_angle"); ADD_PROPERTY(PropertyInfo(Variant::INT, "path_rotation", PROPERTY_HINT_ENUM, "Polygon,Path,PathFollow"), "set_path_rotation", "get_path_rotation"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "path_local"), "set_path_local", "is_path_local"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "path_continuous_u"), "set_path_continuous_u", "is_path_continuous_u"); diff --git a/modules/csg/csg_shape.h b/modules/csg/csg_shape.h index 6ac71b6946..bb7c8be431 100644 --- a/modules/csg/csg_shape.h +++ b/modules/csg/csg_shape.h @@ -35,7 +35,7 @@ #include "scene/3d/path_3d.h" #include "scene/3d/visual_instance_3d.h" -#include "scene/resources/concave_polygon_shape_3d.h" +#include "scene/resources/3d/concave_polygon_shape_3d.h" #include "thirdparty/misc/mikktspace.h" diff --git a/modules/csg/doc_classes/CSGPolygon3D.xml b/modules/csg/doc_classes/CSGPolygon3D.xml index 338adc9b52..5d35c04e25 100644 --- a/modules/csg/doc_classes/CSGPolygon3D.xml +++ b/modules/csg/doc_classes/CSGPolygon3D.xml @@ -39,7 +39,7 @@ When [member mode] is [constant MODE_PATH], the location of the [Path3D] object used to extrude the [member polygon]. </member> <member name="path_rotation" type="int" setter="set_path_rotation" getter="get_path_rotation" enum="CSGPolygon3D.PathRotation"> - When [member mode] is [constant MODE_PATH], the [enum PathRotation] method used to rotate the [member polygon] as it is extruded. + When [member mode] is [constant MODE_PATH], the path rotation method used to rotate the [member polygon] as it is extruded. </member> <member name="path_simplify_angle" type="float" setter="set_path_simplify_angle" getter="get_path_simplify_angle"> When [member mode] is [constant MODE_PATH], extrusions that are less than this angle, will be merged together to reduce polygon count. diff --git a/modules/csg/editor/csg_gizmos.cpp b/modules/csg/editor/csg_gizmos.cpp index ebf0f5a91f..ea7b6d225e 100644 --- a/modules/csg/editor/csg_gizmos.cpp +++ b/modules/csg/editor/csg_gizmos.cpp @@ -44,7 +44,7 @@ CSGShape3DGizmoPlugin::CSGShape3DGizmoPlugin() { helper.instantiate(); - Color gizmo_color = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/csg", Color(0.0, 0.4, 1, 0.15)); + Color gizmo_color = EDITOR_DEF_RST("editors/3d_gizmos/gizmo_colors/csg", Color(0.0, 0.4, 1, 0.15)); create_material("shape_union_material", gizmo_color); create_material("shape_union_solid_material", gizmo_color); gizmo_color.invert(); diff --git a/modules/csg/icons/CSGBox3D.svg b/modules/csg/icons/CSGBox3D.svg index bb3a1f357a..d425180cf5 100644 --- a/modules/csg/icons/CSGBox3D.svg +++ b/modules/csg/icons/CSGBox3D.svg @@ -1 +1 @@ -<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="m8 2 6 3v6l-6 3-6-3V5zm0 12V8l6-3M8 8 2 5" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> +<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="m8 2 6 3v6l-6 3-6-3V5zm0 12V8l6-3M8 8 2 5" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> diff --git a/modules/csg/icons/CSGCapsule3D.svg b/modules/csg/icons/CSGCapsule3D.svg index 33a2d4a115..3c2657999c 100644 --- a/modules/csg/icons/CSGCapsule3D.svg +++ b/modules/csg/icons/CSGCapsule3D.svg @@ -1 +1 @@ -<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M4 6a4 4 0 0 1 8 0v4a4 4 0 0 1-8 0zm0 1.25a2.5 1 0 0 0 8 0m-4-5v12" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M4 6a4 4 0 0 1 8 0v4a4 4 0 0 1-8 0zm0 1.25a2.5 1 0 0 0 8 0m-4-5v12" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> diff --git a/modules/csg/icons/CSGCylinder3D.svg b/modules/csg/icons/CSGCylinder3D.svg index 29d658dc46..19e48b4dba 100644 --- a/modules/csg/icons/CSGCylinder3D.svg +++ b/modules/csg/icons/CSGCylinder3D.svg @@ -1 +1 @@ -<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M2 4v8a6 2 0 0 0 12 0V4A6 2 0 0 0 2 4a6 2 0 0 0 12 0" stroke-width="2" fill="none" stroke="#fc7f7f" mask="url(#a)"/></svg> +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M2 4v8a6 2 0 0 0 12 0V4A6 2 0 0 0 2 4a6 2 0 0 0 12 0" stroke-width="2" fill="none" stroke="#fc7f7f" mask="url(#a)"/></svg> diff --git a/modules/csg/icons/CSGPolygon3D.svg b/modules/csg/icons/CSGPolygon3D.svg index 8d4b61e039..090047248b 100644 --- a/modules/csg/icons/CSGPolygon3D.svg +++ b/modules/csg/icons/CSGPolygon3D.svg @@ -1 +1 @@ -<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="m8 2 6 3.5v5L8 14l-6-3.5v-5h6zm6 3.5L8 9 2 5.5M8 9v5" fill="none" stroke-linejoin="round" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="m8 2 6 3.5v5L8 14l-6-3.5v-5h6zm6 3.5L8 9 2 5.5M8 9v5" fill="none" stroke-linejoin="round" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> diff --git a/modules/csg/icons/CSGSphere3D.svg b/modules/csg/icons/CSGSphere3D.svg index 16d45b3c99..a677ffaf5c 100644 --- a/modules/csg/icons/CSGSphere3D.svg +++ b/modules/csg/icons/CSGSphere3D.svg @@ -1 +1 @@ -<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M8 2a6 6 0 0 0 0 12A6 6 0 0 0 8 2v12M2.05 7.4a6 2 0 0 0 11.9 0" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M8 2a6 6 0 0 0 0 12A6 6 0 0 0 8 2v12M2.05 7.4a6 2 0 0 0 11.9 0" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg> diff --git a/modules/csg/icons/CSGTorus3D.svg b/modules/csg/icons/CSGTorus3D.svg index 27a6b422f9..60c56bd1ca 100644 --- a/modules/csg/icons/CSGTorus3D.svg +++ b/modules/csg/icons/CSGTorus3D.svg @@ -1 +1 @@ -<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><g fill="none" stroke="#fc7f7f" mask="url(#a)"><path d="M2.5 10a6 4 0 0 0 11 0 4 4 0 0 0 0-4 6 4 0 0 0-11 0 4 4 0 0 0 0 4z" stroke-width="2"/><path d="M6.2 7.2a2 1 0 1 0 3.6 0" stroke-width="1.75" stroke-linecap="round"/></g></svg> +<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fefefe"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><g fill="none" stroke="#fc7f7f" mask="url(#a)"><path d="M2.5 10a6 4 0 0 0 11 0 4 4 0 0 0 0-4 6 4 0 0 0-11 0 4 4 0 0 0 0 4z" stroke-width="2"/><path d="M6.2 7.2a2 1 0 1 0 3.6 0" stroke-width="1.75" stroke-linecap="round"/></g></svg> diff --git a/modules/dds/texture_loader_dds.cpp b/modules/dds/texture_loader_dds.cpp index 481bb46c24..b2de6b656e 100644 --- a/modules/dds/texture_loader_dds.cpp +++ b/modules/dds/texture_loader_dds.cpp @@ -44,24 +44,79 @@ enum { DDSD_MIPMAPCOUNT = 0x00020000, DDPF_FOURCC = 0x00000004, DDPF_ALPHAPIXELS = 0x00000001, - DDPF_INDEXED = 0x00000020, - DDPF_RGB = 0x00000040, + DDPF_RGB = 0x00000040 }; +enum DDSFourCC { + DDFCC_DXT1 = PF_FOURCC("DXT1"), + DDFCC_DXT3 = PF_FOURCC("DXT3"), + DDFCC_DXT5 = PF_FOURCC("DXT5"), + DDFCC_ATI1 = PF_FOURCC("ATI1"), + DDFCC_BC4U = PF_FOURCC("BC4U"), + DDFCC_ATI2 = PF_FOURCC("ATI2"), + DDFCC_BC5U = PF_FOURCC("BC5U"), + DDFCC_A2XY = PF_FOURCC("A2XY"), + DDFCC_DX10 = PF_FOURCC("DX10"), + DDFCC_R16F = 111, + DDFCC_RG16F = 112, + DDFCC_RGBA16F = 113, + DDFCC_R32F = 114, + DDFCC_RG32F = 115, + DDFCC_RGBA32F = 116 +}; + +// Reference: https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format +enum DXGIFormat { + DXGI_R32G32B32A32_FLOAT = 2, + DXGI_R16G16B16A16_FLOAT = 10, + DXGI_R32G32_FLOAT = 16, + DXGI_R10G10B10A2_UNORM = 24, + DXGI_R8G8B8A8_UNORM = 28, + DXGI_R16G16_FLOAT = 34, + DXGI_R32_FLOAT = 41, + DXGI_R16_FLOAT = 54, + DXGI_R9G9B9E5 = 67, + DXGI_BC1_UNORM = 71, + DXGI_BC2_UNORM = 74, + DXGI_BC3_UNORM = 77, + DXGI_BC4_UNORM = 80, + DXGI_BC5_UNORM = 83, + DXGI_B5G6R5_UNORM = 85, + DXGI_B5G5R5A1_UNORM = 86, + DXGI_B8G8R8A8_UNORM = 87, + DXGI_BC6H_UF16 = 95, + DXGI_BC6H_SF16 = 96, + DXGI_BC7_UNORM = 98, + DXGI_B4G4R4A4_UNORM = 115 +}; + +// The legacy bitmasked format names here represent the actual data layout in the files, +// while their official names are flipped (e.g. RGBA8 layout is officially called ABGR8). enum DDSFormat { DDS_DXT1, DDS_DXT3, DDS_DXT5, DDS_ATI1, DDS_ATI2, - DDS_A2XY, + DDS_BC6U, + DDS_BC6S, + DDS_BC7U, + DDS_R16F, + DDS_RG16F, + DDS_RGBA16F, + DDS_R32F, + DDS_RG32F, + DDS_RGBA32F, + DDS_RGB9E5, DDS_BGRA8, DDS_BGR8, - DDS_RGBA8, //flipped in dds - DDS_RGB8, //flipped in dds + DDS_RGBA8, + DDS_RGB8, DDS_BGR5A1, DDS_BGR565, DDS_BGR10A2, + DDS_RGB10A2, + DDS_BGRA4, DDS_LUMINANCE, DDS_LUMINANCE_ALPHA, DDS_MAX @@ -70,30 +125,112 @@ enum DDSFormat { struct DDSFormatInfo { const char *name = nullptr; bool compressed = false; - bool palette = false; uint32_t divisor = 0; uint32_t block_size = 0; Image::Format format = Image::Format::FORMAT_BPTC_RGBA; }; static const DDSFormatInfo dds_format_info[DDS_MAX] = { - { "DXT1/BC1", true, false, 4, 8, Image::FORMAT_DXT1 }, - { "DXT3/BC2", true, false, 4, 16, Image::FORMAT_DXT3 }, - { "DXT5/BC3", true, false, 4, 16, Image::FORMAT_DXT5 }, - { "ATI1/BC4", true, false, 4, 8, Image::FORMAT_RGTC_R }, - { "ATI2/3DC/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, - { "A2XY/DXN/BC5", true, false, 4, 16, Image::FORMAT_RGTC_RG }, - { "BGRA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "BGR8", false, false, 1, 3, Image::FORMAT_RGB8 }, - { "RGBA8", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "RGB8", false, false, 1, 3, Image::FORMAT_RGB8 }, - { "BGR5A1", false, false, 1, 2, Image::FORMAT_RGBA8 }, - { "BGR565", false, false, 1, 2, Image::FORMAT_RGB8 }, - { "BGR10A2", false, false, 1, 4, Image::FORMAT_RGBA8 }, - { "GRAYSCALE", false, false, 1, 1, Image::FORMAT_L8 }, - { "GRAYSCALE_ALPHA", false, false, 1, 2, Image::FORMAT_LA8 } + { "DXT1/BC1", true, 4, 8, Image::FORMAT_DXT1 }, + { "DXT3/BC2", true, 4, 16, Image::FORMAT_DXT3 }, + { "DXT5/BC3", true, 4, 16, Image::FORMAT_DXT5 }, + { "ATI1/BC4", true, 4, 8, Image::FORMAT_RGTC_R }, + { "ATI2/A2XY/BC5", true, 4, 16, Image::FORMAT_RGTC_RG }, + { "BC6U", true, 4, 16, Image::FORMAT_BPTC_RGBFU }, + { "BC6S", true, 4, 16, Image::FORMAT_BPTC_RGBF }, + { "BC7U", true, 4, 16, Image::FORMAT_BPTC_RGBA }, + { "R16F", false, 1, 2, Image::FORMAT_RH }, + { "RG16F", false, 1, 4, Image::FORMAT_RGH }, + { "RGBA16F", false, 1, 8, Image::FORMAT_RGBAH }, + { "R32F", false, 1, 4, Image::FORMAT_RF }, + { "RG32F", false, 1, 8, Image::FORMAT_RGF }, + { "RGBA32F", false, 1, 16, Image::FORMAT_RGBAF }, + { "RGB9E5", false, 1, 4, Image::FORMAT_RGBE9995 }, + { "BGRA8", false, 1, 4, Image::FORMAT_RGBA8 }, + { "BGR8", false, 1, 3, Image::FORMAT_RGB8 }, + { "RGBA8", false, 1, 4, Image::FORMAT_RGBA8 }, + { "RGB8", false, 1, 3, Image::FORMAT_RGB8 }, + { "BGR5A1", false, 1, 2, Image::FORMAT_RGBA8 }, + { "BGR565", false, 1, 2, Image::FORMAT_RGB8 }, + { "BGR10A2", false, 1, 4, Image::FORMAT_RGBA8 }, + { "RGB10A2", false, 1, 4, Image::FORMAT_RGBA8 }, + { "BGRA4", false, 1, 2, Image::FORMAT_RGBA8 }, + { "GRAYSCALE", false, 1, 1, Image::FORMAT_L8 }, + { "GRAYSCALE_ALPHA", false, 1, 2, Image::FORMAT_LA8 } }; +static DDSFormat dxgi_to_dds_format(uint32_t p_dxgi_format) { + switch (p_dxgi_format) { + case DXGI_R32G32B32A32_FLOAT: { + return DDS_RGBA32F; + } + case DXGI_R16G16B16A16_FLOAT: { + return DDS_RGBA16F; + } + case DXGI_R32G32_FLOAT: { + return DDS_RG32F; + } + case DXGI_R10G10B10A2_UNORM: { + return DDS_RGB10A2; + } + case DXGI_R8G8B8A8_UNORM: { + return DDS_RGBA8; + } + case DXGI_R16G16_FLOAT: { + return DDS_RG16F; + } + case DXGI_R32_FLOAT: { + return DDS_R32F; + } + case DXGI_R16_FLOAT: { + return DDS_R16F; + } + case DXGI_R9G9B9E5: { + return DDS_RGB9E5; + } + case DXGI_BC1_UNORM: { + return DDS_DXT1; + } + case DXGI_BC2_UNORM: { + return DDS_DXT3; + } + case DXGI_BC3_UNORM: { + return DDS_DXT5; + } + case DXGI_BC4_UNORM: { + return DDS_ATI1; + } + case DXGI_BC5_UNORM: { + return DDS_ATI2; + } + case DXGI_B5G6R5_UNORM: { + return DDS_BGR565; + } + case DXGI_B5G5R5A1_UNORM: { + return DDS_BGR5A1; + } + case DXGI_B8G8R8A8_UNORM: { + return DDS_BGRA8; + } + case DXGI_BC6H_UF16: { + return DDS_BC6U; + } + case DXGI_BC6H_SF16: { + return DDS_BC6S; + } + case DXGI_BC7_UNORM: { + return DDS_BC7U; + } + case DXGI_B4G4R4A4_UNORM: { + return DDS_BGRA4; + } + + default: { + return DDS_MAX; + } + } +} + Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { if (r_error) { *r_error = ERR_CANT_OPEN; @@ -121,15 +258,14 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig /* uint32_t depth = */ f->get_32(); uint32_t mipmaps = f->get_32(); - //skip 11 + // Skip reserved. for (int i = 0; i < 11; i++) { f->get_32(); } - //validate - + // Validate. // We don't check DDSD_CAPS or DDSD_PIXELFORMAT, as they're mandatory when writing, - // but non-mandatory when reading (as some writers don't set them)... + // but non-mandatory when reading (as some writers don't set them). if (magic != DDS_MAGIC || hsize != 124) { ERR_FAIL_V_MSG(Ref<Resource>(), "Invalid or unsupported DDS texture file '" + p_path + "'."); } @@ -145,65 +281,112 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig /* uint32_t caps_1 = */ f->get_32(); /* uint32_t caps_2 = */ f->get_32(); - /* uint32_t caps_ddsx = */ f->get_32(); + /* uint32_t caps_3 = */ f->get_32(); + /* uint32_t caps_4 = */ f->get_32(); - //reserved skip - f->get_32(); + // Skip reserved. f->get_32(); - /* - print_line("DDS width: "+itos(width)); - print_line("DDS height: "+itos(height)); - print_line("DDS mipmaps: "+itos(mipmaps)); + if (f->get_position() < 128) { + f->seek(128); + } - printf("fourcc: %x fflags: %x, rgbbits: %x, fsize: %x\n",format_fourcc,format_flags,format_rgb_bits,format_size); - printf("rmask: %x gmask: %x, bmask: %x, amask: %x\n",format_red_mask,format_green_mask,format_blue_mask,format_alpha_mask); - */ + DDSFormat dds_format = DDS_MAX; - //must avoid this later - while (f->get_position() < 128) { - f->get_8(); - } + if (format_flags & DDPF_FOURCC) { + // FourCC formats. + switch (format_fourcc) { + case DDFCC_DXT1: { + dds_format = DDS_DXT1; + } break; + case DDFCC_DXT3: { + dds_format = DDS_DXT3; + } break; + case DDFCC_DXT5: { + dds_format = DDS_DXT5; + } break; + case DDFCC_ATI1: + case DDFCC_BC4U: { + dds_format = DDS_ATI1; + } break; + case DDFCC_ATI2: + case DDFCC_BC5U: + case DDFCC_A2XY: { + dds_format = DDS_ATI2; + } break; + case DDFCC_R16F: { + dds_format = DDS_R16F; + } break; + case DDFCC_RG16F: { + dds_format = DDS_RG16F; + } break; + case DDFCC_RGBA16F: { + dds_format = DDS_RGBA16F; + } break; + case DDFCC_R32F: { + dds_format = DDS_R32F; + } break; + case DDFCC_RG32F: { + dds_format = DDS_RG32F; + } break; + case DDFCC_RGBA32F: { + dds_format = DDS_RGBA32F; + } break; + case DDFCC_DX10: { + uint32_t dxgi_format = f->get_32(); + /* uint32_t dimension = */ f->get_32(); + /* uint32_t misc_flags_1 = */ f->get_32(); + /* uint32_t array_size = */ f->get_32(); + /* uint32_t misc_flags_2 = */ f->get_32(); + + dds_format = dxgi_to_dds_format(dxgi_format); + } break; + + default: { + ERR_FAIL_V_MSG(Ref<Resource>(), "Unrecognized or unsupported FourCC in DDS '" + p_path + "'."); + } + } + + } else if (format_flags & DDPF_RGB) { + // Channel-bitmasked formats. + if (format_flags & DDPF_ALPHAPIXELS) { + // With alpha. + if (format_rgb_bits == 32 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff && format_alpha_mask == 0xff000000) { + dds_format = DDS_BGRA8; + } else if (format_rgb_bits == 32 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000 && format_alpha_mask == 0xff000000) { + dds_format = DDS_RGBA8; + } else if (format_rgb_bits == 16 && format_red_mask == 0x00007c00 && format_green_mask == 0x000003e0 && format_blue_mask == 0x0000001f && format_alpha_mask == 0x00008000) { + dds_format = DDS_BGR5A1; + } else if (format_rgb_bits == 32 && format_red_mask == 0x3ff00000 && format_green_mask == 0xffc00 && format_blue_mask == 0x3ff && format_alpha_mask == 0xc0000000) { + dds_format = DDS_BGR10A2; + } else if (format_rgb_bits == 32 && format_red_mask == 0x3ff && format_green_mask == 0xffc00 && format_blue_mask == 0x3ff00000 && format_alpha_mask == 0xc0000000) { + dds_format = DDS_RGB10A2; + } else if (format_rgb_bits == 16 && format_red_mask == 0xf00 && format_green_mask == 0xf0 && format_blue_mask == 0xf && format_alpha_mask == 0xf000) { + dds_format = DDS_BGRA4; + } + + } else { + // Without alpha. + if (format_rgb_bits == 24 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff) { + dds_format = DDS_BGR8; + } else if (format_rgb_bits == 24 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000) { + dds_format = DDS_RGB8; + } else if (format_rgb_bits == 16 && format_red_mask == 0x0000f800 && format_green_mask == 0x000007e0 && format_blue_mask == 0x0000001f) { + dds_format = DDS_BGR565; + } + } - DDSFormat dds_format; - - if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT1")) { - dds_format = DDS_DXT1; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT3")) { - dds_format = DDS_DXT3; - - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("DXT5")) { - dds_format = DDS_DXT5; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI1")) { - dds_format = DDS_ATI1; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("ATI2")) { - dds_format = DDS_ATI2; - } else if (format_flags & DDPF_FOURCC && format_fourcc == PF_FOURCC("A2XY")) { - dds_format = DDS_A2XY; - - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff && format_alpha_mask == 0xff000000) { - dds_format = DDS_BGRA8; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff0000 && format_green_mask == 0xff00 && format_blue_mask == 0xff) { - dds_format = DDS_BGR8; - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000 && format_alpha_mask == 0xff000000) { - dds_format = DDS_RGBA8; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 24 && format_red_mask == 0xff && format_green_mask == 0xff00 && format_blue_mask == 0xff0000) { - dds_format = DDS_RGB8; - - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 16 && format_red_mask == 0x00007c00 && format_green_mask == 0x000003e0 && format_blue_mask == 0x0000001f && format_alpha_mask == 0x00008000) { - dds_format = DDS_BGR5A1; - } else if (format_flags & DDPF_RGB && format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 32 && format_red_mask == 0x3ff00000 && format_green_mask == 0xffc00 && format_blue_mask == 0x3ff && format_alpha_mask == 0xc0000000) { - dds_format = DDS_BGR10A2; - } else if (format_flags & DDPF_RGB && !(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0x0000f800 && format_green_mask == 0x000007e0 && format_blue_mask == 0x0000001f) { - dds_format = DDS_BGR565; - } else if (!(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 8 && format_red_mask == 0xff) { - dds_format = DDS_LUMINANCE; - } else if ((format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 16 && format_red_mask == 0xff && format_alpha_mask == 0xff00) { - dds_format = DDS_LUMINANCE_ALPHA; - } else if (format_flags & DDPF_INDEXED && format_rgb_bits == 8) { - dds_format = DDS_BGR565; } else { - //printf("unrecognized fourcc %x format_flags: %x - rgbbits %i - red_mask %x green mask %x blue mask %x alpha mask %x\n", format_fourcc, format_flags, format_rgb_bits, format_red_mask, format_green_mask, format_blue_mask, format_alpha_mask); + // Other formats. + if (format_flags & DDPF_ALPHAPIXELS && format_rgb_bits == 16 && format_red_mask == 0xff && format_alpha_mask == 0xff00) { + dds_format = DDS_LUMINANCE_ALPHA; + } else if (!(format_flags & DDPF_ALPHAPIXELS) && format_rgb_bits == 8 && format_red_mask == 0xff) { + dds_format = DDS_LUMINANCE; + } + } + + // No format detected, error. + if (dds_format == DDS_MAX) { ERR_FAIL_V_MSG(Ref<Resource>(), "Unrecognized or unsupported color layout in DDS '" + p_path + "'."); } @@ -218,17 +401,20 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig uint32_t h = height; if (info.compressed) { - //compressed bc - + // BC compressed. uint32_t size = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; - ERR_FAIL_COND_V(size != pitch, Ref<Resource>()); - ERR_FAIL_COND_V(!(flags & DDSD_LINEARSIZE), Ref<Resource>()); + + if (flags & DDSD_LINEARSIZE) { + ERR_FAIL_COND_V_MSG(size != pitch, Ref<Resource>(), "DDS header flags specify that a linear size of the top-level image is present, but the specified size does not match the expected value."); + } else { + ERR_FAIL_COND_V_MSG(pitch != 0, Ref<Resource>(), "DDS header flags specify that no linear size will given for the top-level image, but a non-zero linear size value is present in the header."); + } for (uint32_t i = 1; i < mipmaps; i++) { w = MAX(1u, w >> 1); h = MAX(1u, h >> 1); + uint32_t bsize = MAX(info.divisor, w) / info.divisor * MAX(info.divisor, h) / info.divisor * info.block_size; - //printf("%i x %i - block: %i\n",w,h,bsize); size += bsize; } @@ -236,50 +422,8 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig uint8_t *wb = src_data.ptrw(); f->get_buffer(wb, size); - } else if (info.palette) { - //indexed - ERR_FAIL_COND_V(!(flags & DDSD_PITCH), Ref<Resource>()); - ERR_FAIL_COND_V(format_rgb_bits != 8, Ref<Resource>()); - - uint32_t size = pitch * height; - ERR_FAIL_COND_V(size != width * height * info.block_size, Ref<Resource>()); - - uint8_t palette[256 * 4]; - f->get_buffer(palette, 256 * 4); - - int colsize = 3; - for (int i = 0; i < 256; i++) { - if (palette[i * 4 + 3] < 255) { - colsize = 4; - } - } - - int w2 = width; - int h2 = height; - - for (uint32_t i = 1; i < mipmaps; i++) { - w2 = (w2 + 1) >> 1; - h2 = (h2 + 1) >> 1; - size += w2 * h2 * info.block_size; - } - - src_data.resize(size + 256 * colsize); - uint8_t *wb = src_data.ptrw(); - f->get_buffer(wb, size); - - for (int i = 0; i < 256; i++) { - int dst_ofs = size + i * colsize; - int src_ofs = i * 4; - wb[dst_ofs + 0] = palette[src_ofs + 2]; - wb[dst_ofs + 1] = palette[src_ofs + 1]; - wb[dst_ofs + 2] = palette[src_ofs + 0]; - if (colsize == 4) { - wb[dst_ofs + 3] = palette[src_ofs + 3]; - } - } } else { - //uncompressed generic... - + // Generic uncompressed. uint32_t size = width * height * info.block_size; for (uint32_t i = 1; i < mipmaps; i++) { @@ -288,9 +432,10 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig size += w * h * info.block_size; } + // Calculate the space these formats will take up after decoding. if (dds_format == DDS_BGR565) { size = size * 3 / 2; - } else if (dds_format == DDS_BGR5A1) { + } else if (dds_format == DDS_BGR5A1 || dds_format == DDS_BGRA4) { size = size * 2; } @@ -298,9 +443,10 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig uint8_t *wb = src_data.ptrw(); f->get_buffer(wb, size); + // Decode nonstandard formats. switch (dds_format) { case DDS_BGR5A1: { - // TO RGBA + // To RGBA8. int colcount = size / 4; for (int i = colcount - 1; i >= 0; i--) { @@ -311,13 +457,16 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig uint8_t b = wb[src_ofs] & 0x1F; uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x3) << 3); uint8_t r = (wb[src_ofs + 1] >> 2) & 0x1F; + wb[dst_ofs + 0] = r << 3; wb[dst_ofs + 1] = g << 3; wb[dst_ofs + 2] = b << 3; wb[dst_ofs + 3] = a ? 255 : 0; } + } break; case DDS_BGR565: { + // To RGB8. int colcount = size / 3; for (int i = colcount - 1; i >= 0; i--) { @@ -327,21 +476,67 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig uint8_t b = wb[src_ofs] & 0x1F; uint8_t g = (wb[src_ofs] >> 5) | ((wb[src_ofs + 1] & 0x7) << 3); uint8_t r = wb[src_ofs + 1] >> 3; + wb[dst_ofs + 0] = r << 3; wb[dst_ofs + 1] = g << 2; - wb[dst_ofs + 2] = b << 3; //b<<3; + wb[dst_ofs + 2] = b << 3; } } break; - case DDS_BGR10A2: { - // TO RGBA + case DDS_BGRA4: { + // To RGBA8. int colcount = size / 4; for (int i = colcount - 1; i >= 0; i--) { + int src_ofs = i * 2; + int dst_ofs = i * 4; + + uint8_t b = wb[src_ofs] & 0x0F; + uint8_t g = wb[src_ofs] & 0xF0; + uint8_t r = wb[src_ofs + 1] & 0x0F; + uint8_t a = wb[src_ofs + 1] & 0xF0; + + wb[dst_ofs] = (r << 4) | r; + wb[dst_ofs + 1] = g | (g >> 4); + wb[dst_ofs + 2] = (b << 4) | b; + wb[dst_ofs + 3] = a | (a >> 4); + } + + } break; + case DDS_RGB10A2: { + // To RGBA8. + int colcount = size / 4; + + for (int i = 0; i < colcount; i++) { int ofs = i * 4; uint32_t w32 = uint32_t(wb[ofs + 0]) | (uint32_t(wb[ofs + 1]) << 8) | (uint32_t(wb[ofs + 2]) << 16) | (uint32_t(wb[ofs + 3]) << 24); + // This method follows the 'standard' way of decoding 10-bit dds files, + // which means the ones created with DirectXTex will be loaded incorrectly. + uint8_t a = (w32 & 0xc0000000) >> 24; + uint8_t r = (w32 & 0x3ff) >> 2; + uint8_t g = (w32 & 0xffc00) >> 12; + uint8_t b = (w32 & 0x3ff00000) >> 22; + + wb[ofs + 0] = r; + wb[ofs + 1] = g; + wb[ofs + 2] = b; + wb[ofs + 3] = a == 0xc0 ? 255 : a; // 0xc0 should be opaque. + } + + } break; + case DDS_BGR10A2: { + // To RGBA8. + int colcount = size / 4; + + for (int i = 0; i < colcount; i++) { + int ofs = i * 4; + + uint32_t w32 = uint32_t(wb[ofs + 0]) | (uint32_t(wb[ofs + 1]) << 8) | (uint32_t(wb[ofs + 2]) << 16) | (uint32_t(wb[ofs + 3]) << 24); + + // This method follows the 'standard' way of decoding 10-bit dds files, + // which means the ones created with DirectXTex will be loaded incorrectly. uint8_t a = (w32 & 0xc0000000) >> 24; uint8_t r = (w32 & 0x3ff00000) >> 22; uint8_t g = (w32 & 0xffc00) >> 12; @@ -350,10 +545,12 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig wb[ofs + 0] = r; wb[ofs + 1] = g; wb[ofs + 2] = b; - wb[ofs + 3] = a == 0xc0 ? 255 : a; //0xc0 should be opaque + wb[ofs + 3] = a == 0xc0 ? 255 : a; // 0xc0 should be opaque. } + } break; case DDS_BGRA8: { + // To RGBA8. int colcount = size / 4; for (int i = 0; i < colcount; i++) { @@ -362,44 +559,12 @@ Ref<Resource> ResourceFormatDDS::load(const String &p_path, const String &p_orig } break; case DDS_BGR8: { + // To RGB8. int colcount = size / 3; for (int i = 0; i < colcount; i++) { SWAP(wb[i * 3 + 0], wb[i * 3 + 2]); } - } break; - case DDS_RGBA8: { - /* do nothing either - int colcount = size/4; - - for(int i=0;i<colcount;i++) { - uint8_t r = wb[i*4+1]; - uint8_t g = wb[i*4+2]; - uint8_t b = wb[i*4+3]; - uint8_t a = wb[i*4+0]; - - wb[i*4+0]=r; - wb[i*4+1]=g; - wb[i*4+2]=b; - wb[i*4+3]=a; - } - */ - } break; - case DDS_RGB8: { - // do nothing - /* - int colcount = size/3; - - for(int i=0;i<colcount;i++) { - SWAP( wb[i*3+0],wb[i*3+2] ); - }*/ - } break; - case DDS_LUMINANCE: { - // do nothing i guess? - - } break; - case DDS_LUMINANCE_ALPHA: { - // do nothing i guess? } break; diff --git a/modules/enet/enet_multiplayer_peer.cpp b/modules/enet/enet_multiplayer_peer.cpp index 63f12ea1c1..910c4ed242 100644 --- a/modules/enet/enet_multiplayer_peer.cpp +++ b/modules/enet/enet_multiplayer_peer.cpp @@ -40,20 +40,20 @@ void ENetMultiplayerPeer::set_target_peer(int p_peer) { int ENetMultiplayerPeer::get_packet_peer() const { ERR_FAIL_COND_V_MSG(!_is_active(), 1, "The multiplayer instance isn't currently active."); - ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); + ERR_FAIL_COND_V(incoming_packets.is_empty(), 1); return incoming_packets.front()->get().from; } MultiplayerPeer::TransferMode ENetMultiplayerPeer::get_packet_mode() const { ERR_FAIL_COND_V_MSG(!_is_active(), TRANSFER_MODE_RELIABLE, "The multiplayer instance isn't currently active."); - ERR_FAIL_COND_V(incoming_packets.size() == 0, TRANSFER_MODE_RELIABLE); + ERR_FAIL_COND_V(incoming_packets.is_empty(), TRANSFER_MODE_RELIABLE); return incoming_packets.front()->get().transfer_mode; } int ENetMultiplayerPeer::get_packet_channel() const { ERR_FAIL_COND_V_MSG(!_is_active(), 1, "The multiplayer instance isn't currently active."); - ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); + ERR_FAIL_COND_V(incoming_packets.is_empty(), 1); int ch = incoming_packets.front()->get().channel; if (ch >= SYSCH_MAX) { // First 2 channels are reserved. return ch - SYSCH_MAX + 1; @@ -321,7 +321,7 @@ int ENetMultiplayerPeer::get_available_packet_count() const { } Error ENetMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { - ERR_FAIL_COND_V_MSG(incoming_packets.size() == 0, ERR_UNAVAILABLE, "No incoming packets available."); + ERR_FAIL_COND_V_MSG(incoming_packets.is_empty(), ERR_UNAVAILABLE, "No incoming packets available."); _pop_current_packet(); diff --git a/modules/enet/enet_packet_peer.cpp b/modules/enet/enet_packet_peer.cpp index f2bf5337ee..edb33fc96b 100644 --- a/modules/enet/enet_packet_peer.cpp +++ b/modules/enet/enet_packet_peer.cpp @@ -90,7 +90,7 @@ int ENetPacketPeer::get_available_packet_count() const { Error ENetPacketPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) { ERR_FAIL_NULL_V(peer, ERR_UNCONFIGURED); - ERR_FAIL_COND_V(!packet_queue.size(), ERR_UNAVAILABLE); + ERR_FAIL_COND_V(packet_queue.is_empty(), ERR_UNAVAILABLE); if (last_packet) { enet_packet_destroy(last_packet); last_packet = nullptr; diff --git a/modules/etcpak/image_compress_etcpak.cpp b/modules/etcpak/image_compress_etcpak.cpp index 14cce2686c..dcd73101c2 100644 --- a/modules/etcpak/image_compress_etcpak.cpp +++ b/modules/etcpak/image_compress_etcpak.cpp @@ -39,13 +39,13 @@ EtcpakType _determine_etc_type(Image::UsedChannels p_channels) { switch (p_channels) { case Image::USED_CHANNELS_L: - return EtcpakType::ETCPAK_TYPE_ETC1; + return EtcpakType::ETCPAK_TYPE_ETC2; case Image::USED_CHANNELS_LA: return EtcpakType::ETCPAK_TYPE_ETC2_ALPHA; case Image::USED_CHANNELS_R: - return EtcpakType::ETCPAK_TYPE_ETC2; + return EtcpakType::ETCPAK_TYPE_ETC2_R; case Image::USED_CHANNELS_RG: - return EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG; + return EtcpakType::ETCPAK_TYPE_ETC2_RG; case Image::USED_CHANNELS_RGB: return EtcpakType::ETCPAK_TYPE_ETC2; case Image::USED_CHANNELS_RGBA: @@ -62,9 +62,9 @@ EtcpakType _determine_dxt_type(Image::UsedChannels p_channels) { case Image::USED_CHANNELS_LA: return EtcpakType::ETCPAK_TYPE_DXT5; case Image::USED_CHANNELS_R: - return EtcpakType::ETCPAK_TYPE_DXT5; + return EtcpakType::ETCPAK_TYPE_RGTC_R; case Image::USED_CHANNELS_RG: - return EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG; + return EtcpakType::ETCPAK_TYPE_RGTC_RG; case Image::USED_CHANNELS_RGB: return EtcpakType::ETCPAK_TYPE_DXT1; case Image::USED_CHANNELS_RGBA: @@ -113,6 +113,12 @@ void _compress_etcpak(EtcpakType p_compresstype, Image *r_img) { } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2) { target_format = Image::FORMAT_ETC2_RGB8; r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. + } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_R) { + target_format = Image::FORMAT_ETC2_R11; + r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. + } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_RG) { + target_format = Image::FORMAT_ETC2_RG11; + r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG) { target_format = Image::FORMAT_ETC2_RA_AS_RG; r_img->convert_rg_to_ra_rgba8(); @@ -127,6 +133,10 @@ void _compress_etcpak(EtcpakType p_compresstype, Image *r_img) { r_img->convert_rg_to_ra_rgba8(); } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5) { target_format = Image::FORMAT_DXT5; + } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_RGTC_R) { + target_format = Image::FORMAT_RGTC_R; + } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_RGTC_RG) { + target_format = Image::FORMAT_RGTC_RG; } else { ERR_FAIL_MSG("Invalid or unsupported etcpak compression format, not ETC or DXT."); } @@ -219,23 +229,54 @@ void _compress_etcpak(EtcpakType p_compresstype, Image *r_img) { // Override the src_mip_read pointer to our temporary Vector. src_mip_read = padded_src.ptr(); } - if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC1) { - CompressEtc1RgbDither(src_mip_read, dest_mip_write, blocks, mip_w); - } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2) { - CompressEtc2Rgb(src_mip_read, dest_mip_write, blocks, mip_w, true); - } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_ALPHA || p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG) { - CompressEtc2Rgba(src_mip_read, dest_mip_write, blocks, mip_w, true); - } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT1) { - CompressDxt1Dither(src_mip_read, dest_mip_write, blocks, mip_w); - } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5 || p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG) { - CompressDxt5(src_mip_read, dest_mip_write, blocks, mip_w); - } else { - ERR_FAIL_MSG("etcpak: Invalid or unsupported compression format."); + + switch (p_compresstype) { + case EtcpakType::ETCPAK_TYPE_ETC1: + CompressEtc1RgbDither(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_ETC2: + CompressEtc2Rgb(src_mip_read, dest_mip_write, blocks, mip_w, true); + break; + + case EtcpakType::ETCPAK_TYPE_ETC2_ALPHA: + case EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG: + CompressEtc2Rgba(src_mip_read, dest_mip_write, blocks, mip_w, true); + break; + + case EtcpakType::ETCPAK_TYPE_ETC2_R: + CompressEacR(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_ETC2_RG: + CompressEacRg(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_DXT1: + CompressDxt1Dither(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_DXT5: + case EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG: + CompressDxt5(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_RGTC_R: + CompressBc4(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + case EtcpakType::ETCPAK_TYPE_RGTC_RG: + CompressBc5(src_mip_read, dest_mip_write, blocks, mip_w); + break; + + default: + ERR_FAIL_MSG("etcpak: Invalid or unsupported compression format."); + break; } } // Replace original image with compressed one. r_img->set_data(width, height, mipmaps, target_format, dest_data); - print_verbose(vformat("etcpak: Encoding took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time))); + print_verbose(vformat("etcpak: Encoding took %d ms.", OS::get_singleton()->get_ticks_msec() - start_time)); } diff --git a/modules/etcpak/image_compress_etcpak.h b/modules/etcpak/image_compress_etcpak.h index ff267631a6..9d5343740b 100644 --- a/modules/etcpak/image_compress_etcpak.h +++ b/modules/etcpak/image_compress_etcpak.h @@ -38,9 +38,13 @@ enum class EtcpakType { ETCPAK_TYPE_ETC2, ETCPAK_TYPE_ETC2_ALPHA, ETCPAK_TYPE_ETC2_RA_AS_RG, + ETCPAK_TYPE_ETC2_R, + ETCPAK_TYPE_ETC2_RG, ETCPAK_TYPE_DXT1, ETCPAK_TYPE_DXT5, ETCPAK_TYPE_DXT5_RA_AS_RG, + ETCPAK_TYPE_RGTC_R, + ETCPAK_TYPE_RGTC_RG, }; void _compress_etc1(Image *r_img); diff --git a/modules/fbx/SCsub b/modules/fbx/SCsub new file mode 100644 index 0000000000..6a791094c6 --- /dev/null +++ b/modules/fbx/SCsub @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +Import("env") +Import("env_modules") + +env_fbx = env_modules.Clone() + +# Thirdparty source files + +thirdparty_obj = [] + +thirdparty_dir = "#thirdparty/ufbx/" +thirdparty_sources = [thirdparty_dir + "ufbx.c"] + +env_fbx.Prepend(CPPPATH=[thirdparty_dir]) + +env_thirdparty = env_fbx.Clone() +env_thirdparty.disable_warnings() + +env_thirdparty.Append( + CPPDEFINES=[ + "UFBX_NO_SUBDIVISION", + "UFBX_NO_TESSELLATION", + "UFBX_NO_GEOMETRY_CACHE", + "UFBX_NO_SCENE_EVALUATION", + "UFBX_NO_INDEX_GENERATION", + "UFBX_NO_SKINNING_EVALUATION", + "UFBX_NO_FORMAT_OBJ", + ] +) + +env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources) +env.modules_sources += thirdparty_obj + +# Godot source files + +module_obj = [] + +env_fbx.add_source_files(module_obj, "*.cpp") +env_fbx.add_source_files(module_obj, "structures/*.cpp") + +if env.editor_build: + env_fbx.add_source_files(module_obj, "editor/*.cpp") + +env.modules_sources += module_obj + +# Needed to force rebuilding the module files when the thirdparty library is updated. +env.Depends(module_obj, thirdparty_obj) diff --git a/modules/fbx/config.py b/modules/fbx/config.py new file mode 100644 index 0000000000..95a4e75885 --- /dev/null +++ b/modules/fbx/config.py @@ -0,0 +1,20 @@ +def can_build(env, platform): + env.module_add_dependencies("fbx", ["gltf"]) + return not env["disable_3d"] + + +def configure(env): + pass + + +def get_doc_classes(): + return [ + "EditorSceneFormatImporterFBX2GLTF", + "EditorSceneFormatImporterUFBX", + "FBXDocument", + "FBXState", + ] + + +def get_doc_path(): + return "doc_classes" diff --git a/modules/fbx/doc_classes/EditorSceneFormatImporterFBX2GLTF.xml b/modules/fbx/doc_classes/EditorSceneFormatImporterFBX2GLTF.xml new file mode 100644 index 0000000000..e30782780f --- /dev/null +++ b/modules/fbx/doc_classes/EditorSceneFormatImporterFBX2GLTF.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorSceneFormatImporterFBX2GLTF" inherits="EditorSceneFormatImporter" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + Importer for the [code].fbx[/code] scene file format. + </brief_description> + <description> + Imports Autodesk FBX 3D scenes by way of converting them to glTF 2.0 using the FBX2glTF command line tool. + The location of the FBX2glTF binary is set via the [member EditorSettings.filesystem/import/fbx2gltf/fbx2gltf_path] editor setting. + This importer is only used if [member ProjectSettings.filesystem/import/fbx2gltf/enabled] is set to [code]true[/code]. + </description> + <tutorials> + </tutorials> +</class> diff --git a/modules/fbx/doc_classes/EditorSceneFormatImporterUFBX.xml b/modules/fbx/doc_classes/EditorSceneFormatImporterUFBX.xml new file mode 100644 index 0000000000..cab58b64cb --- /dev/null +++ b/modules/fbx/doc_classes/EditorSceneFormatImporterUFBX.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorSceneFormatImporterUFBX" inherits="EditorSceneFormatImporter" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + Import FBX files using the ufbx library. + </brief_description> + <description> + EditorSceneFormatImporterUFBX is designed to load FBX files and supports both binary and ASCII FBX files from version 3000 onward. This class supports various 3D object types like meshes, skins, blend shapes, materials, and rigging information. The class aims for feature parity with the official FBX SDK and supports FBX 7.4 specifications. + </description> + <tutorials> + </tutorials> +</class> diff --git a/modules/fbx/doc_classes/FBXDocument.xml b/modules/fbx/doc_classes/FBXDocument.xml new file mode 100644 index 0000000000..db1d77c0f1 --- /dev/null +++ b/modules/fbx/doc_classes/FBXDocument.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="FBXDocument" inherits="GLTFDocument" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + Handles FBX documents. + </brief_description> + <description> + The FBXDocument handles FBX documents. It provides methods to append data from buffers or files, generate scenes, and register/unregister document extensions. + When exporting FBX from Blender, use the "FBX Units Scale" option. The "FBX Units Scale" option sets the correct scale factor and avoids manual adjustments when re-importing into Blender, such as through glTF export. + </description> + <tutorials> + </tutorials> +</class> diff --git a/modules/fbx/doc_classes/FBXState.xml b/modules/fbx/doc_classes/FBXState.xml new file mode 100644 index 0000000000..88c23181bc --- /dev/null +++ b/modules/fbx/doc_classes/FBXState.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="FBXState" inherits="GLTFState" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> + <brief_description> + </brief_description> + <description> + The FBXState handles the state data imported from FBX files. + </description> + <tutorials> + </tutorials> + <members> + <member name="allow_geometry_helper_nodes" type="bool" setter="set_allow_geometry_helper_nodes" getter="get_allow_geometry_helper_nodes" default="false"> + If [code]true[/code], the import process used auxiliary nodes called geometry helper nodes. These nodes help preserve the pivots and transformations of the original 3D model during import. + </member> + </members> +</class> diff --git a/modules/gltf/editor/editor_scene_importer_fbx.cpp b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp index 5e7a8f4e69..b615b5aed8 100644 --- a/modules/gltf/editor/editor_scene_importer_fbx.cpp +++ b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* editor_scene_importer_fbx.cpp */ +/* editor_scene_importer_fbx2gltf.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,27 +28,39 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#include "editor_scene_importer_fbx.h" +#include "editor_scene_importer_fbx2gltf.h" #ifdef TOOLS_ENABLED -#include "../gltf_document.h" +#include "editor_scene_importer_ufbx.h" +#include "modules/gltf/gltf_document.h" #include "core/config/project_settings.h" #include "editor/editor_settings.h" -#include "main/main.h" -uint32_t EditorSceneFormatImporterFBX::get_import_flags() const { +uint32_t EditorSceneFormatImporterFBX2GLTF::get_import_flags() const { return ImportFlags::IMPORT_SCENE | ImportFlags::IMPORT_ANIMATION; } -void EditorSceneFormatImporterFBX::get_extensions(List<String> *r_extensions) const { +void EditorSceneFormatImporterFBX2GLTF::get_extensions(List<String> *r_extensions) const { r_extensions->push_back("fbx"); } -Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t p_flags, +Node *EditorSceneFormatImporterFBX2GLTF::import_scene(const String &p_path, uint32_t p_flags, const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err) { + // FIXME: Hack to work around GH-86309. + if (p_options.has("fbx/importer") && int(p_options["fbx/importer"]) == EditorSceneFormatImporterUFBX::FBX_IMPORTER_UFBX) { + Ref<EditorSceneFormatImporterUFBX> fbx2gltf_importer; + fbx2gltf_importer.instantiate(); + Node *scene = fbx2gltf_importer->import_scene(p_path, p_flags, p_options, r_missing_deps, r_err); + if (r_err && *r_err == OK) { + return scene; + } else { + return nullptr; + } + } + // Get global paths for source and sink. // Don't use `c_escape()` as it can generate broken paths. These paths will be @@ -61,7 +73,7 @@ Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t // Run fbx2gltf. - String fbx2gltf_path = EDITOR_GET("filesystem/import/fbx/fbx2gltf_path"); + String fbx2gltf_path = EDITOR_GET("filesystem/import/fbx2gltf/fbx2gltf_path"); List<String> args; args.push_back("--pbr-metallic-roughness"); @@ -92,6 +104,9 @@ Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t gltf.instantiate(); Ref<GLTFState> state; state.instantiate(); + if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { + state->set_import_as_skeleton_bones(true); + } print_verbose(vformat("glTF path: %s", sink)); Error err = gltf->append_from_file(sink, state, p_flags, p_path.get_base_dir()); if (err != OK) { @@ -110,39 +125,28 @@ Node *EditorSceneFormatImporterFBX::import_scene(const String &p_path, uint32_t #endif } -Variant EditorSceneFormatImporterFBX::get_option_visibility(const String &p_path, bool p_for_animation, +Variant EditorSceneFormatImporterFBX2GLTF::get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) { + if (p_option == "fbx/embedded_image_handling") { + return false; + } + if (p_options.has("fbx/importer") && int(p_options["fbx/importer"]) == EditorSceneFormatImporterUFBX::FBX_IMPORTER_FBX2GLTF && p_option == "fbx/embedded_image_handling") { + return false; + } return true; } -void EditorSceneFormatImporterFBX::get_import_options(const String &p_path, - List<ResourceImporter::ImportOption> *r_options) { -} - -bool EditorFileSystemImportFormatSupportQueryFBX::is_active() const { - String fbx2gltf_path = EDITOR_GET("filesystem/import/fbx/fbx2gltf_path"); - return !FileAccess::exists(fbx2gltf_path); -} +#define ADD_OPTION_ENUM(PATH, ENUM_HINT, VALUE) \ + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::INT, SNAME(PATH), PROPERTY_HINT_ENUM, ENUM_HINT), VALUE)); -Vector<String> EditorFileSystemImportFormatSupportQueryFBX::get_file_extensions() const { - Vector<String> ret; - ret.push_back("fbx"); - return ret; +void EditorSceneFormatImporterFBX2GLTF::get_import_options(const String &p_path, + List<ResourceImporter::ImportOption> *r_options) { } -bool EditorFileSystemImportFormatSupportQueryFBX::query() { - FBXImporterManager::get_singleton()->show_dialog(true); - - while (true) { - OS::get_singleton()->delay_usec(1); - DisplayServer::get_singleton()->process_events(); - Main::iteration(); - if (!FBXImporterManager::get_singleton()->is_visible()) { - break; - } +void EditorSceneFormatImporterFBX2GLTF::handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const { + if (!p_import_params.has("fbx/importer")) { + p_import_params["fbx/importer"] = EditorSceneFormatImporterUFBX::FBX_IMPORTER_UFBX; } - - return false; } #endif // TOOLS_ENABLED diff --git a/modules/gltf/editor/editor_scene_importer_fbx.h b/modules/fbx/editor/editor_scene_importer_fbx2gltf.h index cc60830eac..c68e37f0d8 100644 --- a/modules/gltf/editor/editor_scene_importer_fbx.h +++ b/modules/fbx/editor/editor_scene_importer_fbx2gltf.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* editor_scene_importer_fbx.h */ +/* editor_scene_importer_fbx2gltf.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,20 +28,18 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef EDITOR_SCENE_IMPORTER_FBX_H -#define EDITOR_SCENE_IMPORTER_FBX_H +#ifndef EDITOR_SCENE_IMPORTER_FBX2GLTF_H +#define EDITOR_SCENE_IMPORTER_FBX2GLTF_H #ifdef TOOLS_ENABLED -#include "editor/editor_file_system.h" -#include "editor/fbx_importer_manager.h" -#include "editor/import/resource_importer_scene.h" +#include "editor/import/3d/resource_importer_scene.h" class Animation; class Node; -class EditorSceneFormatImporterFBX : public EditorSceneFormatImporter { - GDCLASS(EditorSceneFormatImporterFBX, EditorSceneFormatImporter); +class EditorSceneFormatImporterFBX2GLTF : public EditorSceneFormatImporter { + GDCLASS(EditorSceneFormatImporterFBX2GLTF, EditorSceneFormatImporter); public: virtual uint32_t get_import_flags() const override; @@ -53,17 +51,9 @@ public: List<ResourceImporter::ImportOption> *r_options) override; virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) override; -}; - -class EditorFileSystemImportFormatSupportQueryFBX : public EditorFileSystemImportFormatSupportQuery { - GDCLASS(EditorFileSystemImportFormatSupportQueryFBX, EditorFileSystemImportFormatSupportQuery); - -public: - virtual bool is_active() const override; - virtual Vector<String> get_file_extensions() const override; - virtual bool query() override; + virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; }; #endif // TOOLS_ENABLED -#endif // EDITOR_SCENE_IMPORTER_FBX_H +#endif // EDITOR_SCENE_IMPORTER_FBX2GLTF_H diff --git a/modules/fbx/editor/editor_scene_importer_ufbx.cpp b/modules/fbx/editor/editor_scene_importer_ufbx.cpp new file mode 100644 index 0000000000..241fdba0c5 --- /dev/null +++ b/modules/fbx/editor/editor_scene_importer_ufbx.cpp @@ -0,0 +1,118 @@ +/**************************************************************************/ +/* editor_scene_importer_ufbx.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_scene_importer_ufbx.h" + +#ifdef TOOLS_ENABLED + +#include "../fbx_document.h" +#include "editor_scene_importer_fbx2gltf.h" + +#include "core/config/project_settings.h" + +uint32_t EditorSceneFormatImporterUFBX::get_import_flags() const { + return ImportFlags::IMPORT_SCENE | ImportFlags::IMPORT_ANIMATION; +} + +void EditorSceneFormatImporterUFBX::get_extensions(List<String> *r_extensions) const { + r_extensions->push_back("fbx"); +} + +Node *EditorSceneFormatImporterUFBX::import_scene(const String &p_path, uint32_t p_flags, + const HashMap<StringName, Variant> &p_options, + List<String> *r_missing_deps, Error *r_err) { + // FIXME: Hack to work around GH-86309. + if (p_options.has("fbx/importer") && int(p_options["fbx/importer"]) == FBX_IMPORTER_FBX2GLTF && GLOBAL_GET("filesystem/import/fbx2gltf/enabled")) { + Ref<EditorSceneFormatImporterFBX2GLTF> fbx2gltf_importer; + fbx2gltf_importer.instantiate(); + Node *scene = fbx2gltf_importer->import_scene(p_path, p_flags, p_options, r_missing_deps, r_err); + if (r_err && *r_err == OK) { + return scene; + } else { + return nullptr; + } + } + Ref<FBXDocument> fbx; + fbx.instantiate(); + Ref<FBXState> state; + state.instantiate(); + print_verbose(vformat("FBX path: %s", p_path)); + String path = ProjectSettings::get_singleton()->globalize_path(p_path); + bool allow_geometry_helper_nodes = p_options.has("fbx/allow_geometry_helper_nodes") ? (bool)p_options["fbx/allow_geometry_helper_nodes"] : false; + if (allow_geometry_helper_nodes) { + state->set_allow_geometry_helper_nodes(allow_geometry_helper_nodes); + } + if (p_options.has("fbx/embedded_image_handling")) { + int32_t enum_option = p_options["fbx/embedded_image_handling"]; + state->set_handle_binary_image(enum_option); + } + if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { + state->set_import_as_skeleton_bones(true); + } + if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { + state->set_import_as_skeleton_bones(true); + } + p_flags |= EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS; + Error err = fbx->append_from_file(path, state, p_flags, p_path.get_base_dir()); + if (err != OK) { + if (r_err) { + *r_err = FAILED; + } + return nullptr; + } + return fbx->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], (bool)p_options["animation/remove_immutable_tracks"]); +} + +Variant EditorSceneFormatImporterUFBX::get_option_visibility(const String &p_path, bool p_for_animation, + const String &p_option, const HashMap<StringName, Variant> &p_options) { + String file_extension = p_path.get_extension().to_lower(); + if (file_extension != "fbx" && p_option.begins_with("fbx/")) { + return false; + } + if ((file_extension != "gltf" && file_extension != "glb") && p_option.begins_with("gltf/")) { + return false; + } + return true; +} + +void EditorSceneFormatImporterUFBX::get_import_options(const String &p_path, + List<ResourceImporter::ImportOption> *r_options) { + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "fbx/importer", PROPERTY_HINT_ENUM, "ufbx,FBX2glTF"), FBX_IMPORTER_UFBX)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::BOOL, "fbx/allow_geometry_helper_nodes"), false)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "fbx/embedded_image_handling", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed as Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), FBXState::HANDLE_BINARY_EXTRACT_TEXTURES)); +} + +void EditorSceneFormatImporterUFBX::handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const { + if (!p_import_params.has("fbx/importer")) { + p_import_params["fbx/importer"] = EditorSceneFormatImporterUFBX::FBX_IMPORTER_UFBX; + } +} + +#endif // TOOLS_ENABLED diff --git a/modules/fbx/editor/editor_scene_importer_ufbx.h b/modules/fbx/editor/editor_scene_importer_ufbx.h new file mode 100644 index 0000000000..b81b8df4c1 --- /dev/null +++ b/modules/fbx/editor/editor_scene_importer_ufbx.h @@ -0,0 +1,62 @@ +/**************************************************************************/ +/* editor_scene_importer_ufbx.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_SCENE_IMPORTER_UFBX_H +#define EDITOR_SCENE_IMPORTER_UFBX_H + +#ifdef TOOLS_ENABLED + +#include "editor/import/3d/resource_importer_scene.h" + +class Animation; +class Node; + +class EditorSceneFormatImporterUFBX : public EditorSceneFormatImporter { + GDCLASS(EditorSceneFormatImporterUFBX, EditorSceneFormatImporter); + +public: + enum FBX_IMPORTER_TYPE { + FBX_IMPORTER_UFBX, + FBX_IMPORTER_FBX2GLTF, + }; + virtual uint32_t get_import_flags() const override; + virtual void get_extensions(List<String> *r_extensions) const override; + virtual Node *import_scene(const String &p_path, uint32_t p_flags, + const HashMap<StringName, Variant> &p_options, + List<String> *r_missing_deps, Error *r_err = nullptr) override; + virtual void get_import_options(const String &p_path, + List<ResourceImporter::ImportOption> *r_options) override; + virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, + const HashMap<StringName, Variant> &p_options) override; + virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; +}; +#endif // TOOLS_ENABLED + +#endif // EDITOR_SCENE_IMPORTER_UFBX_H diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp new file mode 100644 index 0000000000..367117edcb --- /dev/null +++ b/modules/fbx/fbx_document.cpp @@ -0,0 +1,2373 @@ +/**************************************************************************/ +/* fbx_document.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "fbx_document.h" + +#include "core/config/project_settings.h" +#include "core/crypto/crypto_core.h" +#include "core/io/config_file.h" +#include "core/io/file_access.h" +#include "core/io/file_access_memory.h" +#include "core/io/image.h" +#include "core/math/color.h" +#include "scene/3d/bone_attachment_3d.h" +#include "scene/3d/camera_3d.h" +#include "scene/3d/importer_mesh_instance_3d.h" +#include "scene/3d/light_3d.h" +#include "scene/resources/image_texture.h" +#include "scene/resources/material.h" +#include "scene/resources/portable_compressed_texture.h" +#include "scene/resources/surface_tool.h" + +#include "modules/gltf/extensions/gltf_light.h" +#include "modules/gltf/gltf_defines.h" +#include "modules/gltf/skin_tool.h" +#include "modules/gltf/structures/gltf_animation.h" +#include "modules/gltf/structures/gltf_camera.h" + +#ifdef TOOLS_ENABLED +#include "editor/editor_file_system.h" +#endif + +// FIXME: Hardcoded to avoid editor dependency. +#define FBX_IMPORT_USE_NAMED_SKIN_BINDS 16 +#define FBX_IMPORT_DISCARD_MESHES_AND_MATERIALS 32 +#define FBX_IMPORT_FORCE_DISABLE_MESH_COMPRESSION 64 + +#include <ufbx.h> + +static size_t _file_access_read_fn(void *user, void *data, size_t size) { + FileAccess *file = static_cast<FileAccess *>(user); + return (size_t)file->get_buffer((uint8_t *)data, (uint64_t)size); +} + +static bool _file_access_skip_fn(void *user, size_t size) { + FileAccess *file = static_cast<FileAccess *>(user); + file->seek(file->get_position() + size); + return true; +} + +static Vector2 _as_vec2(const ufbx_vec2 &p_vector) { + return Vector2(real_t(p_vector.x), real_t(p_vector.y)); +} + +static Color _as_color(const ufbx_vec4 &p_vector) { + return Color(real_t(p_vector.x), real_t(p_vector.y), real_t(p_vector.z), real_t(p_vector.w)); +} + +static Quaternion _as_quaternion(const ufbx_quat &p_quat) { + return Quaternion(real_t(p_quat.x), real_t(p_quat.y), real_t(p_quat.z), real_t(p_quat.w)); +} + +static Color _material_color(const ufbx_material_map &p_map) { + if (p_map.value_components == 1) { + float r = float(p_map.value_real); + return Color(r, r, r); + } else if (p_map.value_components == 3) { + float r = float(p_map.value_vec3.x); + float g = float(p_map.value_vec3.y); + float b = float(p_map.value_vec3.z); + return Color(r, g, b); + } else { + float r = float(p_map.value_vec4.x); + float g = float(p_map.value_vec4.y); + float b = float(p_map.value_vec4.z); + float a = float(p_map.value_vec4.z); + return Color(r, g, b, a); + } +} + +static Color _material_color(const ufbx_material_map &p_map, const ufbx_material_map &p_factor) { + Color color = _material_color(p_map); + if (p_factor.has_value) { + float factor = float(p_factor.value_real); + color.r *= factor; + color.g *= factor; + color.b *= factor; + } + return color; +} + +static const ufbx_texture *_get_file_texture(const ufbx_texture *p_texture) { + if (!p_texture) { + return nullptr; + } + for (const ufbx_texture *texture : p_texture->file_textures) { + if (texture->file_index != UFBX_NO_INDEX) { + return texture; + } + } + return nullptr; +} + +static Ref<Image> _get_decompressed_image(Ref<Texture2D> texture) { + if (texture.is_null()) { + return Ref<Image>(); + } + Ref<Image> image = texture->get_image(); + if (image.is_null()) { + return Ref<Image>(); + } + image = image->duplicate(); + image->decompress(); + return image; +} + +static Vector<Vector2> _decode_vertex_attrib_vec2(const ufbx_vertex_vec2 &p_attrib, const Vector<uint32_t> &p_indices) { + Vector<Vector2> ret; + + int num_indices = p_indices.size(); + ret.resize(num_indices); + for (int i = 0; i < num_indices; i++) { + ret.write[i] = _as_vec2(p_attrib[p_indices[i]]); + } + return ret; +} + +static Vector<Vector3> _decode_vertex_attrib_vec3(const ufbx_vertex_vec3 &p_attrib, const Vector<uint32_t> &p_indices) { + Vector<Vector3> ret; + + int num_indices = p_indices.size(); + ret.resize(num_indices); + for (int i = 0; i < num_indices; i++) { + ret.write[i] = FBXDocument::_as_vec3(p_attrib[p_indices[i]]); + } + return ret; +} + +static Vector<float> _decode_vertex_attrib_vec3_as_tangent(const ufbx_vertex_vec3 &p_attrib, const Vector<uint32_t> &p_indices) { + Vector<float> ret; + + int num_indices = p_indices.size(); + ret.resize(num_indices * 4); + for (int i = 0; i < num_indices; i++) { + Vector3 v = FBXDocument::_as_vec3(p_attrib[p_indices[i]]); + ret.write[i * 4 + 0] = v.x; + ret.write[i * 4 + 1] = v.y; + ret.write[i * 4 + 2] = v.z; + ret.write[i * 4 + 3] = 1.0f; + } + return ret; +} + +static Vector<Color> _decode_vertex_attrib_color(const ufbx_vertex_vec4 &p_attrib, const Vector<uint32_t> &p_indices) { + Vector<Color> ret; + + int num_indices = p_indices.size(); + ret.resize(num_indices); + for (int i = 0; i < num_indices; i++) { + ret.write[i] = _as_color(p_attrib[p_indices[i]]); + } + return ret; +} + +static Vector3 _encode_vertex_index(uint32_t p_index) { + return Vector3(real_t(p_index & 0xffff), real_t(p_index >> 16), 0.0f); +} + +static uint32_t _decode_vertex_index(const Vector3 &p_vertex) { + return uint32_t(p_vertex.x) | uint32_t(p_vertex.y) << 16; +} + +struct ThreadPoolFBX { + struct Group { + ufbx_thread_pool_context ctx = {}; + WorkerThreadPool::GroupID task_id = {}; + uint32_t start_index = 0; + }; + + WorkerThreadPool *pool = nullptr; + Group groups[UFBX_THREAD_GROUP_COUNT] = {}; +}; + +static void _thread_pool_task(void *user, uint32_t index) { + ThreadPoolFBX::Group *group = (ThreadPoolFBX::Group *)user; + ufbx_thread_pool_run_task(group->ctx, group->start_index + index); +} + +static bool _thread_pool_init_fn(void *user, ufbx_thread_pool_context ctx, const ufbx_thread_pool_info *info) { + ThreadPoolFBX *pool = (ThreadPoolFBX *)user; + for (ThreadPoolFBX::Group &group : pool->groups) { + group.ctx = ctx; + } + return true; +} + +static bool _thread_pool_run_fn(void *user, ufbx_thread_pool_context ctx, uint32_t group, uint32_t start_index, uint32_t count) { + ThreadPoolFBX *pool = (ThreadPoolFBX *)user; + ThreadPoolFBX::Group &pool_group = pool->groups[group]; + pool_group.start_index = start_index; + pool_group.task_id = pool->pool->add_native_group_task(_thread_pool_task, &pool_group, (int)count, -1, true, "ufbx"); + return true; +} + +static bool _thread_pool_wait_fn(void *user, ufbx_thread_pool_context ctx, uint32_t group, uint32_t max_index) { + ThreadPoolFBX *pool = (ThreadPoolFBX *)user; + pool->pool->wait_for_group_task_completion(pool->groups[group].task_id); + return true; +} + +String FBXDocument::_gen_unique_name(HashSet<String> &unique_names, const String &p_name) { + const String s_name = p_name.validate_node_name(); + + String u_name; + int index = 1; + while (true) { + u_name = s_name; + + if (index > 1) { + u_name += itos(index); + } + if (!unique_names.has(u_name)) { + break; + } + index++; + } + + unique_names.insert(u_name); + + return u_name; +} + +String FBXDocument::_sanitize_animation_name(const String &p_name) { + // Animations disallow the normal node invalid characters as well as "," and "[" + // (See animation/animation_player.cpp::add_animation) + + // TODO: Consider adding invalid_characters or a validate_animation_name to animation_player to mirror Node. + String anim_name = p_name.validate_node_name(); + anim_name = anim_name.replace(",", ""); + anim_name = anim_name.replace("[", ""); + return anim_name; +} + +String FBXDocument::_gen_unique_animation_name(Ref<FBXState> p_state, const String &p_name) { + const String s_name = _sanitize_animation_name(p_name); + + String u_name; + int index = 1; + while (true) { + u_name = s_name; + + if (index > 1) { + u_name += itos(index); + } + if (!p_state->unique_animation_names.has(u_name)) { + break; + } + index++; + } + + p_state->unique_animation_names.insert(u_name); + + return u_name; +} + +Error FBXDocument::_parse_scenes(Ref<FBXState> p_state) { + p_state->unique_names.insert("Skeleton3D"); // Reserve skeleton name. + + const ufbx_scene *fbx_scene = p_state->scene.get(); + + // TODO: Multi-document support, would need test files for structure + p_state->scene_name = ""; + + // TODO: Append the root node directly if we use root-based space conversion + for (const ufbx_node *root_node : fbx_scene->root_node->children) { + p_state->root_nodes.push_back(int(root_node->typed_id)); + } + + return OK; +} + +Error FBXDocument::_parse_nodes(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + + for (int node_i = 0; node_i < static_cast<int>(fbx_scene->nodes.count); node_i++) { + const ufbx_node *fbx_node = fbx_scene->nodes[node_i]; + + Ref<GLTFNode> node; + node.instantiate(); + + node->height = int(fbx_node->node_depth); + + if (fbx_node->name.length > 0) { + node->set_name(_as_string(fbx_node->name)); + node->set_original_name(node->get_name()); + } else if (fbx_node->is_root) { + node->set_name("Root"); + } + if (fbx_node->camera) { + node->camera = fbx_node->camera->typed_id; + } + if (fbx_node->light) { + node->light = fbx_node->light->typed_id; + } + if (fbx_node->mesh) { + node->mesh = fbx_node->mesh->typed_id; + } + + { + node->transform.origin = _as_vec3(fbx_node->local_transform.translation); + node->transform.basis.set_quaternion_scale(_as_quaternion(fbx_node->local_transform.rotation), _as_vec3(fbx_node->local_transform.scale)); + + if (fbx_node->bind_pose) { + ufbx_bone_pose *pose = ufbx_get_bone_pose(fbx_node->bind_pose, fbx_node); + ufbx_transform rest_transform = ufbx_matrix_to_transform(&pose->bone_to_parent); + + Vector3 rest_position = _as_vec3(rest_transform.translation); + Quaternion rest_rotation = _as_quaternion(rest_transform.rotation); + Vector3 rest_scale = _as_vec3(rest_transform.scale); + Transform3D godot_rest_xform; + godot_rest_xform.basis.set_quaternion_scale(rest_rotation, rest_scale); + godot_rest_xform.origin = rest_position; + node->set_additional_data("GODOT_rest_transform", godot_rest_xform); + } else { + node->set_additional_data("GODOT_rest_transform", node->transform); + } + } + + for (const ufbx_node *child : fbx_node->children) { + node->children.push_back(child->typed_id); + } + + p_state->nodes.push_back(node); + } + + // build the hierarchy + for (GLTFNodeIndex node_i = 0; node_i < p_state->nodes.size(); node_i++) { + for (int j = 0; j < p_state->nodes[node_i]->children.size(); j++) { + GLTFNodeIndex child_i = p_state->nodes[node_i]->children[j]; + + ERR_FAIL_INDEX_V(child_i, p_state->nodes.size(), ERR_FILE_CORRUPT); + ERR_CONTINUE(p_state->nodes[child_i]->parent != -1); //node already has a parent, wtf. + + p_state->nodes.write[child_i]->parent = node_i; + } + } + + return OK; +} + +Error FBXDocument::_parse_meshes(Ref<FBXState> p_state) { + ufbx_scene *fbx_scene = p_state->scene.get(); + + LocalVector<int> nodes_by_mesh_id; + nodes_by_mesh_id.reserve(fbx_scene->meshes.count); + for (size_t i = 0; i < fbx_scene->meshes.count; i++) { + nodes_by_mesh_id.push_back(-1); + } + for (int i = 0; i < p_state->nodes.size(); i++) { + const Ref<GLTFNode> &node = p_state->nodes[i]; + if (node->mesh >= 0 && (unsigned)node->mesh < nodes_by_mesh_id.size()) { + nodes_by_mesh_id[node->mesh] = i; + } + } + + for (const ufbx_mesh *fbx_mesh : fbx_scene->meshes) { + print_verbose("FBX: Parsing mesh: " + itos(int64_t(fbx_mesh->typed_id))); + + static const Mesh::PrimitiveType primitive_types[] = { + Mesh::PRIMITIVE_TRIANGLES, + Mesh::PRIMITIVE_POINTS, + Mesh::PRIMITIVE_LINES, + }; + + Ref<ImporterMesh> import_mesh; + import_mesh.instantiate(); + String mesh_name = "mesh"; + String original_name; + if (fbx_mesh->name.length > 0) { + mesh_name = _as_string(fbx_mesh->name); + original_name = mesh_name; + } else if (fbx_mesh->typed_id < (unsigned)p_state->nodes.size() && nodes_by_mesh_id[fbx_mesh->typed_id] != -1) { + const Ref<GLTFNode> &node = p_state->nodes[nodes_by_mesh_id[fbx_mesh->typed_id]]; + original_name = node->get_original_name(); + mesh_name = node->get_name(); + } + import_mesh->set_name(_gen_unique_name(p_state->unique_mesh_names, mesh_name)); + + bool use_blend_shapes = false; + if (fbx_mesh->blend_deformers.count > 0) { + use_blend_shapes = true; + } + + Vector<float> blend_weights; + Vector<int> blend_channels; + if (use_blend_shapes) { + print_verbose("FBX: Mesh has targets"); + + import_mesh->set_blend_shape_mode(Mesh::BLEND_SHAPE_MODE_NORMALIZED); + + for (const ufbx_blend_deformer *fbx_deformer : fbx_mesh->blend_deformers) { + for (const ufbx_blend_channel *fbx_channel : fbx_deformer->channels) { + if (fbx_channel->keyframes.count == 0) { + continue; + } + String bs_name; + if (fbx_channel->name.length > 0) { + bs_name = _as_string(fbx_channel->name); + } else { + bs_name = String("morph_") + itos(blend_channels.size()); + } + import_mesh->add_blend_shape(bs_name); + blend_weights.push_back(float(fbx_channel->weight)); + blend_channels.push_back(float(fbx_channel->typed_id)); + } + } + } + + for (const ufbx_mesh_part &fbx_mesh_part : fbx_mesh->material_parts) { + for (Mesh::PrimitiveType primitive : primitive_types) { + uint32_t num_indices = 0; + switch (primitive) { + case Mesh::PRIMITIVE_POINTS: + num_indices = fbx_mesh_part.num_point_faces * 1; + break; + case Mesh::PRIMITIVE_LINES: + num_indices = fbx_mesh_part.num_line_faces * 2; + break; + case Mesh::PRIMITIVE_TRIANGLES: + num_indices = fbx_mesh_part.num_triangles * 3; + break; + case Mesh::PRIMITIVE_TRIANGLE_STRIP: + // FIXME 2021-09-15 fire + break; + case Mesh::PRIMITIVE_LINE_STRIP: + // FIXME 2021-09-15 fire + break; + default: + // FIXME 2021-09-15 fire + break; + } + if (num_indices == 0) { + continue; + } + + Vector<uint32_t> indices; + indices.resize(num_indices); + + uint32_t offset = 0; + for (uint32_t face_index : fbx_mesh_part.face_indices) { + ufbx_face face = fbx_mesh->faces[face_index]; + switch (primitive) { + case Mesh::PRIMITIVE_POINTS: { + if (face.num_indices == 1) { + indices.write[offset] = face.index_begin; + offset += 1; + } + } break; + case Mesh::PRIMITIVE_LINES: + if (face.num_indices == 2) { + indices.write[offset] = face.index_begin; + indices.write[offset + 1] = face.index_begin + 1; + offset += 2; + } + break; + case Mesh::PRIMITIVE_TRIANGLES: + if (face.num_indices >= 3) { + uint32_t *dst = indices.ptrw() + offset; + size_t space = indices.size() - offset; + uint32_t num_triangles = ufbx_triangulate_face(dst, space, fbx_mesh, face); + offset += num_triangles * 3; + + // Godot uses clockwise winding order! + for (uint32_t i = 0; i < num_triangles; i++) { + SWAP(dst[i * 3 + 0], dst[i * 3 + 2]); + } + } + break; + case Mesh::PRIMITIVE_TRIANGLE_STRIP: + // FIXME 2021-09-15 fire + break; + case Mesh::PRIMITIVE_LINE_STRIP: + // FIXME 2021-09-15 fire + break; + default: + // FIXME 2021-09-15 fire + break; + } + } + ERR_CONTINUE((uint64_t)offset != (uint64_t)indices.size()); + + int32_t vertex_num = indices.size(); + bool has_vertex_color = false; + + uint32_t flags = 0; + + Array array; + array.resize(Mesh::ARRAY_MAX); + + // HACK: If we have blend shapes we cannot merge vertices at identical positions + // if they have different indices in the file. To avoid this encode the vertex index + // into the vertex position for the time being. + // Ideally this would be an extra channel in the vertex but as the vertex format is + // fixed and we already use user data for extra UV channels this'll do. + if (use_blend_shapes) { + Vector<Vector3> vertex_indices; + int num_blend_shape_indices = indices.size(); + vertex_indices.resize(num_blend_shape_indices); + for (int i = 0; i < num_blend_shape_indices; i++) { + vertex_indices.write[i] = _encode_vertex_index(fbx_mesh->vertex_indices[indices[i]]); + } + array[Mesh::ARRAY_VERTEX] = vertex_indices; + } else { + array[Mesh::ARRAY_VERTEX] = _decode_vertex_attrib_vec3(fbx_mesh->vertex_position, indices); + } + + // Normals always exist as they're generated if missing, + // see `ufbx_load_opts.generate_missing_normals`. + Vector<Vector3> normals = _decode_vertex_attrib_vec3(fbx_mesh->vertex_normal, indices); + array[Mesh::ARRAY_NORMAL] = normals; + + if (fbx_mesh->vertex_tangent.exists) { + Vector<float> tangents = _decode_vertex_attrib_vec3_as_tangent(fbx_mesh->vertex_tangent, indices); + + // Patch bitangent sign if available + if (fbx_mesh->vertex_bitangent.exists) { + for (int i = 0; i < vertex_num; i++) { + Vector3 tangent = Vector3(tangents[i * 4], tangents[i * 4 + 1], tangents[i * 4 + 2]); + Vector3 bitangent = _as_vec3(fbx_mesh->vertex_bitangent[indices[i]]); + Vector3 generated_bitangent = normals[i].cross(tangent); + if (generated_bitangent.dot(bitangent) < 0.0f) { + tangents.write[i * 4 + 3] = -1.0f; + } + } + } + + array[Mesh::ARRAY_TANGENT] = tangents; + } + + if (fbx_mesh->vertex_uv.exists) { + PackedVector2Array uv_array = _decode_vertex_attrib_vec2(fbx_mesh->vertex_uv, indices); + _process_uv_set(uv_array); + array[Mesh::ARRAY_TEX_UV] = uv_array; + } + + if (fbx_mesh->uv_sets.count >= 2 && fbx_mesh->uv_sets[1].vertex_uv.exists) { + PackedVector2Array uv2_array = _decode_vertex_attrib_vec2(fbx_mesh->uv_sets[1].vertex_uv, indices); + _process_uv_set(uv2_array); + array[Mesh::ARRAY_TEX_UV2] = uv2_array; + } + + for (int uv_i = 2; uv_i < 8; uv_i += 2) { + Vector<float> cur_custom; + Vector<Vector2> texcoord_first; + Vector<Vector2> texcoord_second; + + int texcoord_i = uv_i; + int texcoord_next = texcoord_i + 1; + int num_channels = 0; + if (texcoord_i < static_cast<int>(fbx_mesh->uv_sets.count) && fbx_mesh->uv_sets[texcoord_i].vertex_uv.exists) { + texcoord_first = _decode_vertex_attrib_vec2(fbx_mesh->uv_sets[texcoord_i].vertex_uv, indices); + _process_uv_set(texcoord_first); + num_channels = 2; + } + if (texcoord_next < static_cast<int>(fbx_mesh->uv_sets.count) && fbx_mesh->uv_sets[texcoord_next].vertex_uv.exists) { + texcoord_second = _decode_vertex_attrib_vec2(fbx_mesh->uv_sets[texcoord_next].vertex_uv, indices); + _process_uv_set(texcoord_second); + num_channels = 4; + } + if (!num_channels) { + break; + } + cur_custom.resize(vertex_num * num_channels); + for (int32_t uv_first_i = 0; uv_first_i < texcoord_first.size() && uv_first_i < vertex_num; uv_first_i++) { + int index = uv_first_i * num_channels; + cur_custom.write[index] = texcoord_first[uv_first_i].x; + cur_custom.write[index + 1] = texcoord_first[uv_first_i].y; + } + if (num_channels == 4) { + for (int32_t uv_second_i = 0; uv_second_i < texcoord_second.size() && uv_second_i < vertex_num; uv_second_i++) { + int index = uv_second_i * num_channels; + cur_custom.write[index + 2] = texcoord_second[uv_second_i].x; + cur_custom.write[index + 3] = texcoord_second[uv_second_i].y; + } + _zero_unused_elements(cur_custom, texcoord_second.size(), vertex_num, num_channels); + } else if (num_channels == 2) { + _zero_unused_elements(cur_custom, texcoord_first.size(), vertex_num, num_channels); + } + if (!cur_custom.is_empty()) { + array[Mesh::ARRAY_CUSTOM0 + ((uv_i - 2) / 2)] = cur_custom; // Map uv2-uv7 to custom0-custom2 + int custom_shift = Mesh::ARRAY_FORMAT_CUSTOM0_SHIFT + ((uv_i - 2) / 2) * Mesh::ARRAY_FORMAT_CUSTOM_BITS; + flags |= (num_channels == 2 ? Mesh::ARRAY_CUSTOM_RG_FLOAT : Mesh::ARRAY_CUSTOM_RGBA_FLOAT) << custom_shift; + } + } + + if (fbx_mesh->vertex_color.exists) { + array[Mesh::ARRAY_COLOR] = _decode_vertex_attrib_color(fbx_mesh->vertex_color, indices); + has_vertex_color = true; + } + + int32_t num_skin_weights = 0; + + // Find the first imported skin deformer + for (ufbx_skin_deformer *fbx_skin : fbx_mesh->skin_deformers) { + if (!p_state->skin_indices.has(fbx_skin->typed_id)) { + continue; + } + GLTFSkinIndex skin_i = p_state->skin_indices[fbx_skin->typed_id]; + if (skin_i < 0) { + continue; + } + + // Tag all nodes to use the skin + for (const ufbx_node *node : fbx_mesh->instances) { + p_state->nodes[node->typed_id]->skin = skin_i; + } + + num_skin_weights = fbx_skin->max_weights_per_vertex > 4 ? 8 : 4; + + Vector<int32_t> bones; + Vector<float> weights; + + bones.resize(vertex_num * num_skin_weights); + weights.resize(vertex_num * num_skin_weights); + for (int32_t vertex_i = 0; vertex_i < vertex_num; vertex_i++) { + uint32_t fbx_vertex_index = fbx_mesh->vertex_indices[indices[vertex_i]]; + ufbx_skin_vertex skin_vertex = fbx_skin->vertices[fbx_vertex_index]; + float total_weight = 0.0f; + int32_t num_weights = MIN(int32_t(skin_vertex.num_weights), num_skin_weights); + for (int32_t i = 0; i < num_weights; i++) { + ufbx_skin_weight skin_weight = fbx_skin->weights[skin_vertex.weight_begin + i]; + int index = vertex_i * num_skin_weights + i; + float weight = float(skin_weight.weight); + bones.write[index] = int(skin_weight.cluster_index); + weights.write[index] = weight; + total_weight += weight; + } + if (total_weight > 0.0f) { + for (int32_t i = 0; i < num_weights; i++) { + int index = vertex_i * num_skin_weights + i; + weights.write[index] /= total_weight; + } + } + // Pad the rest with empty weights + for (int32_t i = num_weights; i < num_skin_weights; i++) { + int index = vertex_i * num_skin_weights + i; + bones.write[index] = 0; // TODO: What should this be padded with? + weights.write[index] = 0.0f; + } + } + array[Mesh::ARRAY_BONES] = bones; + array[Mesh::ARRAY_WEIGHTS] = weights; + + if (num_skin_weights == 8) { + flags |= Mesh::ARRAY_FLAG_USE_8_BONE_WEIGHTS; + } + + // Only use the first found skin + break; + } + + bool generate_tangents = (primitive == Mesh::PRIMITIVE_TRIANGLES && !array[Mesh::ARRAY_TANGENT] && array[Mesh::ARRAY_TEX_UV] && array[Mesh::ARRAY_NORMAL]); + + Ref<SurfaceTool> mesh_surface_tool; + mesh_surface_tool.instantiate(); + mesh_surface_tool->create_from_triangle_arrays(array); + mesh_surface_tool->set_skin_weight_count(num_skin_weights == 8 ? SurfaceTool::SKIN_8_WEIGHTS : SurfaceTool::SKIN_4_WEIGHTS); + mesh_surface_tool->index(); + if (generate_tangents) { + //must generate mikktspace tangents.. ergh.. + mesh_surface_tool->generate_tangents(); + } + array = mesh_surface_tool->commit_to_arrays(); + + Array morphs; + //blend shapes + if (use_blend_shapes) { + print_verbose("FBX: Mesh has targets"); + + import_mesh->set_blend_shape_mode(Mesh::BLEND_SHAPE_MODE_NORMALIZED); + + for (const ufbx_blend_deformer *fbx_deformer : fbx_mesh->blend_deformers) { + for (const ufbx_blend_channel *fbx_channel : fbx_deformer->channels) { + if (fbx_channel->keyframes.count == 0) { + continue; + } + + // Use the last shape keyframe by default + ufbx_blend_shape *fbx_shape = fbx_channel->keyframes[fbx_channel->keyframes.count - 1].shape; + + Array array_copy; + array_copy.resize(Mesh::ARRAY_MAX); + + for (int l = 0; l < Mesh::ARRAY_MAX; l++) { + array_copy[l] = array[l]; + } + + Vector<Vector3> varr; + Vector<Vector3> narr; + const Vector<Vector3> src_varr = array[Mesh::ARRAY_VERTEX]; + const Vector<Vector3> src_narr = array[Mesh::ARRAY_NORMAL]; + const int size = src_varr.size(); + ERR_FAIL_COND_V(size == 0, ERR_PARSE_ERROR); + { + varr.resize(size); + narr.resize(size); + + Vector3 *w_varr = varr.ptrw(); + Vector3 *w_narr = narr.ptrw(); + const Vector3 *r_varr = src_varr.ptr(); + const Vector3 *r_narr = src_narr.ptr(); + for (int l = 0; l < size; l++) { + uint32_t vertex_index = _decode_vertex_index(r_varr[l]); + uint32_t offset_index = ufbx_get_blend_shape_offset_index(fbx_shape, vertex_index); + Vector3 position = _as_vec3(fbx_mesh->vertices[vertex_index]); + Vector3 normal = r_narr[l]; + + if (offset_index != UFBX_NO_INDEX && offset_index < fbx_shape->position_offsets.count) { + Vector3 blend_shape_position_offset = _as_vec3(fbx_shape->position_offsets[offset_index]); + w_varr[l] = position + blend_shape_position_offset; + } else { + w_varr[l] = position; + } + + if (offset_index != UFBX_NO_INDEX && offset_index < fbx_shape->normal_offsets.count) { + w_narr[l] = (normal.normalized() + _as_vec3(fbx_shape->normal_offsets[offset_index])).normalized(); + } else { + w_narr[l] = normal; + } + } + } + array_copy[Mesh::ARRAY_VERTEX] = varr; + array_copy[Mesh::ARRAY_NORMAL] = narr; + + Ref<SurfaceTool> blend_surface_tool; + blend_surface_tool.instantiate(); + blend_surface_tool->create_from_triangle_arrays(array_copy); + blend_surface_tool->set_skin_weight_count(num_skin_weights == 8 ? SurfaceTool::SKIN_8_WEIGHTS : SurfaceTool::SKIN_4_WEIGHTS); + if (generate_tangents) { + //must generate mikktspace tangents.. ergh.. + blend_surface_tool->generate_tangents(); + } + array_copy = blend_surface_tool->commit_to_arrays(); + + // Enforce blend shape mask array format + for (int l = 0; l < Mesh::ARRAY_MAX; l++) { + if (!(Mesh::ARRAY_FORMAT_BLEND_SHAPE_MASK & (static_cast<int64_t>(1) << l))) { + array_copy[l] = Variant(); + } + } + + morphs.push_back(array_copy); + } + } + } + + // Decode the original vertex positions now that we're done processing blend shapes. + if (use_blend_shapes) { + Vector<Vector3> varr = array[Mesh::ARRAY_VERTEX]; + Vector3 *w_varr = varr.ptrw(); + const int size = varr.size(); + for (int i = 0; i < size; i++) { + uint32_t vertex_index = _decode_vertex_index(w_varr[i]); + w_varr[i] = _as_vec3(fbx_mesh->vertices[vertex_index]); + } + array[Mesh::ARRAY_VERTEX] = varr; + } + + Ref<Material> mat; + String mat_name; + if (!p_state->discard_meshes_and_materials) { + ufbx_material *fbx_material = nullptr; + if (fbx_mesh_part.index < fbx_mesh->materials.count) { + fbx_material = fbx_mesh->materials[fbx_mesh_part.index]; + } + if (fbx_material) { + const int material = int(fbx_material->typed_id); + ERR_FAIL_INDEX_V(material, p_state->materials.size(), ERR_FILE_CORRUPT); + Ref<Material> mat3d = p_state->materials[material]; + ERR_FAIL_NULL_V(mat3d, ERR_FILE_CORRUPT); + + Ref<BaseMaterial3D> base_material = mat3d; + if (has_vertex_color && base_material.is_valid()) { + base_material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + } + mat = mat3d; + + } else { + Ref<StandardMaterial3D> mat3d; + mat3d.instantiate(); + if (has_vertex_color) { + mat3d->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + } + mat = mat3d; + } + ERR_FAIL_NULL_V(mat, ERR_FILE_CORRUPT); + mat_name = mat->get_name(); + } + import_mesh->add_surface(primitive, array, morphs, + Dictionary(), mat, mat_name, flags); + } + } + + Ref<GLTFMesh> mesh; + mesh.instantiate(); + Dictionary additional_data; + additional_data["blend_channels"] = blend_channels; + mesh->set_additional_data("GODOT_mesh_blend_channels", additional_data); + mesh->set_blend_weights(blend_weights); + mesh->set_mesh(import_mesh); + mesh->set_name(import_mesh->get_name()); + mesh->set_original_name(original_name); + + p_state->meshes.push_back(mesh); + } + + print_verbose("FBX: Total meshes: " + itos(p_state->meshes.size())); + + return OK; +} + +Ref<Image> FBXDocument::_parse_image_bytes_into_image(Ref<FBXState> p_state, const Vector<uint8_t> &p_bytes, const String &p_filename, int p_index) { + Ref<Image> r_image; + r_image.instantiate(); + // Try to import first based on filename. + String filename_lower = p_filename.to_lower(); + if (filename_lower.ends_with(".png")) { + r_image->load_png_from_buffer(p_bytes); + } else if (filename_lower.ends_with(".jpg")) { + r_image->load_jpg_from_buffer(p_bytes); + } else if (filename_lower.ends_with(".tga")) { + r_image->load_tga_from_buffer(p_bytes); + } + // If we didn't pass the above tests, try loading as each option. + if (r_image->is_empty()) { // Try PNG first. + r_image->load_png_from_buffer(p_bytes); + } + if (r_image->is_empty()) { // And then JPEG. + r_image->load_jpg_from_buffer(p_bytes); + } + if (r_image->is_empty()) { // And then TGA. + r_image->load_jpg_from_buffer(p_bytes); + } + // If it still can't be loaded, give up and insert an empty image as placeholder. + if (r_image->is_empty()) { + ERR_PRINT(vformat("FBX: Couldn't load image index '%d'", p_index)); + } + return r_image; +} + +GLTFImageIndex FBXDocument::_parse_image_save_image(Ref<FBXState> p_state, const Vector<uint8_t> &p_bytes, const String &p_file_extension, int p_index, Ref<Image> p_image) { + FBXState::GLTFHandleBinary handling = FBXState::GLTFHandleBinary(p_state->handle_binary_image); + if (p_image->is_empty() || handling == FBXState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) { + if (p_index < 0) { + return -1; + } + p_state->images.push_back(Ref<Texture2D>()); + p_state->source_images.push_back(Ref<Image>()); + return p_state->images.size() - 1; + } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint() && handling == FBXState::GLTFHandleBinary::HANDLE_BINARY_EXTRACT_TEXTURES) { + if (p_state->base_path.is_empty()) { + if (p_index < 0) { + return -1; + } + p_state->images.push_back(Ref<Texture2D>()); + p_state->source_images.push_back(Ref<Image>()); + } else if (p_image->get_name().is_empty()) { + if (p_index < 0) { + return -1; + } + WARN_PRINT(vformat("FBX: Image index '%d' couldn't be named. Skipping it.", p_index)); + p_state->images.push_back(Ref<Texture2D>()); + p_state->source_images.push_back(Ref<Image>()); + } else { + bool must_import = true; + Vector<uint8_t> img_data = p_image->get_data(); + Dictionary generator_parameters; + String file_path = p_state->get_base_path().path_join(p_state->filename.get_basename() + "_" + p_image->get_name()); + file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; + if (FileAccess::exists(file_path + ".import")) { + Ref<ConfigFile> config; + config.instantiate(); + config->load(file_path + ".import"); + if (config->has_section_key("remap", "generator_parameters")) { + generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); + } + if (!generator_parameters.has("md5")) { + must_import = false; // Didn't come from a gltf document; don't overwrite. + } + } + if (must_import) { + String existing_md5 = generator_parameters["md5"]; + unsigned char md5_hash[16]; + CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); + String new_md5 = String::hex_encode_buffer(md5_hash, 16); + generator_parameters["md5"] = new_md5; + if (new_md5 == existing_md5) { + must_import = false; + } + } + if (must_import) { + Error err = OK; + if (p_file_extension.is_empty()) { + // If a file extension was not specified, save the image data to a PNG file. + err = p_image->save_png(file_path); + ERR_FAIL_COND_V(err != OK, -1); + } else { + // If a file extension was specified, save the original bytes to a file with that extension. + Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::WRITE, &err); + ERR_FAIL_COND_V(err != OK, -1); + file->store_buffer(p_bytes); + file->close(); + } + // ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed. + HashMap<StringName, Variant> custom_options; + custom_options[SNAME("mipmaps/generate")] = true; + // Will only use project settings defaults if custom_importer is empty. + EditorFileSystem::get_singleton()->update_file(file_path); + EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters); + } + Ref<Texture2D> saved_image = ResourceLoader::load(_get_texture_path(p_state->get_base_path(), file_path), "Texture2D"); + if (saved_image.is_valid()) { + p_state->images.push_back(saved_image); + p_state->source_images.push_back(saved_image->get_image()); + } else if (p_index < 0) { + return -1; + } else { + WARN_PRINT(vformat("FBX: Image index '%d' couldn't be loaded with the name: %s. Skipping it.", p_index, p_image->get_name())); + // Placeholder to keep count. + p_state->images.push_back(Ref<Texture2D>()); + p_state->source_images.push_back(Ref<Image>()); + } + } + return p_state->images.size() - 1; + } +#endif // TOOLS_ENABLED + if (handling == FBXState::HANDLE_BINARY_EMBED_AS_BASISU) { + Ref<PortableCompressedTexture2D> tex; + tex.instantiate(); + tex->set_name(p_image->get_name()); + tex->set_keep_compressed_buffer(true); + tex->create_from_image(p_image, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL); + p_state->images.push_back(tex); + p_state->source_images.push_back(p_image); + return p_state->images.size() - 1; + } + // This handles the case of HANDLE_BINARY_EMBED_AS_UNCOMPRESSED, and it also serves + // as a fallback for HANDLE_BINARY_EXTRACT_TEXTURES when this is not the editor. + Ref<ImageTexture> tex; + tex.instantiate(); + tex->set_name(p_image->get_name()); + tex->set_image(p_image); + p_state->images.push_back(tex); + p_state->source_images.push_back(p_image); + return p_state->images.size() - 1; +} + +Error FBXDocument::_parse_images(Ref<FBXState> p_state, const String &p_base_path) { + ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER); + + const ufbx_scene *fbx_scene = p_state->scene.get(); + for (int texture_i = 0; texture_i < static_cast<int>(fbx_scene->texture_files.count); texture_i++) { + const ufbx_texture_file &fbx_texture_file = fbx_scene->texture_files[texture_i]; + String path = _as_string(fbx_texture_file.filename); + path = ProjectSettings::get_singleton()->localize_path(path); + if (path.is_absolute_path() && !path.is_resource_file()) { + path = path.get_file(); + } + if (!p_base_path.is_empty()) { + path = p_base_path.path_join(path); + } + path = path.simplify_path(); + Vector<uint8_t> data; + if (fbx_texture_file.content.size > 0 && fbx_texture_file.content.size <= INT_MAX) { + data.resize(int(fbx_texture_file.content.size)); + memcpy(data.ptrw(), fbx_texture_file.content.data, fbx_texture_file.content.size); + } else { + String base_dir = p_state->get_base_path(); + Ref<Texture2D> texture = ResourceLoader::load(_get_texture_path(base_dir, path), "Texture2D"); + if (texture.is_valid()) { + p_state->images.push_back(texture); + p_state->source_images.push_back(texture->get_image()); + continue; + } + // Fallback to loading as byte array. + data = FileAccess::get_file_as_bytes(path); + if (data.size() == 0) { + WARN_PRINT(vformat("FBX: Image index '%d' couldn't be loaded from path: %s because there was no data to load. Skipping it.", texture_i, path)); + p_state->images.push_back(Ref<Texture2D>()); // Placeholder to keep count. + p_state->source_images.push_back(Ref<Image>()); + continue; + } + } + + // Parse the image data from bytes into an Image resource and save if needed. + String file_extension; + Ref<Image> img = _parse_image_bytes_into_image(p_state, data, path, texture_i); + img->set_name(itos(texture_i)); + _parse_image_save_image(p_state, data, file_extension, texture_i, img); + } + + // Create a texture for each file texture. + for (int texture_file_i = 0; texture_file_i < static_cast<int>(fbx_scene->texture_files.count); texture_file_i++) { + Ref<GLTFTexture> texture; + texture.instantiate(); + texture->set_src_image(GLTFImageIndex(texture_file_i)); + p_state->textures.push_back(texture); + } + + print_verbose("FBX: Total images: " + itos(p_state->images.size())); + + return OK; +} + +Ref<Texture2D> FBXDocument::_get_texture(Ref<FBXState> p_state, const GLTFTextureIndex p_texture, int p_texture_types) { + ERR_FAIL_INDEX_V(p_texture, p_state->textures.size(), Ref<Texture2D>()); + const GLTFImageIndex image = p_state->textures[p_texture]->get_src_image(); + ERR_FAIL_INDEX_V(image, p_state->images.size(), Ref<Texture2D>()); + if (FBXState::GLTFHandleBinary(p_state->handle_binary_image) == FBXState::HANDLE_BINARY_EMBED_AS_BASISU) { + ERR_FAIL_INDEX_V(image, p_state->source_images.size(), Ref<Texture2D>()); + Ref<PortableCompressedTexture2D> portable_texture; + portable_texture.instantiate(); + portable_texture->set_keep_compressed_buffer(true); + Ref<Image> new_img = p_state->source_images[image]->duplicate(); + ERR_FAIL_COND_V(new_img.is_null(), Ref<Texture2D>()); + new_img->generate_mipmaps(); + if (p_texture_types) { + portable_texture->create_from_image(new_img, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL, true); + } else { + portable_texture->create_from_image(new_img, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL, false); + } + p_state->images.write[image] = portable_texture; + p_state->source_images.write[image] = new_img; + } + return p_state->images[image]; +} + +Error FBXDocument::_parse_materials(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + for (GLTFMaterialIndex material_i = 0; material_i < static_cast<GLTFMaterialIndex>(fbx_scene->materials.count); material_i++) { + const ufbx_material *fbx_material = fbx_scene->materials[material_i]; + + Ref<StandardMaterial3D> material; + material.instantiate(); + if (fbx_material->name.length > 0) { + material->set_name(_as_string(fbx_material->name)); + } else { + material->set_name(vformat("material_%s", itos(material_i))); + } + material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + Dictionary material_extensions; + + if (fbx_material->pbr.base_color.has_value) { + Color albedo = _material_color(fbx_material->pbr.base_color, fbx_material->pbr.base_factor); + material->set_albedo(albedo); + } + + if (fbx_material->features.double_sided.enabled) { + material->set_cull_mode(BaseMaterial3D::CULL_DISABLED); + } + + const ufbx_texture *base_texture = _get_file_texture(fbx_material->pbr.base_color.texture); + if (base_texture) { + bool wrap = base_texture->wrap_u == UFBX_WRAP_REPEAT && base_texture->wrap_v == UFBX_WRAP_REPEAT; + material->set_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT, wrap); + + Ref<Texture2D> albedo_texture = _get_texture(p_state, GLTFTextureIndex(base_texture->file_index), TEXTURE_TYPE_GENERIC); + + // Search for transparency map. + Ref<Texture2D> transparency_texture; + const ufbx_texture *transparency_sources[] = { + fbx_material->pbr.opacity.texture, + fbx_material->fbx.transparency_color.texture, + }; + for (const ufbx_texture *transparency_source : transparency_sources) { + const ufbx_texture *fbx_transparency_texture = _get_file_texture(transparency_source); + if (fbx_transparency_texture) { + transparency_texture = _get_texture(p_state, GLTFTextureIndex(fbx_transparency_texture->file_index), TEXTURE_TYPE_GENERIC); + if (transparency_texture.is_valid()) { + break; + } + } + } + + // Multiply the albedo alpha with the transparency texture if necessary. + if (albedo_texture.is_valid() && transparency_texture.is_valid() && albedo_texture != transparency_texture) { + Pair<uint64_t, uint64_t> key = { albedo_texture->get_rid().get_id(), transparency_texture->get_rid().get_id() }; + GLTFTextureIndex *texture_index_ptr = p_state->albedo_transparency_textures.getptr(key); + if (texture_index_ptr != nullptr) { + if (*texture_index_ptr >= 0) { + albedo_texture = _get_texture(p_state, *texture_index_ptr, TEXTURE_TYPE_GENERIC); + } + } else { + Ref<Image> albedo_image = _get_decompressed_image(albedo_texture); + Ref<Image> transparency_image = _get_decompressed_image(transparency_texture); + + if (albedo_image.is_valid() && transparency_image.is_valid()) { + albedo_image->convert(Image::Format::FORMAT_RGBA8); + transparency_image->resize(albedo_texture->get_width(), albedo_texture->get_height(), Image::INTERPOLATE_LANCZOS); + for (int y = 0; y < albedo_image->get_height(); y++) { + for (int x = 0; x < albedo_image->get_width(); x++) { + Color albedo_pixel = albedo_image->get_pixel(x, y); + Color transparency_pixel = transparency_image->get_pixel(x, y); + albedo_pixel.a *= transparency_pixel.r; + albedo_image->set_pixel(x, y, albedo_pixel); + } + } + + albedo_image->clear_mipmaps(); + albedo_image->generate_mipmaps(); + + albedo_image->set_name(vformat("alpha_%d", p_state->albedo_transparency_textures.size())); + + GLTFImageIndex new_image = _parse_image_save_image(p_state, PackedByteArray(), "", -1, albedo_image); + if (new_image >= 0) { + Ref<GLTFTexture> new_texture; + new_texture.instantiate(); + new_texture->set_src_image(GLTFImageIndex(new_image)); + p_state->textures.push_back(new_texture); + + GLTFTextureIndex texture_index = p_state->textures.size() - 1; + p_state->albedo_transparency_textures[key] = texture_index; + + albedo_texture = _get_texture(p_state, texture_index, TEXTURE_TYPE_GENERIC); + } else { + WARN_PRINT(vformat("FBX: Could not save modified albedo texture from RID (%d, %d).", key.first, key.second)); + p_state->albedo_transparency_textures[key] = -1; + } + } + } + } + + Image::AlphaMode alpha_mode; + if (albedo_texture.is_valid()) { + Image::AlphaMode *alpha_mode_ptr = p_state->alpha_mode_cache.getptr(albedo_texture->get_rid().get_id()); + if (alpha_mode_ptr != nullptr) { + alpha_mode = *alpha_mode_ptr; + } else { + Ref<Image> albedo_image = _get_decompressed_image(albedo_texture); + alpha_mode = albedo_image->detect_alpha(); + p_state->alpha_mode_cache[albedo_texture->get_rid().get_id()] = alpha_mode; + } + + if (alpha_mode == Image::ALPHA_BLEND) { + material->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA_DEPTH_PRE_PASS); + } else if (alpha_mode == Image::ALPHA_BIT) { + material->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA_SCISSOR); + } + material->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, albedo_texture); + } + + // Combined textures and factors are very unreliable in FBX + material->set_albedo(Color(1, 1, 1)); + + // TODO: Does not support rotation, could be inverted? + material->set_uv1_offset(_as_vec3(base_texture->uv_transform.translation)); + Vector3 scale = _as_vec3(base_texture->uv_transform.scale); + material->set_uv1_scale(scale); + } + + if (fbx_material->features.pbr.enabled) { + if (fbx_material->pbr.metalness.has_value) { + material->set_metallic(float(fbx_material->pbr.metalness.value_real)); + } else { + material->set_metallic(1.0); + } + + if (fbx_material->pbr.roughness.has_value) { + material->set_roughness(float(fbx_material->pbr.roughness.value_real)); + } else { + material->set_roughness(1.0); + } + + const ufbx_texture *metalness_texture = _get_file_texture(fbx_material->pbr.metalness.texture); + if (metalness_texture) { + material->set_texture(BaseMaterial3D::TEXTURE_METALLIC, _get_texture(p_state, GLTFTextureIndex(metalness_texture->file_index), TEXTURE_TYPE_GENERIC)); + material->set_metallic_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_RED); + material->set_metallic(1.0); + } + + const ufbx_texture *roughness_texture = _get_file_texture(fbx_material->pbr.roughness.texture); + if (roughness_texture) { + material->set_texture(BaseMaterial3D::TEXTURE_ROUGHNESS, _get_texture(p_state, GLTFTextureIndex(roughness_texture->file_index), TEXTURE_TYPE_GENERIC)); + material->set_roughness_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_RED); + material->set_roughness(1.0); + } + } + + const ufbx_texture *normal_texture = _get_file_texture(fbx_material->pbr.normal_map.texture); + if (normal_texture) { + material->set_texture(BaseMaterial3D::TEXTURE_NORMAL, _get_texture(p_state, GLTFTextureIndex(normal_texture->file_index), TEXTURE_TYPE_NORMAL)); + material->set_feature(BaseMaterial3D::FEATURE_NORMAL_MAPPING, true); + if (fbx_material->pbr.normal_map.has_value) { + material->set_normal_scale(fbx_material->pbr.normal_map.value_real); + } + } + + const ufbx_texture *occlusion_texture = _get_file_texture(fbx_material->pbr.ambient_occlusion.texture); + if (occlusion_texture) { + material->set_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION, _get_texture(p_state, GLTFTextureIndex(occlusion_texture->file_index), TEXTURE_TYPE_GENERIC)); + material->set_ao_texture_channel(BaseMaterial3D::TEXTURE_CHANNEL_RED); + material->set_feature(BaseMaterial3D::FEATURE_AMBIENT_OCCLUSION, true); + } + + if (fbx_material->pbr.emission_color.has_value) { + material->set_feature(BaseMaterial3D::FEATURE_EMISSION, true); + material->set_emission(_material_color(fbx_material->pbr.emission_color)); + material->set_emission_energy_multiplier(float(fbx_material->pbr.emission_factor.value_real)); + } + + const ufbx_texture *emission_texture = _get_file_texture(fbx_material->pbr.ambient_occlusion.texture); + if (emission_texture) { + material->set_texture(BaseMaterial3D::TEXTURE_EMISSION, _get_texture(p_state, GLTFTextureIndex(emission_texture->file_index), TEXTURE_TYPE_GENERIC)); + material->set_feature(BaseMaterial3D::FEATURE_EMISSION, true); + material->set_emission(Color(0, 0, 0)); + } + + if (fbx_material->features.double_sided.enabled && fbx_material->features.double_sided.is_explicit) { + material->set_cull_mode(BaseMaterial3D::CULL_DISABLED); + } + p_state->materials.push_back(material); + } + + print_verbose("Total materials: " + itos(p_state->materials.size())); + + return OK; +} +Error FBXDocument::_parse_cameras(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + for (GLTFCameraIndex i = 0; i < static_cast<GLTFCameraIndex>(fbx_scene->cameras.count); i++) { + const ufbx_camera *fbx_camera = fbx_scene->cameras[i]; + + Ref<GLTFCamera> camera; + camera.instantiate(); + camera->set_name(_as_string(fbx_camera->name)); + if (fbx_camera->projection_mode == UFBX_PROJECTION_MODE_PERSPECTIVE) { + camera->set_perspective(true); + camera->set_fov(Math::deg_to_rad(real_t(fbx_camera->field_of_view_deg.y))); + } else { + camera->set_perspective(false); + camera->set_size_mag(real_t(fbx_camera->orthographic_size.y)); + } + if (fbx_camera->near_plane != 0.0f) { + camera->set_depth_near(fbx_camera->near_plane); + } + if (fbx_camera->far_plane != 0.0f) { + camera->set_depth_far(fbx_camera->far_plane); + } + p_state->cameras.push_back(camera); + } + + print_verbose("FBX: Total cameras: " + itos(p_state->cameras.size())); + + return OK; +} + +Error FBXDocument::_parse_animations(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + for (GLTFAnimationIndex animation_i = 0; animation_i < static_cast<GLTFAnimationIndex>(fbx_scene->anim_stacks.count); animation_i++) { + const ufbx_anim_stack *fbx_anim_stack = fbx_scene->anim_stacks[animation_i]; + + Ref<GLTFAnimation> animation; + animation.instantiate(); + + if (fbx_anim_stack->name.length > 0) { + const String anim_name = _as_string(fbx_anim_stack->name); + const String anim_name_lower = anim_name.to_lower(); + if (anim_name_lower.begins_with("loop") || anim_name_lower.ends_with("loop") || anim_name_lower.begins_with("cycle") || anim_name_lower.ends_with("cycle")) { + animation->set_loop(true); + } + animation->set_original_name(anim_name); + animation->set_name(_gen_unique_animation_name(p_state, anim_name)); + } + + Dictionary additional_data; + additional_data["time_begin"] = fbx_anim_stack->time_begin; + additional_data["time_end"] = fbx_anim_stack->time_end; + animation->set_additional_data("GODOT_animation_time_begin_time_end", additional_data); + ufbx_bake_opts opts = {}; + ufbx_error error; + ufbx_unique_ptr<ufbx_baked_anim> fbx_baked_anim{ ufbx_bake_anim(fbx_scene, fbx_anim_stack->anim, &opts, &error) }; + if (!fbx_baked_anim) { + char err_buf[512]; + ufbx_format_error(err_buf, sizeof(err_buf), &error); + ERR_FAIL_V_MSG(FAILED, err_buf); + } + + for (const ufbx_baked_node &fbx_baked_node : fbx_baked_anim->nodes) { + const GLTFNodeIndex node = fbx_baked_node.typed_id; + GLTFAnimation::Track &track = animation->get_tracks()[node]; + + for (const ufbx_baked_vec3 &key : fbx_baked_node.translation_keys) { + track.position_track.times.push_back(float(key.time)); + track.position_track.values.push_back(_as_vec3(key.value)); + } + + for (const ufbx_baked_quat &key : fbx_baked_node.rotation_keys) { + track.rotation_track.times.push_back(float(key.time)); + track.rotation_track.values.push_back(_as_quaternion(key.value)); + } + + for (const ufbx_baked_vec3 &key : fbx_baked_node.scale_keys) { + track.scale_track.times.push_back(float(key.time)); + track.scale_track.values.push_back(_as_vec3(key.value)); + } + } + + Dictionary blend_shape_animations; + + for (const ufbx_baked_element &fbx_baked_element : fbx_baked_anim->elements) { + const ufbx_element *fbx_element = fbx_scene->elements[fbx_baked_element.element_id]; + + for (const ufbx_baked_prop &fbx_baked_prop : fbx_baked_element.props) { + String prop_name = _as_string(fbx_baked_prop.name); + + if (fbx_element->type == UFBX_ELEMENT_BLEND_CHANNEL && prop_name == UFBX_DeformPercent) { + const ufbx_blend_channel *fbx_blend_channel = ufbx_as_blend_channel(fbx_element); + + int blend_i = fbx_blend_channel->typed_id; + Vector<real_t> track_times; + Vector<real_t> track_values; + + for (const ufbx_baked_vec3 &key : fbx_baked_prop.keys) { + track_times.push_back(float(key.time)); + track_values.push_back(real_t(key.value.x / 100.0)); + } + + Dictionary track; + track["times"] = track_times; + track["values"] = track_values; + blend_shape_animations[blend_i] = track; + } + } + } + + animation->set_additional_data("GODOT_blend_shape_animations", blend_shape_animations); + + p_state->animations.push_back(animation); + } + + print_verbose("FBX: Total animations '" + itos(p_state->animations.size()) + "'."); + + return OK; +} + +void FBXDocument::_assign_node_names(Ref<FBXState> p_state) { + for (int i = 0; i < p_state->nodes.size(); i++) { + Ref<GLTFNode> fbx_node = p_state->nodes[i]; + + // Any joints get unique names generated when the skeleton is made, unique to the skeleton + if (fbx_node->skeleton >= 0) { + continue; + } + + if (fbx_node->get_name().is_empty()) { + if (fbx_node->mesh >= 0) { + fbx_node->set_name(_gen_unique_name(p_state->unique_names, "Mesh")); + } else if (fbx_node->camera >= 0) { + fbx_node->set_name(_gen_unique_name(p_state->unique_names, "Camera3D")); + } else { + fbx_node->set_name(_gen_unique_name(p_state->unique_names, "Node")); + } + } + + fbx_node->set_name(_gen_unique_name(p_state->unique_names, fbx_node->get_name())); + } +} + +BoneAttachment3D *FBXDocument::_generate_bone_attachment(Ref<FBXState> p_state, Skeleton3D *p_skeleton, const GLTFNodeIndex p_node_index, const GLTFNodeIndex p_bone_index) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + Ref<GLTFNode> bone_node = p_state->nodes[p_bone_index]; + BoneAttachment3D *bone_attachment = memnew(BoneAttachment3D); + print_verbose("FBX: Creating bone attachment for: " + fbx_node->get_name()); + + ERR_FAIL_COND_V(!bone_node->joint, nullptr); + + bone_attachment->set_bone_name(bone_node->get_name()); + + return bone_attachment; +} + +ImporterMeshInstance3D *FBXDocument::_generate_mesh_instance(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + ERR_FAIL_INDEX_V(fbx_node->mesh, p_state->meshes.size(), nullptr); + + ImporterMeshInstance3D *mi = memnew(ImporterMeshInstance3D); + print_verbose("FBX: Creating mesh for: " + fbx_node->get_name()); + + p_state->scene_mesh_instances.insert(p_node_index, mi); + Ref<GLTFMesh> mesh = p_state->meshes.write[fbx_node->mesh]; + if (mesh.is_null()) { + return mi; + } + Ref<ImporterMesh> import_mesh = mesh->get_mesh(); + if (import_mesh.is_null()) { + return mi; + } + mi->set_mesh(import_mesh); + return mi; +} + +Camera3D *FBXDocument::_generate_camera(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + ERR_FAIL_INDEX_V(fbx_node->camera, p_state->cameras.size(), nullptr); + + print_verbose("FBX: Creating camera for: " + fbx_node->get_name()); + + Ref<GLTFCamera> c = p_state->cameras[fbx_node->camera]; + return c->to_node(); +} + +Light3D *FBXDocument::_generate_light(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + ERR_FAIL_INDEX_V(fbx_node->light, p_state->lights.size(), nullptr); + + print_verbose("FBX: Creating light for: " + fbx_node->get_name()); + + Ref<GLTFLight> l = p_state->lights[fbx_node->light]; + Light3D *light = nullptr; + + if (l->get_light_type() == "point") { + light = memnew(OmniLight3D); + } else if (l->get_light_type() == "directional") { + light = memnew(DirectionalLight3D); + } else if (l->get_light_type() == "spot") { + light = memnew(SpotLight3D); + } else { + ERR_FAIL_NULL_V(light, nullptr); + } + + if (light) { + light->set_name(l->get_name()); + light->set_color(l->get_color()); + light->set_param(Light3D::PARAM_ENERGY, l->get_intensity()); + Dictionary additional_data = l->get_additional_data("GODOT_fbx_light"); + if (additional_data.has("castShadows")) { + light->set_shadow(additional_data["castShadows"]); + } + if (additional_data.has("castLight")) { + light->set_visible(additional_data["castLight"]); + } + + Transform3D transform; + DirectionalLight3D *dir_light = Object::cast_to<DirectionalLight3D>(light); + SpotLight3D *spot_light = Object::cast_to<SpotLight3D>(light); + OmniLight3D *omni_light = Object::cast_to<OmniLight3D>(light); + if (dir_light) { + dir_light->set_transform(transform); + } else if (spot_light) { + spot_light->set_transform(transform); + spot_light->set_param(SpotLight3D::PARAM_SPOT_ANGLE, l->get_outer_cone_angle() / 2.0f); + } + if (omni_light || spot_light) { + light->set_param(OmniLight3D::PARAM_RANGE, 4096); + } + +// This is "correct", but FBX files may have unexpected decay modes. +// Also does not match with what FBX2glTF does, so it might be better to not do any of this.. +#if 0 + if (omni_light || spot_light) { + float attenuation = 1.0f; + if (additional_data.has("decay")) { + String decay_type = additional_data["decay"]; + if (decay_type == "none") { + attenuation = 0.001f; + } else if (decay_type == "linear") { + attenuation = 1.0f; + } else if (decay_type == "quadratic") { + attenuation = 2.0f; + } else if (decay_type == "cubic") { + attenuation = 3.0f; + } + } + light->set_param(Light3D::PARAM_ATTENUATION, attenuation); + } +#endif + + if (spot_light) { + // Line of best fit derived from guessing, see https://www.desmos.com/calculator/biiflubp8b + // The points in desmos are not exact, except for (1, infinity). + float angle_ratio = l->get_inner_cone_angle() / l->get_outer_cone_angle(); + float angle_attenuation = 0.2 / (1 - angle_ratio) - 0.1; + light->set_param(SpotLight3D::PARAM_SPOT_ATTENUATION, angle_attenuation); + } + } + + return light; +} + +Node3D *FBXDocument::_generate_spatial(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + Node3D *spatial = memnew(Node3D); + print_verbose("FBX: Converting spatial: " + fbx_node->get_name()); + + return spatial; +} + +void FBXDocument::_generate_scene_node(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + if (fbx_node->skeleton >= 0) { + _generate_skeleton_bone_node(p_state, p_node_index, p_scene_parent, p_scene_root); + return; + } + + Node3D *current_node = nullptr; + + // Is our parent a skeleton + Skeleton3D *active_skeleton = Object::cast_to<Skeleton3D>(p_scene_parent); + + const bool non_bone_parented_to_skeleton = active_skeleton; + + // skinned meshes must not be placed in a bone attachment. + if (non_bone_parented_to_skeleton && fbx_node->skin < 0) { + // Bone Attachment - Parent Case + BoneAttachment3D *bone_attachment = _generate_bone_attachment(p_state, active_skeleton, p_node_index, fbx_node->parent); + + p_scene_parent->add_child(bone_attachment, true); + bone_attachment->set_owner(p_scene_root); + + // There is no fbx_node that represent this, so just directly create a unique name + bone_attachment->set_name(fbx_node->get_name()); + + // We change the scene_parent to our bone attachment now. We do not set current_node because we want to make the node + // and attach it to the bone_attachment + p_scene_parent = bone_attachment; + } + if (!current_node) { + if (fbx_node->skin >= 0 && fbx_node->mesh >= 0 && !fbx_node->children.is_empty()) { + current_node = _generate_spatial(p_state, p_node_index); + Node3D *mesh_inst = _generate_mesh_instance(p_state, p_node_index); + mesh_inst->set_name(fbx_node->get_name()); + + current_node->add_child(mesh_inst, true); + } else if (fbx_node->mesh >= 0) { + current_node = _generate_mesh_instance(p_state, p_node_index); + } else if (fbx_node->camera >= 0) { + current_node = _generate_camera(p_state, p_node_index); + } else if (fbx_node->light >= 0) { + current_node = _generate_light(p_state, p_node_index); + } else { + current_node = _generate_spatial(p_state, p_node_index); + } + } + + ERR_FAIL_NULL(current_node); + + // Add the node we generated and set the owner to the scene root. + p_scene_parent->add_child(current_node, true); + if (current_node != p_scene_root) { + Array args; + args.append(p_scene_root); + current_node->propagate_call(StringName("set_owner"), args); + } + current_node->set_transform(fbx_node->transform); + current_node->set_name(fbx_node->get_name()); + + p_state->scene_nodes.insert(p_node_index, current_node); + for (int i = 0; i < fbx_node->children.size(); ++i) { + _generate_scene_node(p_state, fbx_node->children[i], current_node, p_scene_root); + } +} + +void FBXDocument::_generate_skeleton_bone_node(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root) { + Ref<GLTFNode> fbx_node = p_state->nodes[p_node_index]; + + Node3D *current_node = nullptr; + + Skeleton3D *skeleton = p_state->skeletons[fbx_node->skeleton]->godot_skeleton; + // In this case, this node is already a bone in skeleton. + const bool is_skinned_mesh = (fbx_node->skin >= 0 && fbx_node->mesh >= 0); + const bool requires_extra_node = (fbx_node->mesh >= 0 || fbx_node->camera >= 0 || fbx_node->light >= 0); + + Skeleton3D *active_skeleton = Object::cast_to<Skeleton3D>(p_scene_parent); + if (active_skeleton != skeleton) { + if (active_skeleton) { + // Should no longer be possible. + ERR_PRINT(vformat("FBX: Generating scene detected direct parented Skeletons at node %d", p_node_index)); + BoneAttachment3D *bone_attachment = _generate_bone_attachment(p_state, active_skeleton, p_node_index, fbx_node->parent); + p_scene_parent->add_child(bone_attachment, true); + bone_attachment->set_owner(p_scene_root); + // There is no fbx_node that represent this, so just directly create a unique name + bone_attachment->set_name(_gen_unique_name(p_state->unique_names, "BoneAttachment3D")); + // We change the scene_parent to our bone attachment now. We do not set current_node because we want to make the node + // and attach it to the bone_attachment + p_scene_parent = bone_attachment; + } + if (skeleton->get_parent() == nullptr) { + p_scene_parent->add_child(skeleton, true); + skeleton->set_owner(p_scene_root); + } + } + + active_skeleton = skeleton; + current_node = active_skeleton; + + if (requires_extra_node) { + current_node = nullptr; + // skinned meshes must not be placed in a bone attachment. + if (!is_skinned_mesh) { + // Bone Attachment - Same Node Case + BoneAttachment3D *bone_attachment = _generate_bone_attachment(p_state, active_skeleton, p_node_index, p_node_index); + + p_scene_parent->add_child(bone_attachment, true); + bone_attachment->set_owner(p_scene_root); + + // There is no fbx_node that represent this, so just directly create a unique name + bone_attachment->set_name(fbx_node->get_name()); + + // We change the scene_parent to our bone attachment now. We do not set current_node because we want to make the node + // and attach it to the bone_attachment + p_scene_parent = bone_attachment; + } + // TODO: 20240118 // fire + // // Check if any GLTFDocumentExtension classes want to generate a node for us. + // for (Ref<GLTFDocumentExtension> ext : document_extensions) { + // ERR_CONTINUE(ext.is_null()); + // current_node = ext->generate_scene_node(p_state, fbx_node, p_scene_parent); + // if (current_node) { + // break; + // } + // } + // If none of our GLTFDocumentExtension classes generated us a node, we generate one. + if (!current_node) { + if (fbx_node->mesh >= 0) { + current_node = _generate_mesh_instance(p_state, p_node_index); + } else if (fbx_node->camera >= 0) { + current_node = _generate_camera(p_state, p_node_index); + } else { + current_node = _generate_spatial(p_state, p_node_index); + } + } + // Add the node we generated and set the owner to the scene root. + p_scene_parent->add_child(current_node, true); + if (current_node != p_scene_root) { + Array args; + args.append(p_scene_root); + current_node->propagate_call(StringName("set_owner"), args); + } + // Do not set transform here. Transform is already applied to our bone. + current_node->set_name(fbx_node->get_name()); + } + + p_state->scene_nodes.insert(p_node_index, current_node); + + for (int i = 0; i < fbx_node->children.size(); ++i) { + _generate_scene_node(p_state, fbx_node->children[i], active_skeleton, p_scene_root); + } +} + +void FBXDocument::_import_animation(Ref<FBXState> p_state, AnimationPlayer *p_animation_player, const GLTFAnimationIndex p_index, const float p_bake_fps, const bool p_trimming, const bool p_remove_immutable_tracks) { + Ref<GLTFAnimation> anim = p_state->animations[p_index]; + + String anim_name = anim->get_name(); + if (anim_name.is_empty()) { + // No node represent these, and they are not in the hierarchy, so just make a unique name + anim_name = _gen_unique_name(p_state->unique_names, "Animation"); + } + + Ref<Animation> animation; + animation.instantiate(); + animation->set_name(anim_name); + + if (anim->get_loop()) { + animation->set_loop_mode(Animation::LOOP_LINEAR); + } + + Dictionary additional_animation_data = anim->get_additional_data("GODOT_animation_time_begin_time_end"); + + double anim_start_offset = p_trimming ? double(additional_animation_data["time_begin"]) : 0.0; + + for (const KeyValue<int, GLTFAnimation::Track> &track_i : anim->get_tracks()) { + const GLTFAnimation::Track &track = track_i.value; + //need to find the path: for skeletons, weight tracks will affect the mesh + NodePath node_path; + //for skeletons, transform tracks always affect bones + NodePath transform_node_path; + GLTFNodeIndex node_index = track_i.key; + Node *root = p_animation_player->get_parent(); + ERR_FAIL_NULL(root); + HashMap<GLTFNodeIndex, Node *>::Iterator node_element = p_state->scene_nodes.find(node_index); + ERR_CONTINUE_MSG(!node_element, vformat("Unable to find node %d for animation.", node_index)); + node_path = root->get_path_to(node_element->value); + + const Ref<GLTFNode> fbx_node = p_state->nodes[track_i.key]; + + if (fbx_node->skeleton >= 0) { + const Skeleton3D *sk = p_state->skeletons[fbx_node->skeleton]->godot_skeleton; + ERR_FAIL_NULL(sk); + + const String path = p_animation_player->get_parent()->get_path_to(sk); + const String bone = fbx_node->get_name(); + transform_node_path = path + ":" + bone; + } else { + transform_node_path = node_path; + } + + // Animated TRS properties will not affect a skinned mesh. + const bool transform_affects_skinned_mesh_instance = fbx_node->skeleton < 0 && fbx_node->skin >= 0; + if ((track.rotation_track.values.size() || track.position_track.values.size() || track.scale_track.values.size()) && !transform_affects_skinned_mesh_instance) { + // Make a transform track. + int base_idx = animation->get_track_count(); + int position_idx = -1; + int rotation_idx = -1; + int scale_idx = -1; + + if (track.position_track.values.size()) { + bool is_default = true; // Discard the track if all it contains is default values. + if (p_remove_immutable_tracks) { + Vector3 base_pos = p_state->nodes[track_i.key]->transform.origin; + for (int i = 0; i < track.position_track.times.size(); i++) { + Vector3 value = track.position_track.values[track.position_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i]; + if (!value.is_equal_approx(base_pos)) { + is_default = false; + break; + } + } + } + if (!p_remove_immutable_tracks || !is_default) { + position_idx = base_idx; + animation->add_track(Animation::TYPE_POSITION_3D); + animation->track_set_path(position_idx, transform_node_path); + animation->track_set_imported(position_idx, true); // Helps merging positions later. + base_idx++; + } + } + if (track.rotation_track.values.size()) { + bool is_default = true; // Discard the track if all the track contains is the default values. + if (p_remove_immutable_tracks) { + Quaternion base_rot = p_state->nodes[track_i.key]->transform.basis.get_rotation_quaternion(); + for (int i = 0; i < track.rotation_track.times.size(); i++) { + Quaternion value = track.rotation_track.values[track.rotation_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i].normalized(); + if (!value.is_equal_approx(base_rot)) { + is_default = false; + break; + } + } + } + if (!p_remove_immutable_tracks || !is_default) { + rotation_idx = base_idx; + animation->add_track(Animation::TYPE_ROTATION_3D); + animation->track_set_path(rotation_idx, transform_node_path); + animation->track_set_imported(rotation_idx, true); //helps merging later + base_idx++; + } + } + if (track.scale_track.values.size()) { + bool is_default = true; // Discard the track if all the track contains is the default values. + if (p_remove_immutable_tracks) { + Vector3 base_scale = p_state->nodes[track_i.key]->transform.basis.get_scale(); + for (int i = 0; i < track.scale_track.times.size(); i++) { + Vector3 value = track.scale_track.values[track.scale_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i]; + if (!value.is_equal_approx(base_scale)) { + is_default = false; + break; + } + } + } + if (!p_remove_immutable_tracks || !is_default) { + scale_idx = base_idx; + animation->add_track(Animation::TYPE_SCALE_3D); + animation->track_set_path(scale_idx, transform_node_path); + animation->track_set_imported(scale_idx, true); //helps merging later + base_idx++; + } + } + + if (position_idx != -1) { + animation->track_set_interpolation_type(position_idx, Animation::INTERPOLATION_LINEAR); + for (int j = 0; j < track.position_track.times.size(); j++) { + const float t = track.position_track.times[j] - anim_start_offset; + const Vector3 value = track.position_track.values[j]; + animation->position_track_insert_key(position_idx, t, value); + } + } + + if (rotation_idx != -1) { + animation->track_set_interpolation_type(rotation_idx, Animation::INTERPOLATION_LINEAR); + for (int j = 0; j < track.rotation_track.times.size(); j++) { + const float t = track.rotation_track.times[j] - anim_start_offset; + const Quaternion value = track.rotation_track.values[j]; + animation->rotation_track_insert_key(rotation_idx, t, value); + } + } + + if (scale_idx != -1) { + animation->track_set_interpolation_type(scale_idx, Animation::INTERPOLATION_LINEAR); + for (int j = 0; j < track.scale_track.times.size(); j++) { + const float t = track.scale_track.times[j] - anim_start_offset; + const Vector3 value = track.scale_track.values[j]; + animation->scale_track_insert_key(scale_idx, t, value); + } + } + } + } + + Dictionary blend_shape_animations = anim->get_additional_data("GODOT_blend_shape_animations"); + + for (GLTFNodeIndex node_index = 0; node_index < p_state->nodes.size(); node_index++) { + Ref<GLTFNode> node = p_state->nodes[node_index]; + if (node->mesh < 0) { + continue; + } + + // For meshes, especially skinned meshes, there are cases where it will be added as a child. + NodePath mesh_instance_node_path; + + Node *root = p_animation_player->get_parent(); + ERR_FAIL_NULL(root); + HashMap<GLTFNodeIndex, Node *>::Iterator node_element = p_state->scene_nodes.find(node_index); + ERR_CONTINUE_MSG(!node_element, vformat("Unable to find node %d for animation.", node_index)); + NodePath node_path = root->get_path_to(node_element->value); + HashMap<GLTFNodeIndex, ImporterMeshInstance3D *>::Iterator mesh_instance_element = p_state->scene_mesh_instances.find(node_index); + if (mesh_instance_element) { + mesh_instance_node_path = root->get_path_to(mesh_instance_element->value); + } else { + mesh_instance_node_path = node_path; + } + + Ref<GLTFMesh> mesh = p_state->meshes[node->mesh]; + ERR_CONTINUE(mesh.is_null()); + ERR_CONTINUE(mesh->get_mesh().is_null()); + ERR_CONTINUE(mesh->get_mesh()->get_mesh().is_null()); + + Dictionary mesh_additional_data = mesh->get_additional_data("GODOT_mesh_blend_channels"); + Vector<int> blend_channels = mesh_additional_data["blend_channels"]; + + for (int i = 0; i < blend_channels.size(); i++) { + int blend_i = blend_channels[i]; + if (!blend_shape_animations.has(blend_i)) { + continue; + } + Dictionary blend_track = blend_shape_animations[blend_i]; + + GLTFAnimation::Channel<real_t> weights; + weights.interpolation = GLTFAnimation::INTERP_LINEAR; + weights.times = blend_track["times"]; + weights.values = blend_track["values"]; + + const String blend_path = String(mesh_instance_node_path) + ":" + String(mesh->get_mesh()->get_blend_shape_name(i)); + const int track_idx = animation->get_track_count(); + animation->add_track(Animation::TYPE_BLEND_SHAPE); + animation->track_set_path(track_idx, blend_path); + animation->track_set_imported(track_idx, true); // Helps merging later. + + animation->track_set_interpolation_type(track_idx, Animation::INTERPOLATION_LINEAR); + for (int j = 0; j < weights.times.size(); j++) { + const double t = weights.times[j] - anim_start_offset; + const real_t attribs = weights.values[j]; + animation->blend_shape_track_insert_key(track_idx, t, attribs); + } + } + } + double time_begin = additional_animation_data["time_begin"]; + double time_end = additional_animation_data["time_end"]; + double length = p_trimming ? time_end - time_begin : time_end; + animation->set_length(length); + + Ref<AnimationLibrary> library; + if (!p_animation_player->has_animation_library("")) { + library.instantiate(); + p_animation_player->add_animation_library("", library); + } else { + library = p_animation_player->get_animation_library(""); + } + library->add_animation(anim_name, animation); +} + +void FBXDocument::_process_mesh_instances(Ref<FBXState> p_state, Node *p_scene_root) { + for (GLTFNodeIndex node_i = 0; node_i < p_state->nodes.size(); ++node_i) { + Ref<GLTFNode> node = p_state->nodes[node_i]; + + if (node.is_null() || !(node->skin >= 0 && node->mesh >= 0)) { + continue; + } + + const GLTFSkinIndex skin_i = node->skin; + + ImporterMeshInstance3D *mi = nullptr; + HashMap<GLTFNodeIndex, ImporterMeshInstance3D *>::Iterator mi_element = p_state->scene_mesh_instances.find(node_i); + if (!mi_element) { + HashMap<GLTFNodeIndex, Node *>::Iterator si_element = p_state->scene_nodes.find(node_i); + ERR_CONTINUE_MSG(!si_element, vformat("Unable to find node %d", node_i)); + mi = Object::cast_to<ImporterMeshInstance3D>(si_element->value); + ERR_CONTINUE_MSG(mi == nullptr, vformat("Unable to cast node %d of type %s to ImporterMeshInstance3D", node_i, si_element->value->get_class_name())); + } else { + mi = mi_element->value; + } + + bool is_skin_valid = node->skin >= 0; + bool is_skin_accessible = is_skin_valid && node->skin < p_state->skins.size(); + bool is_valid = is_skin_accessible && p_state->skins.write[node->skin]->skeleton >= 0; + + if (!is_valid) { + continue; + } + + const GLTFSkeletonIndex skel_i = p_state->skins.write[node->skin]->skeleton; + Ref<GLTFSkeleton> fbx_skeleton = p_state->skeletons.write[skel_i]; + Skeleton3D *skeleton = fbx_skeleton->godot_skeleton; + ERR_CONTINUE_MSG(skeleton == nullptr, vformat("Unable to find Skeleton for node %d skin %d", node_i, skin_i)); + + mi->get_parent()->remove_child(mi); + skeleton->add_child(mi, true); + mi->set_owner(skeleton->get_owner()); + + mi->set_skin(p_state->skins.write[skin_i]->godot_skin); + mi->set_skeleton_path(mi->get_path_to(skeleton)); + mi->set_transform(Transform3D()); + } +} + +Error FBXDocument::_parse(Ref<FBXState> p_state, String p_path, Ref<FileAccess> p_file) { + p_state->scene.reset(); + + Error err = ERR_INVALID_DATA; + if (p_file.is_null()) { + return FAILED; + } + + ufbx_load_opts opts = {}; + opts.target_axes = ufbx_axes_right_handed_y_up; + opts.target_unit_meters = 1.0f; + opts.space_conversion = UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY; + if (!p_state->get_allow_geometry_helper_nodes()) { + opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY_NO_FALLBACK; + opts.inherit_mode_handling = UFBX_INHERIT_MODE_HANDLING_IGNORE; + } else { + opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_HELPER_NODES; + opts.inherit_mode_handling = UFBX_INHERIT_MODE_HANDLING_COMPENSATE; + } + opts.pivot_handling = UFBX_PIVOT_HANDLING_ADJUST_TO_PIVOT; + opts.geometry_transform_helper_name.data = "GeometryTransformHelper"; + opts.geometry_transform_helper_name.length = SIZE_MAX; + opts.scale_helper_name.data = "ScaleHelper"; + opts.scale_helper_name.length = SIZE_MAX; + opts.node_depth_limit = 512; + opts.target_camera_axes = ufbx_axes_right_handed_y_up; + opts.target_light_axes = ufbx_axes_right_handed_y_up; + opts.clean_skin_weights = true; + if (p_state->discard_meshes_and_materials) { + opts.ignore_geometry = true; + opts.ignore_embedded = true; + } + opts.generate_missing_normals = true; + + ThreadPoolFBX thread_pool; + thread_pool.pool = WorkerThreadPool::get_singleton(); + + opts.thread_opts.pool.init_fn = &_thread_pool_init_fn; + opts.thread_opts.pool.run_fn = &_thread_pool_run_fn; + opts.thread_opts.pool.wait_fn = &_thread_pool_wait_fn; + opts.thread_opts.pool.user = &thread_pool; + opts.thread_opts.memory_limit = 64 * 1024 * 1024; + + ufbx_error error; + ufbx_stream file_stream = {}; + file_stream.read_fn = &_file_access_read_fn; + file_stream.skip_fn = &_file_access_skip_fn; + file_stream.user = p_file.ptr(); + p_state->scene.reset(ufbx_load_stream(&file_stream, &opts, &error)); + + if (!p_state->scene.get()) { + char err_buf[512]; + ufbx_format_error(err_buf, sizeof(err_buf), &error); + ERR_FAIL_V_MSG(ERR_PARSE_ERROR, err_buf); + } + + err = _parse_fbx_state(p_state, p_path); + ERR_FAIL_COND_V(err != OK, err); + + return OK; +} + +void FBXDocument::_bind_methods() { +} + +Node *FBXDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, bool p_trimming, bool p_remove_immutable_tracks) { + Ref<FBXState> state = p_state; + ERR_FAIL_COND_V(state.is_null(), nullptr); + ERR_FAIL_NULL_V(state, nullptr); + ERR_FAIL_INDEX_V(0, state->root_nodes.size(), nullptr); + GLTFNodeIndex fbx_root = state->root_nodes.write[0]; + Node *fbx_root_node = state->get_scene_node(fbx_root); + Node *root = fbx_root_node; + if (fbx_root_node && fbx_root_node->get_parent()) { + root = fbx_root_node->get_parent(); + } + ERR_FAIL_NULL_V(root, nullptr); + _process_mesh_instances(state, root); + if (state->get_create_animations() && state->animations.size()) { + AnimationPlayer *ap = memnew(AnimationPlayer); + root->add_child(ap, true); + ap->set_owner(root); + for (int i = 0; i < state->animations.size(); i++) { + _import_animation(state, ap, i, p_bake_fps, p_trimming, p_remove_immutable_tracks); + } + } + ERR_FAIL_NULL_V(root, nullptr); + return root; +} + +Error FBXDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags) { + Ref<FBXState> state = p_state; + ERR_FAIL_COND_V(state.is_null(), ERR_INVALID_PARAMETER); + ERR_FAIL_NULL_V(p_bytes.ptr(), ERR_INVALID_DATA); + Error err = FAILED; + state->use_named_skin_binds = p_flags & FBX_IMPORT_USE_NAMED_SKIN_BINDS; + state->discard_meshes_and_materials = p_flags & FBX_IMPORT_DISCARD_MESHES_AND_MATERIALS; + + Ref<FileAccessMemory> file_access; + file_access.instantiate(); + file_access->open_custom(p_bytes.ptr(), p_bytes.size()); + state->base_path = p_base_path.get_base_dir(); + err = _parse(state, state->base_path, file_access); + ERR_FAIL_COND_V(err != OK, err); + // TODO: 202040118 // fire + // for (Ref<GLTFDocumentExtension> ext : get_all_gltf_document_extensions()) { + // ERR_CONTINUE(ext.is_null()); + // err = ext->import_post_parse(state); + // ERR_FAIL_COND_V(err != OK, err); + // } + return OK; +} + +Error FBXDocument::_parse_fbx_state(Ref<FBXState> p_state, const String &p_search_path) { + Error err; + + // Abort parsing if the scene is not loaded. + ERR_FAIL_NULL_V(p_state->scene.get(), ERR_PARSE_ERROR); + + /* PARSE SCENE */ + err = _parse_scenes(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE NODES */ + err = _parse_nodes(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + if (!p_state->discard_meshes_and_materials) { + /* PARSE IMAGES */ + err = _parse_images(p_state, p_search_path); + + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE MATERIALS */ + err = _parse_materials(p_state); + + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + } + + /* PARSE SKINS */ + err = _parse_skins(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* DETERMINE SKELETONS */ + err = SkinTool::_determine_skeletons(p_state->skins, p_state->nodes, p_state->skeletons, p_state->get_import_as_skeleton_bones() ? p_state->root_nodes : Vector<GLTFNodeIndex>()); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* CREATE SKELETONS */ + err = SkinTool::_create_skeletons(p_state->unique_names, p_state->skins, p_state->nodes, p_state->skeleton3d_to_fbx_skeleton, p_state->skeletons, p_state->scene_nodes); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* CREATE SKINS */ + err = SkinTool::_create_skins(p_state->skins, p_state->nodes, p_state->use_named_skin_binds, p_state->unique_names); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE MESHES (we have enough info now) */ + err = _parse_meshes(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE LIGHTS */ + err = _parse_lights(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE CAMERAS */ + err = _parse_cameras(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* PARSE ANIMATIONS */ + err = _parse_animations(p_state); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); + + /* ASSIGN SCENE NAMES */ + _assign_node_names(p_state); + + Node3D *root = memnew(Node3D); + for (int32_t root_i = 0; root_i < p_state->root_nodes.size(); root_i++) { + _generate_scene_node(p_state, p_state->root_nodes[root_i], root, root); + } + + return OK; +} + +Error FBXDocument::append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags, String p_base_path) { + Ref<FBXState> state = p_state; + ERR_FAIL_COND_V(state.is_null(), ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_path.is_empty(), ERR_FILE_NOT_FOUND); + if (p_state == Ref<FBXState>()) { + p_state.instantiate(); + } + state->filename = p_path.get_file().get_basename(); + state->use_named_skin_binds = p_flags & FBX_IMPORT_USE_NAMED_SKIN_BINDS; + state->discard_meshes_and_materials = p_flags & FBX_IMPORT_DISCARD_MESHES_AND_MATERIALS; + Error err; + Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ, &err); + ERR_FAIL_COND_V(err != OK, ERR_FILE_CANT_OPEN); + ERR_FAIL_NULL_V(file, ERR_FILE_CANT_OPEN); + String base_path = p_base_path; + if (base_path.is_empty()) { + base_path = p_path.get_base_dir(); + } + state->base_path = base_path; + err = _parse(p_state, base_path, file); + ERR_FAIL_COND_V(err != OK, err); + // TODO: 20240118 // fire + // for (Ref<GLTFDocumentExtension> ext : document_extensions) { + // ERR_CONTINUE(ext.is_null()); + // err = ext->import_post_parse(p_state); + // ERR_FAIL_COND_V(err != OK, err); + // } + return OK; +} + +void FBXDocument::_process_uv_set(PackedVector2Array &uv_array) { + int uv_size = uv_array.size(); + for (int uv_i = 0; uv_i < uv_size; uv_i++) { + Vector2 &uv = uv_array.write[uv_i]; + uv.y = 1.0 - uv.y; + } +} + +void FBXDocument::_zero_unused_elements(Vector<float> &cur_custom, int start, int end, int num_channels) { + for (int32_t uv_i = start; uv_i < end; uv_i++) { + int index = uv_i * num_channels; + for (int channel = 0; channel < num_channels; channel++) { + cur_custom.write[index + channel] = 0; + } + } +} + +Error FBXDocument::_parse_lights(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + for (size_t i = 0; i < fbx_scene->lights.count; i++) { + const ufbx_light *fbx_light = fbx_scene->lights.data[i]; + Ref<GLTFLight> light; + light.instantiate(); + light->set_name(_as_string(fbx_light->name)); + light->set_color(Color(fbx_light->color.x, fbx_light->color.y, fbx_light->color.z)); + light->set_intensity(fbx_light->intensity); + switch (fbx_light->type) { + case UFBX_LIGHT_POINT: + light->set_light_type("point"); + break; + case UFBX_LIGHT_DIRECTIONAL: + light->set_light_type("directional"); + break; + case UFBX_LIGHT_SPOT: + light->set_light_type("spot"); + break; + case UFBX_LIGHT_AREA: + light->set_light_type("area"); + break; + case UFBX_LIGHT_VOLUME: + light->set_light_type("volume"); + break; + default: + light->set_light_type("unknown"); + break; + } + + Dictionary additional_data; + additional_data["shadow"] = fbx_light->cast_shadows; + if (fbx_light->decay == UFBX_LIGHT_DECAY_NONE) { + additional_data["decay"] = "none"; + + } else if (fbx_light->decay == UFBX_LIGHT_DECAY_LINEAR) { + additional_data["decay"] = "linear"; + + } else if (fbx_light->decay == UFBX_LIGHT_DECAY_QUADRATIC) { + additional_data["decay"] = "quadratic"; + + } else if (fbx_light->decay == UFBX_LIGHT_DECAY_CUBIC) { + additional_data["decay"] = "cubic"; + } + + if (fbx_light->area_shape == UFBX_LIGHT_AREA_SHAPE_RECTANGLE) { + additional_data["areaShape"] = "rectangle"; + } else if (fbx_light->area_shape == UFBX_LIGHT_AREA_SHAPE_SPHERE) { + additional_data["areaShape"] = "sphere"; + } + + light->set_inner_cone_angle(fbx_light->inner_angle); + light->set_outer_cone_angle(fbx_light->outer_angle); + + additional_data["castLight"] = fbx_light->cast_light; + additional_data["castShadows"] = fbx_light->cast_shadows; + light->set_additional_data("GODOT_fbx_light", additional_data); + p_state->lights.push_back(light); + } + print_verbose("FBX: Total lights: " + itos(p_state->lights.size())); + return OK; +} + +String FBXDocument::_get_texture_path(const String &p_base_dir, const String &p_source_file_path) const { + const String tex_file_name = p_source_file_path.get_file(); + const Vector<String> subdirs = { + "", "textures/", "Textures/", "images/", + "Images/", "materials/", "Materials/", + "maps/", "Maps/", "tex/", "Tex/" + }; + String base_dir = p_base_dir; + const String source_file_name = tex_file_name; + while (!base_dir.is_empty()) { + String old_base_dir = base_dir; + for (int i = 0; i < subdirs.size(); ++i) { + String full_path = base_dir.path_join(subdirs[i] + source_file_name); + if (FileAccess::exists(full_path)) { + return full_path.strip_edges(); + } + } + base_dir = base_dir.get_base_dir(); + if (base_dir == old_base_dir) { + break; + } + } + return String(); +} + +Error FBXDocument::_parse_skins(Ref<FBXState> p_state) { + const ufbx_scene *fbx_scene = p_state->scene.get(); + HashMap<GLTFNodeIndex, bool> joint_mapping; + + for (const ufbx_skin_deformer *fbx_skin : fbx_scene->skin_deformers) { + if (fbx_skin->clusters.count == 0) { + p_state->skin_indices.push_back(-1); + continue; + } + + Ref<GLTFSkin> skin; + skin.instantiate(); + + skin->inverse_binds.resize(fbx_skin->clusters.count); + for (int skin_i = 0; skin_i < static_cast<int>(fbx_skin->clusters.count); skin_i++) { + const ufbx_skin_cluster *fbx_cluster = fbx_skin->clusters[skin_i]; + skin->inverse_binds.write[skin_i] = FBXDocument::_as_xform(fbx_cluster->geometry_to_bone); + const GLTFNodeIndex node = fbx_cluster->bone_node->typed_id; + + skin->joints.push_back(node); + skin->joints_original.push_back(node); + p_state->nodes.write[node]->joint = true; + } + + if (fbx_skin->name.length > 0) { + skin->set_name(FBXDocument::_as_string(fbx_skin->name)); + } else { + skin->set_name(vformat("skin_%s", itos(fbx_skin->typed_id))); + } + p_state->skin_indices.push_back(p_state->skins.size()); + p_state->skins.push_back(skin); + } + + for (const ufbx_bone *fbx_bone : fbx_scene->bones) { + for (const ufbx_node *fbx_node : fbx_bone->instances) { + const GLTFNodeIndex node = fbx_node->typed_id; + if (!p_state->nodes.write[node]->joint) { + p_state->nodes.write[node]->joint = true; + + if (!(fbx_node->parent && fbx_node->parent->attrib_type == UFBX_ELEMENT_BONE)) { + Ref<GLTFSkin> skin; + skin.instantiate(); + skin->joints.push_back(node); + skin->joints_original.push_back(node); + skin->set_name(vformat("skin_%s", itos(p_state->skins.size()))); + p_state->skin_indices.push_back(p_state->skins.size()); + p_state->skins.push_back(skin); + } + } + } + } + Error err = SkinTool::_asset_parse_skins( + p_state->skin_indices.duplicate(), + p_state->skins.duplicate(), + p_state->nodes.duplicate(), + p_state->skin_indices, + p_state->skins, + joint_mapping); + if (err != OK) { + return err; + } + for (int i = 0; i < p_state->skins.size(); ++i) { + Ref<GLTFSkin> skin = p_state->skins.write[i]; + ERR_FAIL_COND_V(skin.is_null(), ERR_PARSE_ERROR); + // Expand and verify the skin + ERR_FAIL_COND_V(SkinTool::_expand_skin(p_state->nodes, skin), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(SkinTool::_verify_skin(p_state->nodes, skin), ERR_PARSE_ERROR); + } + + print_verbose("FBX: Total skins: " + itos(p_state->skins.size())); + + for (HashMap<GLTFNodeIndex, bool>::Iterator it = joint_mapping.begin(); it != joint_mapping.end(); ++it) { + GLTFNodeIndex node_index = it->key; + bool is_joint = it->value; + if (is_joint) { + if (p_state->nodes.size() > node_index) { + p_state->nodes.write[node_index]->joint = true; + } + } + } + + return OK; +} + +PackedByteArray FBXDocument::generate_buffer(Ref<GLTFState> p_state) { + return PackedByteArray(); +} + +Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) { + return ERR_UNAVAILABLE; +} + +Error FBXDocument::append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags) { + return ERR_UNAVAILABLE; +} + +Vector3 FBXDocument::_as_vec3(const ufbx_vec3 &p_vector) { + return Vector3(real_t(p_vector.x), real_t(p_vector.y), real_t(p_vector.z)); +} + +String FBXDocument::_as_string(const ufbx_string &p_string) { + return String::utf8(p_string.data, (int)p_string.length); +} + +Transform3D FBXDocument::_as_xform(const ufbx_matrix &p_mat) { + Transform3D xform; + xform.basis.set_column(Vector3::AXIS_X, _as_vec3(p_mat.cols[0])); + xform.basis.set_column(Vector3::AXIS_Y, _as_vec3(p_mat.cols[1])); + xform.basis.set_column(Vector3::AXIS_Z, _as_vec3(p_mat.cols[2])); + xform.set_origin(_as_vec3(p_mat.cols[3])); + return xform; +} diff --git a/modules/fbx/fbx_document.h b/modules/fbx/fbx_document.h new file mode 100644 index 0000000000..ba48eb11ae --- /dev/null +++ b/modules/fbx/fbx_document.h @@ -0,0 +1,107 @@ +/**************************************************************************/ +/* fbx_document.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef FBX_DOCUMENT_H +#define FBX_DOCUMENT_H + +#include "fbx_state.h" + +#include "modules/gltf/gltf_defines.h" +#include "modules/gltf/gltf_document.h" + +#include <ufbx.h> + +class FBXDocument : public GLTFDocument { + GDCLASS(FBXDocument, GLTFDocument); + +public: + enum { + TEXTURE_TYPE_GENERIC = 0, + TEXTURE_TYPE_NORMAL = 1, + }; + + static Transform3D _as_xform(const ufbx_matrix &p_mat); + static String _as_string(const ufbx_string &p_string); + static Vector3 _as_vec3(const ufbx_vec3 &p_vector); + static String _gen_unique_name(HashSet<String> &unique_names, const String &p_name); + +public: + Error append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags = 0, String p_base_path = String()); + Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0); + Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0); + +public: + Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true); + PackedByteArray generate_buffer(Ref<GLTFState> p_state); + Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path); + +protected: + static void _bind_methods(); + +private: + String _get_texture_path(const String &p_base_directory, const String &p_source_file_path) const; + void _process_uv_set(PackedVector2Array &uv_array); + void _zero_unused_elements(Vector<float> &cur_custom, int start, int end, int num_channels); + Error _parse_scenes(Ref<FBXState> p_state); + Error _parse_nodes(Ref<FBXState> p_state); + String _sanitize_animation_name(const String &p_name); + String _gen_unique_animation_name(Ref<FBXState> p_state, const String &p_name); + Ref<Texture2D> _get_texture(Ref<FBXState> p_state, + const GLTFTextureIndex p_texture, int p_texture_type); + Error _parse_meshes(Ref<FBXState> p_state); + Ref<Image> _parse_image_bytes_into_image(Ref<FBXState> p_state, const Vector<uint8_t> &p_bytes, const String &p_filename, int p_index); + GLTFImageIndex _parse_image_save_image(Ref<FBXState> p_state, const Vector<uint8_t> &p_bytes, const String &p_file_extension, int p_index, Ref<Image> p_image); + Error _parse_images(Ref<FBXState> p_state, const String &p_base_path); + Error _parse_materials(Ref<FBXState> p_state); + Error _parse_skins(Ref<FBXState> p_state); + Error _parse_animations(Ref<FBXState> p_state); + BoneAttachment3D *_generate_bone_attachment(Ref<FBXState> p_state, + Skeleton3D *p_skeleton, + const GLTFNodeIndex p_node_index, + const GLTFNodeIndex p_bone_index); + ImporterMeshInstance3D *_generate_mesh_instance(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index); + Camera3D *_generate_camera(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index); + Light3D *_generate_light(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index); + Node3D *_generate_spatial(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index); + void _assign_node_names(Ref<FBXState> p_state); + Error _parse_cameras(Ref<FBXState> p_state); + Error _parse_lights(Ref<FBXState> p_state); + +public: + Error _parse_fbx_state(Ref<FBXState> p_state, const String &p_search_path); + void _process_mesh_instances(Ref<FBXState> p_state, Node *p_scene_root); + void _generate_scene_node(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); + void _generate_skeleton_bone_node(Ref<FBXState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); + void _import_animation(Ref<FBXState> p_state, AnimationPlayer *p_animation_player, + const GLTFAnimationIndex p_index, const float p_bake_fps, const bool p_trimming, const bool p_remove_immutable_tracks); + Error _parse(Ref<FBXState> p_state, String p_path, Ref<FileAccess> p_file); +}; + +#endif // FBX_DOCUMENT_H diff --git a/modules/fbx/fbx_state.cpp b/modules/fbx/fbx_state.cpp new file mode 100644 index 0000000000..0b42a86533 --- /dev/null +++ b/modules/fbx/fbx_state.cpp @@ -0,0 +1,46 @@ +/**************************************************************************/ +/* fbx_state.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "fbx_state.h" + +void FBXState::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_allow_geometry_helper_nodes"), &FBXState::get_allow_geometry_helper_nodes); + ClassDB::bind_method(D_METHOD("set_allow_geometry_helper_nodes", "allow"), &FBXState::set_allow_geometry_helper_nodes); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_geometry_helper_nodes"), "set_allow_geometry_helper_nodes", "get_allow_geometry_helper_nodes"); +} + +bool FBXState::get_allow_geometry_helper_nodes() { + return allow_geometry_helper_nodes; +} + +void FBXState::set_allow_geometry_helper_nodes(bool p_allow_geometry_helper_nodes) { + allow_geometry_helper_nodes = p_allow_geometry_helper_nodes; +} diff --git a/modules/fbx/fbx_state.h b/modules/fbx/fbx_state.h new file mode 100644 index 0000000000..d10550c228 --- /dev/null +++ b/modules/fbx/fbx_state.h @@ -0,0 +1,68 @@ +/**************************************************************************/ +/* fbx_state.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef FBX_STATE_H +#define FBX_STATE_H + +#include "modules/gltf/gltf_defines.h" +#include "modules/gltf/gltf_state.h" +#include "modules/gltf/structures/gltf_skeleton.h" +#include "modules/gltf/structures/gltf_skin.h" +#include "modules/gltf/structures/gltf_texture.h" + +#include <ufbx.h> + +class FBXState : public GLTFState { + GDCLASS(FBXState, GLTFState); + friend class FBXDocument; + friend class SkinTool; + friend class GLTFSkin; + + // Smart pointer that holds the loaded scene. + ufbx_unique_ptr<ufbx_scene> scene; + bool allow_geometry_helper_nodes = false; + + HashMap<uint64_t, Image::AlphaMode> alpha_mode_cache; + HashMap<Pair<uint64_t, uint64_t>, GLTFTextureIndex, PairHash<uint64_t, uint64_t>> albedo_transparency_textures; + + Vector<GLTFSkinIndex> skin_indices; + HashMap<ObjectID, GLTFSkeletonIndex> skeleton3d_to_fbx_skeleton; + HashMap<ObjectID, HashMap<ObjectID, GLTFSkinIndex>> skin_and_skeleton3d_to_fbx_skin; + HashSet<String> unique_mesh_names; // Not in GLTFState because GLTFState prefixes mesh names with the scene name (or _) + +protected: + static void _bind_methods(); + +public: + bool get_allow_geometry_helper_nodes(); + void set_allow_geometry_helper_nodes(bool p_allow_geometry_helper_nodes); +}; + +#endif // FBX_STATE_H diff --git a/modules/fbx/register_types.cpp b/modules/fbx/register_types.cpp new file mode 100644 index 0000000000..7e404847ec --- /dev/null +++ b/modules/fbx/register_types.cpp @@ -0,0 +1,88 @@ +/**************************************************************************/ +/* register_types.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "register_types.h" + +#include "fbx_document.h" + +#ifdef TOOLS_ENABLED +#include "editor/editor_scene_importer_fbx2gltf.h" +#include "editor/editor_scene_importer_ufbx.h" + +#include "core/config/project_settings.h" +#include "editor/editor_node.h" +#include "editor/editor_settings.h" + +static void _editor_init() { + Ref<EditorSceneFormatImporterUFBX> import_fbx; + import_fbx.instantiate(); + ResourceImporterScene::add_scene_importer(import_fbx); + + bool fbx2gltf_enabled = GLOBAL_GET("filesystem/import/fbx2gltf/enabled"); + if (fbx2gltf_enabled) { + Ref<EditorSceneFormatImporterFBX2GLTF> importer; + importer.instantiate(); + ResourceImporterScene::add_scene_importer(importer); + } +} +#endif // TOOLS_ENABLED + +void initialize_fbx_module(ModuleInitializationLevel p_level) { + if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) { + GDREGISTER_CLASS(FBXDocument); + GDREGISTER_CLASS(FBXState); + } + +#ifdef TOOLS_ENABLED + if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { + // Editor-specific API. + ClassDB::APIType prev_api = ClassDB::get_current_api(); + ClassDB::set_current_api(ClassDB::API_EDITOR); + + GDREGISTER_CLASS(EditorSceneFormatImporterUFBX); + + GLOBAL_DEF_RST_BASIC("filesystem/import/fbx2gltf/enabled", true); + GDREGISTER_CLASS(EditorSceneFormatImporterFBX2GLTF); + GLOBAL_DEF_RST("filesystem/import/fbx2gltf/enabled.android", false); + GLOBAL_DEF_RST("filesystem/import/fbx2gltf/enabled.web", false); + + ClassDB::set_current_api(prev_api); + EditorNode::add_init_callback(_editor_init); + } +#endif // TOOLS_ENABLED +} + +void uninitialize_fbx_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + // TODO: 20240118 // fire + // FBXDocument::unregister_all_gltf_document_extensions(); +} diff --git a/modules/fbx/register_types.h b/modules/fbx/register_types.h new file mode 100644 index 0000000000..e4050081c2 --- /dev/null +++ b/modules/fbx/register_types.h @@ -0,0 +1,39 @@ +/**************************************************************************/ +/* register_types.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef FBX_REGISTER_TYPES_H +#define FBX_REGISTER_TYPES_H + +#include "modules/register_module_types.h" + +void initialize_fbx_module(ModuleInitializationLevel p_level); +void uninitialize_fbx_module(ModuleInitializationLevel p_level); + +#endif // FBX_REGISTER_TYPES_H diff --git a/modules/gdscript/README.md b/modules/gdscript/README.md new file mode 100644 index 0000000000..865475d37d --- /dev/null +++ b/modules/gdscript/README.md @@ -0,0 +1,139 @@ +# Basic GDScript module architecture +This provides some basic information in how GDScript is implemented and integrates with the rest of the engine. You can learn more about GDScript in the [documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/gdscript/index.html). It describes the syntax and user facing systems and concepts, and can be used as a reference for what user expectations are. + + +## General design + +GDScript is: + +1. A [gradually typed](https://en.wikipedia.org/wiki/Gradual_typing) language. Type hints are optional and help with static analysis and performance. However, typed code must easily interoperate with untyped code. +2. A tightly designed language. Features are added because they are _needed_, and not because they can be added or are interesting to develop. +3. Primarily an interpreted scripting language: it is compiled to GDScript byte code and interpreted in a GDScript virtual machine. It is meant to be easy to use and develop gameplay in. It is not meant for CPU-intensive algorithms or data processing, and is not optimized for it. For that, [C#](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html) or [GDExtension](https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/what_is_gdextension.html) may be used. + + +## Integration into Godot + +GDScript is integrated into Godot as a module. Since modules are optional, this means that Godot may be built without GDScript and work perfectly fine without it! + +The GDScript module interfaces with Godot's codebase by inheriting from the engine's scripting-related classes. New languages inherit from [`ScriptLanguage`](/core/object/script_language.h), and are registered in Godot's [`ScriptServer`](/core/object/script_language.h). Scripts, referring to a file containing code, are represented in the engine by the `Script` class. Instances of that script, which are used at runtime when actually executing the code, inherit from [`ScriptInstance`](/core/object/script_instance.h). + +To access Godot's internal classes, GDScript uses [`ClassDB`](/core/object/class_db.h). `ClassDB` is where Godot registers classes, methods and properties that it wants exposed to its scripting system. This is how GDScript understands that `Node2D` is a class it can use, and that it has a `get_parent()` method. + +[Built-in GDScript methods](https://docs.godotengine.org/en/latest/classes/class_@gdscript.html#methods) are defined and exported by [`GDScriptUtilityFunctions`](gdscript_utility_functions.h), whereas [global scope methods](https://docs.godotengine.org/en/latest/classes/class_%2540globalscope.html) are registered in [`Variant::_register_variant_utility_functions()`](/core/variant/variant_utility.cpp). + + +## Compilation + +Scripts can be at different stages of compilation. The process isn't entirely linear, but consists of this general order: tokenizing, parsing, analyzing, and finally compiling. This process is the same for scripts in the editor and scripts in an exported game. Scripts are stored as text files in both cases, and the compilation process must happen in full before the bytecode can be passed to the virtual machine and run. + +The main class of the GDScript module is the [`GDScript`](gdscript.h) class, which represents a class defined in GDScript. Each `.gd` file is called a _class file_ because it implicitly defines a class in GDScript, and thus results in an associated `GDScript` object. However, GDScript classes may define [_inner classes_](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#inner-classes), and those are also represented by further `GDScript` objects, even though they are not in files of their own. + +The `GDScript` class contains all the information related to the corresponding GDScript class: its name and path, its members like variables, functions, symbols, signals, implicit methods like initializers, etc. This is the main class that the compilation step deals with. + +A secondary class is `GDScriptInstance`, defined in the same file, containing _runtime_ information for an instance of a `GDScript`, and is more related to the execution of a script by the virtual machine. + + +### Loading source code + +This mostly happens by calling `GDScript::load_source_code()` on a `GDScript` object. Parsing only requires a `String`, so it is entirely possible to parse a script without a `GDScript` object! + + +### Tokenizing (see [`GDScriptTokenizer`](gdscript_tokenizer.h)) + +Tokenizing is the process of converting the source code `String` into a sequence of tokens, which represent language constructs (such as `for` or `if`), identifiers, literals, etc. This happens almost exclusively during the parsing process, which asks for the next token in order to make sense of the source code. The tokenizer is only used outside of the parsing process in very rare exceptions. + + +### Parsing (see [`GDScriptParser`](gdscript_parser.h)) + +The parser takes a sequence of tokens and builds [the abstract syntax tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) of the GDScript program. The AST is used in the analyzing and compilation steps, and the source code `String` and sequence of tokens are discarded. The AST-building process finds syntax errors in a GDScript program and reports them to the user. + +The parser class also defines all the possible nodes of the AST as subtypes of `GDScriptParser::Node`, not to be confused with Godot's scene tree `Node`. For example, `GDScriptParser::IfNode` has two children nodes, one for the code in the `if` block, and one for the code in the `else` block. A `GDScriptParser::FunctionNode` contains children nodes for its name, parameters, return type, body, etc. The parser also defines typechecking data structures like `GDScriptParser::Datatype`. + +The parser was [intentionally designed](https://godotengine.org/article/gdscript-progress-report-writing-new-parser/#less-lookahead) with a look-ahead of a single token. This means that the parser only has access to the current token and the previous token (or, if you prefer, the current token and the next token). This parsing limitation ensures that GDScript will remain syntactically simple and accessible, and that the parsing process cannot become overly complex. + + +### Analysis and typechecking (see [`GDScriptAnalyzer`](gdscript_analyzer.h)) + +The analyzer takes in the AST of a program and verifies that "everything checks out". For example, when analyzing a method call with three parameters, it will check whether the function definition also contains three parameters. If the code is typed, it will check that argument and parameter types are compatible. + +There are two types of functions in the analyzer: `reduce` functions and `resolve` functions. Their parameters always include the AST node that they are attempting to reduce or resolve. +- The `reduce` functions work on GDScript expressions, which return values, and thus their main goal is to populate the `GDScriptParser::Datatype` of the underlying AST node. The datatype is then used to typecheck code that depends on this expression, and gives the compiler necessary information to generate appropriate, safe, and optimized bytecode. +For example, function calls are handled with `reduce_call()`, which must figure out what function is being called and check that the passed arguments match the function's parameters. The type of the underlying `CallNode` will be the return type of the function. +Another example is `reduce_identifier()`, which does _a lot_ of work: given the string of its `IdentifierNode`, it must figure out what that identifier refers to. It could be a local variable, class name, global or class function, function parameter, class or superclass member, or any number of other things. It has to check many different places to find this information! +A secondary goal of the `reduce` functions is to perform [constant folding](https://en.wikipedia.org/wiki/Constant_folding): to determine whether an expression is constant, and if it is, compute its _reduced value_ at this time so it does not need to be computed over and over at runtime! +- The resolve functions work on AST nodes that represent statements, and don't necessarily have values. Their goal is to do work related to program control flow, resolve their child AST nodes, deal with scoping, etc. One of the simplest examples is `resolve_if()`, which reduces the `if` condition, then resolves the `if` body and `else` body if it exists. +The `resolve_for()` function does more work than simply resolving its code block. With `for i in range(10)`, for example, it must also declare and type the new variable `i` within the scope of its code block, as well as make sure `range(10)` is iterable, among other things. +To understand classes and inheritance without introducing cyclic dependency problems that would come from immediate full class code analysis, the analyzer often asks only for class _interfaces_: it needs to know what member variables and methods exist as well as their types, but no more. +This is done through `resolve_class_interface()`, which populates `ClassNode`'s `Datatype` with that information. It first checks for superclass information with `resolve_class_inheritance()`, then populates its member information by calling `resolve_class_member()` on each member. Since this step is only about the class _interface_, methods are resolved with `resolve_function_signature()`, which gets all relevant typing information without resolving the function body! +The remaining steps of resolution, including member variable initialization code, method code, etc, can happen at a later time. + +In fully untyped code, very little static analysis is possible. For example, the analyzer cannot know whether `my_var.some_member` exists when it does not know the type of `my_var`. Therefore, it cannot emit a warning or error because `some_member` _could_ exist - or it could not. The analyzer must trust the programmer. If an error does occur, it will be at runtime. +However, GDScript is gradually typed, so all of these analyses must work when parts of the code are typed and others untyped. Static analysis in a gradually typed language is a best-effort situation: suppose there is a typed variable `var x : int`, and an untyped `var y = "some string"`. We can obviously tell this isn't going to work, but the analyzer will accept the assignment `x = y` without warnings or errors: it only knows that `y` is untyped and can therefore be anything, including the `int` that `x` expects. It must once again trust the programmer to have written code that works. In this instance, the code will error at runtime. +In both these cases, the analyzer handles the uncertainty of untyped code by calling `mark_node_unsafe()` on the respective AST node. This means it didn't have enough information to know whether the code was fully safe or necessarily wrong. Lines with unsafe AST nodes are represented by gray line numbers in the GDScript editor. Green line numbers indicate a line of code without any unsafe nodes. + +This analysis step is also where dependencies are introduced and that information stored for use later. If class `A` extends class `B` or contains a member with type `B` from some other script file, then the analyzer will attempt to load that second script. If `B` contains references to `A`, then a _cyclic_ dependency is introduced. This is OK in many cases, but impossible to resolve in others. + +Clearly, the analyzer is where a lot of the "magic" happens! It determines what constitutes proper code that can actually be compiled, and provides as many safety guarantees as possible with the typing information it is provided with. The more typed the code, the safer and more optimized it will be! + + +#### Cyclic dependencies and member resolution + +Cyclic dependencies from inheritance (`A extends B, B extends A`) are not supported in any programming language. Other cyclic dependencies are supported, such as `A extends B` and `B` uses, contains, or preloads, members of type `A`. + +To see why cyclic dependencies are complicated, suppose there is one between classes `A <-> B`. Partially through the analysis of `A`, we will need information about `B`, and therefore trigger its analysis. However, the analysis of `B` will eventually need information from `A`, which is incomplete because we never finished analyzing it. This would result in members not being found when they actually exist! + +GDScript supports cyclic dependencies due to a few features of the analyzer: + +1. Class interface resolution: when analyzing code of class `A` that depends on some other class `B`, we don't need to resolve the _code_ of `B` (its member initializers, function code, etc). We only need to know what members and methods the class has, as well as their types. These are the only things one class can use to work with, or _interface_ with, another. Because of inheritance, a class's interface depends on its superclass as well, so recursive interface resolution is needed. More details can be found in `GDScriptAnalyzer::resolve_class_interface()`. +2. Out of order member resolution: the analyzer may not even need an entire class interface to be resolved in order to figure out a specific type! For example, if class `A` contains code that references `B.is_alive`, then the analyzer doesn't need to immediately resolve `B`'s entire interface. It may simply check whether `is_alive` exists in `B`, and reduce it for its type information, on-demand. +A fundamental cyclic dependency problem occurs when the types of two different member variables are mutually dependent. This is commonly checked by a pattern that declares a temporary datatype with `GDScriptParser::DataType resolving_datatype;`, followed by `resolving_datatype.kind = GDScriptParser::DataType::RESOLVING;`. If the analyzer attempts to resolve a member on-demand that is already tagged as resolving, then a cyclic dependency problem has been found and can be reported. + + +### Compiling (see [`GDScriptCompiler`](gdscript_compiler.h)) + +Compiling is the final step in making a GDScript executable in the [virtual machine](gdscript_vm.h) (VM). The compiler takes a `GDScript` object and an AST, and uses another class, [`GDScriptByteCodeGenerator`](gdscript_byte_codegen.h), to generate bytecode corresponding to the class. In doing this, it creates the objects that the VM understands how to run, like [`GDScriptFunction`](gdscript_function.h), and completes a few extra tasks needed for compilation, such as populating runtime class member information. + +Importantly, the compilation process of a class, specifically the `GDScriptCompiler::_compile_class()` method, _cannot_ depend on information obtained by calling `GDScriptCompiler::_compile_class()` on another class, for the same cyclic dependency reasons explained in the previous section. +Any information that can only be obtained or populated during the compilation step, when `GDScript` objects become available, must be handled before `GDScriptCompiler::_compile_class()` is called. This process is centralized in `GDScriptCompiler::_prepare_compilation()` which works as the compile-time equivalent of `GDScriptAnalyzer::resolve_class_interface()`: it populates a `GDScript`'s "interface" exclusively with information from the analysis step, and without processing other external classes. This information may then be referenced by other classes without introducing problematic cycles. + +The more typing information a GDScript has, the more optimized the compiled bytecode can be. For example, if `my_var` is untyped, the bytecode for `my_var.some_member` will need to go through several layers of indirection to figure out the type of `my_var` at runtime, and from there determine how to obtain `some_member`. This varies depending on whether `my_var` is a dictionary, a script, or a native class. If the type of `my_var` was known at compile time, the bytecode can directly call the type-specific method for obtaining a member. +Similar optimizations are possible for `my_var.some_func()`. With untyped GDScript, the VM will need to resolve `my_var`'s type at runtime, then, depending on the type, use different methods to resolve the function and call it. When the function is fully resolved during static analysis, native function pointers or GDScript function objects can be compiled into the bytecode and directly called by the VM, removing several layers of indirection. + +Typed code is safer code and faster code! + + +## Loading scripts + +GDScripts can be loaded in a couple of different ways. The main method, used almost everywhere in the engine, is to load scripts through the `ResourceLoader` singleton. In this way, GDScripts are resources like any others: `ResourceLoader::load()` will simply reroute to `ResourceFormatLoaderGDScript::load()`, found in `gdscript.h/cpp`(gdscript.h). This generates a GDScript object which is compiled and ready to use. + +The other method is to manually load the source code, then pass it to a parser, then to an analyzer and then to a compiler. The previous approach does this behind the scenes, alongside some smart caching of scripts and other functionalities. It is used in the [GDScript test runner infrastructure](tests/gdscript_test_runner.h). + + +### Full and shallow scripts + +The `ResourceFormatLoaderGDScript::load()` method simply calls `GDScriptCache::get_full_script()`. The [`GDScriptCache`](gdscript_cache.h) is, as it sounds, a cache for GDScripts. Its two main methods, `get_shallow_script()` and `get_full_script()`, get and cache, respectively, scripts that have been merely parsed, and scripts which have been statically analyzed and fully compiled. Another internal class, `GDScriptParserRef`, found in the same file, provides even more granularity over the different steps of the parsing process, and is used extensively in the analyzer. + +Shallow, or "just parsed" scripts, provide information such as defined classes, class members, and so forth. This is sufficient for many purposes, like obtaining a class interface or checking whether a member exists on a specific class. Full scripts, on the other hand, have been analyzed and compiled and are ready to use. + +The distinction between full and shallow scripts is very important, as shallow scripts cannot create cyclic dependency problems, whereas full scripts can. The analyzer, for example, never asks for full scripts. Choosing when to request a shallow vs a full script is an important but subtle decision. + +In practice, full scripts are simply scripts where `GDScript::reload()` has been called. This critical function is the primary way in which scripts get compiled in Godot, and essentially does all the compilation steps covered so far in order. Whenever a script is loaded, or updated and reloaded in Godot, it will end up going through `GDScript::reload()`, except in very rare circumstances like the test runner. It is an excellent place to start reading and understanding the GDScript module! + + +## Special types of scripts + +Certain types of GDScripts behave slightly differently. For example, autoloads are loaded with `ResourceLoader::load()` during `Main::start()`, very soon after Godot is launched. Many systems aren't initialized at that time, so error reporting is often significantly reduced and may not even show up in the editor. + +Tool scripts, declared with the `@tool` annotation on a GDScript file, run in the editor itself as opposed to just when the game is launched. This leads to a significant increase in complexity, as many things that can be changed in the editor may affect a currently executing tool script. + + +## Other + +There are many other classes in the GDScript module. Here is a brief overview of some of them: + +- Declaration of GDScript warnings in [`GDScriptWarning`](gdscript_warning.h). +- [`GDScriptFunction`](gdscript_function.h), which represents an executable GDScript function. The relevant file contains both static as well as runtime information. +- The [virtual machine](gdscript_vm.cpp) is essentially defined as calling `GDScriptFunction::call()`. +- Editor-related functions can be found in parts of `GDScriptLanguage`, originally declared in [`gdscript.h`](gdscript.h) but defined in [`gdscript_editor.cpp`](gdscript_editor.cpp). Code highlighting can be found in [`GDScriptSyntaxHighlighter`](editor/gdscript_highlighter.h). +- GDScript decompilation is found in [`gdscript_disassembler.cpp`](gdscript_disassembler.h), defined as `GDScriptFunction::disassemble()`. +- Documentation generation from GDScript comments in [`GDScriptDocGen`](editor/gdscript_docgen.h)
\ No newline at end of file diff --git a/modules/gdscript/SCsub b/modules/gdscript/SCsub index 1dc4768186..61accd4fc9 100644 --- a/modules/gdscript/SCsub +++ b/modules/gdscript/SCsub @@ -19,6 +19,8 @@ if env.editor_build: # Using a define in the disabled case, to avoid having an extra define # in regular builds where all modules are enabled. env_gdscript.Append(CPPDEFINES=["GDSCRIPT_NO_LSP"]) + # Also needed in main env to unexpose --lsp-port option. + env.Append(CPPDEFINES=["GDSCRIPT_NO_LSP"]) if env["tests"]: diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 3da6bcf10c..7ececce613 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -57,12 +57,11 @@ [/codeblock] </description> </method> - <method name="convert" is_deprecated="true"> + <method name="convert" deprecated="Use [method @GlobalScope.type_convert] instead."> <return type="Variant" /> <param index="0" name="what" type="Variant" /> <param index="1" name="type" type="int" /> <description> - [i]Deprecated.[/i] Use [method @GlobalScope.type_convert] instead. Converts [param what] to [param type] in the best way possible. The [param type] uses the [enum Variant.Type] values. [codeblock] var a = [4, 2.5, 1.2] @@ -148,7 +147,7 @@ <return type="int" /> <param index="0" name="var" type="Variant" /> <description> - Returns the length of the given Variant [param var]. The length can be the character count of a [String], the element count of any array type or the size of a [Dictionary]. For every other Variant type, a run-time error is generated and execution is stopped. + Returns the length of the given Variant [param var]. The length can be the character count of a [String] or [StringName], the element count of any array type, or the size of a [Dictionary]. For every other Variant type, a run-time error is generated and execution is stopped. [codeblock] a = [1, 2, 3, 4] len(a) # Returns 4 @@ -162,7 +161,7 @@ <return type="Resource" /> <param index="0" name="path" type="String" /> <description> - Returns a [Resource] from the filesystem located at the absolute [param path]. Unless it's already referenced elsewhere (such as in another script or in the scene), the resource is loaded from disk on function call, which might cause a slight delay, especially when loading large scenes. To avoid unnecessary delays when loading something multiple times, either store the resource in a variable or use [method preload]. + Returns a [Resource] from the filesystem located at the absolute [param path]. Unless it's already referenced elsewhere (such as in another script or in the scene), the resource is loaded from disk on function call, which might cause a slight delay, especially when loading large scenes. To avoid unnecessary delays when loading something multiple times, either store the resource in a variable or use [method preload]. This method is equivalent of using [method ResourceLoader.load] with [constant ResourceLoader.CACHE_MODE_REUSE]. [b]Note:[/b] Resource paths can be obtained by right-clicking on a resource in the FileSystem dock and choosing "Copy Path", or by dragging the file from the FileSystem dock into the current script. [codeblock] # Load a scene called "main" located in the root of the project directory and cache it in a variable. @@ -210,7 +209,7 @@ [b]Note:[/b] Calling this function from a [Thread] is not supported. Doing so will instead print the thread ID. </description> </method> - <method name="range" qualifiers="vararg"> + <method name="range" qualifiers="vararg" keywords="seq"> <return type="Array" /> <description> Returns an array with the given range. [method range] can be called in three ways: @@ -580,7 +579,7 @@ <param index="2" name="step" type="float" default="1.0" /> <param index="3" name="extra_hints" type="String" default="""" /> <description> - Export an [int] or [float] property as a range value. The range must be defined by [param min] and [param max], as well as an optional [param step] and a variety of extra hints. The [param step] defaults to [code]1[/code] for integer properties. For floating-point numbers this value depends on your [code]EditorSettings.interface/inspector/default_float_step[/code] setting. + Export an [int] or [float] property as a range value. The range must be defined by [param min] and [param max], as well as an optional [param step] and a variety of extra hints. The [param step] defaults to [code]1[/code] for integer properties. For floating-point numbers this value depends on your [member EditorSettings.interface/inspector/default_float_step] setting. If hints [code]"or_greater"[/code] and [code]"or_less"[/code] are provided, the editor widget will not cap the value at range boundaries. The [code]"exp"[/code] hint will make the edited values on range to change exponentially. The [code]"hide_slider"[/code] hint will hide the slider element of the editor widget. Hints also allow to indicate the units for the edited value. Using [code]"radians_as_degrees"[/code] you can specify that the actual value is in radians, but should be displayed in degrees in the Inspector dock (the range values are also in degrees). [code]"degrees"[/code] allows to add a degree sign as a unit suffix (the value is unchanged). Finally, a custom suffix can be provided using [code]"suffix:unit"[/code], where "unit" can be any string. See also [constant PROPERTY_HINT_RANGE]. @@ -598,6 +597,17 @@ [/codeblock] </description> </annotation> + <annotation name="@export_storage"> + <return type="void" /> + <description> + Export a property with [constant PROPERTY_USAGE_STORAGE] flag. The property is not displayed in the editor, but it is serialized and stored in the scene or resource file. This can be useful for [annotation @tool] scripts. Also the property value is copied when [method Resource.duplicate] or [method Node.duplicate] is called, unlike non-exported variables. + [codeblock] + var a # Not stored in the file, not displayed in the editor. + @export_storage var b # Stored in the file, not displayed in the editor. + @export var c: int # Stored in the file, displayed in the editor. + [/codeblock] + </description> + </annotation> <annotation name="@export_subgroup"> <return type="void" /> <param index="0" name="name" type="String" /> diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp index c3979dd290..601db5414b 100644 --- a/modules/gdscript/editor/gdscript_docgen.cpp +++ b/modules/gdscript/editor/gdscript_docgen.cpp @@ -32,21 +32,29 @@ #include "../gdscript.h" -String GDScriptDocGen::_get_script_path(const String &p_path) { +#include "core/config/project_settings.h" + +HashMap<String, String> GDScriptDocGen::singletons; + +String GDScriptDocGen::_get_script_name(const String &p_path) { + const HashMap<String, String>::ConstIterator E = singletons.find(p_path); + if (E) { + return E->value; + } return p_path.trim_prefix("res://").quote(); } String GDScriptDocGen::_get_class_name(const GDP::ClassNode &p_class) { const GDP::ClassNode *curr_class = &p_class; if (!curr_class->identifier) { // All inner classes have an identifier, so this is the outer class. - return _get_script_path(curr_class->fqcn); + return _get_script_name(curr_class->fqcn); } String full_name = curr_class->identifier->name; while (curr_class->outer) { curr_class = curr_class->outer; if (!curr_class->identifier) { // All inner classes have an identifier, so this is the outer class. - return vformat("%s.%s", _get_script_path(curr_class->fqcn), full_name); + return vformat("%s.%s", _get_script_name(curr_class->fqcn), full_name); } full_name = vformat("%s.%s", curr_class->identifier->name, full_name); } @@ -64,8 +72,8 @@ void GDScriptDocGen::_doctype_from_gdtype(const GDType &p_gdtype, String &r_type r_type = p_is_return ? "void" : "null"; return; } - if (p_gdtype.builtin_type == Variant::ARRAY && p_gdtype.has_container_element_type()) { - _doctype_from_gdtype(p_gdtype.get_container_element_type(), r_type, r_enum); + if (p_gdtype.builtin_type == Variant::ARRAY && p_gdtype.has_container_element_type(0)) { + _doctype_from_gdtype(p_gdtype.get_container_element_type(0), r_type, r_enum); if (!r_enum.is_empty()) { r_type = "int[]"; r_enum += "[]"; @@ -97,12 +105,12 @@ void GDScriptDocGen::_doctype_from_gdtype(const GDType &p_gdtype, String &r_type return; } if (!p_gdtype.script_type->get_path().is_empty()) { - r_type = _get_script_path(p_gdtype.script_type->get_path()); + r_type = _get_script_name(p_gdtype.script_type->get_path()); return; } } if (!p_gdtype.script_path.is_empty()) { - r_type = _get_script_path(p_gdtype.script_path); + r_type = _get_script_name(p_gdtype.script_path); return; } r_type = "Object"; @@ -221,24 +229,25 @@ String GDScriptDocGen::_docvalue_from_variant(const Variant &p_variant, int p_re } } -void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_class) { +void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_class) { p_script->_clear_doc(); DocData::ClassDoc &doc = p_script->doc; - doc.script_path = _get_script_path(p_script->get_script_path()); + doc.is_script_doc = true; + if (p_script->local_name == StringName()) { - doc.name = doc.script_path; + // This is an outer unnamed class. + doc.name = _get_script_name(p_script->get_script_path()); } else { + // This is an inner or global outer class. doc.name = p_script->local_name; + if (p_script->_owner) { + doc.name = p_script->_owner->doc.name + "." + doc.name; + } } - if (p_script->_owner) { - doc.name = p_script->_owner->doc.name + "." + doc.name; - doc.script_path = doc.script_path + "." + doc.name; - } - - doc.is_script_doc = true; + doc.script_path = p_script->get_script_path(); if (p_script->base.is_valid() && p_script->base->is_valid()) { if (!p_script->base->doc.name.is_empty()) { @@ -259,7 +268,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c doc.tutorials.append(td); } doc.is_deprecated = p_class->doc_data.is_deprecated; + doc.deprecated_message = p_class->doc_data.deprecated_message; doc.is_experimental = p_class->doc_data.is_experimental; + doc.experimental_message = p_class->doc_data.experimental_message; for (const GDP::ClassNode::Member &member : p_class->members) { switch (member.type) { @@ -271,7 +282,7 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c // Recursively generate inner class docs. // Needs inner GDScripts to exist: previously generated in GDScriptCompiler::make_scripts(). - GDScriptDocGen::generate_docs(*p_script->subclasses[class_name], inner_class); + GDScriptDocGen::_generate_docs(*p_script->subclasses[class_name], inner_class); } break; case GDP::ClassNode::Member::CONSTANT: { @@ -286,7 +297,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c const_doc.is_value_valid = true; const_doc.description = m_const->doc_data.description; const_doc.is_deprecated = m_const->doc_data.is_deprecated; + const_doc.deprecated_message = m_const->doc_data.deprecated_message; const_doc.is_experimental = m_const->doc_data.is_experimental; + const_doc.experimental_message = m_const->doc_data.experimental_message; doc.constants.push_back(const_doc); } break; @@ -300,7 +313,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c method_doc.name = func_name; method_doc.description = m_func->doc_data.description; method_doc.is_deprecated = m_func->doc_data.is_deprecated; + method_doc.deprecated_message = m_func->doc_data.deprecated_message; method_doc.is_experimental = m_func->doc_data.is_experimental; + method_doc.experimental_message = m_func->doc_data.experimental_message; method_doc.qualifiers = m_func->is_static ? "static" : ""; if (m_func->return_type) { @@ -340,7 +355,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c signal_doc.name = signal_name; signal_doc.description = m_signal->doc_data.description; signal_doc.is_deprecated = m_signal->doc_data.is_deprecated; + signal_doc.deprecated_message = m_signal->doc_data.deprecated_message; signal_doc.is_experimental = m_signal->doc_data.is_experimental; + signal_doc.experimental_message = m_signal->doc_data.experimental_message; for (const GDScriptParser::ParameterNode *p : m_signal->parameters) { DocData::ArgumentDoc arg_doc; @@ -362,7 +379,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c prop_doc.name = var_name; prop_doc.description = m_var->doc_data.description; prop_doc.is_deprecated = m_var->doc_data.is_deprecated; + prop_doc.deprecated_message = m_var->doc_data.deprecated_message; prop_doc.is_experimental = m_var->doc_data.is_experimental; + prop_doc.experimental_message = m_var->doc_data.experimental_message; _doctype_from_gdtype(m_var->get_datatype(), prop_doc.type, prop_doc.enumeration); switch (m_var->property) { @@ -408,7 +427,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c DocData::EnumDoc enum_doc; enum_doc.description = m_enum->doc_data.description; enum_doc.is_deprecated = m_enum->doc_data.is_deprecated; + enum_doc.deprecated_message = m_enum->doc_data.deprecated_message; enum_doc.is_experimental = m_enum->doc_data.is_experimental; + enum_doc.experimental_message = m_enum->doc_data.experimental_message; doc.enums[name] = enum_doc; for (const GDP::EnumNode::Value &val : m_enum->values) { @@ -419,7 +440,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c const_doc.enumeration = name; const_doc.description = val.doc_data.description; const_doc.is_deprecated = val.doc_data.is_deprecated; + const_doc.deprecated_message = val.doc_data.deprecated_message; const_doc.is_experimental = val.doc_data.is_experimental; + const_doc.experimental_message = val.doc_data.experimental_message; doc.constants.push_back(const_doc); } @@ -439,7 +462,9 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c const_doc.enumeration = "@unnamed_enums"; const_doc.description = m_enum_val.doc_data.description; const_doc.is_deprecated = m_enum_val.doc_data.is_deprecated; + const_doc.deprecated_message = m_enum_val.doc_data.deprecated_message; const_doc.is_experimental = m_enum_val.doc_data.is_experimental; + const_doc.experimental_message = m_enum_val.doc_data.experimental_message; doc.constants.push_back(const_doc); } break; @@ -451,3 +476,13 @@ void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_c // Add doc to the outer-most class. p_script->_add_doc(doc); } + +void GDScriptDocGen::generate_docs(GDScript *p_script, const GDP::ClassNode *p_class) { + for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) { + if (E.value.is_singleton) { + singletons[E.value.path] = E.key; + } + } + _generate_docs(p_script, p_class); + singletons.clear(); +} diff --git a/modules/gdscript/editor/gdscript_docgen.h b/modules/gdscript/editor/gdscript_docgen.h index a326c02c5f..651a4fb198 100644 --- a/modules/gdscript/editor/gdscript_docgen.h +++ b/modules/gdscript/editor/gdscript_docgen.h @@ -39,10 +39,13 @@ class GDScriptDocGen { using GDP = GDScriptParser; using GDType = GDP::DataType; - static String _get_script_path(const String &p_path); + static HashMap<String, String> singletons; // Script path to singleton name. + + static String _get_script_name(const String &p_path); static String _get_class_name(const GDP::ClassNode &p_class); static void _doctype_from_gdtype(const GDType &p_gdtype, String &r_type, String &r_enum, bool p_is_return = false); static String _docvalue_from_variant(const Variant &p_variant, int p_recursion_level = 1); + static void _generate_docs(GDScript *p_script, const GDP::ClassNode *p_class); public: static void generate_docs(GDScript *p_script, const GDP::ClassNode *p_class); diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp index 8dbd262b22..439555bacb 100644 --- a/modules/gdscript/editor/gdscript_highlighter.cpp +++ b/modules/gdscript/editor/gdscript_highlighter.cpp @@ -35,6 +35,7 @@ #include "core/config/project_settings.h" #include "editor/editor_settings.h" +#include "editor/themes/editor_theme_manager.h" Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) { Dictionary color_map; @@ -62,13 +63,15 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l bool in_member_variable = false; bool in_lambda = false; - bool in_function_name = false; - bool in_variable_declaration = false; + bool in_function_name = false; // Any call. + bool in_function_declaration = false; // Only declaration. + bool in_var_const_declaration = false; bool in_signal_declaration = false; bool expect_type = false; - int in_function_args = 0; - int in_function_arg_dicts = 0; + int in_declaration_params = 0; // The number of opened `(` after func/signal name. + int in_declaration_param_dicts = 0; // The number of opened `{` inside func params. + int in_type_params = 0; // The number of opened `[` after type name. Color keyword_color; Color color; @@ -149,7 +152,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l // Check if it's the whole line. if (end_key_length == 0 || color_regions[c].line_only || from + end_key_length > line_length) { // Don't skip comments, for highlighting markers. - if (color_regions[in_region].start_key.begins_with("#")) { + if (color_regions[in_region].type == ColorRegion::TYPE_COMMENT) { break; } if (from + end_key_length > line_length) { @@ -171,7 +174,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } // Don't skip comments, for highlighting markers. - if (j == line_length && !color_regions[in_region].start_key.begins_with("#")) { + if (j == line_length && color_regions[in_region].type != ColorRegion::TYPE_COMMENT) { continue; } } @@ -179,13 +182,13 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l // If we are in one, find the end key. if (in_region != -1) { Color region_color = color_regions[in_region].color; - if (in_node_path && (color_regions[in_region].start_key == "\"" || color_regions[in_region].start_key == "\'")) { + if (in_node_path && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = node_path_color; } - if (in_node_ref && (color_regions[in_region].start_key == "\"" || color_regions[in_region].start_key == "\'")) { + if (in_node_ref && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = node_ref_color; } - if (in_string_name && (color_regions[in_region].start_key == "\"" || color_regions[in_region].start_key == "\'")) { + if (in_string_name && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = string_name_color; } @@ -193,7 +196,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l highlighter_info["color"] = region_color; color_map[j] = highlighter_info; - if (color_regions[in_region].start_key.begins_with("#")) { + if (color_regions[in_region].type == ColorRegion::TYPE_COMMENT) { int marker_start_pos = from; int marker_len = 0; while (from <= line_length) { @@ -444,12 +447,15 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l if (str[k] == '(') { in_function_name = true; - } else if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::VAR) || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FOR)) { - in_variable_declaration = true; + if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) { + in_function_declaration = true; + } + } else if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::VAR) || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FOR) || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::CONST)) { + in_var_const_declaration = true; } // Check for lambda. - if (in_function_name && prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) { + if (in_function_declaration) { k = j - 1; while (k > 0 && is_whitespace(str[k])) { k--; @@ -474,52 +480,66 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l } if (is_a_symbol) { - if (in_function_args > 0) { + if (in_declaration_params > 0) { switch (str[j]) { case '(': - in_function_args += 1; + in_declaration_params += 1; break; case ')': - in_function_args -= 1; + in_declaration_params -= 1; break; case '{': - in_function_arg_dicts += 1; + in_declaration_param_dicts += 1; break; case '}': - in_function_arg_dicts -= 1; + in_declaration_param_dicts -= 1; break; } - } else if (in_function_name && str[j] == '(') { - in_function_args = 1; - in_function_arg_dicts = 0; - } - - if (expect_type && (prev_is_char || str[j] == '=') && str[j] != '[' && str[j] != '.') { - expect_type = false; + } else if ((in_function_declaration || in_signal_declaration || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) && str[j] == '(') { + in_declaration_params = 1; + in_declaration_param_dicts = 0; } - if (j > 0 && str[j - 1] == '-' && str[j] == '>') { - expect_type = true; - } - - if (in_variable_declaration || in_function_args > 0) { - int k = j; - // Skip space. - while (k < line_length && is_whitespace(str[k])) { - k++; + if (expect_type) { + switch (str[j]) { + case '[': + in_type_params += 1; + break; + case ']': + in_type_params -= 1; + break; + case ',': + if (in_type_params <= 0) { + expect_type = false; + } + break; + case ' ': + case '\t': + case '.': + break; + default: + expect_type = false; + break; } - - if (str[k] == ':' && in_function_arg_dicts == 0) { - // Has type hint. + } else { + if (j > 0 && str[j - 1] == '-' && str[j] == '>') { + expect_type = true; + in_type_params = 0; + } + if ((in_var_const_declaration || (in_declaration_params == 1 && in_declaration_param_dicts == 0)) && str[j] == ':') { expect_type = true; + in_type_params = 0; } } - in_variable_declaration = false; - in_signal_declaration = false; - in_function_name = false; - in_lambda = false; - in_member_variable = false; + if (!is_whitespace(str[j])) { + in_function_declaration = false; + in_var_const_declaration = false; + in_signal_declaration = false; + in_function_name = false; + in_lambda = false; + in_member_variable = false; + } } if (!in_raw_string && in_region == -1 && str[j] == 'r' && j < line_length - 1 && (str[j + 1] == '"' || str[j + 1] == '\'')) { @@ -581,7 +601,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l color = member_color; } else if (in_function_name) { next_type = FUNCTION; - if (!in_lambda && prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) { + if (!in_lambda && in_function_declaration) { color = function_definition_color; } else { color = function_color; @@ -737,7 +757,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { for (const String &comment : comments) { String beg = comment.get_slice(" ", 0); String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String(); - add_color_region(beg, end, comment_color, end.is_empty()); + add_color_region(ColorRegion::TYPE_COMMENT, beg, end, comment_color, end.is_empty()); } /* Doc comments */ @@ -747,18 +767,20 @@ void GDScriptSyntaxHighlighter::_update_cache() { for (const String &doc_comment : doc_comments) { String beg = doc_comment.get_slice(" ", 0); String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slice(" ", 1) : String(); - add_color_region(beg, end, doc_comment_color, end.is_empty()); + add_color_region(ColorRegion::TYPE_COMMENT, beg, end, doc_comment_color, end.is_empty()); } + /* Code regions */ + const Color code_region_color = Color(EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color").operator Color(), 1.0); + add_color_region(ColorRegion::TYPE_CODE_REGION, "#region", "", code_region_color, true); + add_color_region(ColorRegion::TYPE_CODE_REGION, "#endregion", "", code_region_color, true); + /* Strings */ string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); - List<String> strings; - gdscript->get_string_delimiters(&strings); - for (const String &string : strings) { - String beg = string.get_slice(" ", 0); - String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 1) : String(); - add_color_region(beg, end, string_color, end.is_empty()); - } + add_color_region(ColorRegion::TYPE_STRING, "\"", "\"", string_color); + add_color_region(ColorRegion::TYPE_STRING, "'", "'", string_color); + add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "\"\"\"", "\"\"\"", string_color); + add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "'''", "'''", string_color); const Ref<Script> scr = _get_edited_resource(); if (scr.is_valid()) { @@ -790,7 +812,7 @@ void GDScriptSyntaxHighlighter::_update_cache() { const String text_edit_color_theme = EDITOR_GET("text_editor/theme/color_theme"); const bool godot_2_theme = text_edit_color_theme == "Godot 2"; - if (godot_2_theme || EditorSettings::get_singleton()->is_dark_theme()) { + if (godot_2_theme || EditorThemeManager::is_dark_theme()) { function_definition_color = Color(0.4, 0.9, 1.0); global_function_color = Color(0.64, 0.64, 0.96); node_path_color = Color(0.72, 0.77, 0.49); @@ -891,20 +913,17 @@ void GDScriptSyntaxHighlighter::_update_cache() { } } -void GDScriptSyntaxHighlighter::add_color_region(const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only) { - for (int i = 0; i < p_start_key.length(); i++) { - ERR_FAIL_COND_MSG(!is_symbol(p_start_key[i]), "color regions must start with a symbol"); - } +void GDScriptSyntaxHighlighter::add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only) { + ERR_FAIL_COND_MSG(p_start_key.is_empty(), "Color region start key cannot be empty."); + ERR_FAIL_COND_MSG(!is_symbol(p_start_key[0]), "Color region start key must start with a symbol."); - if (p_end_key.length() > 0) { - for (int i = 0; i < p_end_key.length(); i++) { - ERR_FAIL_COND_MSG(!is_symbol(p_end_key[i]), "color regions must end with a symbol"); - } + if (!p_end_key.is_empty()) { + ERR_FAIL_COND_MSG(!is_symbol(p_end_key[0]), "Color region end key must start with a symbol."); } int at = 0; for (int i = 0; i < color_regions.size(); i++) { - ERR_FAIL_COND_MSG(color_regions[i].start_key == p_start_key, "color region with start key '" + p_start_key + "' already exists."); + ERR_FAIL_COND_MSG(color_regions[i].start_key == p_start_key, "Color region with start key '" + p_start_key + "' already exists."); if (p_start_key.length() < color_regions[i].start_key.length()) { at++; } else { @@ -913,6 +932,7 @@ void GDScriptSyntaxHighlighter::add_color_region(const String &p_start_key, cons } ColorRegion color_region; + color_region.type = p_type; color_region.color = p_color; color_region.start_key = p_start_key; color_region.end_key = p_end_key; diff --git a/modules/gdscript/editor/gdscript_highlighter.h b/modules/gdscript/editor/gdscript_highlighter.h index 090857f397..eb7bb7d801 100644 --- a/modules/gdscript/editor/gdscript_highlighter.h +++ b/modules/gdscript/editor/gdscript_highlighter.h @@ -39,6 +39,15 @@ class GDScriptSyntaxHighlighter : public EditorSyntaxHighlighter { private: struct ColorRegion { + enum Type { + TYPE_NONE, + TYPE_STRING, // `"` and `'`, optional prefix `&`, `^`, or `r`. + TYPE_MULTILINE_STRING, // `"""` and `'''`, optional prefix `r`. + TYPE_COMMENT, // `#` and `##`. + TYPE_CODE_REGION, // `#region` and `#endregion`. + }; + + Type type = TYPE_NONE; Color color; String start_key; String end_key; @@ -94,7 +103,7 @@ private: Color comment_marker_colors[COMMENT_MARKER_MAX]; HashMap<String, CommentMarkerLevel> comment_markers; - void add_color_region(const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only = false); + void add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only = false); public: virtual void _update_cache() override; diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp index becc2876f9..316281209a 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp @@ -40,17 +40,14 @@ void GDScriptEditorTranslationParserPlugin::get_recognized_extensions(List<Strin } Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) { - // Extract all translatable strings using the parsed tree from GDSriptParser. + // Extract all translatable strings using the parsed tree from GDScriptParser. // The strategy is to find all ExpressionNode and AssignmentNode from the tree and extract strings if relevant, i.e // Search strings in ExpressionNode -> CallNode -> tr(), set_text(), set_placeholder() etc. // Search strings in AssignmentNode -> text = "__", tooltip_text = "__" etc. Error err; Ref<Resource> loaded_res = ResourceLoader::load(p_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err); - if (err) { - ERR_PRINT("Failed to load " + p_path); - return err; - } + ERR_FAIL_COND_V_MSG(err, err, "Failed to load " + p_path); ids = r_ids; ids_ctx_plural = r_ids_ctx_plural; @@ -59,11 +56,11 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve GDScriptParser parser; err = parser.parse(source_code, p_path, false); - ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to parse GDScript with GDScriptParser."); + ERR_FAIL_COND_V_MSG(err, err, "Failed to parse GDScript with GDScriptParser."); GDScriptAnalyzer analyzer(&parser); err = analyzer.analyze(); - ERR_FAIL_COND_V_MSG(err != OK, err, "Failed to analyze GDScript with GDScriptAnalyzer."); + ERR_FAIL_COND_V_MSG(err, err, "Failed to analyze GDScript with GDScriptAnalyzer."); // Traverse through the parsed tree from GDScriptParser. GDScriptParser::ClassNode *c = parser.get_tree(); @@ -80,7 +77,7 @@ bool GDScriptEditorTranslationParserPlugin::_is_constant_string(const GDScriptPa void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) { for (int i = 0; i < p_class->members.size(); i++) { const GDScriptParser::ClassNode::Member &m = p_class->members[i]; - // There are 7 types of Member, but only class, function and variable can contain translatable strings. + // Other member types can't contain translatable strings. switch (m.type) { case GDScriptParser::ClassNode::Member::CLASS: _traverse_class(m.m_class); @@ -89,7 +86,11 @@ void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser _traverse_function(m.function); break; case GDScriptParser::ClassNode::Member::VARIABLE: - _read_variable(m.variable); + _assess_expression(m.variable->initializer); + if (m.variable->property == GDScriptParser::VariableNode::PROP_INLINE) { + _traverse_function(m.variable->setter); + _traverse_function(m.variable->getter); + } break; default: break; @@ -98,11 +99,14 @@ void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser } void GDScriptEditorTranslationParserPlugin::_traverse_function(const GDScriptParser::FunctionNode *p_func) { - _traverse_block(p_func->body); -} + if (!p_func) { + return; + } -void GDScriptEditorTranslationParserPlugin::_read_variable(const GDScriptParser::VariableNode *p_var) { - _assess_expression(p_var->initializer); + for (int i = 0; i < p_func->parameters.size(); i++) { + _assess_expression(p_func->parameters[i]->initializer); + } + _traverse_block(p_func->body); } void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser::SuiteNode *p_suite) { @@ -114,53 +118,51 @@ void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser for (int i = 0; i < statements.size(); i++) { const GDScriptParser::Node *statement = statements[i]; - // Statements with Node type constant, break, continue, pass, breakpoint are skipped because they can't contain translatable strings. + // BREAK, BREAKPOINT, CONSTANT, CONTINUE, and PASS are skipped because they can't contain translatable strings. switch (statement->type) { - case GDScriptParser::Node::VARIABLE: - _assess_expression(static_cast<const GDScriptParser::VariableNode *>(statement)->initializer); - break; + case GDScriptParser::Node::ASSERT: { + const GDScriptParser::AssertNode *assert_node = static_cast<const GDScriptParser::AssertNode *>(statement); + _assess_expression(assert_node->condition); + _assess_expression(assert_node->message); + } break; + case GDScriptParser::Node::ASSIGNMENT: { + _assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(statement)); + } break; + case GDScriptParser::Node::FOR: { + const GDScriptParser::ForNode *for_node = static_cast<const GDScriptParser::ForNode *>(statement); + _assess_expression(for_node->list); + _traverse_block(for_node->loop); + } break; case GDScriptParser::Node::IF: { const GDScriptParser::IfNode *if_node = static_cast<const GDScriptParser::IfNode *>(statement); _assess_expression(if_node->condition); - //FIXME : if the elif logic is changed in GDScriptParser, then this probably will have to change as well. See GDScriptParser::TreePrinter::print_if(). _traverse_block(if_node->true_block); _traverse_block(if_node->false_block); - break; - } - case GDScriptParser::Node::FOR: { - const GDScriptParser::ForNode *for_node = static_cast<const GDScriptParser::ForNode *>(statement); - _assess_expression(for_node->list); - _traverse_block(for_node->loop); - break; - } - case GDScriptParser::Node::WHILE: { - const GDScriptParser::WhileNode *while_node = static_cast<const GDScriptParser::WhileNode *>(statement); - _assess_expression(while_node->condition); - _traverse_block(while_node->loop); - break; - } + } break; case GDScriptParser::Node::MATCH: { const GDScriptParser::MatchNode *match_node = static_cast<const GDScriptParser::MatchNode *>(statement); _assess_expression(match_node->test); for (int j = 0; j < match_node->branches.size(); j++) { + _traverse_block(match_node->branches[j]->guard_body); _traverse_block(match_node->branches[j]->block); } - break; - } - case GDScriptParser::Node::RETURN: + } break; + case GDScriptParser::Node::RETURN: { _assess_expression(static_cast<const GDScriptParser::ReturnNode *>(statement)->return_value); - break; - case GDScriptParser::Node::ASSERT: - _assess_expression((static_cast<const GDScriptParser::AssertNode *>(statement))->condition); - break; - case GDScriptParser::Node::ASSIGNMENT: - _assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(statement)); - break; - default: + } break; + case GDScriptParser::Node::VARIABLE: { + _assess_expression(static_cast<const GDScriptParser::VariableNode *>(statement)->initializer); + } break; + case GDScriptParser::Node::WHILE: { + const GDScriptParser::WhileNode *while_node = static_cast<const GDScriptParser::WhileNode *>(statement); + _assess_expression(while_node->condition); + _traverse_block(while_node->loop); + } break; + default: { if (statement->is_expression()) { _assess_expression(static_cast<const GDScriptParser::ExpressionNode *>(statement)); } - break; + } break; } } } @@ -172,31 +174,30 @@ void GDScriptEditorTranslationParserPlugin::_assess_expression(const GDScriptPar return; } - // ExpressionNode of type await, cast, get_node, identifier, literal, preload, self, subscript, unary are ignored as they can't be CallNode - // containing translation strings. + // GET_NODE, IDENTIFIER, LITERAL, PRELOAD, SELF, and TYPE are skipped because they can't contain translatable strings. switch (p_expression->type) { case GDScriptParser::Node::ARRAY: { const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(p_expression); for (int i = 0; i < array_node->elements.size(); i++) { _assess_expression(array_node->elements[i]); } - break; - } - case GDScriptParser::Node::ASSIGNMENT: + } break; + case GDScriptParser::Node::ASSIGNMENT: { _assess_assignment(static_cast<const GDScriptParser::AssignmentNode *>(p_expression)); - break; + } break; + case GDScriptParser::Node::AWAIT: { + _assess_expression(static_cast<const GDScriptParser::AwaitNode *>(p_expression)->to_await); + } break; case GDScriptParser::Node::BINARY_OPERATOR: { const GDScriptParser::BinaryOpNode *binary_op_node = static_cast<const GDScriptParser::BinaryOpNode *>(p_expression); _assess_expression(binary_op_node->left_operand); _assess_expression(binary_op_node->right_operand); - break; - } + } break; case GDScriptParser::Node::CALL: { - const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_expression); - _extract_from_call(call_node); - for (int i = 0; i < call_node->arguments.size(); i++) { - _assess_expression(call_node->arguments[i]); - } + _assess_call(static_cast<const GDScriptParser::CallNode *>(p_expression)); + } break; + case GDScriptParser::Node::CAST: { + _assess_expression(static_cast<const GDScriptParser::CastNode *>(p_expression)->operand); } break; case GDScriptParser::Node::DICTIONARY: { const GDScriptParser::DictionaryNode *dict_node = static_cast<const GDScriptParser::DictionaryNode *>(p_expression); @@ -204,21 +205,38 @@ void GDScriptEditorTranslationParserPlugin::_assess_expression(const GDScriptPar _assess_expression(dict_node->elements[i].key); _assess_expression(dict_node->elements[i].value); } - break; - } + } break; + case GDScriptParser::Node::LAMBDA: { + _traverse_function(static_cast<const GDScriptParser::LambdaNode *>(p_expression)->function); + } break; + case GDScriptParser::Node::SUBSCRIPT: { + const GDScriptParser::SubscriptNode *subscript_node = static_cast<const GDScriptParser::SubscriptNode *>(p_expression); + _assess_expression(subscript_node->base); + if (!subscript_node->is_attribute) { + _assess_expression(subscript_node->index); + } + } break; case GDScriptParser::Node::TERNARY_OPERATOR: { const GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<const GDScriptParser::TernaryOpNode *>(p_expression); _assess_expression(ternary_op_node->condition); _assess_expression(ternary_op_node->true_expr); _assess_expression(ternary_op_node->false_expr); - break; - } - default: - break; + } break; + case GDScriptParser::Node::TYPE_TEST: { + _assess_expression(static_cast<const GDScriptParser::TypeTestNode *>(p_expression)->operand); + } break; + case GDScriptParser::Node::UNARY_OPERATOR: { + _assess_expression(static_cast<const GDScriptParser::UnaryOpNode *>(p_expression)->operand); + } break; + default: { + } break; } } void GDScriptEditorTranslationParserPlugin::_assess_assignment(const GDScriptParser::AssignmentNode *p_assignment) { + _assess_expression(p_assignment->assignee); + _assess_expression(p_assignment->assigned_value); + // Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____" StringName assignee_name; @@ -236,26 +254,18 @@ void GDScriptEditorTranslationParserPlugin::_assess_assignment(const GDScriptPar if (assignee_name != StringName() && assignment_patterns.has(assignee_name) && _is_constant_string(p_assignment->assigned_value)) { // If the assignment is towards one of the extract patterns (text, tooltip_text etc.), and the value is a constant string, we collect the string. ids->push_back(p_assignment->assigned_value->reduced_value); - } else if (assignee_name == fd_filters && p_assignment->assigned_value->type == GDScriptParser::Node::CALL) { - // FileDialog.filters accepts assignment in the form of PackedStringArray. For example, - // get_node("FileDialog").filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]). - - const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_assignment->assigned_value); - if (!call_node->arguments.is_empty() && call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { - const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]); - - // Extract the name in "extension ; name" of PackedStringArray. - for (int i = 0; i < array_node->elements.size(); i++) { - _extract_fd_constant_strings(array_node->elements[i]); - } - } - } else { - // If the assignee is not in extract patterns or the assigned_value is not a constant string, try to see if the assigned_value contains tr(). - _assess_expression(p_assignment->assigned_value); + } else if (assignee_name == fd_filters) { + // Extract from `get_node("FileDialog").filters = <filter array>`. + _extract_fd_filter_array(p_assignment->assigned_value); } } -void GDScriptEditorTranslationParserPlugin::_extract_from_call(const GDScriptParser::CallNode *p_call) { +void GDScriptEditorTranslationParserPlugin::_assess_call(const GDScriptParser::CallNode *p_call) { + _assess_expression(p_call->callee); + for (int i = 0; i < p_call->arguments.size(); i++) { + _assess_expression(p_call->arguments[i]); + } + // Extract the translatable strings coming from function calls. For example: // tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____"). @@ -300,52 +310,56 @@ void GDScriptEditorTranslationParserPlugin::_extract_from_call(const GDScriptPar ids_ctx_plural->push_back(id_ctx_plural); } } else if (first_arg_patterns.has(function_name)) { - if (_is_constant_string(p_call->arguments[0])) { + if (!p_call->arguments.is_empty() && _is_constant_string(p_call->arguments[0])) { ids->push_back(p_call->arguments[0]->reduced_value); } } else if (second_arg_patterns.has(function_name)) { - if (_is_constant_string(p_call->arguments[1])) { + if (p_call->arguments.size() > 1 && _is_constant_string(p_call->arguments[1])) { ids->push_back(p_call->arguments[1]->reduced_value); } } else if (function_name == fd_add_filter) { // Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images"). - _extract_fd_constant_strings(p_call->arguments[0]); - } else if (function_name == fd_set_filter && p_call->arguments[0]->type == GDScriptParser::Node::CALL) { - // FileDialog.set_filters() accepts assignment in the form of PackedStringArray. For example, - // get_node("FileDialog").set_filters( PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])). - - const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_call->arguments[0]); - if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { - const GDScriptParser::ArrayNode *array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]); - for (int i = 0; i < array_node->elements.size(); i++) { - _extract_fd_constant_strings(array_node->elements[i]); - } + if (!p_call->arguments.is_empty()) { + _extract_fd_filter_string(p_call->arguments[0]); } - } - - if (p_call->callee && p_call->callee->type == GDScriptParser::Node::SUBSCRIPT) { - const GDScriptParser::SubscriptNode *subscript_node = static_cast<const GDScriptParser::SubscriptNode *>(p_call->callee); - if (subscript_node->base && subscript_node->base->type == GDScriptParser::Node::CALL) { - const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(subscript_node->base); - _extract_from_call(call_node); + } else if (function_name == fd_set_filter) { + // Extract from `get_node("FileDialog").set_filters(<filter array>)`. + if (!p_call->arguments.is_empty()) { + _extract_fd_filter_array(p_call->arguments[0]); } } } -void GDScriptEditorTranslationParserPlugin::_extract_fd_constant_strings(const GDScriptParser::ExpressionNode *p_expression) { +void GDScriptEditorTranslationParserPlugin::_extract_fd_filter_string(const GDScriptParser::ExpressionNode *p_expression) { // Extract the name in "extension ; name". - if (_is_constant_string(p_expression)) { - String arg_val = p_expression->reduced_value; - PackedStringArray arr = arg_val.split(";", true); - if (arr.size() != 2) { - ERR_PRINT("Argument for setting FileDialog has bad format."); - return; - } + PackedStringArray arr = p_expression->reduced_value.operator String().split(";", true); + ERR_FAIL_COND_MSG(arr.size() != 2, "Argument for setting FileDialog has bad format."); ids->push_back(arr[1].strip_edges()); } } +void GDScriptEditorTranslationParserPlugin::_extract_fd_filter_array(const GDScriptParser::ExpressionNode *p_expression) { + const GDScriptParser::ArrayNode *array_node = nullptr; + + if (p_expression->type == GDScriptParser::Node::ARRAY) { + // Extract from `["*.png ; PNG Images","*.gd ; GDScript Files"]` (implicit cast to `PackedStringArray`). + array_node = static_cast<const GDScriptParser::ArrayNode *>(p_expression); + } else if (p_expression->type == GDScriptParser::Node::CALL) { + // Extract from `PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])`. + const GDScriptParser::CallNode *call_node = static_cast<const GDScriptParser::CallNode *>(p_expression); + if (call_node->get_callee_type() == GDScriptParser::Node::IDENTIFIER && call_node->function_name == SNAME("PackedStringArray") && !call_node->arguments.is_empty() && call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { + array_node = static_cast<const GDScriptParser::ArrayNode *>(call_node->arguments[0]); + } + } + + if (array_node) { + for (int i = 0; i < array_node->elements.size(); i++) { + _extract_fd_filter_string(array_node->elements[i]); + } + } +} + GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() { assignment_patterns.insert("text"); assignment_patterns.insert("placeholder_text"); diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.h b/modules/gdscript/editor/gdscript_translation_parser_plugin.h index 580c2a80cd..fe876134c2 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.h +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.h @@ -59,11 +59,12 @@ class GDScriptEditorTranslationParserPlugin : public EditorTranslationParserPlug void _traverse_function(const GDScriptParser::FunctionNode *p_func); void _traverse_block(const GDScriptParser::SuiteNode *p_suite); - void _read_variable(const GDScriptParser::VariableNode *p_var); void _assess_expression(const GDScriptParser::ExpressionNode *p_expression); void _assess_assignment(const GDScriptParser::AssignmentNode *p_assignment); - void _extract_from_call(const GDScriptParser::CallNode *p_call); - void _extract_fd_constant_strings(const GDScriptParser::ExpressionNode *p_expression); + void _assess_call(const GDScriptParser::CallNode *p_call); + + void _extract_fd_filter_string(const GDScriptParser::ExpressionNode *p_expression); + void _extract_fd_filter_array(const GDScriptParser::ExpressionNode *p_expression); public: virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override; diff --git a/modules/gdscript/editor/script_templates/CharacterBody2D/basic_movement.gd b/modules/gdscript/editor/script_templates/CharacterBody2D/basic_movement.gd index 28ab080dd2..bd4816827f 100644 --- a/modules/gdscript/editor/script_templates/CharacterBody2D/basic_movement.gd +++ b/modules/gdscript/editor/script_templates/CharacterBody2D/basic_movement.gd @@ -6,14 +6,11 @@ extends _BASE_ const SPEED = 300.0 const JUMP_VELOCITY = -400.0 -# Get the gravity from the project settings to be synced with RigidBody nodes. -var gravity: int = ProjectSettings.get_setting("physics/2d/default_gravity") - func _physics_process(delta: float) -> void: # Add the gravity. if not is_on_floor(): - velocity.y += gravity * delta + velocity += get_gravity() * delta # Handle jump. if Input.is_action_just_pressed("ui_accept") and is_on_floor(): diff --git a/modules/gdscript/editor/script_templates/CharacterBody3D/basic_movement.gd b/modules/gdscript/editor/script_templates/CharacterBody3D/basic_movement.gd index 9b0e4be4ed..f9c4f70a24 100644 --- a/modules/gdscript/editor/script_templates/CharacterBody3D/basic_movement.gd +++ b/modules/gdscript/editor/script_templates/CharacterBody3D/basic_movement.gd @@ -6,14 +6,11 @@ extends _BASE_ const SPEED = 5.0 const JUMP_VELOCITY = 4.5 -# Get the gravity from the project settings to be synced with RigidBody nodes. -var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") - func _physics_process(delta: float) -> void: # Add the gravity. if not is_on_floor(): - velocity.y -= gravity * delta + velocity += get_gravity() * delta # Handle jump. if Input.is_action_just_pressed("ui_accept") and is_on_floor(): diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 58aa7a08d4..94aa077014 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -35,6 +35,7 @@ #include "gdscript_compiler.h" #include "gdscript_parser.h" #include "gdscript_rpc_callable.h" +#include "gdscript_tokenizer_buffer.h" #include "gdscript_warning.h" #ifdef TOOLS_ENABLED @@ -94,7 +95,7 @@ Variant GDScriptNativeClass::_new() { } Object *GDScriptNativeClass::instantiate() { - return ClassDB::instantiate(name); + return ClassDB::instantiate_no_placeholders(name); } Variant GDScriptNativeClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { @@ -162,13 +163,14 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco _super_implicit_constructor(this, instance, r_error); if (r_error.error != Callable::CallError::CALL_OK) { + String error_text = Variant::get_call_error_text(instance->owner, "@implicit_new", nullptr, 0, r_error); instance->script = Ref<GDScript>(); instance->owner->set_script_instance(nullptr); { MutexLock lock(GDScriptLanguage::singleton->mutex); instances.erase(p_owner); } - ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance."); + ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); } if (p_argcount < 0) { @@ -179,13 +181,14 @@ GDScriptInstance *GDScript::_create_instance(const Variant **p_args, int p_argco if (initializer != nullptr) { initializer->call(instance, p_args, p_argcount, r_error); if (r_error.error != Callable::CallError::CALL_OK) { + String error_text = Variant::get_call_error_text(instance->owner, "_init", p_args, p_argcount, r_error); instance->script = Ref<GDScript>(); instance->owner->set_script_instance(nullptr); { MutexLock lock(GDScriptLanguage::singleton->mutex); instances.erase(p_owner); } - ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance."); + ERR_FAIL_V_MSG(nullptr, "Error constructing a GDScriptInstance: " + error_text); } } //@TODO make thread safe @@ -738,7 +741,12 @@ Error GDScript::reload(bool p_keep_state) { valid = false; GDScriptParser parser; - Error err = parser.parse(source, path, false); + Error err; + if (!binary_tokens.is_empty()) { + err = parser.parse_binary(binary_tokens, path); + } else { + err = parser.parse(source, path, false); + } if (err) { if (EngineDebugger::is_active()) { GDScriptLanguage::get_singleton()->debug_break_parse(_get_debug_path(), parser.get_errors().front()->get().line, "Parser Error: " + parser.get_errors().front()->get().message); @@ -992,6 +1000,7 @@ void GDScript::set_path(const String &p_path, bool p_take_over) { String old_path = path; path = p_path; + path_valid = true; GDScriptCache::move_script(old_path, p_path); for (KeyValue<StringName, Ref<GDScript>> &kv : subclasses) { @@ -1000,6 +1009,9 @@ void GDScript::set_path(const String &p_path, bool p_take_over) { } String GDScript::get_script_path() const { + if (!path_valid && !get_path().is_empty()) { + return get_path(); + } return path; } @@ -1035,6 +1047,7 @@ Error GDScript::load_source_code(const String &p_path) { source = s; path = p_path; + path_valid = true; #ifdef TOOLS_ENABLED source_changed_cache = true; set_edited(false); @@ -1043,6 +1056,19 @@ Error GDScript::load_source_code(const String &p_path) { return OK; } +void GDScript::set_binary_tokens_source(const Vector<uint8_t> &p_binary_tokens) { + binary_tokens = p_binary_tokens; +} + +const Vector<uint8_t> &GDScript::get_binary_tokens_source() const { + return binary_tokens; +} + +Vector<uint8_t> GDScript::get_as_binary_tokens() const { + GDScriptTokenizerBuffer tokenizer; + return tokenizer.parse_code_string(source, GDScriptTokenizerBuffer::COMPRESS_NONE); +} + const HashMap<StringName, GDScriptFunction *> &GDScript::debug_get_member_functions() const { return member_functions; } @@ -1112,8 +1138,7 @@ GDScript *GDScript::find_class(const String &p_qualified_name) { // Starts at index 1 because index 0 was handled above. for (int i = 1; result != nullptr && i < class_names.size(); i++) { - String current_name = class_names[i]; - if (HashMap<StringName, Ref<GDScript>>::Iterator E = result->subclasses.find(current_name)) { + if (HashMap<StringName, Ref<GDScript>>::Iterator E = result->subclasses.find(class_names[i])) { result = E->value.ptr(); } else { // Couldn't find inner class. @@ -1145,14 +1170,14 @@ GDScript *GDScript::get_root_script() { RBSet<GDScript *> GDScript::get_dependencies() { RBSet<GDScript *> dependencies; - _get_dependencies(dependencies, this); + _collect_dependencies(dependencies, this); dependencies.erase(this); return dependencies; } -RBSet<GDScript *> GDScript::get_inverted_dependencies() { - RBSet<GDScript *> inverted_dependencies; +HashMap<GDScript *, RBSet<GDScript *>> GDScript::get_all_dependencies() { + HashMap<GDScript *, RBSet<GDScript *>> all_dependencies; List<GDScript *> scripts; { @@ -1166,51 +1191,42 @@ RBSet<GDScript *> GDScript::get_inverted_dependencies() { } for (GDScript *scr : scripts) { - if (scr == nullptr || scr == this || scr->destructing) { + if (scr == nullptr || scr->destructing) { continue; } - - RBSet<GDScript *> scr_dependencies = scr->get_dependencies(); - if (scr_dependencies.has(this)) { - inverted_dependencies.insert(scr); - } + all_dependencies.insert(scr, scr->get_dependencies()); } - return inverted_dependencies; + return all_dependencies; } RBSet<GDScript *> GDScript::get_must_clear_dependencies() { RBSet<GDScript *> dependencies = get_dependencies(); RBSet<GDScript *> must_clear_dependencies; - HashMap<GDScript *, RBSet<GDScript *>> inverted_dependencies; - - for (GDScript *E : dependencies) { - inverted_dependencies.insert(E, E->get_inverted_dependencies()); - } + HashMap<GDScript *, RBSet<GDScript *>> all_dependencies = get_all_dependencies(); RBSet<GDScript *> cant_clear; - for (KeyValue<GDScript *, RBSet<GDScript *>> &E : inverted_dependencies) { + for (KeyValue<GDScript *, RBSet<GDScript *>> &E : all_dependencies) { + if (dependencies.has(E.key)) { + continue; + } for (GDScript *F : E.value) { - if (!dependencies.has(F)) { - cant_clear.insert(E.key); - for (GDScript *G : E.key->get_dependencies()) { - cant_clear.insert(G); - } - break; + if (dependencies.has(F)) { + cant_clear.insert(F); } } } - for (KeyValue<GDScript *, RBSet<GDScript *>> &E : inverted_dependencies) { - if (cant_clear.has(E.key) || ScriptServer::is_global_class(E.key->get_fully_qualified_name())) { + for (GDScript *E : dependencies) { + if (cant_clear.has(E) || ScriptServer::is_global_class(E->get_fully_qualified_name())) { continue; } - must_clear_dependencies.insert(E.key); + must_clear_dependencies.insert(E); } cant_clear.clear(); dependencies.clear(); - inverted_dependencies.clear(); + all_dependencies.clear(); return must_clear_dependencies; } @@ -1260,52 +1276,55 @@ GDScript *GDScript::_get_gdscript_from_variant(const Variant &p_variant) { return Object::cast_to<GDScript>(obj); } -void GDScript::_get_dependencies(RBSet<GDScript *> &p_dependencies, const GDScript *p_except) { +void GDScript::_collect_function_dependencies(GDScriptFunction *p_func, RBSet<GDScript *> &p_dependencies, const GDScript *p_except) { + if (p_func == nullptr) { + return; + } + for (GDScriptFunction *lambda : p_func->lambdas) { + _collect_function_dependencies(lambda, p_dependencies, p_except); + } + for (const Variant &V : p_func->constants) { + GDScript *scr = _get_gdscript_from_variant(V); + if (scr != nullptr && scr != p_except) { + scr->_collect_dependencies(p_dependencies, p_except); + } + } +} + +void GDScript::_collect_dependencies(RBSet<GDScript *> &p_dependencies, const GDScript *p_except) { if (p_dependencies.has(this)) { return; } - p_dependencies.insert(this); + if (this != p_except) { + p_dependencies.insert(this); + } for (const KeyValue<StringName, GDScriptFunction *> &E : member_functions) { - if (E.value == nullptr) { - continue; - } - for (const Variant &V : E.value->constants) { - GDScript *scr = _get_gdscript_from_variant(V); - if (scr != nullptr && scr != p_except) { - scr->_get_dependencies(p_dependencies, p_except); - } - } + _collect_function_dependencies(E.value, p_dependencies, p_except); } if (implicit_initializer) { - for (const Variant &V : implicit_initializer->constants) { - GDScript *scr = _get_gdscript_from_variant(V); - if (scr != nullptr && scr != p_except) { - scr->_get_dependencies(p_dependencies, p_except); - } - } + _collect_function_dependencies(implicit_initializer, p_dependencies, p_except); } if (implicit_ready) { - for (const Variant &V : implicit_ready->constants) { - GDScript *scr = _get_gdscript_from_variant(V); - if (scr != nullptr && scr != p_except) { - scr->_get_dependencies(p_dependencies, p_except); - } - } + _collect_function_dependencies(implicit_ready, p_dependencies, p_except); + } + + if (static_initializer) { + _collect_function_dependencies(static_initializer, p_dependencies, p_except); } for (KeyValue<StringName, Ref<GDScript>> &E : subclasses) { if (E.value != p_except) { - E.value->_get_dependencies(p_dependencies, p_except); + E.value->_collect_dependencies(p_dependencies, p_except); } } for (const KeyValue<StringName, Variant> &E : constants) { GDScript *scr = _get_gdscript_from_variant(E.value); if (scr != nullptr && scr != p_except) { - scr->_get_dependencies(p_dependencies, p_except); + scr->_collect_dependencies(p_dependencies, p_except); } } } @@ -1386,6 +1405,53 @@ String GDScript::debug_get_script_name(const Ref<Script> &p_script) { } #endif +String GDScript::canonicalize_path(const String &p_path) { + if (p_path.get_extension() == "gdc") { + return p_path.get_basename() + ".gd"; + } + return p_path; +} + +GDScript::UpdatableFuncPtr::UpdatableFuncPtr(GDScriptFunction *p_function) { + if (p_function == nullptr) { + return; + } + + ptr = p_function; + script = ptr->get_script(); + ERR_FAIL_NULL(script); + + MutexLock script_lock(script->func_ptrs_to_update_mutex); + list_element = script->func_ptrs_to_update.push_back(this); +} + +GDScript::UpdatableFuncPtr::~UpdatableFuncPtr() { + ERR_FAIL_NULL(script); + + if (list_element) { + MutexLock script_lock(script->func_ptrs_to_update_mutex); + list_element->erase(); + list_element = nullptr; + } +} + +void GDScript::_recurse_replace_function_ptrs(const HashMap<GDScriptFunction *, GDScriptFunction *> &p_replacements) const { + MutexLock lock(func_ptrs_to_update_mutex); + for (UpdatableFuncPtr *updatable : func_ptrs_to_update) { + HashMap<GDScriptFunction *, GDScriptFunction *>::ConstIterator replacement = p_replacements.find(updatable->ptr); + if (replacement) { + updatable->ptr = replacement->value; + } else { + // Probably a lambda from another reload, ignore. + updatable->ptr = nullptr; + } + } + + for (HashMap<StringName, Ref<GDScript>>::ConstIterator subscript = subclasses.begin(); subscript; ++subscript) { + subscript->value->_recurse_replace_function_ptrs(p_replacements); + } +} + void GDScript::clear(ClearData *p_clear_data) { if (clearing) { return; @@ -1403,6 +1469,13 @@ void GDScript::clear(ClearData *p_clear_data) { is_root = true; } + { + MutexLock lock(func_ptrs_to_update_mutex); + for (UpdatableFuncPtr *updatable : func_ptrs_to_update) { + updatable->ptr = nullptr; + } + } + RBSet<GDScript *> must_clear_dependencies = get_must_clear_dependencies(); for (GDScript *E : must_clear_dependencies) { clear_data->scripts.insert(E); @@ -1472,6 +1545,13 @@ GDScript::~GDScript() { } destructing = true; + if (is_print_verbose_enabled()) { + MutexLock lock(func_ptrs_to_update_mutex); + if (!func_ptrs_to_update.is_empty()) { + print_line(vformat("GDScript: %d orphaned lambdas becoming invalid at destruction of script '%s'.", func_ptrs_to_update.size(), fully_qualified_name)); + } + } + clear(); { @@ -1616,7 +1696,7 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { { HashMap<StringName, MethodInfo>::ConstIterator E = sptr->_signals.find(p_name); if (E) { - r_ret = Signal(this->owner, E->key); + r_ret = Signal(owner, E->key); return true; } } @@ -1625,9 +1705,9 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { HashMap<StringName, GDScriptFunction *>::ConstIterator E = sptr->member_functions.find(p_name); if (E) { if (sptr->rpc_config.has(p_name)) { - r_ret = Callable(memnew(GDScriptRPCCallable(this->owner, E->key))); + r_ret = Callable(memnew(GDScriptRPCCallable(owner, E->key))); } else { - r_ret = Callable(this->owner, E->key); + r_ret = Callable(owner, E->key); } return true; } @@ -2105,7 +2185,7 @@ void GDScriptLanguage::finish() { void GDScriptLanguage::profiling_start() { #ifdef DEBUG_ENABLED - MutexLock lock(this->mutex); + MutexLock lock(mutex); SelfList<GDScriptFunction> *elem = function_list.first(); while (elem) { @@ -2118,6 +2198,8 @@ void GDScriptLanguage::profiling_start() { elem->self()->profile.last_frame_call_count = 0; elem->self()->profile.last_frame_self_time = 0; elem->self()->profile.last_frame_total_time = 0; + elem->self()->profile.native_calls.clear(); + elem->self()->profile.last_native_calls.clear(); elem = elem->next(); } @@ -2125,9 +2207,16 @@ void GDScriptLanguage::profiling_start() { #endif } +void GDScriptLanguage::profiling_set_save_native_calls(bool p_enable) { +#ifdef DEBUG_ENABLED + MutexLock lock(mutex); + profile_native_calls = p_enable; +#endif +} + void GDScriptLanguage::profiling_stop() { #ifdef DEBUG_ENABLED - MutexLock lock(this->mutex); + MutexLock lock(mutex); profiling = false; #endif @@ -2137,19 +2226,34 @@ int GDScriptLanguage::profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int current = 0; #ifdef DEBUG_ENABLED - MutexLock lock(this->mutex); + MutexLock lock(mutex); + profiling_collate_native_call_data(true); SelfList<GDScriptFunction> *elem = function_list.first(); while (elem) { if (current >= p_info_max) { break; } + int last_non_internal = current; p_info_arr[current].call_count = elem->self()->profile.call_count.get(); p_info_arr[current].self_time = elem->self()->profile.self_time.get(); p_info_arr[current].total_time = elem->self()->profile.total_time.get(); p_info_arr[current].signature = elem->self()->profile.signature; - elem = elem->next(); current++; + + int nat_time = 0; + HashMap<String, GDScriptFunction::Profile::NativeProfile>::ConstIterator nat_calls = elem->self()->profile.native_calls.begin(); + while (nat_calls) { + p_info_arr[current].call_count = nat_calls->value.call_count; + p_info_arr[current].total_time = nat_calls->value.total_time; + p_info_arr[current].self_time = nat_calls->value.total_time; + p_info_arr[current].signature = nat_calls->value.signature; + nat_time += nat_calls->value.total_time; + current++; + ++nat_calls; + } + p_info_arr[last_non_internal].internal_time = nat_time; + elem = elem->next(); } #endif @@ -2160,19 +2264,35 @@ int GDScriptLanguage::profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_ int current = 0; #ifdef DEBUG_ENABLED - MutexLock lock(this->mutex); + MutexLock lock(mutex); + profiling_collate_native_call_data(false); SelfList<GDScriptFunction> *elem = function_list.first(); while (elem) { if (current >= p_info_max) { break; } if (elem->self()->profile.last_frame_call_count > 0) { + int last_non_internal = current; p_info_arr[current].call_count = elem->self()->profile.last_frame_call_count; p_info_arr[current].self_time = elem->self()->profile.last_frame_self_time; p_info_arr[current].total_time = elem->self()->profile.last_frame_total_time; p_info_arr[current].signature = elem->self()->profile.signature; current++; + + int nat_time = 0; + HashMap<String, GDScriptFunction::Profile::NativeProfile>::ConstIterator nat_calls = elem->self()->profile.last_native_calls.begin(); + while (nat_calls) { + p_info_arr[current].call_count = nat_calls->value.call_count; + p_info_arr[current].total_time = nat_calls->value.total_time; + p_info_arr[current].self_time = nat_calls->value.total_time; + p_info_arr[current].internal_time = nat_calls->value.total_time; + p_info_arr[current].signature = nat_calls->value.signature; + nat_time += nat_calls->value.total_time; + current++; + ++nat_calls; + } + p_info_arr[last_non_internal].internal_time = nat_time; } elem = elem->next(); } @@ -2181,6 +2301,33 @@ int GDScriptLanguage::profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_ return current; } +void GDScriptLanguage::profiling_collate_native_call_data(bool p_accumulated) { +#ifdef DEBUG_ENABLED + // The same native call can be called from multiple functions, so join them together here. + // Only use the name of the function (ie signature.split[2]). + HashMap<String, GDScriptFunction::Profile::NativeProfile *> seen_nat_calls; + SelfList<GDScriptFunction> *elem = function_list.first(); + while (elem) { + HashMap<String, GDScriptFunction::Profile::NativeProfile> *nat_calls = p_accumulated ? &elem->self()->profile.native_calls : &elem->self()->profile.last_native_calls; + HashMap<String, GDScriptFunction::Profile::NativeProfile>::Iterator it = nat_calls->begin(); + + while (it != nat_calls->end()) { + Vector<String> sig = it->value.signature.split("::"); + HashMap<String, GDScriptFunction::Profile::NativeProfile *>::ConstIterator already_found = seen_nat_calls.find(sig[2]); + if (already_found) { + already_found->value->total_time += it->value.total_time; + already_found->value->call_count += it->value.call_count; + elem->self()->profile.last_native_calls.remove(it); + } else { + seen_nat_calls.insert(sig[2], &it->value); + } + ++it; + } + elem = elem->next(); + } +#endif +} + struct GDScriptDepSort { //must support sorting so inheritance works properly (parent must be reloaded first) bool operator()(const Ref<GDScript> &A, const Ref<GDScript> &B) const { @@ -2204,39 +2351,43 @@ struct GDScriptDepSort { void GDScriptLanguage::reload_all_scripts() { #ifdef DEBUG_ENABLED print_verbose("GDScript: Reloading all scripts"); - List<Ref<GDScript>> scripts; + Array scripts; { - MutexLock lock(this->mutex); + MutexLock lock(mutex); SelfList<GDScript> *elem = script_list.first(); while (elem) { - // Scripts will reload all subclasses, so only reload root scripts. - if (elem->self()->is_root_script() && elem->self()->get_path().is_resource_file()) { + if (elem->self()->get_path().is_resource_file()) { print_verbose("GDScript: Found: " + elem->self()->get_path()); scripts.push_back(Ref<GDScript>(elem->self())); //cast to gdscript to avoid being erased by accident } elem = elem->next(); } - } - //as scripts are going to be reloaded, must proceed without locking here - - scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order - - for (Ref<GDScript> &scr : scripts) { - print_verbose("GDScript: Reloading: " + scr->get_path()); - scr->load_source_code(scr->get_path()); - scr->reload(true); +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + // Reload all pointers to existing singletons so that tool scripts can work with the reloaded extensions. + List<Engine::Singleton> singletons; + Engine::get_singleton()->get_singletons(&singletons); + for (const Engine::Singleton &E : singletons) { + if (globals.has(E.name)) { + _add_global(E.name, E.ptr); + } + } + } +#endif } + + reload_scripts(scripts, true); #endif } -void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) { +void GDScriptLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload) { #ifdef DEBUG_ENABLED List<Ref<GDScript>> scripts; { - MutexLock lock(this->mutex); + MutexLock lock(mutex); SelfList<GDScript> *elem = script_list.first(); while (elem) { @@ -2257,7 +2408,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order for (Ref<GDScript> &scr : scripts) { - bool reload = scr == p_script || to_reload.has(scr->get_base()); + bool reload = p_scripts.has(scr) || to_reload.has(scr->get_base()); if (!reload) { continue; @@ -2280,7 +2431,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so } } -//same thing for placeholders + //same thing for placeholders #ifdef TOOLS_ENABLED while (scr->placeholders.size()) { @@ -2308,6 +2459,8 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so for (KeyValue<Ref<GDScript>, HashMap<ObjectID, List<Pair<StringName, Variant>>>> &E : to_reload) { Ref<GDScript> scr = E.key; + print_verbose("GDScript: Reloading: " + scr->get_path()); + scr->load_source_code(scr->get_path()); scr->reload(p_soft_reload); //restore state if saved @@ -2355,21 +2508,29 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so #endif } +void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) { + Array scripts; + scripts.push_back(p_script); + reload_scripts(scripts, p_soft_reload); +} + void GDScriptLanguage::frame() { calls = 0; #ifdef DEBUG_ENABLED if (profiling) { - MutexLock lock(this->mutex); + MutexLock lock(mutex); SelfList<GDScriptFunction> *elem = function_list.first(); while (elem) { elem->self()->profile.last_frame_call_count = elem->self()->profile.frame_call_count.get(); elem->self()->profile.last_frame_self_time = elem->self()->profile.frame_self_time.get(); elem->self()->profile.last_frame_total_time = elem->self()->profile.frame_total_time.get(); + elem->self()->profile.last_native_calls = elem->self()->profile.native_calls; elem->self()->profile.frame_call_count.set(0); elem->self()->profile.frame_self_time.set(0); elem->self()->profile.frame_total_time.set(0); + elem->self()->profile.native_calls.clear(); elem = elem->next(); } } @@ -2443,7 +2604,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const { } } -bool GDScriptLanguage::is_control_flow_keyword(String p_keyword) const { +bool GDScriptLanguage::is_control_flow_keyword(const String &p_keyword) const { // Please keep alphabetical order. return p_keyword == "break" || p_keyword == "continue" || @@ -2656,11 +2817,12 @@ Ref<GDScript> GDScriptLanguage::get_script_by_fully_qualified_name(const String Ref<Resource> ResourceFormatLoaderGDScript::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { Error err; - Ref<GDScript> scr = GDScriptCache::get_full_script(p_path, err, "", p_cache_mode == CACHE_MODE_IGNORE); + bool ignoring = p_cache_mode == CACHE_MODE_IGNORE || p_cache_mode == CACHE_MODE_IGNORE_DEEP; + Ref<GDScript> scr = GDScriptCache::get_full_script(p_original_path, err, "", ignoring); if (err && scr.is_valid()) { // If !scr.is_valid(), the error was likely from scr->load_source_code(), which already generates an error. - ERR_PRINT_ED(vformat(R"(Failed to load script "%s" with error "%s".)", p_path, error_names[err])); + ERR_PRINT_ED(vformat(R"(Failed to load script "%s" with error "%s".)", p_original_path, error_names[err])); } if (r_error) { @@ -2673,6 +2835,7 @@ Ref<Resource> ResourceFormatLoaderGDScript::load(const String &p_path, const Str void ResourceFormatLoaderGDScript::get_recognized_extensions(List<String> *p_extensions) const { p_extensions->push_back("gd"); + p_extensions->push_back("gdc"); } bool ResourceFormatLoaderGDScript::handles_type(const String &p_type) const { @@ -2681,7 +2844,7 @@ bool ResourceFormatLoaderGDScript::handles_type(const String &p_type) const { String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) const { String el = p_path.get_extension().to_lower(); - if (el == "gd") { + if (el == "gd" || el == "gdc") { return "GDScript"; } return ""; diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index d335ec85ee..7c471c285b 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -86,6 +86,8 @@ class GDScript : public Script { friend class GDScriptAnalyzer; friend class GDScriptCompiler; friend class GDScriptDocGen; + friend class GDScriptLambdaCallable; + friend class GDScriptLambdaSelfCallable; friend class GDScriptLanguage; friend struct GDScriptUtilityFunctionsDefinitions; @@ -108,6 +110,36 @@ class GDScript : public Script { HashMap<StringName, MethodInfo> _signals; Dictionary rpc_config; + struct LambdaInfo { + int capture_count; + bool use_self; + }; + + HashMap<GDScriptFunction *, LambdaInfo> lambda_info; + +public: + class UpdatableFuncPtr { + friend class GDScript; + + GDScriptFunction *ptr = nullptr; + GDScript *script = nullptr; + List<UpdatableFuncPtr *>::Element *list_element = nullptr; + + public: + GDScriptFunction *operator->() const { return ptr; } + operator GDScriptFunction *() const { return ptr; } + + UpdatableFuncPtr(GDScriptFunction *p_function); + ~UpdatableFuncPtr(); + }; + +private: + // List is used here because a ptr to elements are stored, so the memory locations need to be stable + List<UpdatableFuncPtr *> func_ptrs_to_update; + Mutex func_ptrs_to_update_mutex; + + void _recurse_replace_function_ptrs(const HashMap<GDScriptFunction *, GDScriptFunction *> &p_replacements) const; + #ifdef TOOLS_ENABLED // For static data storage during hot-reloading. HashMap<StringName, MemberInfo> old_static_variables_indices; @@ -144,7 +176,9 @@ class GDScript : public Script { bool clearing = false; //exported members String source; + Vector<uint8_t> binary_tokens; String path; + bool path_valid = false; // False if using default path. StringName local_name; // Inner class identifier or `class_name`. StringName global_name; // `class_name`. String fully_qualified_name; @@ -179,7 +213,8 @@ class GDScript : public Script { void _get_script_signal_list(List<MethodInfo> *r_list, bool p_include_base) const; GDScript *_get_gdscript_from_variant(const Variant &p_variant); - void _get_dependencies(RBSet<GDScript *> &p_dependencies, const GDScript *p_except); + void _collect_function_dependencies(GDScriptFunction *p_func, RBSet<GDScript *> &p_dependencies, const GDScript *p_except); + void _collect_dependencies(RBSet<GDScript *> &p_dependencies, const GDScript *p_except); protected: bool _get(const StringName &p_name, Variant &r_ret) const; @@ -195,6 +230,11 @@ public: static String debug_get_script_name(const Ref<Script> &p_script); #endif + static String canonicalize_path(const String &p_path); + _FORCE_INLINE_ static bool is_canonically_equal_paths(const String &p_path_a, const String &p_path_b) { + return canonicalize_path(p_path_a) == canonicalize_path(p_path_b); + } + _FORCE_INLINE_ StringName get_local_name() const { return local_name; } void clear(GDScript::ClearData *p_clear_data = nullptr); @@ -220,7 +260,7 @@ public: const Ref<GDScriptNativeClass> &get_native() const { return native; } RBSet<GDScript *> get_dependencies(); - RBSet<GDScript *> get_inverted_dependencies(); + HashMap<GDScript *, RBSet<GDScript *>> get_all_dependencies(); RBSet<GDScript *> get_must_clear_dependencies(); virtual bool has_script_signal(const StringName &p_signal) const override; @@ -263,6 +303,10 @@ public: String get_script_path() const; Error load_source_code(const String &p_path); + void set_binary_tokens_source(const Vector<uint8_t> &p_binary_tokens); + const Vector<uint8_t> &get_binary_tokens_source() const; + Vector<uint8_t> get_as_binary_tokens() const; + bool get_property_default_value(const StringName &p_property, Variant &r_value) const override; virtual void get_script_method_list(List<MethodInfo> *p_list) const override; @@ -405,6 +449,7 @@ class GDScriptLanguage : public ScriptLanguage { SelfList<GDScriptFunction>::List function_list; bool profiling; + bool profile_native_calls; uint64_t script_frame_time; HashMap<String, ObjectID> orphan_subclasses; @@ -500,13 +545,13 @@ public: /* EDITOR FUNCTIONS */ virtual void get_reserved_words(List<String> *p_words) const override; - virtual bool is_control_flow_keyword(String p_keywords) const override; + virtual bool is_control_flow_keyword(const String &p_keywords) const override; virtual void get_comment_delimiters(List<String> *p_delimiters) const override; virtual void get_doc_comment_delimiters(List<String> *p_delimiters) const override; virtual void get_string_delimiters(List<String> *p_delimiters) const override; virtual bool is_using_templates() override; virtual Ref<Script> make_template(const String &p_template, const String &p_class_name, const String &p_base_class_name) const override; - virtual Vector<ScriptTemplate> get_built_in_templates(StringName p_object) override; + virtual Vector<ScriptTemplate> get_built_in_templates(const StringName &p_object) override; virtual bool validate(const String &p_script, const String &p_path = "", List<String> *r_functions = nullptr, List<ScriptLanguage::ScriptError> *r_errors = nullptr, List<ScriptLanguage::Warning> *r_warnings = nullptr, HashSet<int> *r_safe_lines = nullptr) const override; virtual Script *create_script() const override; #ifndef DISABLE_DEPRECATED @@ -541,6 +586,7 @@ public: virtual String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems = -1, int p_max_depth = -1) override; virtual void reload_all_scripts() override; + virtual void reload_scripts(const Array &p_scripts, bool p_soft_reload) override; virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override; virtual void frame() override; @@ -551,6 +597,8 @@ public: virtual void profiling_start() override; virtual void profiling_stop() override; + virtual void profiling_set_save_native_calls(bool p_enable) override; + void profiling_collate_native_call_data(bool p_accumulated); virtual int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) override; virtual int profiling_get_frame_data(ProfilingInfo *p_info_arr, int p_info_max) override; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index f3f95ba50b..98d4a19a87 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -31,6 +31,7 @@ #include "gdscript_analyzer.h" #include "gdscript.h" +#include "gdscript_utility_callable.h" #include "gdscript_utility_functions.h" #include "core/config/engine.h" @@ -360,7 +361,7 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c push_error(vformat(R"(Class "%s" hides a built-in type.)", class_name), p_class->identifier); } else if (class_exists(class_name)) { push_error(vformat(R"(Class "%s" hides a native class.)", class_name), p_class->identifier); - } else if (ScriptServer::is_global_class(class_name) && (ScriptServer::get_global_class_path(class_name) != parser->script_path || p_class != parser->head)) { + } else if (ScriptServer::is_global_class(class_name) && (!GDScript::is_canonically_equal_paths(ScriptServer::get_global_class_path(class_name), parser->script_path) || p_class != parser->head)) { push_error(vformat(R"(Class "%s" hides a global script class.)", class_name), p_class->identifier); } else if (ProjectSettings::get_singleton()->has_autoload(class_name) && ProjectSettings::get_singleton()->get_autoload(class_name).is_singleton) { push_error(vformat(R"(Class "%s" hides an autoload singleton.)", class_name), p_class->identifier); @@ -424,7 +425,7 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c if (ScriptServer::is_global_class(name)) { String base_path = ScriptServer::get_global_class_path(name); - if (base_path == parser->script_path) { + if (GDScript::is_canonically_equal_paths(base_path, parser->script_path)) { base = parser->head->get_datatype(); } else { Ref<GDScriptParserRef> base_parser = get_parser_for(base_path); @@ -685,10 +686,10 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result.builtin_type = GDScriptParser::get_builtin_type(first); if (result.builtin_type == Variant::ARRAY) { - GDScriptParser::DataType container_type = type_from_metatype(resolve_datatype(p_type->container_type)); + GDScriptParser::DataType container_type = type_from_metatype(resolve_datatype(p_type->get_container_type_or_null(0))); if (container_type.kind != GDScriptParser::DataType::VARIANT) { container_type.is_constant = false; - result.set_container_element_type(container_type); + result.set_container_element_type(0, container_type); } } } else if (class_exists(first)) { @@ -697,7 +698,7 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type result.builtin_type = Variant::OBJECT; result.native_type = first; } else if (ScriptServer::is_global_class(first)) { - if (parser->script_path == ScriptServer::get_global_class_path(first)) { + if (GDScript::is_canonically_equal_paths(parser->script_path, ScriptServer::get_global_class_path(first))) { result = parser->head->get_datatype(); } else { String path = ScriptServer::get_global_class_path(first); @@ -829,15 +830,23 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type } } - if (result.builtin_type != Variant::ARRAY && p_type->container_type != nullptr) { - push_error("Only arrays can specify the collection element type.", p_type); + if (!p_type->container_types.is_empty()) { + if (result.builtin_type == Variant::ARRAY) { + if (p_type->container_types.size() != 1) { + push_error("Arrays require exactly one collection element type.", p_type); + return bad_type; + } + } else { + push_error("Only arrays can specify collection element types.", p_type); + return bad_type; + } } p_type->set_datatype(result); return result; } -void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, StringName p_name, const GDScriptParser::Node *p_source) { +void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source) { ERR_FAIL_COND(!p_class->has_member(p_name)); resolve_class_member(p_class, p_class->members_indices[p_name], p_source); } @@ -1585,7 +1594,13 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * GDScriptParser::FunctionNode *previous_function = parser->current_function; parser->current_function = p_function; bool previous_static_context = static_context; - static_context = p_function->is_static; + if (p_is_lambda) { + // For lambdas this is determined from the context, the `static` keyword is not allowed. + p_function->is_static = static_context; + } else { + // For normal functions, this is determined in the parser by the `static` keyword. + static_context = p_function->is_static; + } GDScriptParser::DataType prev_datatype = p_function->get_datatype(); @@ -1885,8 +1900,8 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi if (p_assignable->initializer->type == GDScriptParser::Node::ARRAY) { GDScriptParser::ArrayNode *array = static_cast<GDScriptParser::ArrayNode *>(p_assignable->initializer); - if (has_specified_type && specified_type.has_container_element_type()) { - update_array_literal_element_type(array, specified_type.get_container_element_type()); + if (has_specified_type && specified_type.has_container_element_type(0)) { + update_array_literal_element_type(array, specified_type.get_container_element_type(0)); } } @@ -1949,7 +1964,7 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi } else { push_error(vformat(R"(Cannot assign a value of type %s to %s "%s" with specified type %s.)", initializer_type.to_string(), p_kind, p_assignable->identifier->name, specified_type.to_string()), p_assignable->initializer); } - } else if (specified_type.has_container_element_type() && !initializer_type.has_container_element_type()) { + } else if (specified_type.has_container_element_type(0) && !initializer_type.has_container_element_type(0)) { mark_node_unsafe(p_assignable->initializer); #ifdef DEBUG_ENABLED } else if (specified_type.builtin_type == Variant::INT && initializer_type.builtin_type == Variant::FLOAT) { @@ -2121,8 +2136,8 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { if (list_type.is_variant()) { variable_type.kind = GDScriptParser::DataType::VARIANT; mark_node_unsafe(p_for->list); - } else if (list_type.has_container_element_type()) { - variable_type = list_type.get_container_element_type(); + } else if (list_type.has_container_element_type(0)) { + variable_type = list_type.get_container_element_type(0); variable_type.type_source = list_type.type_source; } else if (list_type.is_typed_container_type()) { variable_type = list_type.get_typed_container_type(); @@ -2371,8 +2386,8 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) { result.builtin_type = Variant::NIL; result.is_constant = true; } else { - if (p_return->return_value->type == GDScriptParser::Node::ARRAY && has_expected_type && expected_type.has_container_element_type()) { - update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_return->return_value), expected_type.get_container_element_type()); + if (p_return->return_value->type == GDScriptParser::Node::ARRAY && has_expected_type && expected_type.has_container_element_type(0)) { + update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_return->return_value), expected_type.get_container_element_type(0)); } if (has_expected_type && expected_type.is_hard_type() && p_return->return_value->is_constant) { update_const_expression_builtin_type(p_return->return_value, expected_type, "return"); @@ -2592,7 +2607,7 @@ void GDScriptAnalyzer::update_const_expression_builtin_type(GDScriptParser::Expr // This function determines which type is that (if any). void GDScriptAnalyzer::update_array_literal_element_type(GDScriptParser::ArrayNode *p_array, const GDScriptParser::DataType &p_element_type) { GDScriptParser::DataType expected_type = p_element_type; - expected_type.unset_container_element_type(); // Nested types (like `Array[Array[int]]`) are not currently supported. + expected_type.container_element_types.clear(); // Nested types (like `Array[Array[int]]`) are not currently supported. for (int i = 0; i < p_array->elements.size(); i++) { GDScriptParser::ExpressionNode *element_node = p_array->elements[i]; @@ -2615,7 +2630,7 @@ void GDScriptAnalyzer::update_array_literal_element_type(GDScriptParser::ArrayNo } GDScriptParser::DataType array_type = p_array->get_datatype(); - array_type.set_container_element_type(expected_type); + array_type.set_container_element_type(0, expected_type); p_array->set_datatype(array_type); } @@ -2662,8 +2677,8 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig } // Check if assigned value is an array literal, so we can make it a typed array too if appropriate. - if (p_assignment->assigned_value->type == GDScriptParser::Node::ARRAY && assignee_type.is_hard_type() && assignee_type.has_container_element_type()) { - update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_assignment->assigned_value), assignee_type.get_container_element_type()); + if (p_assignment->assigned_value->type == GDScriptParser::Node::ARRAY && assignee_type.is_hard_type() && assignee_type.has_container_element_type(0)) { + update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_assignment->assigned_value), assignee_type.get_container_element_type(0)); } if (p_assignment->operation == GDScriptParser::AssignmentNode::OP_NONE && assignee_type.is_hard_type() && p_assignment->assigned_value->is_constant) { @@ -2741,7 +2756,7 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig // weak non-variant assignee and incompatible result downgrades_assignee = true; } - } else if (assignee_type.has_container_element_type() && !op_type.has_container_element_type()) { + } else if (assignee_type.has_container_element_type(0) && !op_type.has_container_element_type(0)) { // typed array assignee and untyped array result mark_node_unsafe(p_assignment); } @@ -3305,8 +3320,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a // If the function requires typed arrays we must make literals be typed. for (const KeyValue<int, GDScriptParser::ArrayNode *> &E : arrays) { int index = E.key; - if (index < par_types.size() && par_types[index].is_hard_type() && par_types[index].has_container_element_type()) { - update_array_literal_element_type(E.value, par_types[index].get_container_element_type()); + if (index < par_types.size() && par_types[index].is_hard_type() && par_types[index].has_container_element_type(0)) { + update_array_literal_element_type(E.value, par_types[index].get_container_element_type(0)); } } validate_call_arg(par_types, default_arg_count, method_flags.has_flag(METHOD_FLAG_VARARG), p_call); @@ -3317,15 +3332,16 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } if (is_self && static_context && !method_flags.has_flag(METHOD_FLAG_STATIC)) { - if (parser->current_function) { - // Get the parent function above any lambda. - GDScriptParser::FunctionNode *parent_function = parser->current_function; - while (parent_function->source_lambda) { - parent_function = parent_function->source_lambda->parent_function; - } + // Get the parent function above any lambda. + GDScriptParser::FunctionNode *parent_function = parser->current_function; + while (parent_function && parent_function->source_lambda) { + parent_function = parent_function->source_lambda->parent_function; + } + + if (parent_function) { push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call); } else { - push_error(vformat(R"*(Cannot call non-static function "%s()" for static variable initializer.)*", p_call->function_name), p_call); + push_error(vformat(R"*(Cannot call non-static function "%s()" from a static variable initializer.)*", p_call->function_name), p_call); } } else if (!is_self && base_type.is_meta_type && !method_flags.has_flag(METHOD_FLAG_STATIC)) { base_type.is_meta_type = false; // For `to_string()`. @@ -3395,8 +3411,8 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a if (!found && (is_self || (base_type.is_hard_type() && base_type.kind == GDScriptParser::DataType::BUILTIN))) { String base_name = is_self && !p_call->is_super ? "self" : base_type.to_string(); #ifdef SUGGEST_GODOT4_RENAMES - String rename_hint = String(); - if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) { + String rename_hint; + if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::RENAMED_IN_GODOT_4_HINT)).booleanize()) { const char *renamed_function_name = check_for_renamed_identifier(p_call->function_name, p_call->type); if (renamed_function_name) { rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", String(renamed_function_name) + "()"); @@ -3437,8 +3453,8 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { } } - if (p_cast->operand->type == GDScriptParser::Node::ARRAY && cast_type.has_container_element_type()) { - update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_cast->operand), cast_type.get_container_element_type()); + if (p_cast->operand->type == GDScriptParser::Node::ARRAY && cast_type.has_container_element_type(0)) { + update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_cast->operand), cast_type.get_container_element_type(0)); } if (!cast_type.is_variant()) { @@ -3605,8 +3621,8 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod p_identifier->set_datatype(type_from_variant(result, p_identifier)); } else if (base.is_hard_type()) { #ifdef SUGGEST_GODOT4_RENAMES - String rename_hint = String(); - if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) { + String rename_hint; + if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::RENAMED_IN_GODOT_4_HINT)).booleanize()) { const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); if (renamed_identifier_name) { rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name); @@ -3621,7 +3637,7 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod switch (base.builtin_type) { case Variant::NIL: { if (base.is_hard_type()) { - push_error(vformat(R"(Invalid get index "%s" on base Nil)", name), p_identifier); + push_error(vformat(R"(Cannot get property "%s" on a null object.)", name), p_identifier); } return; } @@ -3643,10 +3659,14 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod return; } } + if (Variant::has_builtin_method(base.builtin_type, name)) { + p_identifier->set_datatype(make_callable_type(Variant::get_builtin_method_info(base.builtin_type, name))); + return; + } if (base.is_hard_type()) { #ifdef SUGGEST_GODOT4_RENAMES - String rename_hint = String(); - if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) { + String rename_hint; + if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::RENAMED_IN_GODOT_4_HINT)).booleanize()) { const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); if (renamed_identifier_name) { rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name); @@ -3762,6 +3782,60 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod } } + // Check non-GDScript scripts. + Ref<Script> script_type = base.script_type; + + if (base_class == nullptr && script_type.is_valid()) { + List<PropertyInfo> property_list; + script_type->get_script_property_list(&property_list); + + for (const PropertyInfo &property_info : property_list) { + if (property_info.name != p_identifier->name) { + continue; + } + + const GDScriptParser::DataType property_type = GDScriptAnalyzer::type_from_property(property_info, false, false); + + p_identifier->set_datatype(property_type); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_VARIABLE; + return; + } + + MethodInfo method_info = script_type->get_method_info(p_identifier->name); + + if (method_info.name == p_identifier->name) { + p_identifier->set_datatype(make_callable_type(method_info)); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_FUNCTION; + return; + } + + List<MethodInfo> signal_list; + script_type->get_script_signal_list(&signal_list); + + for (const MethodInfo &signal_info : signal_list) { + if (signal_info.name != p_identifier->name) { + continue; + } + + const GDScriptParser::DataType signal_type = make_signal_type(signal_info); + + p_identifier->set_datatype(signal_type); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_SIGNAL; + return; + } + + HashMap<StringName, Variant> constant_map; + script_type->get_constants(&constant_map); + + if (constant_map.has(p_identifier->name)) { + Variant constant = constant_map.get(p_identifier->name); + + p_identifier->set_datatype(make_builtin_meta_type(constant.get_type())); + p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_CONSTANT; + return; + } + } + // Check native members. No need for native class recursion because Node exposes all Object's properties. const StringName &native = base.native_type; @@ -3908,15 +3982,16 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident bool source_is_variable = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE; bool source_is_signal = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_SIGNAL; if ((source_is_variable || source_is_signal) && static_context) { - if (parser->current_function) { - // Get the parent function above any lambda. - GDScriptParser::FunctionNode *parent_function = parser->current_function; - while (parent_function->source_lambda) { - parent_function = parent_function->source_lambda->parent_function; - } + // Get the parent function above any lambda. + GDScriptParser::FunctionNode *parent_function = parser->current_function; + while (parent_function && parent_function->source_lambda) { + parent_function = parent_function->source_lambda->parent_function; + } + + if (parent_function) { push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier); } else { - push_error(vformat(R"*(Cannot access %s "%s" for a static variable initializer.)*", source_is_signal ? "signal" : "instance variable", p_identifier->name), p_identifier); + push_error(vformat(R"*(Cannot access %s "%s" from a static variable initializer.)*", source_is_signal ? "signal" : "instance variable", p_identifier->name), p_identifier); } } @@ -4043,6 +4118,19 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident return; } + if (Variant::has_utility_function(name) || GDScriptUtilityFunctions::function_exists(name)) { + p_identifier->is_constant = true; + p_identifier->reduced_value = Callable(memnew(GDScriptUtilityCallable(name))); + MethodInfo method_info; + if (GDScriptUtilityFunctions::function_exists(name)) { + method_info = GDScriptUtilityFunctions::get_function_info(name); + } else { + method_info = Variant::get_utility_function_info(name); + } + p_identifier->set_datatype(make_callable_type(method_info)); + return; + } + // Allow "Variant" here since it might be used for nested enums. if (can_be_builtin && name == SNAME("Variant")) { GDScriptParser::DataType variant; @@ -4055,23 +4143,18 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident } // Not found. - // Check if it's a builtin function. - if (GDScriptUtilityFunctions::function_exists(name)) { - push_error(vformat(R"(Built-in function "%s" cannot be used as an identifier.)", name), p_identifier); - } else { #ifdef SUGGEST_GODOT4_RENAMES - String rename_hint = String(); - if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::Code::RENAMED_IN_GODOT_4_HINT)).booleanize()) { - const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); - if (renamed_identifier_name) { - rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name); - } + String rename_hint; + if (GLOBAL_GET(GDScriptWarning::get_settings_path_from_code(GDScriptWarning::RENAMED_IN_GODOT_4_HINT)).booleanize()) { + const char *renamed_identifier_name = check_for_renamed_identifier(name, p_identifier->type); + if (renamed_identifier_name) { + rename_hint = " " + vformat(R"(Did you mean to use "%s"?)", renamed_identifier_name); } - push_error(vformat(R"(Identifier "%s" not declared in the current scope.%s)", name, rename_hint), p_identifier); + } + push_error(vformat(R"(Identifier "%s" not declared in the current scope.%s)", name, rename_hint), p_identifier); #else - push_error(vformat(R"(Identifier "%s" not declared in the current scope.)", name), p_identifier); + push_error(vformat(R"(Identifier "%s" not declared in the current scope.)", name), p_identifier); #endif // SUGGEST_GODOT4_RENAMES - } GDScriptParser::DataType dummy; dummy.kind = GDScriptParser::DataType::VARIANT; p_identifier->set_datatype(dummy); // Just so type is set to something. @@ -4136,8 +4219,8 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { } else { // TODO: Don't load if validating: use completion cache. - // Must load GDScript and PackedScenes separately to permit cyclic references - // as ResourceLoader::load() detect and reject those. + // Must load GDScript separately to permit cyclic references + // as ResourceLoader::load() detects and rejects those. if (ResourceLoader::get_resource_type(p_preload->resolved_path) == "GDScript") { Error err = OK; Ref<GDScript> res = GDScriptCache::get_shallow_script(p_preload->resolved_path, err, parser->script_path); @@ -4145,13 +4228,6 @@ void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { if (err != OK) { push_error(vformat(R"(Could not preload resource script "%s".)", p_preload->resolved_path), p_preload->path); } - } else if (ResourceLoader::get_resource_type(p_preload->resolved_path) == "PackedScene") { - Error err = OK; - Ref<PackedScene> res = GDScriptCache::get_packed_scene(p_preload->resolved_path, err, parser->script_path); - p_preload->resource = res; - if (err != OK) { - push_error(vformat(R"(Could not preload resource scene "%s".)", p_preload->resolved_path), p_preload->path); - } } else { p_preload->resource = ResourceLoader::load(p_preload->resolved_path); if (p_preload->resource.is_null()) { @@ -4343,7 +4419,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri } } else if (base_type.kind != GDScriptParser::DataType::BUILTIN && !index_type.is_variant()) { if (index_type.builtin_type != Variant::STRING && index_type.builtin_type != Variant::STRING_NAME) { - push_error(vformat(R"(Only String or StringName can be used as index for type "%s", but received a "%s".)", base_type.to_string(), index_type.to_string()), p_subscript->index); + push_error(vformat(R"(Only "String" or "StringName" can be used as index for type "%s", but received "%s".)", base_type.to_string(), index_type.to_string()), p_subscript->index); } } @@ -4424,8 +4500,8 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri break; // Can have an element type. case Variant::ARRAY: - if (base_type.has_container_element_type()) { - result_type = base_type.get_container_element_type(); + if (base_type.has_container_element_type(0)) { + result_type = base_type.get_container_element_type(0); result_type.type_source = base_type.type_source; } else { result_type.kind = GDScriptParser::DataType::VARIANT; @@ -4589,7 +4665,7 @@ Variant GDScriptAnalyzer::make_expression_reduced_value(GDScriptParser::Expressi } Variant GDScriptAnalyzer::make_array_reduced_value(GDScriptParser::ArrayNode *p_array, bool &is_reduced) { - Array array = p_array->get_datatype().has_container_element_type() ? make_array_from_element_datatype(p_array->get_datatype().get_container_element_type()) : Array(); + Array array = p_array->get_datatype().has_container_element_type(0) ? make_array_from_element_datatype(p_array->get_datatype().get_container_element_type(0)) : Array(); array.resize(p_array->elements.size()); for (int i = 0; i < p_array->elements.size(); i++) { @@ -4711,8 +4787,8 @@ Variant GDScriptAnalyzer::make_variable_default_value(GDScriptParser::VariableNo GDScriptParser::DataType datatype = p_variable->get_datatype(); if (datatype.is_hard_type()) { if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) { - if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type()) { - result = make_array_from_element_datatype(datatype.get_container_element_type()); + if (datatype.builtin_type == Variant::ARRAY && datatype.has_container_element_type(0)) { + result = make_array_from_element_datatype(datatype.get_container_element_type(0)); } else { VariantInternal::initialize(&result, datatype.builtin_type); } @@ -4739,11 +4815,11 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_variant(const Variant &p_va if (p_value.get_type() == Variant::ARRAY) { const Array &array = p_value; if (array.get_typed_script()) { - result.set_container_element_type(type_from_metatype(make_script_meta_type(array.get_typed_script()))); + result.set_container_element_type(0, type_from_metatype(make_script_meta_type(array.get_typed_script()))); } else if (array.get_typed_class_name()) { - result.set_container_element_type(type_from_metatype(make_native_meta_type(array.get_typed_class_name()))); + result.set_container_element_type(0, type_from_metatype(make_native_meta_type(array.get_typed_class_name()))); } else if (array.get_typed_builtin() != Variant::NIL) { - result.set_container_element_type(type_from_metatype(make_builtin_meta_type((Variant::Type)array.get_typed_builtin()))); + result.set_container_element_type(0, type_from_metatype(make_builtin_meta_type((Variant::Type)array.get_typed_builtin()))); } } else if (p_value.get_type() == Variant::OBJECT) { // Object is treated as a native type, not a builtin type. @@ -4834,8 +4910,19 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo } result.builtin_type = p_property.type; if (p_property.type == Variant::OBJECT) { - result.kind = GDScriptParser::DataType::NATIVE; - result.native_type = p_property.class_name == StringName() ? SNAME("Object") : p_property.class_name; + if (ScriptServer::is_global_class(p_property.class_name)) { + result.kind = GDScriptParser::DataType::SCRIPT; + result.script_path = ScriptServer::get_global_class_path(p_property.class_name); + result.native_type = ScriptServer::get_global_class_native_base(p_property.class_name); + + Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name)); + if (scr.is_valid()) { + result.script_type = scr; + } + } else { + result.kind = GDScriptParser::DataType::NATIVE; + result.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name; + } } else { result.kind = GDScriptParser::DataType::BUILTIN; result.builtin_type = p_property.type; @@ -4865,7 +4952,7 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo ERR_FAIL_V_MSG(result, "Could not find element type from property hint of a typed array."); } elem_type.is_constant = false; - result.set_container_element_type(elem_type); + result.set_container_element_type(0, elem_type); } else if (p_property.type == Variant::INT) { // Check if it's enum. if ((p_property.usage & PROPERTY_USAGE_CLASS_IS_ENUM) && p_property.class_name != StringName()) { @@ -5217,7 +5304,7 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator bool hard_operation = p_a.is_hard_type() && p_b.is_hard_type(); if (p_operation == Variant::OP_ADD && a_type == Variant::ARRAY && b_type == Variant::ARRAY) { - if (p_a.has_container_element_type() && p_b.has_container_element_type() && p_a.get_container_element_type() == p_b.get_container_element_type()) { + if (p_a.has_container_element_type(0) && p_b.has_container_element_type(0) && p_a.get_container_element_type(0) == p_b.get_container_element_type(0)) { r_valid = true; result = p_a; result.type_source = hard_operation ? GDScriptParser::DataType::ANNOTATED_INFERRED : GDScriptParser::DataType::INFERRED; @@ -5241,8 +5328,21 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator return result; } -// TODO: Add safe/unsafe return variable (for variant cases) bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion, const GDScriptParser::Node *p_source_node) { +#ifdef DEBUG_ENABLED + if (p_source_node) { + if (p_target.kind == GDScriptParser::DataType::ENUM) { + if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) { + parser->push_warning(p_source_node, GDScriptWarning::INT_AS_ENUM_WITHOUT_CAST); + } + } + } +#endif + return check_type_compatibility(p_target, p_source, p_allow_implicit_conversion, p_source_node); +} + +// TODO: Add safe/unsafe return variable (for variant cases) +bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion, const GDScriptParser::Node *p_source_node) { // These return "true" so it doesn't affect users negatively. ERR_FAIL_COND_V_MSG(!p_target.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset target type"); ERR_FAIL_COND_V_MSG(!p_source.is_set(), true, "Parser bug (please report): Trying to check compatibility of unset value type"); @@ -5268,8 +5368,8 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ } if (valid && p_target.builtin_type == Variant::ARRAY && p_source.builtin_type == Variant::ARRAY) { // Check the element type. - if (p_target.has_container_element_type() && p_source.has_container_element_type()) { - valid = p_target.get_container_element_type() == p_source.get_container_element_type(); + if (p_target.has_container_element_type(0) && p_source.has_container_element_type(0)) { + valid = p_target.get_container_element_type(0) == p_source.get_container_element_type(0); } } return valid; @@ -5277,11 +5377,6 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ if (p_target.kind == GDScriptParser::DataType::ENUM) { if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) { -#ifdef DEBUG_ENABLED - if (p_source_node) { - parser->push_warning(p_source_node, GDScriptWarning::INT_AS_ENUM_WITHOUT_CAST); - } -#endif return true; } if (p_source.kind == GDScriptParser::DataType::ENUM) { @@ -5459,12 +5554,15 @@ void GDScriptAnalyzer::resolve_pending_lambda_bodies() { } GDScriptParser::LambdaNode *previous_lambda = current_lambda; + bool previous_static_context = static_context; List<GDScriptParser::LambdaNode *> lambdas = pending_body_resolution_lambdas; pending_body_resolution_lambdas.clear(); for (GDScriptParser::LambdaNode *lambda : lambdas) { current_lambda = lambda; + static_context = lambda->function->is_static; + resolve_function_body(lambda->function, true); int captures_amount = lambda->captures.size(); @@ -5493,6 +5591,7 @@ void GDScriptAnalyzer::resolve_pending_lambda_bodies() { } current_lambda = previous_lambda; + static_context = previous_static_context; } bool GDScriptAnalyzer::class_exists(const StringName &p_class) const { diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index ec155706df..e398ccfdbb 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -62,7 +62,7 @@ class GDScriptAnalyzer { void decide_suite_type(GDScriptParser::Node *p_suite, GDScriptParser::Node *p_statement); void resolve_annotation(GDScriptParser::AnnotationNode *p_annotation); - void resolve_class_member(GDScriptParser::ClassNode *p_class, StringName p_name, const GDScriptParser::Node *p_source = nullptr); + void resolve_class_member(GDScriptParser::ClassNode *p_class, const StringName &p_name, const GDScriptParser::Node *p_source = nullptr); void resolve_class_member(GDScriptParser::ClassNode *p_class, int p_index, const GDScriptParser::Node *p_source = nullptr); void resolve_class_interface(GDScriptParser::ClassNode *p_class, const GDScriptParser::Node *p_source = nullptr); void resolve_class_interface(GDScriptParser::ClassNode *p_class, bool p_recursive); @@ -147,6 +147,7 @@ public: Variant make_variable_default_value(GDScriptParser::VariableNode *p_variable); const HashMap<String, Ref<GDScriptParserRef>> &get_depended_parsers(); + static bool check_type_compatibility(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr); GDScriptAnalyzer(GDScriptParser *p_parser); }; diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index 25e20c0e76..36b157521d 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -429,7 +429,7 @@ void GDScriptByteCodeGenerator::set_initial_line(int p_line) { (m_var.type.has_type && m_var.type.kind == GDScriptDataType::BUILTIN) #define IS_BUILTIN_TYPE(m_var, m_type) \ - (m_var.type.has_type && m_var.type.kind == GDScriptDataType::BUILTIN && m_var.type.builtin_type == m_type) + (m_var.type.has_type && m_var.type.kind == GDScriptDataType::BUILTIN && m_var.type.builtin_type == m_type && m_type != Variant::NIL) void GDScriptByteCodeGenerator::write_type_adjust(const Address &p_target, Variant::Type p_new_type) { switch (p_new_type) { @@ -623,8 +623,8 @@ void GDScriptByteCodeGenerator::write_binary_operator(const Address &p_target, V void GDScriptByteCodeGenerator::write_type_test(const Address &p_target, const Address &p_source, const GDScriptDataType &p_type) { switch (p_type.kind) { case GDScriptDataType::BUILTIN: { - if (p_type.builtin_type == Variant::ARRAY && p_type.has_container_element_type()) { - const GDScriptDataType &element_type = p_type.get_container_element_type(); + if (p_type.builtin_type == Variant::ARRAY && p_type.has_container_element_type(0)) { + const GDScriptDataType &element_type = p_type.get_container_element_type(0); append_opcode(GDScriptFunction::OPCODE_TYPE_TEST_ARRAY); append(p_target); append(p_source); @@ -878,8 +878,8 @@ void GDScriptByteCodeGenerator::write_get_static_variable(const Address &p_targe void GDScriptByteCodeGenerator::write_assign_with_conversion(const Address &p_target, const Address &p_source) { switch (p_target.type.kind) { case GDScriptDataType::BUILTIN: { - if (p_target.type.builtin_type == Variant::ARRAY && p_target.type.has_container_element_type()) { - const GDScriptDataType &element_type = p_target.type.get_container_element_type(); + if (p_target.type.builtin_type == Variant::ARRAY && p_target.type.has_container_element_type(0)) { + const GDScriptDataType &element_type = p_target.type.get_container_element_type(0); append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_ARRAY); append(p_target); append(p_source); @@ -924,8 +924,8 @@ void GDScriptByteCodeGenerator::write_assign_with_conversion(const Address &p_ta } void GDScriptByteCodeGenerator::write_assign(const Address &p_target, const Address &p_source) { - if (p_target.type.kind == GDScriptDataType::BUILTIN && p_target.type.builtin_type == Variant::ARRAY && p_target.type.has_container_element_type()) { - const GDScriptDataType &element_type = p_target.type.get_container_element_type(); + if (p_target.type.kind == GDScriptDataType::BUILTIN && p_target.type.builtin_type == Variant::ARRAY && p_target.type.has_container_element_type(0)) { + const GDScriptDataType &element_type = p_target.type.get_container_element_type(0); append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_ARRAY); append(p_target); append(p_source); @@ -1666,9 +1666,9 @@ void GDScriptByteCodeGenerator::write_return(const Address &p_return_value) { // If this is a typed function, then we need to check for potential conversions. if (function->return_type.has_type) { - if (function->return_type.kind == GDScriptDataType::BUILTIN && function->return_type.builtin_type == Variant::ARRAY && function->return_type.has_container_element_type()) { + if (function->return_type.kind == GDScriptDataType::BUILTIN && function->return_type.builtin_type == Variant::ARRAY && function->return_type.has_container_element_type(0)) { // Typed array. - const GDScriptDataType &element_type = function->return_type.get_container_element_type(); + const GDScriptDataType &element_type = function->return_type.get_container_element_type(0); append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_ARRAY); append(p_return_value); append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS)); @@ -1691,8 +1691,8 @@ void GDScriptByteCodeGenerator::write_return(const Address &p_return_value) { } else { switch (function->return_type.kind) { case GDScriptDataType::BUILTIN: { - if (function->return_type.builtin_type == Variant::ARRAY && function->return_type.has_container_element_type()) { - const GDScriptDataType &element_type = function->return_type.get_container_element_type(); + if (function->return_type.builtin_type == Variant::ARRAY && function->return_type.has_container_element_type(0)) { + const GDScriptDataType &element_type = function->return_type.get_container_element_type(0); append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_ARRAY); append(p_return_value); append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS)); diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 9bface6136..f902cb10cc 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -121,7 +121,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { RBMap<MethodBind *, int> method_bind_map; RBMap<GDScriptFunction *, int> lambdas_map; -#if DEBUG_ENABLED +#ifdef DEBUG_ENABLED // Keep method and property names for pointer and validated operations. // Used when disassembling the bytecode. Vector<String> operator_names; diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index 18609d0b80..7c27127dce 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -37,7 +37,6 @@ #include "core/io/file_access.h" #include "core/templates/vector.h" -#include "scene/resources/packed_scene.h" bool GDScriptParserRef::is_valid() const { return parser != nullptr; @@ -67,10 +66,15 @@ Error GDScriptParserRef::raise_status(Status p_new_status) { while (p_new_status > status) { switch (status) { - case EMPTY: + case EMPTY: { status = PARSED; - result = parser->parse(GDScriptCache::get_source_code(path), path, false); - break; + String remapped_path = ResourceLoader::path_remap(path); + if (remapped_path.get_extension().to_lower() == "gdc") { + result = parser->parse_binary(GDScriptCache::get_binary_tokens(remapped_path), path); + } else { + result = parser->parse(GDScriptCache::get_source_code(remapped_path), path, false); + } + } break; case PARSED: { status = INHERITANCE_SOLVED; Error inheritance_result = get_analyzer()->resolve_inheritance(); @@ -139,13 +143,6 @@ void GDScriptCache::move_script(const String &p_from, const String &p_to) { return; } - for (KeyValue<String, HashSet<String>> &E : singleton->packed_scene_dependencies) { - if (E.value.has(p_from)) { - E.value.insert(p_to); - E.value.erase(p_from); - } - } - if (singleton->parser_map.has(p_from) && !p_from.is_empty()) { singleton->parser_map[p_to] = singleton->parser_map[p_from]; } @@ -173,15 +170,6 @@ void GDScriptCache::remove_script(const String &p_path) { return; } - for (KeyValue<String, HashSet<String>> &E : singleton->packed_scene_dependencies) { - if (!E.value.has(p_path)) { - continue; - } - E.value.erase(p_path); - } - - GDScriptCache::clear_unreferenced_packed_scenes(); - if (singleton->parser_map.has(p_path)) { singleton->parser_map[p_path]->clear(); singleton->parser_map.erase(p_path); @@ -205,7 +193,8 @@ Ref<GDScriptParserRef> GDScriptCache::get_parser(const String &p_path, GDScriptP return ref; } } else { - if (!FileAccess::exists(p_path)) { + String remapped_path = ResourceLoader::path_remap(p_path); + if (!FileAccess::exists(remapped_path)) { r_error = ERR_FILE_NOT_FOUND; return ref; } @@ -239,6 +228,20 @@ String GDScriptCache::get_source_code(const String &p_path) { return source; } +Vector<uint8_t> GDScriptCache::get_binary_tokens(const String &p_path) { + Vector<uint8_t> buffer; + Error err = OK; + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err); + ERR_FAIL_COND_V_MSG(err != OK, buffer, "Failed to open binary GDScript file '" + p_path + "'."); + + uint64_t len = f->get_length(); + buffer.resize(len); + uint64_t read = f->get_buffer(buffer.ptrw(), buffer.size()); + ERR_FAIL_COND_V_MSG(read != len, Vector<uint8_t>(), "Failed to read binary GDScript file '" + p_path + "'."); + + return buffer; +} + Ref<GDScript> GDScriptCache::get_shallow_script(const String &p_path, Error &r_error, const String &p_owner) { MutexLock lock(singleton->mutex); if (!p_owner.is_empty()) { @@ -251,10 +254,20 @@ Ref<GDScript> GDScriptCache::get_shallow_script(const String &p_path, Error &r_e return singleton->shallow_gdscript_cache[p_path]; } + String remapped_path = ResourceLoader::path_remap(p_path); + Ref<GDScript> script; script.instantiate(); script->set_path(p_path, true); - r_error = script->load_source_code(p_path); + if (remapped_path.get_extension().to_lower() == "gdc") { + Vector<uint8_t> buffer = get_binary_tokens(remapped_path); + if (buffer.is_empty()) { + r_error = ERR_FILE_CANT_READ; + } + script->set_binary_tokens_source(buffer); + } else { + r_error = script->load_source_code(remapped_path); + } if (r_error) { return Ref<GDScript>(); // Returns null and does not cache when the script fails to load. @@ -287,15 +300,25 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro if (script.is_null()) { script = get_shallow_script(p_path, r_error); - if (r_error) { + // Only exit early if script failed to load, otherwise let reload report errors. + if (script.is_null()) { return script; } } if (p_update_from_disk) { - r_error = script->load_source_code(p_path); - if (r_error) { - return script; + if (p_path.get_extension().to_lower() == "gdc") { + Vector<uint8_t> buffer = get_binary_tokens(p_path); + if (buffer.is_empty()) { + r_error = ERR_FILE_CANT_READ; + return script; + } + script->set_binary_tokens_source(buffer); + } else { + r_error = script->load_source_code(p_path); + if (r_error) { + return script; + } } } @@ -360,57 +383,6 @@ void GDScriptCache::remove_static_script(const String &p_fqcn) { singleton->static_gdscript_cache.erase(p_fqcn); } -Ref<PackedScene> GDScriptCache::get_packed_scene(const String &p_path, Error &r_error, const String &p_owner) { - MutexLock lock(singleton->mutex); - - if (singleton->packed_scene_cache.has(p_path)) { - singleton->packed_scene_dependencies[p_path].insert(p_owner); - return singleton->packed_scene_cache[p_path]; - } - - Ref<PackedScene> scene = ResourceCache::get_ref(p_path); - if (scene.is_valid()) { - singleton->packed_scene_cache[p_path] = scene; - singleton->packed_scene_dependencies[p_path].insert(p_owner); - return scene; - } - scene.instantiate(); - - r_error = OK; - if (p_path.is_empty()) { - r_error = ERR_FILE_BAD_PATH; - return scene; - } - - scene->set_path(p_path); - singleton->packed_scene_cache[p_path] = scene; - singleton->packed_scene_dependencies[p_path].insert(p_owner); - - scene->reload_from_file(); - return scene; -} - -void GDScriptCache::clear_unreferenced_packed_scenes() { - if (singleton == nullptr) { - return; - } - - MutexLock lock(singleton->mutex); - - if (singleton->cleared) { - return; - } - - for (KeyValue<String, HashSet<String>> &E : singleton->packed_scene_dependencies) { - if (E.value.size() > 0 || !ResourceLoader::is_imported(E.key)) { - continue; - } - - singleton->packed_scene_dependencies.erase(E.key); - singleton->packed_scene_cache.erase(E.key); - } -} - void GDScriptCache::clear() { if (singleton == nullptr) { return; @@ -433,16 +405,10 @@ void GDScriptCache::clear() { E->clear(); } - singleton->packed_scene_dependencies.clear(); - singleton->packed_scene_cache.clear(); - parser_map_refs.clear(); singleton->parser_map.clear(); singleton->shallow_gdscript_cache.clear(); singleton->full_gdscript_cache.clear(); - - singleton->packed_scene_cache.clear(); - singleton->packed_scene_dependencies.clear(); } GDScriptCache::GDScriptCache() { diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h index 0a0f403e44..fc7abbd46e 100644 --- a/modules/gdscript/gdscript_cache.h +++ b/modules/gdscript/gdscript_cache.h @@ -37,7 +37,6 @@ #include "core/os/mutex.h" #include "core/templates/hash_map.h" #include "core/templates/hash_set.h" -#include "scene/resources/packed_scene.h" class GDScriptAnalyzer; class GDScriptParser; @@ -81,8 +80,6 @@ class GDScriptCache { HashMap<String, Ref<GDScript>> full_gdscript_cache; HashMap<String, Ref<GDScript>> static_gdscript_cache; HashMap<String, HashSet<String>> dependencies; - HashMap<String, Ref<PackedScene>> packed_scene_cache; - HashMap<String, HashSet<String>> packed_scene_dependencies; friend class GDScript; friend class GDScriptParserRef; @@ -99,6 +96,7 @@ public: static void remove_script(const String &p_path); static Ref<GDScriptParserRef> get_parser(const String &p_path, GDScriptParserRef::Status status, Error &r_error, const String &p_owner = String()); static String get_source_code(const String &p_path); + static Vector<uint8_t> get_binary_tokens(const String &p_path); static Ref<GDScript> get_shallow_script(const String &p_path, Error &r_error, const String &p_owner = String()); static Ref<GDScript> get_full_script(const String &p_path, Error &r_error, const String &p_owner = String(), bool p_update_from_disk = false); static Ref<GDScript> get_cached_script(const String &p_path); @@ -106,9 +104,6 @@ public: static void add_static_script(Ref<GDScript> p_script); static void remove_static_script(const String &p_fqcn); - static Ref<PackedScene> get_packed_scene(const String &p_path, Error &r_error, const String &p_owner = ""); - static void clear_unreferenced_packed_scenes(); - static void clear(); GDScriptCache(); diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index bf648abc9e..13ed66710c 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -37,6 +37,7 @@ #include "core/config/engine.h" #include "core/config/project_settings.h" +#include "core/core_string_names.h" bool GDScriptCompiler::_is_class_member_property(CodeGen &codegen, const StringName &p_name) { if (codegen.function_node && codegen.function_node->is_static) { @@ -196,8 +197,8 @@ GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::D } } - if (p_datatype.has_container_element_type()) { - result.set_container_element_type(_gdtype_from_datatype(p_datatype.get_container_element_type(), p_owner, false)); + for (int i = 0; i < p_datatype.container_element_types.size(); i++) { + result.set_container_element_type(i, _gdtype_from_datatype(p_datatype.get_container_element_type_or_variant(i), p_owner, false)); } return result; @@ -322,9 +323,13 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code if (member.type == GDScriptParser::ClassNode::Member::FUNCTION || member.type == GDScriptParser::ClassNode::Member::SIGNAL) { // Get like it was a property. GDScriptCodeGenerator::Address temp = codegen.add_temporary(); // TODO: Get type here. - GDScriptCodeGenerator::Address self(GDScriptCodeGenerator::Address::SELF); - gen->write_get_named(temp, identifier, self); + GDScriptCodeGenerator::Address base(GDScriptCodeGenerator::Address::SELF); + if (member.type == GDScriptParser::ClassNode::Member::FUNCTION && member.function->is_static) { + base = GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::CLASS); + } + + gen->write_get_named(temp, identifier, base); return temp; } } @@ -341,7 +346,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code scr = scr->_base; } - if (nc && (ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) { + if (nc && (identifier == CoreStringNames::get_singleton()->_free || ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) { // Get like it was a property. GDScriptCodeGenerator::Address temp = codegen.add_temporary(); // TODO: Get type here. GDScriptCodeGenerator::Address self(GDScriptCodeGenerator::Address::SELF); @@ -507,8 +512,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code values.push_back(val); } - if (array_type.has_container_element_type()) { - gen->write_construct_typed_array(result, array_type.get_container_element_type(), values); + if (array_type.has_container_element_type(0)) { + gen->write_construct_typed_array(result, array_type.get_container_element_type(0), values); } else { gen->write_construct_array(result, values); } @@ -1371,6 +1376,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code return GDScriptCodeGenerator::Address(); } + codegen.script->lambda_info.insert(function, { (int)lambda->captures.size(), lambda->use_self }); gen->write_lambda(result, function, captures, lambda->use_self); for (int i = 0; i < captures.size(); i++) { @@ -2132,8 +2138,8 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui initialized = true; } else if (local_type.has_type) { // Initialize with default for type. - if (local_type.has_container_element_type()) { - codegen.generator->write_construct_typed_array(local, local_type.get_container_element_type(), Vector<GDScriptCodeGenerator::Address>()); + if (local_type.has_container_element_type(0)) { + codegen.generator->write_construct_typed_array(local, local_type.get_container_element_type(0), Vector<GDScriptCodeGenerator::Address>()); initialized = true; } else if (local_type.kind == GDScriptDataType::BUILTIN) { codegen.generator->write_construct(local, local_type.builtin_type, Vector<GDScriptCodeGenerator::Address>()); @@ -2275,8 +2281,8 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_ GDScriptCodeGenerator::Address dst_address(GDScriptCodeGenerator::Address::MEMBER, codegen.script->member_indices[field->identifier->name].index, field_type); - if (field_type.has_container_element_type()) { - codegen.generator->write_construct_typed_array(dst_address, field_type.get_container_element_type(), Vector<GDScriptCodeGenerator::Address>()); + if (field_type.has_container_element_type(0)) { + codegen.generator->write_construct_typed_array(dst_address, field_type.get_container_element_type(0), Vector<GDScriptCodeGenerator::Address>()); } else if (field_type.kind == GDScriptDataType::BUILTIN) { codegen.generator->write_construct(dst_address, field_type.builtin_type, Vector<GDScriptCodeGenerator::Address>()); } @@ -2465,9 +2471,9 @@ GDScriptFunction *GDScriptCompiler::_make_static_initializer(Error &r_error, GDS if (field_type.has_type) { codegen.generator->write_newline(field->start_line); - if (field_type.has_container_element_type()) { + if (field_type.has_container_element_type(0)) { GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type); - codegen.generator->write_construct_typed_array(temp, field_type.get_container_element_type(), Vector<GDScriptCodeGenerator::Address>()); + codegen.generator->write_construct_typed_array(temp, field_type.get_container_element_type(0), Vector<GDScriptCodeGenerator::Address>()); codegen.generator->write_set_static_variable(temp, class_addr, p_script->static_variables_indices[field->identifier->name].index); codegen.generator->pop_temporary(); } else if (field_type.kind == GDScriptDataType::BUILTIN) { @@ -2631,6 +2637,7 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP p_script->implicit_ready = nullptr; p_script->static_initializer = nullptr; p_script->rpc_config.clear(); + p_script->lambda_info.clear(); p_script->clearing = false; @@ -3040,6 +3047,132 @@ void GDScriptCompiler::make_scripts(GDScript *p_script, const GDScriptParser::Cl } } +GDScriptCompiler::FunctionLambdaInfo GDScriptCompiler::_get_function_replacement_info(GDScriptFunction *p_func, int p_index, int p_depth, GDScriptFunction *p_parent_func) { + FunctionLambdaInfo info; + info.function = p_func; + info.parent = p_parent_func; + info.script = p_func->get_script(); + info.name = p_func->get_name(); + info.line = p_func->_initial_line; + info.index = p_index; + info.depth = p_depth; + info.capture_count = 0; + info.use_self = false; + info.arg_count = p_func->_argument_count; + info.default_arg_count = p_func->_default_arg_count; + info.sublambdas = _get_function_lambda_replacement_info(p_func, p_depth, p_parent_func); + + ERR_FAIL_NULL_V(info.script, info); + GDScript::LambdaInfo *extra_info = info.script->lambda_info.getptr(p_func); + if (extra_info != nullptr) { + info.capture_count = extra_info->capture_count; + info.use_self = extra_info->use_self; + } else { + info.capture_count = 0; + info.use_self = false; + } + + return info; +} + +Vector<GDScriptCompiler::FunctionLambdaInfo> GDScriptCompiler::_get_function_lambda_replacement_info(GDScriptFunction *p_func, int p_depth, GDScriptFunction *p_parent_func) { + Vector<FunctionLambdaInfo> result; + // Only scrape the lambdas inside p_func. + for (int i = 0; i < p_func->lambdas.size(); ++i) { + result.push_back(_get_function_replacement_info(p_func->lambdas[i], i, p_depth + 1, p_func)); + } + return result; +} + +GDScriptCompiler::ScriptLambdaInfo GDScriptCompiler::_get_script_lambda_replacement_info(GDScript *p_script) { + ScriptLambdaInfo info; + + if (p_script->implicit_initializer) { + info.implicit_initializer_info = _get_function_lambda_replacement_info(p_script->implicit_initializer); + } + if (p_script->implicit_ready) { + info.implicit_ready_info = _get_function_lambda_replacement_info(p_script->implicit_ready); + } + if (p_script->static_initializer) { + info.static_initializer_info = _get_function_lambda_replacement_info(p_script->static_initializer); + } + + for (const KeyValue<StringName, GDScriptFunction *> &E : p_script->member_functions) { + info.member_function_infos.insert(E.key, _get_function_lambda_replacement_info(E.value)); + } + + for (const KeyValue<StringName, Ref<GDScript>> &KV : p_script->get_subclasses()) { + info.subclass_info.insert(KV.key, _get_script_lambda_replacement_info(KV.value.ptr())); + } + + return info; +} + +bool GDScriptCompiler::_do_function_infos_match(const FunctionLambdaInfo &p_old_info, const FunctionLambdaInfo *p_new_info) { + if (p_new_info == nullptr) { + return false; + } + + if (p_new_info->capture_count != p_old_info.capture_count || p_new_info->use_self != p_old_info.use_self) { + return false; + } + + int old_required_arg_count = p_old_info.arg_count - p_old_info.default_arg_count; + int new_required_arg_count = p_new_info->arg_count - p_new_info->default_arg_count; + if (new_required_arg_count > old_required_arg_count || p_new_info->arg_count < old_required_arg_count) { + return false; + } + + return true; +} + +void GDScriptCompiler::_get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const FunctionLambdaInfo &p_old_info, const FunctionLambdaInfo *p_new_info) { + ERR_FAIL_COND(r_replacements.has(p_old_info.function)); + if (!_do_function_infos_match(p_old_info, p_new_info)) { + p_new_info = nullptr; + } + + r_replacements.insert(p_old_info.function, p_new_info != nullptr ? p_new_info->function : nullptr); + _get_function_ptr_replacements(r_replacements, p_old_info.sublambdas, p_new_info != nullptr ? &p_new_info->sublambdas : nullptr); +} + +void GDScriptCompiler::_get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const Vector<FunctionLambdaInfo> &p_old_infos, const Vector<FunctionLambdaInfo> *p_new_infos) { + for (int i = 0; i < p_old_infos.size(); ++i) { + const FunctionLambdaInfo &old_info = p_old_infos[i]; + const FunctionLambdaInfo *new_info = nullptr; + if (p_new_infos != nullptr && p_new_infos->size() == p_old_infos.size()) { + // For now only attempt if the size is the same. + new_info = &p_new_infos->get(i); + } + _get_function_ptr_replacements(r_replacements, old_info, new_info); + } +} + +void GDScriptCompiler::_get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const ScriptLambdaInfo &p_old_info, const ScriptLambdaInfo *p_new_info) { + _get_function_ptr_replacements(r_replacements, p_old_info.implicit_initializer_info, p_new_info != nullptr ? &p_new_info->implicit_initializer_info : nullptr); + _get_function_ptr_replacements(r_replacements, p_old_info.implicit_ready_info, p_new_info != nullptr ? &p_new_info->implicit_ready_info : nullptr); + _get_function_ptr_replacements(r_replacements, p_old_info.static_initializer_info, p_new_info != nullptr ? &p_new_info->static_initializer_info : nullptr); + + for (const KeyValue<StringName, Vector<FunctionLambdaInfo>> &old_kv : p_old_info.member_function_infos) { + _get_function_ptr_replacements(r_replacements, old_kv.value, p_new_info != nullptr ? p_new_info->member_function_infos.getptr(old_kv.key) : nullptr); + } + for (int i = 0; i < p_old_info.other_function_infos.size(); ++i) { + const FunctionLambdaInfo &old_other_info = p_old_info.other_function_infos[i]; + const FunctionLambdaInfo *new_other_info = nullptr; + if (p_new_info != nullptr && p_new_info->other_function_infos.size() == p_old_info.other_function_infos.size()) { + // For now only attempt if the size is the same. + new_other_info = &p_new_info->other_function_infos[i]; + } + // Needs to be called on all old lambdas, even if there's no replacement. + _get_function_ptr_replacements(r_replacements, old_other_info, new_other_info); + } + for (const KeyValue<StringName, ScriptLambdaInfo> &old_kv : p_old_info.subclass_info) { + const ScriptLambdaInfo &old_subinfo = old_kv.value; + const ScriptLambdaInfo *new_subinfo = p_new_info != nullptr ? p_new_info->subclass_info.getptr(old_kv.key) : nullptr; + _get_function_ptr_replacements(r_replacements, old_subinfo, new_subinfo); + } +} + Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_script, bool p_keep_state) { err_line = -1; err_column = -1; @@ -3050,6 +3183,8 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri source = p_script->get_path(); + ScriptLambdaInfo old_lambda_info = _get_script_lambda_replacement_info(p_script); + // Create scripts for subclasses beforehand so they can be referenced make_scripts(p_script, root, p_keep_state); @@ -3065,6 +3200,12 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri return err; } + ScriptLambdaInfo new_lambda_info = _get_script_lambda_replacement_info(p_script); + + HashMap<GDScriptFunction *, GDScriptFunction *> func_ptr_replacements; + _get_function_ptr_replacements(func_ptr_replacements, old_lambda_info, &new_lambda_info); + main_script->_recurse_replace_function_ptrs(func_ptr_replacements); + if (has_static_data && !root->annotated_static_unload) { GDScriptCache::add_static_script(p_script); } diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h index 099bd00a2e..0adbe1ed8e 100644 --- a/modules/gdscript/gdscript_compiler.h +++ b/modules/gdscript/gdscript_compiler.h @@ -44,6 +44,34 @@ class GDScriptCompiler { HashSet<GDScript *> parsing_classes; GDScript *main_script = nullptr; + struct FunctionLambdaInfo { + GDScriptFunction *function = nullptr; + GDScriptFunction *parent = nullptr; + GDScript *script = nullptr; + StringName name; + int line = 0; + int index = 0; + int depth = 0; + //uint64_t code_hash; + //int code_size; + int capture_count = 0; + bool use_self = false; + int arg_count = 0; + int default_arg_count = 0; + //Vector<GDScriptDataType> argument_types; + //GDScriptDataType return_type; + Vector<FunctionLambdaInfo> sublambdas; + }; + + struct ScriptLambdaInfo { + Vector<FunctionLambdaInfo> implicit_initializer_info; + Vector<FunctionLambdaInfo> implicit_ready_info; + Vector<FunctionLambdaInfo> static_initializer_info; + HashMap<StringName, Vector<FunctionLambdaInfo>> member_function_infos; + Vector<FunctionLambdaInfo> other_function_infos; + HashMap<StringName, ScriptLambdaInfo> subclass_info; + }; + struct CodeGen { GDScript *script = nullptr; const GDScriptParser::ClassNode *class_node = nullptr; @@ -137,6 +165,13 @@ class GDScriptCompiler { Error _parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter); Error _prepare_compilation(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); Error _compile_class(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); + FunctionLambdaInfo _get_function_replacement_info(GDScriptFunction *p_func, int p_index = -1, int p_depth = 0, GDScriptFunction *p_parent_func = nullptr); + Vector<FunctionLambdaInfo> _get_function_lambda_replacement_info(GDScriptFunction *p_func, int p_depth = 0, GDScriptFunction *p_parent_func = nullptr); + ScriptLambdaInfo _get_script_lambda_replacement_info(GDScript *p_script); + bool _do_function_infos_match(const FunctionLambdaInfo &p_old_info, const FunctionLambdaInfo *p_new_info); + void _get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const FunctionLambdaInfo &p_old_info, const FunctionLambdaInfo *p_new_info); + void _get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const Vector<FunctionLambdaInfo> &p_old_infos, const Vector<FunctionLambdaInfo> *p_new_infos); + void _get_function_ptr_replacements(HashMap<GDScriptFunction *, GDScriptFunction *> &r_replacements, const ScriptLambdaInfo &p_old_info, const ScriptLambdaInfo *p_new_info); int err_line = 0; int err_column = 0; StringName source; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 724715d9e5..12ff22c878 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -104,7 +104,7 @@ Ref<Script> GDScriptLanguage::make_template(const String &p_template, const Stri return scr; } -Vector<ScriptLanguage::ScriptTemplate> GDScriptLanguage::get_built_in_templates(StringName p_object) { +Vector<ScriptLanguage::ScriptTemplate> GDScriptLanguage::get_built_in_templates(const StringName &p_object) { Vector<ScriptLanguage::ScriptTemplate> templates; #ifdef TOOLS_ENABLED for (int i = 0; i < TEMPLATES_ARRAY_SIZE; i++) { @@ -210,7 +210,7 @@ bool GDScriptLanguage::supports_documentation() const { } int GDScriptLanguage::find_function(const String &p_function, const String &p_code) const { - GDScriptTokenizer tokenizer; + GDScriptTokenizerText tokenizer; tokenizer.set_source_code(p_code); int indent = 0; GDScriptTokenizer::Token current = tokenizer.scan(); @@ -537,7 +537,7 @@ struct GDScriptCompletionIdentifier { // appears. For example, if you are completing code in a class that inherits Node2D, a property found on Node2D // will have a "better" (lower) location "score" than a property that is found on CanvasItem. -static int _get_property_location(StringName p_class, StringName p_property) { +static int _get_property_location(const StringName &p_class, const StringName &p_property) { if (!ClassDB::has_property(p_class, p_property)) { return ScriptLanguage::LOCATION_OTHER; } @@ -552,7 +552,20 @@ static int _get_property_location(StringName p_class, StringName p_property) { return depth | ScriptLanguage::LOCATION_PARENT_MASK; } -static int _get_constant_location(StringName p_class, StringName p_constant) { +static int _get_property_location(Ref<Script> p_script, const StringName &p_property) { + int depth = 0; + Ref<Script> scr = p_script; + while (scr.is_valid()) { + if (scr->get_member_line(p_property) != -1) { + return depth | ScriptLanguage::LOCATION_PARENT_MASK; + } + depth++; + scr = scr->get_base_script(); + } + return depth + _get_property_location(p_script->get_instance_base_type(), p_property); +} + +static int _get_constant_location(const StringName &p_class, const StringName &p_constant) { if (!ClassDB::has_integer_constant(p_class, p_constant)) { return ScriptLanguage::LOCATION_OTHER; } @@ -567,7 +580,20 @@ static int _get_constant_location(StringName p_class, StringName p_constant) { return depth | ScriptLanguage::LOCATION_PARENT_MASK; } -static int _get_signal_location(StringName p_class, StringName p_signal) { +static int _get_constant_location(Ref<Script> p_script, const StringName &p_constant) { + int depth = 0; + Ref<Script> scr = p_script; + while (scr.is_valid()) { + if (scr->get_member_line(p_constant) != -1) { + return depth | ScriptLanguage::LOCATION_PARENT_MASK; + } + depth++; + scr = scr->get_base_script(); + } + return depth + _get_constant_location(p_script->get_instance_base_type(), p_constant); +} + +static int _get_signal_location(const StringName &p_class, const StringName &p_signal) { if (!ClassDB::has_signal(p_class, p_signal)) { return ScriptLanguage::LOCATION_OTHER; } @@ -582,7 +608,20 @@ static int _get_signal_location(StringName p_class, StringName p_signal) { return depth | ScriptLanguage::LOCATION_PARENT_MASK; } -static int _get_method_location(StringName p_class, StringName p_method) { +static int _get_signal_location(Ref<Script> p_script, const StringName &p_signal) { + int depth = 0; + Ref<Script> scr = p_script; + while (scr.is_valid()) { + if (scr->get_member_line(p_signal) != -1) { + return depth | ScriptLanguage::LOCATION_PARENT_MASK; + } + depth++; + scr = scr->get_base_script(); + } + return depth + _get_signal_location(p_script->get_instance_base_type(), p_signal); +} + +static int _get_method_location(const StringName &p_class, const StringName &p_method) { if (!ClassDB::has_method(p_class, p_method)) { return ScriptLanguage::LOCATION_OTHER; } @@ -597,7 +636,7 @@ static int _get_method_location(StringName p_class, StringName p_method) { return depth | ScriptLanguage::LOCATION_PARENT_MASK; } -static int _get_enum_constant_location(StringName p_class, StringName p_enum_constant) { +static int _get_enum_constant_location(const StringName &p_class, const StringName &p_enum_constant) { if (!ClassDB::get_integer_constant_enum(p_class, p_enum_constant)) { return ScriptLanguage::LOCATION_OTHER; } @@ -620,9 +659,9 @@ static String _trim_parent_class(const String &p_class, const String &p_base_cla } Vector<String> names = p_class.split(".", false, 1); if (names.size() == 2) { - String first = names[0]; - String rest = names[1]; + const String &first = names[0]; if (ClassDB::class_exists(p_base_class) && ClassDB::class_exists(first) && ClassDB::is_parent_class(p_base_class, first)) { + const String &rest = names[1]; return rest; } } @@ -739,18 +778,24 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio const GDScriptParser::CallNode *call = static_cast<const GDScriptParser::CallNode *>(par->initializer); if (call->is_constant && call->reduced) { def_val = call->function_name.operator String() + call->reduced_value.operator String(); + } else { + def_val = call->function_name.operator String() + (call->arguments.is_empty() ? "()" : "(...)"); } } break; case GDScriptParser::Node::ARRAY: { const GDScriptParser::ArrayNode *arr = static_cast<const GDScriptParser::ArrayNode *>(par->initializer); if (arr->is_constant && arr->reduced) { def_val = arr->reduced_value.operator String(); + } else { + def_val = arr->elements.is_empty() ? "[]" : "[...]"; } } break; case GDScriptParser::Node::DICTIONARY: { const GDScriptParser::DictionaryNode *dict = static_cast<const GDScriptParser::DictionaryNode *>(par->initializer); if (dict->is_constant && dict->reduced) { def_val = dict->reduced_value.operator String(); + } else { + def_val = dict->elements.is_empty() ? "{}" : "{...}"; } } break; case GDScriptParser::Node::SUBSCRIPT: { @@ -783,17 +828,21 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio return arghint; } -static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_list) { +static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_list, const StringName &p_required_type = StringName()) { const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; + const bool requires_type = p_required_type; for (int i = 0; i < p_dir->get_file_count(); i++) { + if (requires_type && !ClassDB::is_parent_class(p_dir->get_file_type(i), p_required_type)) { + continue; + } ScriptLanguage::CodeCompletionOption option(p_dir->get_file_path(i), ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH); option.insert_text = option.display.quote(quote_style); r_list.insert(option.display, option); } for (int i = 0; i < p_dir->get_subdir_count(); i++) { - _get_directory_contents(p_dir->get_subdir(i), r_list); + _get_directory_contents(p_dir->get_subdir(i), r_list, p_required_type); } } @@ -968,9 +1017,9 @@ static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, } } -static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth); +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, bool p_types_only, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth); -static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, bool p_only_functions, bool p_static, bool p_parent_only, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth) { +static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, bool p_only_functions, bool p_types_only, bool p_static, bool p_parent_only, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth) { ERR_FAIL_COND(p_recursion_depth > COMPLETION_RECURSION_LIMIT); if (!p_parent_only) { @@ -984,13 +1033,13 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, ScriptLanguage::CodeCompletionOption option; switch (member.type) { case GDScriptParser::ClassNode::Member::VARIABLE: - if (p_only_functions || outer || (p_static && !member.variable->is_static)) { + if (p_types_only || p_only_functions || outer || (p_static && !member.variable->is_static)) { continue; } option = ScriptLanguage::CodeCompletionOption(member.variable->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); break; case GDScriptParser::ClassNode::Member::CONSTANT: - if (p_only_functions) { + if (p_types_only || p_only_functions) { continue; } if (r_result.has(member.constant->identifier->name)) { @@ -1008,7 +1057,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, option = ScriptLanguage::CodeCompletionOption(member.m_class->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CLASS, location); break; case GDScriptParser::ClassNode::Member::ENUM_VALUE: - if (p_only_functions) { + if (p_types_only || p_only_functions) { continue; } option = ScriptLanguage::CodeCompletionOption(member.enum_value.identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location); @@ -1020,7 +1069,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, option = ScriptLanguage::CodeCompletionOption(member.m_enum->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_ENUM, location); break; case GDScriptParser::ClassNode::Member::FUNCTION: - if (outer || (p_static && !member.function->is_static) || member.function->identifier->name.operator String().begins_with("@")) { + if (p_types_only || outer || (p_static && !member.function->is_static) || member.function->identifier->name.operator String().begins_with("@")) { continue; } option = ScriptLanguage::CodeCompletionOption(member.function->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, location); @@ -1031,7 +1080,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, } break; case GDScriptParser::ClassNode::Member::SIGNAL: - if (p_only_functions || outer || p_static) { + if (p_types_only || p_only_functions || outer || p_static) { continue; } option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); @@ -1043,6 +1092,10 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, } r_result.insert(option.display, option); } + if (p_types_only) { + break; // Otherwise, it will fill the results with types from the outer class (which is undesired for that case). + } + outer = true; clss = clss->outer; classes_processed++; @@ -1054,15 +1107,15 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, base_type.type = p_class->base_type; base_type.type.is_meta_type = p_static; - _find_identifiers_in_base(base_type, p_only_functions, r_result, p_recursion_depth + 1); + _find_identifiers_in_base(base_type, p_only_functions, p_types_only, r_result, p_recursion_depth + 1); } -static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth) { +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, bool p_types_only, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, int p_recursion_depth) { ERR_FAIL_COND(p_recursion_depth > COMPLETION_RECURSION_LIMIT); GDScriptParser::DataType base_type = p_base.type; - if (base_type.is_meta_type && base_type.kind != GDScriptParser::DataType::BUILTIN && base_type.kind != GDScriptParser::DataType::ENUM) { + if (!p_types_only && base_type.is_meta_type && base_type.kind != GDScriptParser::DataType::BUILTIN && base_type.kind != GDScriptParser::DataType::ENUM) { ScriptLanguage::CodeCompletionOption option("new", ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, ScriptLanguage::LOCATION_LOCAL); option.insert_text += "("; r_result.insert(option.display, option); @@ -1071,19 +1124,27 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base while (!base_type.has_no_type()) { switch (base_type.kind) { case GDScriptParser::DataType::CLASS: { - _find_identifiers_in_class(base_type.class_type, p_only_functions, base_type.is_meta_type, false, r_result, p_recursion_depth); + _find_identifiers_in_class(base_type.class_type, p_only_functions, p_types_only, base_type.is_meta_type, false, r_result, p_recursion_depth); // This already finds all parent identifiers, so we are done. base_type = GDScriptParser::DataType(); } break; case GDScriptParser::DataType::SCRIPT: { Ref<Script> scr = base_type.script_type; if (scr.is_valid()) { - if (!p_only_functions) { + if (p_types_only) { + // TODO: Need to implement Script::get_script_enum_list and retrieve the enum list from a script. + } else if (!p_only_functions) { if (!base_type.is_meta_type) { List<PropertyInfo> members; scr->get_script_property_list(&members); for (const PropertyInfo &E : members) { - int location = p_recursion_depth + _get_property_location(scr->get_class_name(), E.name); + if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) { + continue; + } + if (E.name.contains("/")) { + continue; + } + int location = p_recursion_depth + _get_property_location(scr, E.name); ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); r_result.insert(option.display, option); } @@ -1091,7 +1152,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base List<MethodInfo> signals; scr->get_script_signal_list(&signals); for (const MethodInfo &E : signals) { - int location = p_recursion_depth + _get_signal_location(scr->get_class_name(), E.name); + int location = p_recursion_depth + _get_signal_location(scr, E.name); ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location); r_result.insert(option.display, option); } @@ -1099,26 +1160,28 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base HashMap<StringName, Variant> constants; scr->get_constants(&constants); for (const KeyValue<StringName, Variant> &E : constants) { - int location = p_recursion_depth + _get_constant_location(scr->get_class_name(), E.key); + int location = p_recursion_depth + _get_constant_location(scr, E.key); ScriptLanguage::CodeCompletionOption option(E.key.operator String(), ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location); r_result.insert(option.display, option); } } - List<MethodInfo> methods; - scr->get_script_method_list(&methods); - for (const MethodInfo &E : methods) { - if (E.name.begins_with("@")) { - continue; - } - int location = p_recursion_depth + _get_method_location(scr->get_class_name(), E.name); - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, location); - if (E.arguments.size()) { - option.insert_text += "("; - } else { - option.insert_text += "()"; + if (!p_types_only) { + List<MethodInfo> methods; + scr->get_script_method_list(&methods); + for (const MethodInfo &E : methods) { + if (E.name.begins_with("@")) { + continue; + } + int location = p_recursion_depth + _get_method_location(scr->get_class_name(), E.name); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, location); + if (E.arguments.size()) { + option.insert_text += "("; + } else { + option.insert_text += "()"; + } + r_result.insert(option.display, option); } - r_result.insert(option.display, option); } Ref<Script> base_script = scr->get_base_script(); @@ -1139,6 +1202,16 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base return; } + if (p_types_only) { + List<StringName> enums; + ClassDB::get_enum_list(type, &enums); + for (const StringName &E : enums) { + ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_ENUM); + r_result.insert(option.display, option); + } + return; + } + if (!p_only_functions) { List<String> constants; ClassDB::get_integer_constant_list(type, &constants); @@ -1152,7 +1225,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base List<PropertyInfo> pinfo; ClassDB::get_property_list(type, &pinfo); for (const PropertyInfo &E : pinfo) { - if (E.usage & (PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY)) { + if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) { continue; } if (E.name.contains("/")) { @@ -1197,6 +1270,10 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base } break; case GDScriptParser::DataType::ENUM: case GDScriptParser::DataType::BUILTIN: { + if (p_types_only) { + return; + } + Callable::CallError err; Variant tmp; Variant::construct(base_type.builtin_type, tmp, nullptr, 0, err); @@ -1204,6 +1281,8 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base return; } + int location = ScriptLanguage::LOCATION_OTHER; + if (!p_only_functions) { List<PropertyInfo> members; if (p_base.value.get_type() != Variant::NIL) { @@ -1213,8 +1292,15 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base } for (const PropertyInfo &E : members) { + if (E.usage & (PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_INTERNAL)) { + continue; + } if (!String(E.name).contains("/")) { - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, location); + if (base_type.kind == GDScriptParser::DataType::ENUM) { + // Sort enum members in their declaration order. + location += 1; + } if (GDScriptParser::theme_color_names.has(E.name)) { option.theme_color_name = GDScriptParser::theme_color_names[E.name]; } @@ -1230,7 +1316,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base // Enum types are static and cannot change, therefore we skip non-const dictionary methods. continue; } - ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION); + ScriptLanguage::CodeCompletionOption option(E.name, ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION, location); if (E.arguments.size()) { option.insert_text += "("; } else { @@ -1255,7 +1341,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context } if (p_context.current_class) { - _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth); + _find_identifiers_in_class(p_context.current_class, p_only_functions, false, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth); } List<StringName> functions; @@ -1355,7 +1441,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context } } -static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) { +static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value, GDScriptParser::CompletionContext &p_context) { GDScriptCompletionIdentifier ci; ci.value = p_value; ci.type.is_constant = true; @@ -1377,8 +1463,22 @@ static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) { scr = obj->get_script(); } if (scr.is_valid()) { - ci.type.script_type = scr; + ci.type.script_path = scr->get_path(); + + if (scr->get_path().ends_with(".gd")) { + Error err; + Ref<GDScriptParserRef> parser = GDScriptCache::get_parser(scr->get_path(), GDScriptParserRef::INTERFACE_SOLVED, err); + if (err == OK) { + ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + ci.type.class_type = parser->get_parser()->get_tree(); + ci.type.kind = GDScriptParser::DataType::CLASS; + p_context.dependent_parsers.push_back(parser); + return ci; + } + } + ci.type.kind = GDScriptParser::DataType::SCRIPT; + ci.type.script_type = scr; ci.type.native_type = scr->get_instance_base_type(); } else { ci.type.kind = GDScriptParser::DataType::NATIVE; @@ -1403,8 +1503,19 @@ static GDScriptCompletionIdentifier _type_from_property(const PropertyInfo &p_pr ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; ci.type.builtin_type = p_property.type; if (p_property.type == Variant::OBJECT) { - ci.type.kind = GDScriptParser::DataType::NATIVE; - ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name; + if (ScriptServer::is_global_class(p_property.class_name)) { + ci.type.kind = GDScriptParser::DataType::SCRIPT; + ci.type.script_path = ScriptServer::get_global_class_path(p_property.class_name); + ci.type.native_type = ScriptServer::get_global_class_native_base(p_property.class_name); + + Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name)); + if (scr.is_valid()) { + ci.type.script_type = scr; + } + } else { + ci.type.kind = GDScriptParser::DataType::NATIVE; + ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name; + } } else { ci.type.kind = GDScriptParser::DataType::BUILTIN; } @@ -1466,7 +1577,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (p_expression->is_constant) { // Already has a value, so just use that. - r_type = _type_from_variant(p_expression->reduced_value); + r_type = _type_from_variant(p_expression->reduced_value, p_context); switch (p_expression->get_datatype().kind) { case GDScriptParser::DataType::ENUM: case GDScriptParser::DataType::CLASS: @@ -1480,16 +1591,13 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, switch (p_expression->type) { case GDScriptParser::Node::LITERAL: { const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(p_expression); - r_type = _type_from_variant(literal->value); + r_type = _type_from_variant(literal->value, p_context); found = true; } break; case GDScriptParser::Node::SELF: { if (p_context.current_class) { - if (p_context.type != GDScriptParser::COMPLETION_SUPER_METHOD) { - r_type.type = p_context.current_class->get_datatype(); - } else { - r_type.type = p_context.current_class->base_type; - } + r_type.type = p_context.current_class->get_datatype(); + r_type.type.is_meta_type = false; found = true; } } break; @@ -1664,7 +1772,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (!which.is_empty()) { // Try singletons first if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(which)) { - r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which]); + r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which], p_context); found = true; } else { for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) { @@ -1715,7 +1823,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (ce.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) { if (ret.get_type() != Variant::OBJECT || ret.operator Object *() != nullptr) { - r_type = _type_from_variant(ret); + r_type = _type_from_variant(ret, p_context); found = true; } } @@ -1743,7 +1851,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (base.value.get_type() == Variant::DICTIONARY && base.value.operator Dictionary().has(String(subscript->attribute->name))) { Variant value = base.value.operator Dictionary()[String(subscript->attribute->name)]; - r_type = _type_from_variant(value); + r_type = _type_from_variant(value, p_context); found = true; break; } @@ -1795,7 +1903,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (base.value.in(index.value)) { Variant value = base.value.get(index.value); - r_type = _type_from_variant(value); + r_type = _type_from_variant(value, p_context); found = true; break; } @@ -1850,7 +1958,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, bool valid = false; Variant res = base_val.get(index.value, &valid); if (valid) { - r_type = _type_from_variant(res); + r_type = _type_from_variant(res, p_context); r_type.value = Variant(); r_type.type.is_constant = false; found = true; @@ -1908,7 +2016,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, found = false; break; } - r_type = _type_from_variant(res); + r_type = _type_from_variant(res, p_context); if (!v1_use_value || !v2_use_value) { r_type.value = Variant(); r_type.type.is_constant = false; @@ -1997,6 +2105,21 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, default: break; } + } else { + if (p_context.current_class) { + GDScriptCompletionIdentifier base_identifier; + + GDScriptCompletionIdentifier base; + base.value = p_context.base; + base.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + base.type.kind = GDScriptParser::DataType::CLASS; + base.type.class_type = p_context.current_class; + base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static; + + if (_guess_identifier_type_from_base(p_context, base, p_identifier->name, base_identifier)) { + id_type = base_identifier.type; + } + } } while (suite) { @@ -2050,8 +2173,15 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, if (last_assigned_expression && last_assign_line < p_context.current_line) { GDScriptParser::CompletionContext c = p_context; c.current_line = last_assign_line; - r_type.assigned_expression = last_assigned_expression; - if (_guess_expression_type(c, last_assigned_expression, r_type)) { + GDScriptCompletionIdentifier assigned_type; + if (_guess_expression_type(c, last_assigned_expression, assigned_type)) { + if (id_type.is_set() && assigned_type.type.is_set() && !GDScriptAnalyzer::check_type_compatibility(id_type, assigned_type.type)) { + // The assigned type is incompatible. The annotated type takes priority. + r_type.assigned_expression = last_assigned_expression; + r_type.type = id_type; + } else { + r_type = assigned_type; + } return true; } } @@ -2109,20 +2239,6 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, return true; } - // Check current class (including inheritance). - if (p_context.current_class) { - GDScriptCompletionIdentifier base; - base.value = p_context.base; - base.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - base.type.kind = GDScriptParser::DataType::CLASS; - base.type.class_type = p_context.current_class; - base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static; - - if (_guess_identifier_type_from_base(p_context, base, p_identifier->name, r_type)) { - return true; - } - } - // Check global scripts. if (ScriptServer::is_global_class(p_identifier->name)) { String script = ScriptServer::get_global_class_path(p_identifier->name); @@ -2143,7 +2259,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, } else { Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier->name)); if (scr.is_valid()) { - r_type = _type_from_variant(scr); + r_type = _type_from_variant(scr, p_context); r_type.type.is_meta_type = true; return true; } @@ -2153,7 +2269,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, // Check global variables (including autoloads). if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(p_identifier->name)) { - r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name]); + r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name], p_context); return true; } @@ -2206,7 +2322,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & const GDScriptParser::ExpressionNode *init = member.variable->initializer; if (init->is_constant) { r_type.value = init->reduced_value; - r_type = _type_from_variant(init->reduced_value); + r_type = _type_from_variant(init->reduced_value, p_context); return true; } else if (init->start_line == p_context.current_line) { return false; @@ -2233,7 +2349,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & r_type.enumeration = member.m_enum->identifier->name; return true; case GDScriptParser::ClassNode::Member::ENUM_VALUE: - r_type = _type_from_variant(member.enum_value.value); + r_type = _type_from_variant(member.enum_value.value, p_context); return true; case GDScriptParser::ClassNode::Member::SIGNAL: r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; @@ -2269,7 +2385,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & HashMap<StringName, Variant> constants; scr->get_constants(&constants); if (constants.has(p_identifier)) { - r_type = _type_from_variant(constants[p_identifier]); + r_type = _type_from_variant(constants[p_identifier], p_context); return true; } @@ -2333,7 +2449,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext & bool valid = false; Variant res = tmp.get(p_identifier, &valid); if (valid) { - r_type = _type_from_variant(res); + r_type = _type_from_variant(res, p_context); r_type.value = Variant(); r_type.type.is_constant = false; return true; @@ -2601,6 +2717,64 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c r_arghint = _make_arguments_hint(info, p_argidx); } + if (p_argidx == 1 && p_context.node && p_context.node->type == GDScriptParser::Node::CALL && ClassDB::is_parent_class(class_name, SNAME("Tween")) && p_method == SNAME("tween_property")) { + // Get tweened objects properties. + GDScriptParser::ExpressionNode *tweened_object = static_cast<GDScriptParser::CallNode *>(p_context.node)->arguments[0]; + StringName native_type = tweened_object->datatype.native_type; + switch (tweened_object->datatype.kind) { + case GDScriptParser::DataType::SCRIPT: { + Ref<Script> script = tweened_object->datatype.script_type; + native_type = script->get_instance_base_type(); + int n = 0; + while (script.is_valid()) { + List<PropertyInfo> properties; + script->get_script_property_list(&properties); + for (const PropertyInfo &E : properties) { + if (E.usage & (PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_INTERNAL)) { + continue; + } + ScriptLanguage::CodeCompletionOption option(E.name.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n); + r_result.insert(option.display, option); + } + script = script->get_base_script(); + n++; + } + } break; + case GDScriptParser::DataType::CLASS: { + GDScriptParser::ClassNode *clss = tweened_object->datatype.class_type; + native_type = clss->base_type.native_type; + int n = 0; + while (clss) { + for (GDScriptParser::ClassNode::Member member : clss->members) { + if (member.type == GDScriptParser::ClassNode::Member::VARIABLE) { + ScriptLanguage::CodeCompletionOption option(member.get_name().quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::CodeCompletionLocation::LOCATION_LOCAL + n); + r_result.insert(option.display, option); + } + } + if (clss->base_type.kind == GDScriptParser::DataType::Kind::CLASS) { + clss = clss->base_type.class_type; + n++; + } else { + native_type = clss->base_type.native_type; + clss = nullptr; + } + } + } break; + default: + break; + } + + List<PropertyInfo> properties; + ClassDB::get_property_list(native_type, &properties); + for (const PropertyInfo &E : properties) { + if (E.usage & (PROPERTY_USAGE_SUBGROUP | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_INTERNAL)) { + continue; + } + ScriptLanguage::CodeCompletionOption option(E.name.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_MEMBER); + r_result.insert(option.display, option); + } + } + if (p_argidx == 0 && ClassDB::is_parent_class(class_name, SNAME("Node")) && (p_method == SNAME("get_node") || p_method == SNAME("has_node"))) { // Get autoloads List<PropertyInfo> props; @@ -2633,6 +2807,16 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c r_result.insert(option.display, option); } } + if (EDITOR_GET("text_editor/completion/complete_file_paths")) { + if (p_argidx == 0 && p_method == SNAME("change_scene_to_file") && ClassDB::is_parent_class(class_name, SNAME("SceneTree"))) { + HashMap<String, ScriptLanguage::CodeCompletionOption> list; + _get_directory_contents(EditorFileSystem::get_singleton()->get_filesystem(), list, SNAME("PackedScene")); + for (const KeyValue<String, ScriptLanguage::CodeCompletionOption> &key_value_pair : list) { + ScriptLanguage::CodeCompletionOption option = key_value_pair.value; + r_result.insert(option.display, option); + } + } + } base_type.kind = GDScriptParser::DataType::UNRESOLVED; } break; @@ -2667,6 +2851,7 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co if (p_context.base == nullptr) { return false; } + const GDScriptParser::GetNodeNode *get_node = nullptr; switch (p_subscript->base->type) { @@ -2675,6 +2860,11 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co } break; case GDScriptParser::Node::IDENTIFIER: { + if (p_subscript->base->datatype.type_source == GDScriptParser::DataType::ANNOTATED_EXPLICIT) { + // Annotated type takes precedence. + return false; + } + const GDScriptParser::IdentifierNode *identifier_node = static_cast<GDScriptParser::IdentifierNode *>(p_subscript->base); switch (identifier_node->source) { @@ -2715,10 +2905,19 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co if (r_base != nullptr) { *r_base = node; } - r_base_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - r_base_type.kind = GDScriptParser::DataType::NATIVE; + + r_base_type.type_source = GDScriptParser::DataType::INFERRED; r_base_type.builtin_type = Variant::OBJECT; r_base_type.native_type = node->get_class_name(); + + Ref<Script> scr = node->get_script(); + if (scr.is_null()) { + r_base_type.kind = GDScriptParser::DataType::NATIVE; + } else { + r_base_type.kind = GDScriptParser::DataType::SCRIPT; + r_base_type.script_type = scr; + } + return true; } } @@ -2745,8 +2944,6 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c const GDScriptParser::CallNode *call = static_cast<const GDScriptParser::CallNode *>(p_call); GDScriptParser::Node::Type callee_type = call->get_callee_type(); - GDScriptCompletionIdentifier connect_base; - if (callee_type == GDScriptParser::Node::SUBSCRIPT) { const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(call->callee); @@ -2989,7 +3186,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c break; } - _find_identifiers_in_base(base, is_function, options, 0); + _find_identifiers_in_base(base, is_function, false, options, 0); } } break; case GDScriptParser::COMPLETION_SUBSCRIPT: { @@ -2999,7 +3196,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c break; } - _find_identifiers_in_base(base, false, options, 0); + _find_identifiers_in_base(base, false, false, options, 0); } break; case GDScriptParser::COMPLETION_TYPE_ATTRIBUTE: { if (!completion_context.current_class) { @@ -3007,25 +3204,41 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c } const GDScriptParser::TypeNode *type = static_cast<const GDScriptParser::TypeNode *>(completion_context.node); bool found = true; + GDScriptCompletionIdentifier base; base.type.kind = GDScriptParser::DataType::CLASS; base.type.type_source = GDScriptParser::DataType::INFERRED; base.type.is_constant = true; - base.type.class_type = completion_context.current_class; - base.value = completion_context.base; - for (int i = 0; i < completion_context.current_argument; i++) { - GDScriptCompletionIdentifier ci; - if (!_guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci)) { - found = false; - break; + if (completion_context.current_argument == 1) { + StringName type_name = type->type_chain[0]->name; + + if (ClassDB::class_exists(type_name)) { + base.type.kind = GDScriptParser::DataType::NATIVE; + base.type.native_type = type_name; + } else if (ScriptServer::is_global_class(type_name)) { + base.type.kind = GDScriptParser::DataType::SCRIPT; + String scr_path = ScriptServer::get_global_class_path(type_name); + base.type.script_type = ResourceLoader::load(scr_path); + } + } + + if (base.type.kind == GDScriptParser::DataType::CLASS) { + base.type.class_type = completion_context.current_class; + base.value = completion_context.base; + + for (int i = 0; i < completion_context.current_argument; i++) { + GDScriptCompletionIdentifier ci; + if (!_guess_identifier_type_from_base(completion_context, base, type->type_chain[i]->name, ci)) { + found = false; + break; + } + base = ci; } - base = ci; } - // TODO: Improve this to only list types. if (found) { - _find_identifiers_in_base(base, false, options, 0); + _find_identifiers_in_base(base, false, true, options, 0); } r_forced = true; } break; @@ -3114,6 +3327,11 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c List<String> opts; p_owner->get_argument_options("get_node", 0, &opts); + bool for_unique_name = false; + if (completion_context.node != nullptr && completion_context.node->type == GDScriptParser::Node::GET_NODE && !static_cast<GDScriptParser::GetNodeNode *>(completion_context.node)->use_dollar) { + for_unique_name = true; + } + for (const String &E : opts) { r_forced = true; String opt = E.strip_edges(); @@ -3122,9 +3340,25 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c // or handle NodePaths which are valid identifiers and don't need quotes. opt = opt.unquote(); } - // The path needs quotes if it's not a valid identifier (with an exception - // for "/" as path separator, which also doesn't require quotes). - if (!opt.replace("/", "_").is_valid_identifier()) { + + if (for_unique_name) { + if (!opt.begins_with("%")) { + continue; + } + opt = opt.substr(1); + } + + // The path needs quotes if at least one of its components (excluding `/` separations) + // is not a valid identifier. + bool path_needs_quote = false; + for (const String &part : opt.split("/")) { + if (!part.is_valid_identifier()) { + path_needs_quote = true; + break; + } + } + + if (path_needs_quote) { // Ignore quote_style and just use double quotes for paths with apostrophes. // Double quotes don't need to be checked because they're not valid in node and property names. opt = opt.quote(opt.contains("'") ? "\"" : quote_style); // Handle user preference. @@ -3133,11 +3367,13 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c options.insert(option.display, option); } - // Get autoloads. - for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) { - String path = "/root/" + E.key; - ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH); - options.insert(option.display, option); + if (!for_unique_name) { + // Get autoloads. + for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) { + String path = "/root/" + E.key; + ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH); + options.insert(option.display, option); + } } } } break; @@ -3145,7 +3381,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c if (!completion_context.current_class) { break; } - _find_identifiers_in_class(completion_context.current_class, true, false, true, options, 0); + _find_identifiers_in_class(completion_context.current_class, true, false, false, true, options, 0); } break; } @@ -3331,6 +3567,12 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co } if (ClassDB::has_property(class_name, p_symbol, true)) { + PropertyInfo prop_info; + ClassDB::get_property_info(class_name, p_symbol, &prop_info, true); + if (prop_info.usage & PROPERTY_USAGE_INTERNAL) { + return ERR_CANT_RESOLVE; + } + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY; r_result.class_name = base_type.native_type; r_result.class_member = p_symbol; @@ -3444,14 +3686,50 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co analyzer.analyze(); if (context.current_class && context.current_class->extends.size() > 0) { + StringName class_name = context.current_class->extends[0]->name; + bool success = false; - ClassDB::get_integer_constant(context.current_class->extends[0]->name, p_symbol, &success); + ClassDB::get_integer_constant(class_name, p_symbol, &success); if (success) { r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT; - r_result.class_name = context.current_class->extends[0]->name; + r_result.class_name = class_name; r_result.class_member = p_symbol; return OK; } + do { + List<StringName> enums; + ClassDB::get_enum_list(class_name, &enums, true); + for (const StringName &enum_name : enums) { + if (enum_name == p_symbol) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM; + r_result.class_name = class_name; + r_result.class_member = p_symbol; + return OK; + } + } + class_name = ClassDB::get_parent_class_nocheck(class_name); + } while (class_name != StringName()); + } + + const GDScriptParser::TypeNode *type_node = dynamic_cast<const GDScriptParser::TypeNode *>(context.node); + if (type_node != nullptr && !type_node->type_chain.is_empty()) { + StringName class_name = type_node->type_chain[0]->name; + if (ScriptServer::is_global_class(class_name)) { + class_name = ScriptServer::get_global_class_native_base(class_name); + } + do { + List<StringName> enums; + ClassDB::get_enum_list(class_name, &enums, true); + for (const StringName &enum_name : enums) { + if (enum_name == p_symbol) { + r_result.type = ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM; + r_result.class_name = class_name; + r_result.class_member = p_symbol; + return OK; + } + } + class_name = ClassDB::get_parent_class_nocheck(class_name); + } while (class_name != StringName()); } bool is_function = false; @@ -3481,7 +3759,8 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co case GDScriptParser::COMPLETION_ASSIGN: case GDScriptParser::COMPLETION_CALL_ARGUMENTS: case GDScriptParser::COMPLETION_IDENTIFIER: - case GDScriptParser::COMPLETION_PROPERTY_METHOD: { + case GDScriptParser::COMPLETION_PROPERTY_METHOD: + case GDScriptParser::COMPLETION_SUBSCRIPT: { GDScriptParser::DataType base_type; if (context.current_class) { if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) { diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index c9b543fbb9..177c68533e 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -45,10 +45,9 @@ class GDScriptInstance; class GDScript; class GDScriptDataType { -private: - GDScriptDataType *container_element_type = nullptr; - public: + Vector<GDScriptDataType> container_element_types; + enum Kind { UNINITIALIZED, BUILTIN, @@ -76,19 +75,20 @@ public: case BUILTIN: { Variant::Type var_type = p_variant.get_type(); bool valid = builtin_type == var_type; - if (valid && builtin_type == Variant::ARRAY && has_container_element_type()) { + if (valid && builtin_type == Variant::ARRAY && has_container_element_type(0)) { Array array = p_variant; if (array.is_typed()) { + GDScriptDataType array_container_type = get_container_element_type(0); Variant::Type array_builtin_type = (Variant::Type)array.get_typed_builtin(); StringName array_native_type = array.get_typed_class_name(); Ref<Script> array_script_type_ref = array.get_typed_script(); if (array_script_type_ref.is_valid()) { - valid = (container_element_type->kind == SCRIPT || container_element_type->kind == GDSCRIPT) && container_element_type->script_type == array_script_type_ref.ptr(); + valid = (array_container_type.kind == SCRIPT || array_container_type.kind == GDSCRIPT) && array_container_type.script_type == array_script_type_ref.ptr(); } else if (array_native_type != StringName()) { - valid = container_element_type->kind == NATIVE && container_element_type->native_type == array_native_type; + valid = array_container_type.kind == NATIVE && array_container_type.native_type == array_native_type; } else { - valid = container_element_type->kind == BUILTIN && container_element_type->builtin_type == array_builtin_type; + valid = array_container_type.kind == BUILTIN && array_container_type.builtin_type == array_builtin_type; } } else { valid = false; @@ -147,24 +147,32 @@ public: return false; } - void set_container_element_type(const GDScriptDataType &p_element_type) { - container_element_type = memnew(GDScriptDataType(p_element_type)); + void set_container_element_type(int p_index, const GDScriptDataType &p_element_type) { + ERR_FAIL_COND(p_index < 0); + while (p_index >= container_element_types.size()) { + container_element_types.push_back(GDScriptDataType()); + } + container_element_types.write[p_index] = GDScriptDataType(p_element_type); + } + + GDScriptDataType get_container_element_type(int p_index) const { + ERR_FAIL_INDEX_V(p_index, container_element_types.size(), GDScriptDataType()); + return container_element_types[p_index]; } - GDScriptDataType get_container_element_type() const { - ERR_FAIL_NULL_V(container_element_type, GDScriptDataType()); - return *container_element_type; + GDScriptDataType get_container_element_type_or_variant(int p_index) const { + if (p_index < 0 || p_index >= container_element_types.size()) { + return GDScriptDataType(); + } + return container_element_types[p_index]; } - bool has_container_element_type() const { - return container_element_type != nullptr; + bool has_container_element_type(int p_index) const { + return p_index >= 0 && p_index < container_element_types.size(); } - void unset_container_element_type() { - if (container_element_type) { - memdelete(container_element_type); - } - container_element_type = nullptr; + bool has_container_element_types() const { + return !container_element_types.is_empty(); } GDScriptDataType() = default; @@ -176,19 +184,14 @@ public: native_type = p_other.native_type; script_type = p_other.script_type; script_type_ref = p_other.script_type_ref; - unset_container_element_type(); - if (p_other.has_container_element_type()) { - set_container_element_type(p_other.get_container_element_type()); - } + container_element_types = p_other.container_element_types; } GDScriptDataType(const GDScriptDataType &p_other) { *this = p_other; } - ~GDScriptDataType() { - unset_container_element_type(); - } + ~GDScriptDataType() {} }; class GDScriptFunction { @@ -471,6 +474,13 @@ private: uint64_t last_frame_call_count = 0; uint64_t last_frame_self_time = 0; uint64_t last_frame_total_time = 0; + typedef struct NativeProfile { + uint64_t call_count; + uint64_t total_time; + String signature; + } NativeProfile; + HashMap<String, NativeProfile> native_calls; + HashMap<String, NativeProfile> last_native_calls; } profile; #endif @@ -511,6 +521,7 @@ public: void debug_get_stack_member_state(int p_line, List<Pair<StringName, int>> *r_stackvars) const; #ifdef DEBUG_ENABLED + void _profile_native_call(uint64_t p_t_taken, const String &p_function_name, const String &p_instance_class_name = String()); void disassemble(const Vector<String> &p_code_lines) const; #endif diff --git a/modules/gdscript/gdscript_lambda_callable.cpp b/modules/gdscript/gdscript_lambda_callable.cpp index 9d0fce0928..f6fa17c84f 100644 --- a/modules/gdscript/gdscript_lambda_callable.cpp +++ b/modules/gdscript/gdscript_lambda_callable.cpp @@ -44,11 +44,18 @@ bool GDScriptLambdaCallable::compare_less(const CallableCustom *p_a, const Calla return p_a < p_b; } +bool GDScriptLambdaCallable::is_valid() const { + return CallableCustom::is_valid() && function != nullptr; +} + uint32_t GDScriptLambdaCallable::hash() const { return h; } String GDScriptLambdaCallable::get_as_text() const { + if (function == nullptr) { + return "<invalid lambda>"; + } if (function->get_name() != StringName()) { return function->get_name().operator String() + "(lambda)"; } @@ -74,6 +81,12 @@ StringName GDScriptLambdaCallable::get_method() const { void GDScriptLambdaCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { int captures_amount = captures.size(); + if (function == nullptr) { + r_return_value = Variant(); + r_call_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; + return; + } + if (captures_amount > 0) { Vector<const Variant *> args; args.resize(p_argcount + captures_amount); @@ -126,9 +139,11 @@ void GDScriptLambdaCallable::call(const Variant **p_arguments, int p_argcount, V } } -GDScriptLambdaCallable::GDScriptLambdaCallable(Ref<GDScript> p_script, GDScriptFunction *p_function, const Vector<Variant> &p_captures) { +GDScriptLambdaCallable::GDScriptLambdaCallable(Ref<GDScript> p_script, GDScriptFunction *p_function, const Vector<Variant> &p_captures) : + function(p_function) { + ERR_FAIL_NULL(p_script.ptr()); + ERR_FAIL_NULL(p_function); script = p_script; - function = p_function; captures = p_captures; h = (uint32_t)hash_murmur3_one_64((uint64_t)this); @@ -144,11 +159,18 @@ bool GDScriptLambdaSelfCallable::compare_less(const CallableCustom *p_a, const C return p_a < p_b; } +bool GDScriptLambdaSelfCallable::is_valid() const { + return CallableCustom::is_valid() && function != nullptr; +} + uint32_t GDScriptLambdaSelfCallable::hash() const { return h; } String GDScriptLambdaSelfCallable::get_as_text() const { + if (function == nullptr) { + return "<invalid lambda>"; + } if (function->get_name() != StringName()) { return function->get_name().operator String() + "(lambda)"; } @@ -178,6 +200,12 @@ void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcoun int captures_amount = captures.size(); + if (function == nullptr) { + r_return_value = Variant(); + r_call_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; + return; + } + if (captures_amount > 0) { Vector<const Variant *> args; args.resize(p_argcount + captures_amount); @@ -230,18 +258,22 @@ void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcoun } } -GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Ref<RefCounted> p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) { +GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Ref<RefCounted> p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) : + function(p_function) { + ERR_FAIL_NULL(p_self.ptr()); + ERR_FAIL_NULL(p_function); reference = p_self; object = p_self.ptr(); - function = p_function; captures = p_captures; h = (uint32_t)hash_murmur3_one_64((uint64_t)this); } -GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Object *p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) { +GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Object *p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures) : + function(p_function) { + ERR_FAIL_NULL(p_self); + ERR_FAIL_NULL(p_function); object = p_self; - function = p_function; captures = p_captures; h = (uint32_t)hash_murmur3_one_64((uint64_t)this); diff --git a/modules/gdscript/gdscript_lambda_callable.h b/modules/gdscript/gdscript_lambda_callable.h index 1c7a18fb9d..2c5d01aa16 100644 --- a/modules/gdscript/gdscript_lambda_callable.h +++ b/modules/gdscript/gdscript_lambda_callable.h @@ -31,17 +31,18 @@ #ifndef GDSCRIPT_LAMBDA_CALLABLE_H #define GDSCRIPT_LAMBDA_CALLABLE_H +#include "gdscript.h" + #include "core/object/ref_counted.h" #include "core/templates/vector.h" #include "core/variant/callable.h" #include "core/variant/variant.h" -class GDScript; class GDScriptFunction; class GDScriptInstance; class GDScriptLambdaCallable : public CallableCustom { - GDScriptFunction *function = nullptr; + GDScript::UpdatableFuncPtr function; Ref<GDScript> script; uint32_t h; @@ -51,6 +52,7 @@ class GDScriptLambdaCallable : public CallableCustom { static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b); public: + bool is_valid() const override; uint32_t hash() const override; String get_as_text() const override; CompareEqualFunc get_compare_equal_func() const override; @@ -59,13 +61,15 @@ public: StringName get_method() const override; void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; + GDScriptLambdaCallable(GDScriptLambdaCallable &) = delete; + GDScriptLambdaCallable(const GDScriptLambdaCallable &) = delete; GDScriptLambdaCallable(Ref<GDScript> p_script, GDScriptFunction *p_function, const Vector<Variant> &p_captures); virtual ~GDScriptLambdaCallable() = default; }; // Lambda callable that references a particular object, so it can use `self` in the body. class GDScriptLambdaSelfCallable : public CallableCustom { - GDScriptFunction *function = nullptr; + GDScript::UpdatableFuncPtr function; Ref<RefCounted> reference; // For objects that are RefCounted, keep a reference. Object *object = nullptr; // For non RefCounted objects, use a direct pointer. uint32_t h; @@ -76,6 +80,7 @@ class GDScriptLambdaSelfCallable : public CallableCustom { static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b); public: + bool is_valid() const override; uint32_t hash() const override; String get_as_text() const override; CompareEqualFunc get_compare_equal_func() const override; @@ -83,6 +88,8 @@ public: ObjectID get_object() const override; void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; + GDScriptLambdaSelfCallable(GDScriptLambdaSelfCallable &) = delete; + GDScriptLambdaSelfCallable(const GDScriptLambdaSelfCallable &) = delete; GDScriptLambdaSelfCallable(Ref<RefCounted> p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures); GDScriptLambdaSelfCallable(Object *p_self, GDScriptFunction *p_function, const Vector<Variant> &p_captures); virtual ~GDScriptLambdaSelfCallable() = default; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index db7b3e7ace..4d4eadf0fa 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -31,6 +31,7 @@ #include "gdscript_parser.h" #include "gdscript.h" +#include "gdscript_tokenizer_buffer.h" #include "core/config/project_settings.h" #include "core/io/file_access.h" @@ -73,8 +74,11 @@ Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { HashMap<String, String> GDScriptParser::theme_color_names; #endif +HashMap<StringName, GDScriptParser::AnnotationInfo> GDScriptParser::valid_annotations; + void GDScriptParser::cleanup() { builtin_types.clear(); + valid_annotations.clear(); } void GDScriptParser::get_annotation_list(List<MethodInfo> *r_annotations) const { @@ -89,41 +93,43 @@ bool GDScriptParser::annotation_exists(const String &p_annotation_name) const { GDScriptParser::GDScriptParser() { // Register valid annotations. - // TODO: Should this be static? - register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); - register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation); - register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation); - - register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); - // Export annotations. - register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); - register_annotation(MethodInfo("@export_enum", PropertyInfo(Variant::STRING, "names")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_ENUM, Variant::NIL>, varray(), true); - register_annotation(MethodInfo("@export_file", PropertyInfo(Variant::STRING, "filter")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FILE, Variant::STRING>, varray(""), true); - register_annotation(MethodInfo("@export_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_DIR, Variant::STRING>); - register_annotation(MethodInfo("@export_global_file", PropertyInfo(Variant::STRING, "filter")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_FILE, Variant::STRING>, varray(""), true); - register_annotation(MethodInfo("@export_global_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_DIR, Variant::STRING>); - register_annotation(MethodInfo("@export_multiline"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_MULTILINE_TEXT, Variant::STRING>); - register_annotation(MethodInfo("@export_placeholder", PropertyInfo(Variant::STRING, "placeholder")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_PLACEHOLDER_TEXT, Variant::STRING>); - register_annotation(MethodInfo("@export_range", PropertyInfo(Variant::FLOAT, "min"), PropertyInfo(Variant::FLOAT, "max"), PropertyInfo(Variant::FLOAT, "step"), PropertyInfo(Variant::STRING, "extra_hints")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_RANGE, Variant::FLOAT>, varray(1.0, ""), true); - register_annotation(MethodInfo("@export_exp_easing", PropertyInfo(Variant::STRING, "hints")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_EXP_EASING, Variant::FLOAT>, varray(""), true); - register_annotation(MethodInfo("@export_color_no_alpha"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_COLOR_NO_ALPHA, Variant::COLOR>); - register_annotation(MethodInfo("@export_node_path", PropertyInfo(Variant::STRING, "type")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NODE_PATH_VALID_TYPES, Variant::NODE_PATH>, varray(""), true); - register_annotation(MethodInfo("@export_flags", PropertyInfo(Variant::STRING, "names")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FLAGS, Variant::INT>, varray(), true); - register_annotation(MethodInfo("@export_flags_2d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_RENDER, Variant::INT>); - register_annotation(MethodInfo("@export_flags_2d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_PHYSICS, Variant::INT>); - register_annotation(MethodInfo("@export_flags_2d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_NAVIGATION, Variant::INT>); - register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>); - register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>); - register_annotation(MethodInfo("@export_flags_3d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_NAVIGATION, Variant::INT>); - register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>); - // Export grouping annotations. - register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>); - register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray("")); - register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, varray("")); - // Warning annotations. - register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), true); - // Networking. - register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0)); + if (unlikely(valid_annotations.is_empty())) { + register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); + register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation); + register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation); + + register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); + // Export annotations. + register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); + register_annotation(MethodInfo("@export_storage"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>); + register_annotation(MethodInfo("@export_enum", PropertyInfo(Variant::STRING, "names")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_ENUM, Variant::NIL>, varray(), true); + register_annotation(MethodInfo("@export_file", PropertyInfo(Variant::STRING, "filter")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FILE, Variant::STRING>, varray(""), true); + register_annotation(MethodInfo("@export_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_DIR, Variant::STRING>); + register_annotation(MethodInfo("@export_global_file", PropertyInfo(Variant::STRING, "filter")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_FILE, Variant::STRING>, varray(""), true); + register_annotation(MethodInfo("@export_global_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_GLOBAL_DIR, Variant::STRING>); + register_annotation(MethodInfo("@export_multiline"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_MULTILINE_TEXT, Variant::STRING>); + register_annotation(MethodInfo("@export_placeholder", PropertyInfo(Variant::STRING, "placeholder")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_PLACEHOLDER_TEXT, Variant::STRING>); + register_annotation(MethodInfo("@export_range", PropertyInfo(Variant::FLOAT, "min"), PropertyInfo(Variant::FLOAT, "max"), PropertyInfo(Variant::FLOAT, "step"), PropertyInfo(Variant::STRING, "extra_hints")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_RANGE, Variant::FLOAT>, varray(1.0, ""), true); + register_annotation(MethodInfo("@export_exp_easing", PropertyInfo(Variant::STRING, "hints")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_EXP_EASING, Variant::FLOAT>, varray(""), true); + register_annotation(MethodInfo("@export_color_no_alpha"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_COLOR_NO_ALPHA, Variant::COLOR>); + register_annotation(MethodInfo("@export_node_path", PropertyInfo(Variant::STRING, "type")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NODE_PATH_VALID_TYPES, Variant::NODE_PATH>, varray(""), true); + register_annotation(MethodInfo("@export_flags", PropertyInfo(Variant::STRING, "names")), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_FLAGS, Variant::INT>, varray(), true); + register_annotation(MethodInfo("@export_flags_2d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_RENDER, Variant::INT>); + register_annotation(MethodInfo("@export_flags_2d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_PHYSICS, Variant::INT>); + register_annotation(MethodInfo("@export_flags_2d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_2D_NAVIGATION, Variant::INT>); + register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_RENDER, Variant::INT>); + register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_PHYSICS, Variant::INT>); + register_annotation(MethodInfo("@export_flags_3d_navigation"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_3D_NAVIGATION, Variant::INT>); + register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>); + // Export grouping annotations. + register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>); + register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray("")); + register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, varray("")); + // Warning annotations. + register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), true); + // Networking. + register_annotation(MethodInfo("@rpc", PropertyInfo(Variant::STRING, "mode"), PropertyInfo(Variant::STRING, "sync"), PropertyInfo(Variant::STRING, "transfer_mode"), PropertyInfo(Variant::INT, "transfer_channel")), AnnotationInfo::FUNCTION, &GDScriptParser::rpc_annotation, varray("authority", "call_remote", "unreliable", 0)); + } #ifdef DEBUG_ENABLED is_ignoring_warnings = !(bool)GLOBAL_GET("debug/gdscript/warnings/enable"); @@ -222,7 +228,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } - if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { return; } CompletionContext context; @@ -230,7 +236,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node context.current_class = current_class; context.current_function = current_function; context.current_suite = current_suite; - context.current_line = tokenizer.get_cursor_line(); + context.current_line = tokenizer->get_cursor_line(); context.current_argument = p_argument; context.node = p_node; completion_context = context; @@ -240,7 +246,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Typ if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } - if (previous.cursor_place != GDScriptTokenizer::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizer::CURSOR_END && current.cursor_place == GDScriptTokenizer::CURSOR_NONE) { + if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { return; } CompletionContext context; @@ -248,7 +254,7 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Typ context.current_class = current_class; context.current_function = current_function; context.current_suite = current_suite; - context.current_line = tokenizer.get_cursor_line(); + context.current_line = tokenizer->get_cursor_line(); context.builtin_type = p_builtin_type; completion_context = context; } @@ -261,7 +267,7 @@ void GDScriptParser::push_completion_call(Node *p_call) { call.call = p_call; call.argument = 0; completion_call_stack.push_back(call); - if (previous.cursor_place == GDScriptTokenizer::CURSOR_MIDDLE || previous.cursor_place == GDScriptTokenizer::CURSOR_END || current.cursor_place == GDScriptTokenizer::CURSOR_BEGINNING) { + if (previous.cursor_place == GDScriptTokenizerText::CURSOR_MIDDLE || previous.cursor_place == GDScriptTokenizerText::CURSOR_END || current.cursor_place == GDScriptTokenizerText::CURSOR_BEGINNING) { completion_call = call; } } @@ -324,17 +330,21 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ source = source.replace_first(String::chr(0xFFFF), String()); } - tokenizer.set_source_code(source); - tokenizer.set_cursor_position(cursor_line, cursor_column); - script_path = p_script_path; - current = tokenizer.scan(); + GDScriptTokenizerText *text_tokenizer = memnew(GDScriptTokenizerText); + text_tokenizer->set_source_code(source); + + tokenizer = text_tokenizer; + + tokenizer->set_cursor_position(cursor_line, cursor_column); + script_path = p_script_path.simplify_path(); + current = tokenizer->scan(); // Avoid error or newline as the first token. // The latter can mess with the parser when opening files filled exclusively with comments and newlines. while (current.type == GDScriptTokenizer::Token::ERROR || current.type == GDScriptTokenizer::Token::NEWLINE) { if (current.type == GDScriptTokenizer::Token::ERROR) { push_error(current.literal); } - current = tokenizer.scan(); + current = tokenizer->scan(); } #ifdef DEBUG_ENABLED @@ -355,6 +365,9 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ parse_program(); pop_multiline(); + memdelete(text_tokenizer); + tokenizer = nullptr; + #ifdef DEBUG_ENABLED if (multiline_stack.size() > 0) { ERR_PRINT("Parser bug: Imbalanced multiline stack."); @@ -368,6 +381,41 @@ Error GDScriptParser::parse(const String &p_source_code, const String &p_script_ } } +Error GDScriptParser::parse_binary(const Vector<uint8_t> &p_binary, const String &p_script_path) { + GDScriptTokenizerBuffer *buffer_tokenizer = memnew(GDScriptTokenizerBuffer); + Error err = buffer_tokenizer->set_code_buffer(p_binary); + + if (err) { + memdelete(buffer_tokenizer); + return err; + } + + tokenizer = buffer_tokenizer; + script_path = p_script_path; + current = tokenizer->scan(); + // Avoid error or newline as the first token. + // The latter can mess with the parser when opening files filled exclusively with comments and newlines. + while (current.type == GDScriptTokenizer::Token::ERROR || current.type == GDScriptTokenizer::Token::NEWLINE) { + if (current.type == GDScriptTokenizer::Token::ERROR) { + push_error(current.literal); + } + current = tokenizer->scan(); + } + + push_multiline(false); // Keep one for the whole parsing. + parse_program(); + pop_multiline(); + + memdelete(buffer_tokenizer); + tokenizer = nullptr; + + if (errors.is_empty()) { + return OK; + } else { + return ERR_PARSE_ERROR; + } +} + GDScriptTokenizer::Token GDScriptParser::advance() { lambda_ended = false; // Empty marker since we're past the end in any case. @@ -375,16 +423,16 @@ GDScriptTokenizer::Token GDScriptParser::advance() { ERR_FAIL_COND_V_MSG(current.type == GDScriptTokenizer::Token::TK_EOF, current, "GDScript parser bug: Trying to advance past the end of stream."); } if (for_completion && !completion_call_stack.is_empty()) { - if (completion_call.call == nullptr && tokenizer.is_past_cursor()) { + if (completion_call.call == nullptr && tokenizer->is_past_cursor()) { completion_call = completion_call_stack.back()->get(); passed_cursor = true; } } previous = current; - current = tokenizer.scan(); + current = tokenizer->scan(); while (current.type == GDScriptTokenizer::Token::ERROR) { push_error(current.literal); - current = tokenizer.scan(); + current = tokenizer->scan(); } if (previous.type != GDScriptTokenizer::Token::DEDENT) { // `DEDENT` belongs to the next non-empty line. for (Node *n : nodes_in_progress) { @@ -453,19 +501,19 @@ void GDScriptParser::synchronize() { void GDScriptParser::push_multiline(bool p_state) { multiline_stack.push_back(p_state); - tokenizer.set_multiline_mode(p_state); + tokenizer->set_multiline_mode(p_state); if (p_state) { // Consume potential whitespace tokens already waiting in line. while (current.type == GDScriptTokenizer::Token::NEWLINE || current.type == GDScriptTokenizer::Token::INDENT || current.type == GDScriptTokenizer::Token::DEDENT) { - current = tokenizer.scan(); // Don't call advance() here, as we don't want to change the previous token. + current = tokenizer->scan(); // Don't call advance() here, as we don't want to change the previous token. } } } void GDScriptParser::pop_multiline() { - ERR_FAIL_COND_MSG(multiline_stack.size() == 0, "Parser bug: trying to pop from multiline stack without available value."); + ERR_FAIL_COND_MSG(multiline_stack.is_empty(), "Parser bug: trying to pop from multiline stack without available value."); multiline_stack.pop_back(); - tokenizer.set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); + tokenizer->set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); } bool GDScriptParser::is_statement_end_token() const { @@ -504,7 +552,7 @@ void GDScriptParser::end_statement(const String &p_context) { void GDScriptParser::parse_program() { head = alloc_node<ClassNode>(); - head->fqcn = script_path; + head->fqcn = GDScript::canonicalize_path(script_path); current_class = head; bool can_have_class_or_extends = true; @@ -584,7 +632,7 @@ void GDScriptParser::parse_program() { complete_extents(head); #ifdef TOOLS_ENABLED - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = MIN(max_script_doc_line, head->end_line); while (line > 0) { if (comments.has(line) && comments[line].new_line && comments[line].comment.begins_with("##")) { @@ -593,6 +641,7 @@ void GDScriptParser::parse_program() { } line--; } + #endif // TOOLS_ENABLED if (!check(GDScriptTokenizer::Token::TK_EOF)) { @@ -625,7 +674,7 @@ GDScriptParser::ClassNode *GDScriptParser::find_class(const String &p_qualified_ // Starts at index 1 because index 0 was handled above. for (int i = 1; result != nullptr && i < class_names.size(); i++) { - String current_name = class_names[i]; + const String ¤t_name = class_names[i]; GDScriptParser::ClassNode *next = nullptr; if (result->has_member(current_name)) { GDScriptParser::ClassNode::Member member = result->get_member(current_name); @@ -661,7 +710,7 @@ GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) { if (n_class->outer) { String fqcn = n_class->outer->fqcn; if (fqcn.is_empty()) { - fqcn = script_path; + fqcn = GDScript::canonicalize_path(script_path); } n_class->fqcn = fqcn + "::" + n_class->identifier->name; } else { @@ -789,7 +838,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b if (has_comment(member->start_line, true)) { // Inline doc comment. member->doc_data = parse_class_doc_comment(member->start_line, true); - } else if (has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. Don't check `min_member_doc_line` because a class ends parsing after its members. // This may not work correctly for cases like `var a; class B`, but it doesn't matter in practice. member->doc_data = parse_class_doc_comment(doc_comment_line); @@ -798,7 +847,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b if (has_comment(member->start_line, true)) { // Inline doc comment. member->doc_data = parse_doc_comment(member->start_line, true); - } else if (doc_comment_line >= min_member_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (doc_comment_line >= min_member_doc_line && has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. member->doc_data = parse_doc_comment(doc_comment_line); } @@ -1116,7 +1165,12 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) { case VariableNode::PROP_INLINE: { FunctionNode *function = alloc_node<FunctionNode>(); - consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "get".)"); + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected ")" after "get(".)*"); + consume(GDScriptTokenizer::Token::COLON, R"*(Expected ":" after "get()".)*"); + } else { + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" or "(" after "get".)"); + } IdentifierNode *identifier = alloc_node<IdentifierNode>(); complete_extents(identifier); @@ -1264,8 +1318,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { EnumNode *enum_node = alloc_node<EnumNode>(); bool named = false; - if (check(GDScriptTokenizer::Token::IDENTIFIER)) { - advance(); + if (match(GDScriptTokenizer::Token::IDENTIFIER)) { enum_node->identifier = parse_identifier(); named = true; } @@ -1349,7 +1402,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) { if (i == enum_node->values.size() - 1 || enum_node->values[i + 1].line > enum_value_line) { doc_data = parse_doc_comment(enum_value_line, true); } - } else if (doc_comment_line >= min_enum_value_doc_line && has_comment(doc_comment_line, true) && tokenizer.get_comments()[doc_comment_line].new_line) { + } else if (doc_comment_line >= min_enum_value_doc_line && has_comment(doc_comment_line, true) && tokenizer->get_comments()[doc_comment_line].new_line) { // Normal doc comment. doc_data = parse_doc_comment(doc_comment_line); } @@ -2338,6 +2391,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_identifier(ExpressionNode IdentifierNode *identifier = alloc_node<IdentifierNode>(); complete_extents(identifier); identifier->name = previous.get_identifier(); + if (identifier->name.operator String().is_empty()) { + print_line("Empty identifier found."); + } identifier->suite = current_suite; if (current_suite != nullptr && current_suite->has_local(identifier->name)) { @@ -3042,7 +3098,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_call(ExpressionNode *p_pre // Allow for trailing comma. break; } - bool use_identifier_completion = current.cursor_place == GDScriptTokenizer::CURSOR_END || current.cursor_place == GDScriptTokenizer::CURSOR_MIDDLE; + bool use_identifier_completion = current.cursor_place == GDScriptTokenizerText::CURSOR_END || current.cursor_place == GDScriptTokenizerText::CURSOR_MIDDLE; ExpressionNode *argument = parse_expression(false); if (argument == nullptr) { push_error(R"(Expected expression as the function argument.)"); @@ -3212,7 +3268,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p // Reset the multiline stack since we don't want the multiline mode one in the lambda body. push_multiline(false); if (multiline_context) { - tokenizer.push_expression_indented_block(); + tokenizer->push_expression_indented_block(); } push_multiline(true); // For the parameters. @@ -3259,9 +3315,9 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p if (multiline_context) { // If we're in multiline mode, we want to skip the spurious DEDENT and NEWLINE tokens. while (check(GDScriptTokenizer::Token::DEDENT) || check(GDScriptTokenizer::Token::INDENT) || check(GDScriptTokenizer::Token::NEWLINE)) { - current = tokenizer.scan(); // Not advance() since we don't want to change the previous token. + current = tokenizer->scan(); // Not advance() since we don't want to change the previous token. } - tokenizer.pop_expression_indented_block(); + tokenizer->pop_expression_indented_block(); } current_function = previous_function; @@ -3277,6 +3333,19 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p } GDScriptParser::ExpressionNode *GDScriptParser::parse_type_test(ExpressionNode *p_previous_operand, bool p_can_assign) { + // x is not int + // ^ ^^^ ExpressionNode, TypeNode + // ^^^^^^^^^^^^ TypeTestNode + // ^^^^^^^^^^^^ UnaryOpNode + UnaryOpNode *not_node = nullptr; + if (match(GDScriptTokenizer::Token::NOT)) { + not_node = alloc_node<UnaryOpNode>(); + not_node->operation = UnaryOpNode::OP_LOGIC_NOT; + not_node->variant_op = Variant::OP_NOT; + reset_extents(not_node, p_previous_operand); + update_extents(not_node); + } + TypeTestNode *type_test = alloc_node<TypeTestNode>(); reset_extents(type_test, p_previous_operand); update_extents(type_test); @@ -3285,8 +3354,21 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_type_test(ExpressionNode * type_test->test_type = parse_type(); complete_extents(type_test); + if (not_node != nullptr) { + not_node->operand = type_test; + complete_extents(not_node); + } + if (type_test->test_type == nullptr) { - push_error(R"(Expected type specifier after "is".)"); + if (not_node == nullptr) { + push_error(R"(Expected type specifier after "is".)"); + } else { + push_error(R"(Expected type specifier after "is not".)"); + } + } + + if (not_node != nullptr) { + return not_node; } return type_test; @@ -3337,14 +3419,21 @@ GDScriptParser::TypeNode *GDScriptParser::parse_type(bool p_allow_void) { if (match(GDScriptTokenizer::Token::BRACKET_OPEN)) { // Typed collection (like Array[int]). - type->container_type = parse_type(false); // Don't allow void for array element type. - if (type->container_type == nullptr) { - push_error(R"(Expected type for collection after "[".)"); - complete_extents(type); - type = nullptr; - } else if (type->container_type->container_type != nullptr) { - push_error("Nested typed collections are not supported."); - } + bool first_pass = true; + do { + TypeNode *container_type = parse_type(false); // Don't allow void for element type. + if (container_type == nullptr) { + push_error(vformat(R"(Expected type for collection after "%s".)", first_pass ? "[" : ",")); + complete_extents(type); + type = nullptr; + break; + } else if (container_type->container_types.size() > 0) { + push_error("Nested typed collections are not supported."); + } else { + type->container_types.append(container_type); + } + first_pass = false; + } while (match(GDScriptTokenizer::Token::COMMA)); consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected closing "]" after collection type.)"); if (type != nullptr) { complete_extents(type); @@ -3477,20 +3566,20 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons } bool GDScriptParser::has_comment(int p_line, bool p_must_be_doc) { - bool has_comment = tokenizer.get_comments().has(p_line); + bool has_comment = tokenizer->get_comments().has(p_line); // If there are no comments or if we don't care whether the comment // is a docstring, we have our result. if (!p_must_be_doc || !has_comment) { return has_comment; } - return tokenizer.get_comments()[p_line].comment.begins_with("##"); + return tokenizer->get_comments()[p_line].comment.begins_with("##"); } GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool p_single_line) { ERR_FAIL_COND_V(!has_comment(p_line, true), MemberDocData()); - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = p_line; if (!p_single_line) { @@ -3521,11 +3610,17 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool if (state == DOC_LINE_NORMAL) { String stripped_line = doc_line.strip_edges(); - if (stripped_line.begins_with("@deprecated")) { + if (stripped_line == "@deprecated" || stripped_line.begins_with("@deprecated:")) { result.is_deprecated = true; + if (stripped_line.begins_with("@deprecated:")) { + result.deprecated_message = stripped_line.trim_prefix("@deprecated:").strip_edges(); + } continue; - } else if (stripped_line.begins_with("@experimental")) { + } else if (stripped_line == "@experimental" || stripped_line.begins_with("@experimental:")) { result.is_experimental = true; + if (stripped_line.begins_with("@experimental:")) { + result.experimental_message = stripped_line.trim_prefix("@experimental:").strip_edges(); + } continue; } } @@ -3539,7 +3634,7 @@ GDScriptParser::MemberDocData GDScriptParser::parse_doc_comment(int p_line, bool GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, bool p_single_line) { ERR_FAIL_COND_V(!has_comment(p_line, true), ClassDocData()); - const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer.get_comments(); + const HashMap<int, GDScriptTokenizer::CommentData> &comments = tokenizer->get_comments(); int line = p_line; if (!p_single_line) { @@ -3624,11 +3719,17 @@ GDScriptParser::ClassDocData GDScriptParser::parse_class_doc_comment(int p_line, result.tutorials.append(Pair<String, String>(title, link)); continue; - } else if (stripped_line.begins_with("@deprecated")) { + } else if (stripped_line == "@deprecated" || stripped_line.begins_with("@deprecated:")) { result.is_deprecated = true; + if (stripped_line.begins_with("@deprecated:")) { + result.deprecated_message = stripped_line.trim_prefix("@deprecated:").strip_edges(); + } continue; - } else if (stripped_line.begins_with("@experimental")) { + } else if (stripped_line == "@experimental" || stripped_line.begins_with("@experimental:")) { result.is_experimental = true; + if (stripped_line.begins_with("@experimental:")) { + result.experimental_message = stripped_line.trim_prefix("@experimental:").strip_edges(); + } continue; } } @@ -3844,12 +3945,12 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifdef DEBUG_ENABLED - if (this->_is_tool) { + if (_is_tool) { push_error(R"("@tool" annotation can only be used once.)", p_annotation); return false; } #endif // DEBUG_ENABLED - this->_is_tool = true; + _is_tool = true; return true; } @@ -3985,19 +4086,19 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node } hint_string += arg_string; } - variable->export_info.hint_string = hint_string; // This is called after the analyzer is done finding the type, so this should be set here. DataType export_type = variable->get_datatype(); + bool use_default_variable_type_check = true; if (p_annotation->name == SNAME("@export_range")) { if (export_type.builtin_type == Variant::INT) { variable->export_info.type = Variant::INT; } } else if (p_annotation->name == SNAME("@export_multiline")) { - if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type()) { - DataType inner_type = export_type.get_container_element_type(); + if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type(0)) { + DataType inner_type = export_type.get_container_element_type(0); if (inner_type.builtin_type != Variant::STRING) { push_error(vformat(R"("%s" annotation on arrays requires a string type but type "%s" was given instead.)", p_annotation->name.operator String(), inner_type.to_string()), variable); return false; @@ -4021,11 +4122,9 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node return true; } - } + } else if (p_annotation->name == SNAME("@export")) { + use_default_variable_type_check = false; - // WARNING: Do not merge with the previous `else if`! Otherwise `else` (default variable type check) - // will not work for the above annotations. `@export` and `@export_enum` validate the type separately. - if (p_annotation->name == SNAME("@export")) { if (variable->datatype_specifier == nullptr && variable->initializer == nullptr) { push_error(R"(Cannot use simple "@export" annotation with variable without type or initializer, since type can't be inferred.)", p_annotation); return false; @@ -4033,8 +4132,8 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node bool is_array = false; - if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type()) { - export_type = export_type.get_container_element_type(); // Use inner type for. + if (export_type.builtin_type == Variant::ARRAY && export_type.has_container_element_type(0)) { + export_type = export_type.get_container_element_type(0); // Use inner type for. is_array = true; } @@ -4143,6 +4242,8 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node variable->export_info.type = Variant::ARRAY; } } else if (p_annotation->name == SNAME("@export_enum")) { + use_default_variable_type_check = false; + Variant::Type enum_type = Variant::INT; if (export_type.kind == DataType::BUILTIN && export_type.builtin_type == Variant::STRING) { @@ -4160,7 +4261,15 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node push_error(vformat(R"("@export_enum" annotation requires a variable of type "int" or "String" but type "%s" was given instead.)", export_type.to_string()), variable); return false; } - } else { + } else if (p_annotation->name == SNAME("@export_storage")) { + use_default_variable_type_check = false; // Can be applied to a variable of any type. + + // Save the info because the compiler uses export info for overwriting member info. + variable->export_info = export_type.to_property_info(variable->identifier->name); + variable->export_info.usage |= PROPERTY_USAGE_STORAGE; + } + + if (use_default_variable_type_check) { // Validate variable type with export. if (!export_type.is_variant() && (export_type.kind != DataType::BUILTIN || export_type.builtin_type != t_type)) { // Allow float/int conversion. @@ -4344,8 +4453,8 @@ String GDScriptParser::DataType::to_string() const { if (builtin_type == Variant::NIL) { return "null"; } - if (builtin_type == Variant::ARRAY && has_container_element_type()) { - return vformat("Array[%s]", container_element_type->to_string()); + if (builtin_type == Variant::ARRAY && has_container_element_type(0)) { + return vformat("Array[%s]", container_element_types[0].to_string()); } return Variant::get_type_name(builtin_type); case NATIVE: @@ -4398,36 +4507,36 @@ PropertyInfo GDScriptParser::DataType::to_property_info(const String &p_name) co switch (kind) { case BUILTIN: result.type = builtin_type; - if (builtin_type == Variant::ARRAY && has_container_element_type()) { - const DataType *elem_type = container_element_type; - switch (elem_type->kind) { + if (builtin_type == Variant::ARRAY && has_container_element_type(0)) { + const DataType elem_type = get_container_element_type(0); + switch (elem_type.kind) { case BUILTIN: result.hint = PROPERTY_HINT_ARRAY_TYPE; - result.hint_string = Variant::get_type_name(elem_type->builtin_type); + result.hint_string = Variant::get_type_name(elem_type.builtin_type); break; case NATIVE: result.hint = PROPERTY_HINT_ARRAY_TYPE; - result.hint_string = elem_type->native_type; + result.hint_string = elem_type.native_type; break; case SCRIPT: result.hint = PROPERTY_HINT_ARRAY_TYPE; - if (elem_type->script_type.is_valid() && elem_type->script_type->get_global_name() != StringName()) { - result.hint_string = elem_type->script_type->get_global_name(); + if (elem_type.script_type.is_valid() && elem_type.script_type->get_global_name() != StringName()) { + result.hint_string = elem_type.script_type->get_global_name(); } else { - result.hint_string = elem_type->native_type; + result.hint_string = elem_type.native_type; } break; case CLASS: result.hint = PROPERTY_HINT_ARRAY_TYPE; - if (elem_type->class_type != nullptr && elem_type->class_type->get_global_name() != StringName()) { - result.hint_string = elem_type->class_type->get_global_name(); + if (elem_type.class_type != nullptr && elem_type.class_type->get_global_name() != StringName()) { + result.hint_string = elem_type.class_type->get_global_name(); } else { - result.hint_string = elem_type->native_type; + result.hint_string = elem_type.native_type; } break; case ENUM: result.hint = PROPERTY_HINT_ARRAY_TYPE; - result.hint_string = String(elem_type->native_type).replace("::", "."); + result.hint_string = String(elem_type.native_type).replace("::", "."); break; case VARIANT: case RESOLVING: @@ -4986,6 +5095,9 @@ void GDScriptParser::TreePrinter::print_function(FunctionNode *p_function, const for (const AnnotationNode *E : p_function->annotations) { print_annotation(E); } + if (p_function->is_static) { + push_text("Static "); + } push_text(p_context); push_text(" "); if (p_function->identifier) { @@ -5330,6 +5442,9 @@ void GDScriptParser::TreePrinter::print_variable(VariableNode *p_variable) { print_annotation(E); } + if (p_variable->is_static) { + push_text("Static "); + } push_text("Variable "); print_identifier(p_variable->identifier); diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 62a2f4f98c..6664e6df04 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -101,11 +101,9 @@ public: struct WhileNode; class DataType { - private: - // Private access so we can control memory management. - DataType *container_element_type = nullptr; - public: + Vector<DataType> container_element_types; + enum Kind { BUILTIN, NATIVE, @@ -152,24 +150,39 @@ public: _FORCE_INLINE_ String to_string_strict() const { return is_hard_type() ? to_string() : "Variant"; } PropertyInfo to_property_info(const String &p_name) const; - _FORCE_INLINE_ void set_container_element_type(const DataType &p_type) { - container_element_type = memnew(DataType(p_type)); + _FORCE_INLINE_ static DataType get_variant_type() { // Default DataType for container elements. + DataType datatype; + datatype.kind = VARIANT; + datatype.type_source = INFERRED; + return datatype; } - _FORCE_INLINE_ DataType get_container_element_type() const { - ERR_FAIL_NULL_V(container_element_type, DataType()); - return *container_element_type; + _FORCE_INLINE_ void set_container_element_type(int p_index, const DataType &p_type) { + ERR_FAIL_COND(p_index < 0); + while (p_index >= container_element_types.size()) { + container_element_types.push_back(get_variant_type()); + } + container_element_types.write[p_index] = DataType(p_type); } - _FORCE_INLINE_ bool has_container_element_type() const { - return container_element_type != nullptr; + _FORCE_INLINE_ DataType get_container_element_type(int p_index) const { + ERR_FAIL_INDEX_V(p_index, container_element_types.size(), get_variant_type()); + return container_element_types[p_index]; } - _FORCE_INLINE_ void unset_container_element_type() { - if (container_element_type) { - memdelete(container_element_type); - }; - container_element_type = nullptr; + _FORCE_INLINE_ DataType get_container_element_type_or_variant(int p_index) const { + if (p_index < 0 || p_index >= container_element_types.size()) { + return get_variant_type(); + } + return container_element_types[p_index]; + } + + _FORCE_INLINE_ bool has_container_element_type(int p_index) const { + return p_index >= 0 && p_index < container_element_types.size(); + } + + _FORCE_INLINE_ bool has_container_element_types() const { + return !container_element_types.is_empty(); } bool is_typed_container_type() const; @@ -178,11 +191,11 @@ public: bool operator==(const DataType &p_other) const { if (type_source == UNDETECTED || p_other.type_source == UNDETECTED) { - return true; // Can be consireded equal for parsing purposes. + return true; // Can be considered equal for parsing purposes. } if (type_source == INFERRED || p_other.type_source == INFERRED) { - return true; // Can be consireded equal for parsing purposes. + return true; // Can be considered equal for parsing purposes. } if (kind != p_other.kind) { @@ -210,7 +223,7 @@ public: } bool operator!=(const DataType &p_other) const { - return !(this->operator==(p_other)); + return !(*this == p_other); } void operator=(const DataType &p_other) { @@ -229,10 +242,7 @@ public: class_type = p_other.class_type; method_info = p_other.method_info; enum_values = p_other.enum_values; - unset_container_element_type(); - if (p_other.has_container_element_type()) { - set_container_element_type(p_other.get_container_element_type()); - } + container_element_types = p_other.container_element_types; } DataType() = default; @@ -241,9 +251,7 @@ public: *this = p_other; } - ~DataType() { - unset_container_element_type(); - } + ~DataType() {} }; struct ParserError { @@ -266,13 +274,17 @@ public: String description; Vector<Pair<String, String>> tutorials; bool is_deprecated = false; + String deprecated_message; bool is_experimental = false; + String experimental_message; }; struct MemberDocData { String description; bool is_deprecated = false; + String deprecated_message; bool is_experimental = false; + String experimental_message; }; #endif // TOOLS_ENABLED @@ -838,7 +850,7 @@ public: HashMap<StringName, int> parameters_indices; TypeNode *return_type = nullptr; SuiteNode *body = nullptr; - bool is_static = false; + bool is_static = false; // For lambdas it's determined in the analyzer. bool is_coroutine = false; Variant rpc_config; MethodInfo info; @@ -1183,7 +1195,11 @@ public: struct TypeNode : public Node { Vector<IdentifierNode *> type_chain; - TypeNode *container_type = nullptr; + Vector<TypeNode *> container_types; + + TypeNode *get_container_type_or_null(int p_index) const { + return p_index >= 0 && p_index < container_types.size() ? container_types[p_index] : nullptr; + } TypeNode() { type = TYPE; @@ -1324,7 +1340,7 @@ private: HashSet<int> unsafe_lines; #endif - GDScriptTokenizer tokenizer; + GDScriptTokenizer *tokenizer = nullptr; GDScriptTokenizer::Token previous; GDScriptTokenizer::Token current; @@ -1358,7 +1374,7 @@ private: AnnotationAction apply = nullptr; MethodInfo info; }; - HashMap<StringName, AnnotationInfo> valid_annotations; + static HashMap<StringName, AnnotationInfo> valid_annotations; List<AnnotationNode *> annotation_stack; typedef ExpressionNode *(GDScriptParser::*ParseFunction)(ExpressionNode *p_previous_operand, bool p_can_assign); @@ -1458,7 +1474,7 @@ private: SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false); // Annotations AnnotationNode *parse_annotation(uint32_t p_valid_targets); - bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false); + static bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false); bool validate_annotation_arguments(AnnotationNode *p_annotation); void clear_unused_annotations(); bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); @@ -1528,6 +1544,7 @@ private: public: Error parse(const String &p_source_code, const String &p_script_path, bool p_for_completion); + Error parse_binary(const Vector<uint8_t> &p_binary, const String &p_script_path); ClassNode *get_tree() const { return head; } bool is_tool() const { return _is_tool; } ClassNode *find_class(const String &p_qualified_name) const; diff --git a/modules/gdscript/gdscript_rpc_callable.cpp b/modules/gdscript/gdscript_rpc_callable.cpp index a6d2388a91..df014d3cfe 100644 --- a/modules/gdscript/gdscript_rpc_callable.cpp +++ b/modules/gdscript/gdscript_rpc_callable.cpp @@ -73,6 +73,7 @@ void GDScriptRPCCallable::call(const Variant **p_arguments, int p_argcount, Vari } GDScriptRPCCallable::GDScriptRPCCallable(Object *p_object, const StringName &p_method) { + ERR_FAIL_NULL(p_object); object = p_object; method = p_method; h = method.hash(); diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index 98a3a1268f..2940af585d 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -256,7 +256,7 @@ String GDScriptTokenizer::get_token_name(Token::Type p_token_type) { return token_names[p_token_type]; } -void GDScriptTokenizer::set_source_code(const String &p_source_code) { +void GDScriptTokenizerText::set_source_code(const String &p_source_code) { source = p_source_code; if (source.is_empty()) { _source = U""; @@ -270,34 +270,34 @@ void GDScriptTokenizer::set_source_code(const String &p_source_code) { position = 0; } -void GDScriptTokenizer::set_cursor_position(int p_line, int p_column) { +void GDScriptTokenizerText::set_cursor_position(int p_line, int p_column) { cursor_line = p_line; cursor_column = p_column; } -void GDScriptTokenizer::set_multiline_mode(bool p_state) { +void GDScriptTokenizerText::set_multiline_mode(bool p_state) { multiline_mode = p_state; } -void GDScriptTokenizer::push_expression_indented_block() { +void GDScriptTokenizerText::push_expression_indented_block() { indent_stack_stack.push_back(indent_stack); } -void GDScriptTokenizer::pop_expression_indented_block() { - ERR_FAIL_COND(indent_stack_stack.size() == 0); +void GDScriptTokenizerText::pop_expression_indented_block() { + ERR_FAIL_COND(indent_stack_stack.is_empty()); indent_stack = indent_stack_stack.back()->get(); indent_stack_stack.pop_back(); } -int GDScriptTokenizer::get_cursor_line() const { +int GDScriptTokenizerText::get_cursor_line() const { return cursor_line; } -int GDScriptTokenizer::get_cursor_column() const { +int GDScriptTokenizerText::get_cursor_column() const { return cursor_column; } -bool GDScriptTokenizer::is_past_cursor() const { +bool GDScriptTokenizerText::is_past_cursor() const { if (line < cursor_line) { return false; } @@ -310,7 +310,7 @@ bool GDScriptTokenizer::is_past_cursor() const { return true; } -char32_t GDScriptTokenizer::_advance() { +char32_t GDScriptTokenizerText::_advance() { if (unlikely(_is_at_end())) { return '\0'; } @@ -329,11 +329,11 @@ char32_t GDScriptTokenizer::_advance() { return _peek(-1); } -void GDScriptTokenizer::push_paren(char32_t p_char) { +void GDScriptTokenizerText::push_paren(char32_t p_char) { paren_stack.push_back(p_char); } -bool GDScriptTokenizer::pop_paren(char32_t p_expected) { +bool GDScriptTokenizerText::pop_paren(char32_t p_expected) { if (paren_stack.is_empty()) { return false; } @@ -343,13 +343,13 @@ bool GDScriptTokenizer::pop_paren(char32_t p_expected) { return actual == p_expected; } -GDScriptTokenizer::Token GDScriptTokenizer::pop_error() { +GDScriptTokenizer::Token GDScriptTokenizerText::pop_error() { Token error = error_stack.back()->get(); error_stack.pop_back(); return error; } -GDScriptTokenizer::Token GDScriptTokenizer::make_token(Token::Type p_type) { +GDScriptTokenizer::Token GDScriptTokenizerText::make_token(Token::Type p_type) { Token token(p_type); token.start_line = start_line; token.end_line = line; @@ -408,35 +408,35 @@ GDScriptTokenizer::Token GDScriptTokenizer::make_token(Token::Type p_type) { return token; } -GDScriptTokenizer::Token GDScriptTokenizer::make_literal(const Variant &p_literal) { +GDScriptTokenizer::Token GDScriptTokenizerText::make_literal(const Variant &p_literal) { Token token = make_token(Token::LITERAL); token.literal = p_literal; return token; } -GDScriptTokenizer::Token GDScriptTokenizer::make_identifier(const StringName &p_identifier) { +GDScriptTokenizer::Token GDScriptTokenizerText::make_identifier(const StringName &p_identifier) { Token identifier = make_token(Token::IDENTIFIER); identifier.literal = p_identifier; return identifier; } -GDScriptTokenizer::Token GDScriptTokenizer::make_error(const String &p_message) { +GDScriptTokenizer::Token GDScriptTokenizerText::make_error(const String &p_message) { Token error = make_token(Token::ERROR); error.literal = p_message; return error; } -void GDScriptTokenizer::push_error(const String &p_message) { +void GDScriptTokenizerText::push_error(const String &p_message) { Token error = make_error(p_message); error_stack.push_back(error); } -void GDScriptTokenizer::push_error(const Token &p_error) { +void GDScriptTokenizerText::push_error(const Token &p_error) { error_stack.push_back(p_error); } -GDScriptTokenizer::Token GDScriptTokenizer::make_paren_error(char32_t p_paren) { +GDScriptTokenizer::Token GDScriptTokenizerText::make_paren_error(char32_t p_paren) { if (paren_stack.is_empty()) { return make_error(vformat("Closing \"%c\" doesn't have an opening counterpart.", p_paren)); } @@ -445,7 +445,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::make_paren_error(char32_t p_paren) { return error; } -GDScriptTokenizer::Token GDScriptTokenizer::check_vcs_marker(char32_t p_test, Token::Type p_double_type) { +GDScriptTokenizer::Token GDScriptTokenizerText::check_vcs_marker(char32_t p_test, Token::Type p_double_type) { const char32_t *next = _current + 1; int chars = 2; // Two already matched. @@ -469,7 +469,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::check_vcs_marker(char32_t p_test, To } } -GDScriptTokenizer::Token GDScriptTokenizer::annotation() { +GDScriptTokenizer::Token GDScriptTokenizerText::annotation() { if (is_unicode_identifier_start(_peek())) { _advance(); // Consume start character. } else { @@ -550,7 +550,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::annotation() { #define MAX_KEYWORD_LENGTH 10 #ifdef DEBUG_ENABLED -void GDScriptTokenizer::make_keyword_list() { +void GDScriptTokenizerText::make_keyword_list() { #define KEYWORD_LINE(keyword, token_type) keyword, #define KEYWORD_GROUP_IGNORE(group) keyword_list = { @@ -561,7 +561,7 @@ void GDScriptTokenizer::make_keyword_list() { } #endif // DEBUG_ENABLED -GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { +GDScriptTokenizer::Token GDScriptTokenizerText::potential_identifier() { bool only_ascii = _peek(-1) < 128; // Consume all identifier characters. @@ -611,7 +611,9 @@ GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { static_assert(keyword_length <= MAX_KEYWORD_LENGTH, "There's a keyword longer than the defined maximum length"); \ static_assert(keyword_length >= MIN_KEYWORD_LENGTH, "There's a keyword shorter than the defined minimum length"); \ if (keyword_length == len && name == keyword) { \ - return make_token(token_type); \ + Token kw = make_token(token_type); \ + kw.literal = name; \ + return kw; \ } \ } @@ -646,7 +648,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::potential_identifier() { #undef MIN_KEYWORD_LENGTH #undef KEYWORDS -void GDScriptTokenizer::newline(bool p_make_token) { +void GDScriptTokenizerText::newline(bool p_make_token) { // Don't overwrite previous newline, nor create if we want a line continuation. if (p_make_token && !pending_newline && !line_continuation) { Token newline(Token::NEWLINE); @@ -667,11 +669,12 @@ void GDScriptTokenizer::newline(bool p_make_token) { leftmost_column = 1; } -GDScriptTokenizer::Token GDScriptTokenizer::number() { +GDScriptTokenizer::Token GDScriptTokenizerText::number() { int base = 10; bool has_decimal = false; bool has_exponent = false; bool has_error = false; + bool need_digits = false; bool (*digit_check_func)(char32_t) = is_digit; // Sign before hexadecimal or binary. @@ -686,11 +689,13 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { // Hexadecimal. base = 16; digit_check_func = is_hex_digit; + need_digits = true; _advance(); } else if (_peek() == 'b') { // Binary. base = 2; digit_check_func = is_binary_digit; + need_digits = true; _advance(); } } @@ -717,6 +722,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { } previous_was_underscore = true; } else { + need_digits = false; previous_was_underscore = false; } _advance(); @@ -820,6 +826,16 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { } } + if (need_digits) { + // No digits in hex or bin literal. + Token error = make_error(vformat(R"(Expected %s digit after "0%c".)", (base == 16 ? "hexadecimal" : "binary"), (base == 16 ? 'x' : 'b'))); + error.start_column = column; + error.leftmost_column = column; + error.end_column = column + 1; + error.rightmost_column = column + 1; + return error; + } + // Detect extra decimal point. if (!has_error && has_decimal && _peek() == '.' && _peek(1) != '.') { Token error = make_error("Cannot use a decimal point twice in a number."); @@ -854,7 +870,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::number() { } } -GDScriptTokenizer::Token GDScriptTokenizer::string() { +GDScriptTokenizer::Token GDScriptTokenizerText::string() { enum StringType { STRING_REGULAR, STRING_NAME, @@ -1140,7 +1156,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::string() { return make_literal(string); } -void GDScriptTokenizer::check_indent() { +void GDScriptTokenizerText::check_indent() { ERR_FAIL_COND_MSG(column != 1, "Checking tokenizer indentation in the middle of a line."); if (_is_at_end()) { @@ -1309,13 +1325,13 @@ void GDScriptTokenizer::check_indent() { } } -String GDScriptTokenizer::_get_indent_char_name(char32_t ch) { +String GDScriptTokenizerText::_get_indent_char_name(char32_t ch) { ERR_FAIL_COND_V(ch != ' ' && ch != '\t', String(&ch, 1).c_escape()); return ch == ' ' ? "space" : "tab"; } -void GDScriptTokenizer::_skip_whitespace() { +void GDScriptTokenizerText::_skip_whitespace() { if (pending_indents != 0) { // Still have some indent/dedent tokens to give. return; @@ -1377,7 +1393,7 @@ void GDScriptTokenizer::_skip_whitespace() { } } -GDScriptTokenizer::Token GDScriptTokenizer::scan() { +GDScriptTokenizer::Token GDScriptTokenizerText::scan() { if (has_error()) { return pop_error(); } @@ -1439,6 +1455,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::scan() { if (_peek() != '\n') { return make_error("Expected new line after \"\\\"."); } + continuation_lines.push_back(line); _advance(); newline(false); line_continuation = true; @@ -1659,7 +1676,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::scan() { } } -GDScriptTokenizer::GDScriptTokenizer() { +GDScriptTokenizerText::GDScriptTokenizerText() { #ifdef TOOLS_ENABLED if (EditorSettings::get_singleton()) { tab_size = EditorSettings::get_singleton()->get_setting("text_editor/behavior/indent/size"); diff --git a/modules/gdscript/gdscript_tokenizer.h b/modules/gdscript/gdscript_tokenizer.h index 6dd8a98652..5d76375173 100644 --- a/modules/gdscript/gdscript_tokenizer.h +++ b/modules/gdscript/gdscript_tokenizer.h @@ -37,6 +37,12 @@ #include "core/templates/vector.h" #include "core/variant/variant.h" +#ifdef MINGW_ENABLED +#undef CONST +#undef IN +#undef VOID +#endif + class GDScriptTokenizer { public: enum CursorPlace { @@ -175,14 +181,13 @@ public: bool can_precede_bin_op() const; bool is_identifier() const; bool is_node_name() const; - StringName get_identifier() const { return source; } + StringName get_identifier() const { return literal; } Token(Type p_type) { type = p_type; } - Token() { - } + Token() {} }; #ifdef TOOLS_ENABLED @@ -197,12 +202,26 @@ public: new_line = p_new_line; } }; - const HashMap<int, CommentData> &get_comments() const { - return comments; - } + virtual const HashMap<int, CommentData> &get_comments() const = 0; #endif // TOOLS_ENABLED -private: + static String get_token_name(Token::Type p_token_type); + + virtual int get_cursor_line() const = 0; + virtual int get_cursor_column() const = 0; + virtual void set_cursor_position(int p_line, int p_column) = 0; + virtual void set_multiline_mode(bool p_state) = 0; + virtual bool is_past_cursor() const = 0; + virtual void push_expression_indented_block() = 0; // For lambdas, or blocks inside expressions. + virtual void pop_expression_indented_block() = 0; // For lambdas, or blocks inside expressions. + virtual bool is_text() = 0; + + virtual Token scan() = 0; + + virtual ~GDScriptTokenizer() {} +}; + +class GDScriptTokenizerText : public GDScriptTokenizer { String source; const char32_t *_source = nullptr; const char32_t *_current = nullptr; @@ -229,6 +248,7 @@ private: char32_t indent_char = '\0'; int position = 0; int length = 0; + Vector<int> continuation_lines; #ifdef DEBUG_ENABLED Vector<String> keyword_list; #endif // DEBUG_ENABLED @@ -269,20 +289,28 @@ private: Token annotation(); public: - Token scan(); - void set_source_code(const String &p_source_code); - int get_cursor_line() const; - int get_cursor_column() const; - void set_cursor_position(int p_line, int p_column); - void set_multiline_mode(bool p_state); - bool is_past_cursor() const; - static String get_token_name(Token::Type p_token_type); - void push_expression_indented_block(); // For lambdas, or blocks inside expressions. - void pop_expression_indented_block(); // For lambdas, or blocks inside expressions. + const Vector<int> &get_continuation_lines() const { return continuation_lines; } + + virtual int get_cursor_line() const override; + virtual int get_cursor_column() const override; + virtual void set_cursor_position(int p_line, int p_column) override; + virtual void set_multiline_mode(bool p_state) override; + virtual bool is_past_cursor() const override; + virtual void push_expression_indented_block() override; // For lambdas, or blocks inside expressions. + virtual void pop_expression_indented_block() override; // For lambdas, or blocks inside expressions. + virtual bool is_text() override { return true; } + +#ifdef TOOLS_ENABLED + virtual const HashMap<int, CommentData> &get_comments() const override { + return comments; + } +#endif // TOOLS_ENABLED + + virtual Token scan() override; - GDScriptTokenizer(); + GDScriptTokenizerText(); }; #endif // GDSCRIPT_TOKENIZER_H diff --git a/modules/gdscript/gdscript_tokenizer_buffer.cpp b/modules/gdscript/gdscript_tokenizer_buffer.cpp new file mode 100644 index 0000000000..db523ea941 --- /dev/null +++ b/modules/gdscript/gdscript_tokenizer_buffer.cpp @@ -0,0 +1,493 @@ +/**************************************************************************/ +/* gdscript_tokenizer_buffer.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_tokenizer_buffer.h" + +#include "core/io/compression.h" +#include "core/io/marshalls.h" + +#define TOKENIZER_VERSION 100 + +int GDScriptTokenizerBuffer::_token_to_binary(const Token &p_token, Vector<uint8_t> &r_buffer, int p_start, HashMap<StringName, uint32_t> &r_identifiers_map, HashMap<Variant, uint32_t, VariantHasher, VariantComparator> &r_constants_map) { + int pos = p_start; + + int token_type = p_token.type & TOKEN_MASK; + + switch (p_token.type) { + case GDScriptTokenizer::Token::ANNOTATION: + case GDScriptTokenizer::Token::IDENTIFIER: { + // Add identifier to map. + int identifier_pos; + StringName id = p_token.get_identifier(); + if (r_identifiers_map.has(id)) { + identifier_pos = r_identifiers_map[id]; + } else { + identifier_pos = r_identifiers_map.size(); + r_identifiers_map[id] = identifier_pos; + } + token_type |= identifier_pos << TOKEN_BITS; + } break; + case GDScriptTokenizer::Token::ERROR: + case GDScriptTokenizer::Token::LITERAL: { + // Add literal to map. + int constant_pos; + if (r_constants_map.has(p_token.literal)) { + constant_pos = r_constants_map[p_token.literal]; + } else { + constant_pos = r_constants_map.size(); + r_constants_map[p_token.literal] = constant_pos; + } + token_type |= constant_pos << TOKEN_BITS; + } break; + default: + break; + } + + // Encode token. + int token_len; + if (token_type & TOKEN_MASK) { + token_len = 8; + r_buffer.resize(pos + token_len); + encode_uint32(token_type | TOKEN_BYTE_MASK, &r_buffer.write[pos]); + pos += 4; + } else { + token_len = 5; + r_buffer.resize(pos + token_len); + r_buffer.write[pos] = token_type; + pos++; + } + encode_uint32(p_token.start_line, &r_buffer.write[pos]); + return token_len; +} + +GDScriptTokenizer::Token GDScriptTokenizerBuffer::_binary_to_token(const uint8_t *p_buffer) { + Token token; + const uint8_t *b = p_buffer; + + uint32_t token_type = decode_uint32(b); + token.type = (Token::Type)(token_type & TOKEN_MASK); + if (token_type & TOKEN_BYTE_MASK) { + b += 4; + } else { + b++; + } + token.start_line = decode_uint32(b); + token.end_line = token.start_line; + + token.literal = token.get_name(); + if (token.type == Token::CONST_NAN) { + token.literal = String("NAN"); // Special case since name and notation are different. + } + + switch (token.type) { + case GDScriptTokenizer::Token::ANNOTATION: + case GDScriptTokenizer::Token::IDENTIFIER: { + // Get name from map. + int identifier_pos = token_type >> TOKEN_BITS; + if (unlikely(identifier_pos >= identifiers.size())) { + Token error; + error.type = Token::ERROR; + error.literal = "Identifier index out of bounds."; + return error; + } + token.literal = identifiers[identifier_pos]; + } break; + case GDScriptTokenizer::Token::ERROR: + case GDScriptTokenizer::Token::LITERAL: { + // Get literal from map. + int constant_pos = token_type >> TOKEN_BITS; + if (unlikely(constant_pos >= constants.size())) { + Token error; + error.type = Token::ERROR; + error.literal = "Constant index out of bounds."; + return error; + } + token.literal = constants[constant_pos]; + } break; + default: + break; + } + + return token; +} + +Error GDScriptTokenizerBuffer::set_code_buffer(const Vector<uint8_t> &p_buffer) { + const uint8_t *buf = p_buffer.ptr(); + ERR_FAIL_COND_V(p_buffer.size() < 12 || p_buffer[0] != 'G' || p_buffer[1] != 'D' || p_buffer[2] != 'S' || p_buffer[3] != 'C', ERR_INVALID_DATA); + + int version = decode_uint32(&buf[4]); + ERR_FAIL_COND_V_MSG(version > TOKENIZER_VERSION, ERR_INVALID_DATA, "Binary GDScript is too recent! Please use a newer engine version."); + + int decompressed_size = decode_uint32(&buf[8]); + + Vector<uint8_t> contents; + if (decompressed_size == 0) { + contents = p_buffer.slice(12); + } else { + contents.resize(decompressed_size); + int result = Compression::decompress(contents.ptrw(), contents.size(), &buf[12], p_buffer.size() - 12, Compression::MODE_ZSTD); + ERR_FAIL_COND_V_MSG(result != decompressed_size, ERR_INVALID_DATA, "Error decompressing GDScript tokenizer buffer."); + } + + int total_len = contents.size(); + buf = contents.ptr(); + uint32_t identifier_count = decode_uint32(&buf[0]); + uint32_t constant_count = decode_uint32(&buf[4]); + uint32_t token_line_count = decode_uint32(&buf[8]); + uint32_t token_count = decode_uint32(&buf[16]); + + const uint8_t *b = &buf[20]; + total_len -= 20; + + identifiers.resize(identifier_count); + for (uint32_t i = 0; i < identifier_count; i++) { + uint32_t len = decode_uint32(b); + total_len -= 4; + ERR_FAIL_COND_V((len * 4u) > (uint32_t)total_len, ERR_INVALID_DATA); + b += 4; + Vector<uint32_t> cs; + cs.resize(len); + for (uint32_t j = 0; j < len; j++) { + uint8_t tmp[4]; + for (uint32_t k = 0; k < 4; k++) { + tmp[k] = b[j * 4 + k] ^ 0xb6; + } + cs.write[j] = decode_uint32(tmp); + } + + String s(reinterpret_cast<const char32_t *>(cs.ptr()), len); + b += len * 4; + total_len -= len * 4; + identifiers.write[i] = s; + } + + constants.resize(constant_count); + for (uint32_t i = 0; i < constant_count; i++) { + Variant v; + int len; + Error err = decode_variant(v, b, total_len, &len, false); + if (err) { + return err; + } + b += len; + total_len -= len; + constants.write[i] = v; + } + + for (uint32_t i = 0; i < token_line_count; i++) { + ERR_FAIL_COND_V(total_len < 8, ERR_INVALID_DATA); + uint32_t token_index = decode_uint32(b); + b += 4; + uint32_t line = decode_uint32(b); + b += 4; + total_len -= 8; + token_lines[token_index] = line; + } + for (uint32_t i = 0; i < token_line_count; i++) { + ERR_FAIL_COND_V(total_len < 8, ERR_INVALID_DATA); + uint32_t token_index = decode_uint32(b); + b += 4; + uint32_t column = decode_uint32(b); + b += 4; + total_len -= 8; + token_columns[token_index] = column; + } + + tokens.resize(token_count); + for (uint32_t i = 0; i < token_count; i++) { + int token_len = 5; + if ((*b) & TOKEN_BYTE_MASK) { + token_len = 8; + } + ERR_FAIL_COND_V(total_len < token_len, ERR_INVALID_DATA); + Token token = _binary_to_token(b); + b += token_len; + ERR_FAIL_INDEX_V(token.type, Token::TK_MAX, ERR_INVALID_DATA); + tokens.write[i] = token; + total_len -= token_len; + } + + ERR_FAIL_COND_V(total_len > 0, ERR_INVALID_DATA); + + return OK; +} + +Vector<uint8_t> GDScriptTokenizerBuffer::parse_code_string(const String &p_code, CompressMode p_compress_mode) { + HashMap<StringName, uint32_t> identifier_map; + HashMap<Variant, uint32_t, VariantHasher, VariantComparator> constant_map; + Vector<uint8_t> token_buffer; + HashMap<uint32_t, uint32_t> token_lines; + HashMap<uint32_t, uint32_t> token_columns; + + GDScriptTokenizerText tokenizer; + tokenizer.set_source_code(p_code); + tokenizer.set_multiline_mode(true); // Ignore whitespace tokens. + Token current = tokenizer.scan(); + int token_pos = 0; + int last_token_line = 0; + int token_counter = 0; + + while (current.type != Token::TK_EOF) { + int token_len = _token_to_binary(current, token_buffer, token_pos, identifier_map, constant_map); + token_pos += token_len; + if (token_counter > 0 && current.start_line > last_token_line) { + token_lines[token_counter] = current.start_line; + token_columns[token_counter] = current.start_column; + } + last_token_line = current.end_line; + + current = tokenizer.scan(); + token_counter++; + } + + // Reverse maps. + Vector<StringName> rev_identifier_map; + rev_identifier_map.resize(identifier_map.size()); + for (const KeyValue<StringName, uint32_t> &E : identifier_map) { + rev_identifier_map.write[E.value] = E.key; + } + Vector<Variant> rev_constant_map; + rev_constant_map.resize(constant_map.size()); + for (const KeyValue<Variant, uint32_t> &E : constant_map) { + rev_constant_map.write[E.value] = E.key; + } + HashMap<uint32_t, uint32_t> rev_token_lines; + for (const KeyValue<uint32_t, uint32_t> &E : token_lines) { + rev_token_lines[E.value] = E.key; + } + + // Remove continuation lines from map. + for (int line : tokenizer.get_continuation_lines()) { + if (rev_token_lines.has(line + 1)) { + token_lines.erase(rev_token_lines[line + 1]); + token_columns.erase(rev_token_lines[line + 1]); + } + } + + Vector<uint8_t> contents; + contents.resize(20); + encode_uint32(identifier_map.size(), &contents.write[0]); + encode_uint32(constant_map.size(), &contents.write[4]); + encode_uint32(token_lines.size(), &contents.write[8]); + encode_uint32(token_counter, &contents.write[16]); + + int buf_pos = 20; + + // Save identifiers. + for (const StringName &id : rev_identifier_map) { + String s = id.operator String(); + int len = s.length(); + + contents.resize(buf_pos + (len + 1) * 4); + + encode_uint32(len, &contents.write[buf_pos]); + buf_pos += 4; + + for (int i = 0; i < len; i++) { + uint8_t tmp[4]; + encode_uint32(s[i], tmp); + + for (int b = 0; b < 4; b++) { + contents.write[buf_pos + b] = tmp[b] ^ 0xb6; + } + + buf_pos += 4; + } + } + + // Save constants. + for (const Variant &v : rev_constant_map) { + int len; + // Objects cannot be constant, never encode objects. + Error err = encode_variant(v, nullptr, len, false); + ERR_FAIL_COND_V_MSG(err != OK, Vector<uint8_t>(), "Error when trying to encode Variant."); + contents.resize(buf_pos + len); + encode_variant(v, &contents.write[buf_pos], len, false); + buf_pos += len; + } + + // Save lines and columns. + contents.resize(buf_pos + token_lines.size() * 16); + for (const KeyValue<uint32_t, uint32_t> &e : token_lines) { + encode_uint32(e.key, &contents.write[buf_pos]); + buf_pos += 4; + encode_uint32(e.value, &contents.write[buf_pos]); + buf_pos += 4; + } + for (const KeyValue<uint32_t, uint32_t> &e : token_columns) { + encode_uint32(e.key, &contents.write[buf_pos]); + buf_pos += 4; + encode_uint32(e.value, &contents.write[buf_pos]); + buf_pos += 4; + } + + // Store tokens. + contents.append_array(token_buffer); + + Vector<uint8_t> buf; + + // Save header. + buf.resize(12); + buf.write[0] = 'G'; + buf.write[1] = 'D'; + buf.write[2] = 'S'; + buf.write[3] = 'C'; + encode_uint32(TOKENIZER_VERSION, &buf.write[4]); + + switch (p_compress_mode) { + case COMPRESS_NONE: + encode_uint32(0u, &buf.write[8]); + buf.append_array(contents); + break; + + case COMPRESS_ZSTD: { + encode_uint32(contents.size(), &buf.write[8]); + Vector<uint8_t> compressed; + int max_size = Compression::get_max_compressed_buffer_size(contents.size(), Compression::MODE_ZSTD); + compressed.resize(max_size); + + int compressed_size = Compression::compress(compressed.ptrw(), contents.ptr(), contents.size(), Compression::MODE_ZSTD); + ERR_FAIL_COND_V_MSG(compressed_size < 0, Vector<uint8_t>(), "Error compressing GDScript tokenizer buffer."); + compressed.resize(compressed_size); + + buf.append_array(compressed); + } break; + } + + return buf; +} + +int GDScriptTokenizerBuffer::get_cursor_line() const { + return 0; +} + +int GDScriptTokenizerBuffer::get_cursor_column() const { + return 0; +} + +void GDScriptTokenizerBuffer::set_cursor_position(int p_line, int p_column) { +} + +void GDScriptTokenizerBuffer::set_multiline_mode(bool p_state) { + multiline_mode = p_state; +} + +bool GDScriptTokenizerBuffer::is_past_cursor() const { + return false; +} + +void GDScriptTokenizerBuffer::push_expression_indented_block() { + indent_stack_stack.push_back(indent_stack); +} + +void GDScriptTokenizerBuffer::pop_expression_indented_block() { + ERR_FAIL_COND(indent_stack_stack.is_empty()); + indent_stack = indent_stack_stack.back()->get(); + indent_stack_stack.pop_back(); +} + +GDScriptTokenizer::Token GDScriptTokenizerBuffer::scan() { + // Add final newline. + if (current >= tokens.size() && !last_token_was_newline) { + Token newline; + newline.type = Token::NEWLINE; + newline.start_line = current_line; + newline.end_line = current_line; + last_token_was_newline = true; + return newline; + } + + // Resolve pending indentation change. + if (pending_indents > 0) { + pending_indents--; + Token indent; + indent.type = Token::INDENT; + indent.start_line = current_line; + indent.end_line = current_line; + return indent; + } else if (pending_indents < 0) { + pending_indents++; + Token dedent; + dedent.type = Token::DEDENT; + dedent.start_line = current_line; + dedent.end_line = current_line; + return dedent; + } + + if (current >= tokens.size()) { + if (!indent_stack.is_empty()) { + pending_indents -= indent_stack.size(); + indent_stack.clear(); + return scan(); + } + Token eof; + eof.type = Token::TK_EOF; + return eof; + }; + + if (!last_token_was_newline && token_lines.has(current)) { + current_line = token_lines[current]; + uint32_t current_column = token_columns[current]; + + // Check if there's a need to indent/dedent. + if (!multiline_mode) { + uint32_t previous_indent = 0; + if (!indent_stack.is_empty()) { + previous_indent = indent_stack.back()->get(); + } + if (current_column - 1 > previous_indent) { + pending_indents++; + indent_stack.push_back(current_column - 1); + } else { + while (current_column - 1 < previous_indent) { + pending_indents--; + indent_stack.pop_back(); + if (indent_stack.is_empty()) { + break; + } + previous_indent = indent_stack.back()->get(); + } + } + + Token newline; + newline.type = Token::NEWLINE; + newline.start_line = current_line; + newline.end_line = current_line; + last_token_was_newline = true; + + return newline; + } + } + + last_token_was_newline = false; + + Token token = tokens[current++]; + return token; +} diff --git a/modules/gdscript/gdscript_tokenizer_buffer.h b/modules/gdscript/gdscript_tokenizer_buffer.h new file mode 100644 index 0000000000..55df66e50f --- /dev/null +++ b/modules/gdscript/gdscript_tokenizer_buffer.h @@ -0,0 +1,93 @@ +/**************************************************************************/ +/* gdscript_tokenizer_buffer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_TOKENIZER_BUFFER_H +#define GDSCRIPT_TOKENIZER_BUFFER_H + +#include "gdscript_tokenizer.h" + +class GDScriptTokenizerBuffer : public GDScriptTokenizer { +public: + enum CompressMode { + COMPRESS_NONE, + COMPRESS_ZSTD, + }; + + enum { + TOKEN_BYTE_MASK = 0x80, + TOKEN_BITS = 8, + TOKEN_MASK = (1 << (TOKEN_BITS - 1)) - 1, + }; + + Vector<StringName> identifiers; + Vector<Variant> constants; + Vector<int> continuation_lines; + HashMap<int, int> token_lines; + HashMap<int, int> token_columns; + Vector<Token> tokens; + int current = 0; + uint32_t current_line = 1; + + bool multiline_mode = false; + List<int> indent_stack; + List<List<int>> indent_stack_stack; // For lambdas, which require manipulating the indentation point. + int pending_indents = 0; + bool last_token_was_newline = false; + +#ifdef TOOLS_ENABLED + HashMap<int, CommentData> dummy; +#endif // TOOLS_ENABLED + + static int _token_to_binary(const Token &p_token, Vector<uint8_t> &r_buffer, int p_start, HashMap<StringName, uint32_t> &r_identifiers_map, HashMap<Variant, uint32_t, VariantHasher, VariantComparator> &r_constants_map); + Token _binary_to_token(const uint8_t *p_buffer); + +public: + Error set_code_buffer(const Vector<uint8_t> &p_buffer); + static Vector<uint8_t> parse_code_string(const String &p_code, CompressMode p_compress_mode); + + virtual int get_cursor_line() const override; + virtual int get_cursor_column() const override; + virtual void set_cursor_position(int p_line, int p_column) override; + virtual void set_multiline_mode(bool p_state) override; + virtual bool is_past_cursor() const override; + virtual void push_expression_indented_block() override; // For lambdas, or blocks inside expressions. + virtual void pop_expression_indented_block() override; // For lambdas, or blocks inside expressions. + virtual bool is_text() override { return false; }; + +#ifdef TOOLS_ENABLED + virtual const HashMap<int, CommentData> &get_comments() const override { + return dummy; + } +#endif // TOOLS_ENABLED + + virtual Token scan() override; +}; + +#endif // GDSCRIPT_TOKENIZER_BUFFER_H diff --git a/modules/gdscript/gdscript_utility_callable.cpp b/modules/gdscript/gdscript_utility_callable.cpp new file mode 100644 index 0000000000..7708a18044 --- /dev/null +++ b/modules/gdscript/gdscript_utility_callable.cpp @@ -0,0 +1,111 @@ +/**************************************************************************/ +/* gdscript_utility_callable.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "gdscript_utility_callable.h" + +#include "core/templates/hashfuncs.h" + +bool GDScriptUtilityCallable::compare_equal(const CallableCustom *p_a, const CallableCustom *p_b) { + return p_a->hash() == p_b->hash(); +} + +bool GDScriptUtilityCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) { + return p_a->hash() < p_b->hash(); +} + +uint32_t GDScriptUtilityCallable::hash() const { + return h; +} + +String GDScriptUtilityCallable::get_as_text() const { + String scope; + switch (type) { + case TYPE_INVALID: + scope = "<invalid scope>"; + break; + case TYPE_GLOBAL: + scope = "@GlobalScope"; + break; + case TYPE_GDSCRIPT: + scope = "@GDScript"; + break; + } + return vformat("%s::%s (Callable)", scope, function_name); +} + +CallableCustom::CompareEqualFunc GDScriptUtilityCallable::get_compare_equal_func() const { + return compare_equal; +} + +CallableCustom::CompareLessFunc GDScriptUtilityCallable::get_compare_less_func() const { + return compare_less; +} + +bool GDScriptUtilityCallable::is_valid() const { + return type != TYPE_INVALID; +} + +StringName GDScriptUtilityCallable::get_method() const { + return function_name; +} + +ObjectID GDScriptUtilityCallable::get_object() const { + return ObjectID(); +} + +void GDScriptUtilityCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { + switch (type) { + case TYPE_INVALID: + r_return_value = vformat(R"(Trying to call invalid utility function "%s".)", function_name); + r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; + r_call_error.argument = 0; + r_call_error.expected = 0; + break; + case TYPE_GLOBAL: + Variant::call_utility_function(function_name, &r_return_value, p_arguments, p_argcount, r_call_error); + break; + case TYPE_GDSCRIPT: + gdscript_function(&r_return_value, p_arguments, p_argcount, r_call_error); + break; + } +} + +GDScriptUtilityCallable::GDScriptUtilityCallable(const StringName &p_function_name) { + function_name = p_function_name; + if (GDScriptUtilityFunctions::function_exists(p_function_name)) { + type = TYPE_GDSCRIPT; + gdscript_function = GDScriptUtilityFunctions::get_function(p_function_name); + } else if (Variant::has_utility_function(p_function_name)) { + type = TYPE_GLOBAL; + } else { + ERR_PRINT(vformat(R"(Unknown utility function "%s".)", p_function_name)); + } + h = p_function_name.hash(); +} diff --git a/modules/gdscript/gdscript_utility_callable.h b/modules/gdscript/gdscript_utility_callable.h new file mode 100644 index 0000000000..675bc4ddd9 --- /dev/null +++ b/modules/gdscript/gdscript_utility_callable.h @@ -0,0 +1,65 @@ +/**************************************************************************/ +/* gdscript_utility_callable.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GDSCRIPT_UTILITY_CALLABLE_H +#define GDSCRIPT_UTILITY_CALLABLE_H + +#include "gdscript_utility_functions.h" + +#include "core/variant/callable.h" + +class GDScriptUtilityCallable : public CallableCustom { + StringName function_name; + enum Type { + TYPE_INVALID, + TYPE_GLOBAL, + TYPE_GDSCRIPT, + }; + Type type = TYPE_INVALID; + GDScriptUtilityFunctions::FunctionPtr gdscript_function = nullptr; + uint32_t h = 0; + + static bool compare_equal(const CallableCustom *p_a, const CallableCustom *p_b); + static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b); + +public: + uint32_t hash() const override; + String get_as_text() const override; + CompareEqualFunc get_compare_equal_func() const override; + CompareLessFunc get_compare_less_func() const override; + bool is_valid() const override; + StringName get_method() const override; + ObjectID get_object() const override; + void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; + + GDScriptUtilityCallable(const StringName &p_function_name); +}; + +#endif // GDSCRIPT_UTILITY_CALLABLE_H diff --git a/modules/gdscript/gdscript_utility_functions.cpp b/modules/gdscript/gdscript_utility_functions.cpp index 40c564c36b..f8cb460e40 100644 --- a/modules/gdscript/gdscript_utility_functions.cpp +++ b/modules/gdscript/gdscript_utility_functions.cpp @@ -194,9 +194,9 @@ struct GDScriptUtilityFunctionsDefinitions { // Calculate how many. int count = 0; if (incr > 0) { - count = ((to - from - 1) / incr) + 1; + count = Math::division_round_up(to - from, incr); } else { - count = ((from - to - 1) / -incr) + 1; + count = Math::division_round_up(from - to, -incr); } Error err = arr.resize(count); @@ -470,7 +470,8 @@ struct GDScriptUtilityFunctionsDefinitions { static inline void len(Variant *r_ret, const Variant **p_args, int p_arg_count, Callable::CallError &r_error) { VALIDATE_ARG_COUNT(1); switch (p_args[0]->get_type()) { - case Variant::STRING: { + case Variant::STRING: + case Variant::STRING_NAME: { String d = *p_args[0]; *r_ret = d.length(); } break; diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index b723ecc185..1a8c22cc11 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -36,6 +36,18 @@ #include "core/os/os.h" #ifdef DEBUG_ENABLED + +static bool _profile_count_as_native(const Object *p_base_obj, const StringName &p_methodname) { + if (!p_base_obj) { + return false; + } + StringName cname = p_base_obj->get_class_name(); + if ((p_methodname == "new" && cname == "GDScript") || p_methodname == "call") { + return false; + } + return ClassDB::class_exists(cname) && ClassDB::has_method(cname, p_methodname, false); +} + static String _get_element_type(Variant::Type builtin_type, const StringName &native_type, const Ref<Script> &script_type) { if (script_type.is_valid() && script_type->is_valid()) { return GDScript::debug_get_script_name(script_type); @@ -84,6 +96,18 @@ static String _get_var_type(const Variant *p_var) { return basestr; } + +void GDScriptFunction::_profile_native_call(uint64_t p_t_taken, const String &p_func_name, const String &p_instance_class_name) { + HashMap<String, Profile::NativeProfile>::Iterator inner_prof = profile.native_calls.find(p_func_name); + if (inner_prof) { + inner_prof->value.call_count += 1; + } else { + String sig = vformat("%s::0::%s%s%s", get_script()->get_script_path(), p_instance_class_name, p_instance_class_name.is_empty() ? "" : ".", p_func_name); + inner_prof = profile.native_calls.insert(p_func_name, Profile::NativeProfile{ 1, 0, sig }); + } + inner_prof->value.total_time += p_t_taken; +} + #endif // DEBUG_ENABLED Variant GDScriptFunction::_get_default_variant_for_data_type(const GDScriptDataType &p_data_type) { @@ -91,8 +115,8 @@ Variant GDScriptFunction::_get_default_variant_for_data_type(const GDScriptDataT if (p_data_type.builtin_type == Variant::ARRAY) { Array array; // Typed array. - if (p_data_type.has_container_element_type()) { - const GDScriptDataType &element_type = p_data_type.get_container_element_type(); + if (p_data_type.has_container_element_type(0)) { + const GDScriptDataType &element_type = p_data_type.get_container_element_type(0); array.set_typed(element_type.builtin_type, element_type.native_type, element_type.script_type); } @@ -631,10 +655,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } bool exit_ok = false; bool awaited = false; -#endif - -#ifdef DEBUG_ENABLED - int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0 }; + int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? (int)p_instance->members.size() : 0 }; #endif Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr }; @@ -662,6 +683,14 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a uint32_t op_signature = _code_ptr[ip + 5]; uint32_t actual_signature = (a->get_type() << 8) | (b->get_type()); +#ifdef DEBUG_ENABLED + if (op == Variant::OP_DIVIDE || op == Variant::OP_MODULE) { + // Don't optimize division and modulo since there's not check for division by zero with validated calls. + op_signature = 0xFFFF; + _code_ptr[ip + 5] = op_signature; + } +#endif + // Check if this is the first run. If so, store the current signature for the optimized path. if (unlikely(op_signature == 0)) { static Mutex initializer_mutex; @@ -844,8 +873,12 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GET_VARIANT_PTR(value, 2); bool valid; +#ifdef DEBUG_ENABLED + Variant::VariantSetError err_code; + dst->set(*index, *value, &valid, &err_code); +#else dst->set(*index, *value, &valid); - +#endif #ifdef DEBUG_ENABLED if (!valid) { Object *obj = dst->get_validated_object(); @@ -862,7 +895,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } else { v = "of type '" + _get_var_type(index) + "'"; } - err_text = "Invalid set index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'"; + err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; + if (err_code == Variant::VariantSetError::SET_INDEXED_ERR) { + err_text = "Invalid assignment of index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + } } OPCODE_BREAK; } @@ -893,7 +929,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } else { v = "of type '" + _get_var_type(index) + "'"; } - err_text = "Invalid set index " + v + " (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'"; + err_text = "Invalid assignment of property or key " + v + " with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; OPCODE_BREAK; } #endif @@ -943,7 +979,8 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a bool valid; #ifdef DEBUG_ENABLED // Allow better error message in cases where src and dst are the same stack position. - Variant ret = src->get(*index, &valid); + Variant::VariantGetError err_code; + Variant ret = src->get(*index, &valid, &err_code); #else *dst = src->get(*index, &valid); @@ -956,7 +993,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } else { v = "of type '" + _get_var_type(index) + "'"; } - err_text = "Invalid get index " + v + " (on base: '" + _get_var_type(src) + "')."; + err_text = "Invalid access to property or key " + v + " on a base object of type '" + _get_var_type(src) + "'."; + if (err_code == Variant::VariantGetError::GET_INDEXED_ERR) { + err_text = "Invalid access of index " + v + " on a base object of type: '" + _get_var_type(src) + "'."; + } OPCODE_BREAK; } *dst = ret; @@ -992,7 +1032,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } else { v = "of type '" + _get_var_type(key) + "'"; } - err_text = "Invalid get index " + v + " (on base: '" + _get_var_type(src) + "')."; + err_text = "Invalid access to property or key " + v + " on a base object of type '" + _get_var_type(src) + "'."; OPCODE_BREAK; } *dst = ret; @@ -1057,7 +1097,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (read_only_property) { err_text = vformat(R"(Cannot set value into property "%s" (on base "%s") because it is read-only.)", String(*index), _get_var_type(dst)); } else { - err_text = "Invalid set index '" + String(*index) + "' (on base: '" + _get_var_type(dst) + "') with value of type '" + _get_var_type(value) + "'."; + err_text = "Invalid assignment of property or key '" + String(*index) + "' with value of type '" + _get_var_type(value) + "' on a base object of type '" + _get_var_type(dst) + "'."; } OPCODE_BREAK; } @@ -1102,7 +1142,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #endif #ifdef DEBUG_ENABLED if (!valid) { - err_text = "Invalid get index '" + index->operator String() + "' (on base: '" + _get_var_type(src) + "')."; + err_text = "Invalid access to property or key '" + index->operator String() + "' on a base object of type '" + _get_var_type(src) + "'."; OPCODE_BREAK; } *dst = ret; @@ -1653,16 +1693,14 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (GDScriptLanguage::get_singleton()->profiling) { call_time = OS::get_singleton()->get_ticks_usec(); } - + Variant::Type base_type = base->get_type(); + Object *base_obj = base->get_validated_object(); + StringName base_class = base_obj ? base_obj->get_class_name() : StringName(); #endif + Callable::CallError err; if (call_ret) { GET_INSTRUCTION_ARG(ret, argc + 1); -#ifdef DEBUG_ENABLED - Variant::Type base_type = base->get_type(); - Object *base_obj = base->get_validated_object(); - StringName base_class = base_obj ? base_obj->get_class_name() : StringName(); -#endif base->callp(*methodname, (const Variant **)argptrs, argc, *ret, err); #ifdef DEBUG_ENABLED if (ret->get_type() == Variant::NIL) { @@ -1696,8 +1734,13 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a base->callp(*methodname, (const Variant **)argptrs, argc, ret, err); } #ifdef DEBUG_ENABLED + if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + if (GDScriptLanguage::get_singleton()->profile_native_calls && _profile_count_as_native(base_obj, *methodname)) { + _profile_native_call(t_taken, *methodname, base_class); + } + function_call_time += t_taken; } if (err.error != Callable::CallError::CALL_OK) { @@ -1774,8 +1817,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED uint64_t call_time = 0; - - if (GDScriptLanguage::get_singleton()->profiling) { + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { call_time = OS::get_singleton()->get_ticks_usec(); } #endif @@ -1789,8 +1831,11 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } #ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; + + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; } if (err.error != Callable::CallError::CALL_OK) { @@ -1843,22 +1888,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a const Variant **argptrs = const_cast<const Variant **>(instruction_args); -#ifdef DEBUG_ENABLED - uint64_t call_time = 0; - - if (GDScriptLanguage::get_singleton()->profiling) { - call_time = OS::get_singleton()->get_ticks_usec(); - } -#endif - Callable::CallError err; Variant::call_static(builtin_type, *methodname, argptrs, argc, *ret, err); #ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; - } - if (err.error != Callable::CallError::CALL_OK) { err_text = _get_call_error(err, "static function '" + methodname->operator String() + "' in type '" + Variant::get_type_name(builtin_type) + "'", argptrs); OPCODE_BREAK; @@ -1887,8 +1920,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED uint64_t call_time = 0; - - if (GDScriptLanguage::get_singleton()->profiling) { + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { call_time = OS::get_singleton()->get_ticks_usec(); } #endif @@ -1897,15 +1929,17 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a *ret = method->call(nullptr, argptrs, argc, err); #ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; } +#endif if (err.error != Callable::CallError::CALL_OK) { err_text = _get_call_error(err, "static function '" + method->get_name().operator String() + "' in type '" + method->get_instance_class().operator String() + "'", argptrs); OPCODE_BREAK; } -#endif ip += 3; } @@ -1943,8 +1977,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED uint64_t call_time = 0; - - if (GDScriptLanguage::get_singleton()->profiling) { + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { call_time = OS::get_singleton()->get_ticks_usec(); } #endif @@ -1953,10 +1986,13 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a method->validated_call(base_obj, (const Variant **)argptrs, ret); #ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; } #endif + ip += 3; } DISPATCH_OPCODE; @@ -1990,8 +2026,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a Variant **argptrs = instruction_args; #ifdef DEBUG_ENABLED uint64_t call_time = 0; - - if (GDScriptLanguage::get_singleton()->profiling) { + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { call_time = OS::get_singleton()->get_ticks_usec(); } #endif @@ -2001,10 +2036,13 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a method->validated_call(base_obj, (const Variant **)argptrs, nullptr); #ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; } #endif + ip += 3; } DISPATCH_OPCODE; @@ -2025,22 +2063,9 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a Variant::ValidatedBuiltInMethod method = _builtin_methods_ptr[_code_ptr[ip + 2]]; Variant **argptrs = instruction_args; -#ifdef DEBUG_ENABLED - uint64_t call_time = 0; - if (GDScriptLanguage::get_singleton()->profiling) { - call_time = OS::get_singleton()->get_ticks_usec(); - } -#endif - GET_INSTRUCTION_ARG(ret, argc + 1); method(base, (const Variant **)argptrs, argc, ret); -#ifdef DEBUG_ENABLED - if (GDScriptLanguage::get_singleton()->profiling) { - function_call_time += OS::get_singleton()->get_ticks_usec() - call_time; - } -#endif - ip += 3; } DISPATCH_OPCODE; @@ -2459,7 +2484,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a Variant::construct(ret_type, retvalue, const_cast<const Variant **>(&r), 1, ce); } else { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", Variant::get_type_name(r->get_type()), Variant::get_type_name(ret_type)); #endif // DEBUG_ENABLED @@ -2489,9 +2514,9 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (r->get_type() != Variant::ARRAY) { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return a value of type "%s" where expected return type is "Array[%s]".)", - _get_var_type(r), _get_element_type(builtin_type, native_type, *script_type)); -#endif // DEBUG_ENABLED + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "Array[%s]".)", + Variant::get_type_name(r->get_type()), Variant::get_type_name(builtin_type)); +#endif OPCODE_BREAK; } @@ -2522,7 +2547,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a GD_ERR_BREAK(!nc); if (r->get_type() != Variant::OBJECT && r->get_type() != Variant::NIL) { - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", Variant::get_type_name(r->get_type()), nc->get_name()); OPCODE_BREAK; } @@ -2540,7 +2565,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #endif // DEBUG_ENABLED if (ret_obj && !ClassDB::is_parent_class(ret_obj->get_class_name(), nc->get_name())) { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", ret_obj->get_class_name(), nc->get_name()); #endif // DEBUG_ENABLED OPCODE_BREAK; @@ -2563,7 +2588,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (r->get_type() != Variant::OBJECT && r->get_type() != Variant::NIL) { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", Variant::get_type_name(r->get_type()), GDScript::debug_get_script_name(Ref<Script>(base_type))); #endif // DEBUG_ENABLED OPCODE_BREAK; @@ -2585,7 +2610,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a ScriptInstance *ret_inst = ret_obj->get_script_instance(); if (!ret_inst) { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", ret_obj->get_class_name(), GDScript::debug_get_script_name(Ref<GDScript>(base_type))); #endif // DEBUG_ENABLED OPCODE_BREAK; @@ -2604,7 +2629,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (!valid) { #ifdef DEBUG_ENABLED - err_text = vformat(R"(Trying to return value of type "%s" from a function which the return type is "%s".)", + err_text = vformat(R"(Trying to return value of type "%s" from a function whose return type is "%s".)", GDScript::debug_get_script_name(ret_obj->get_script_instance()->get_script()), GDScript::debug_get_script_name(Ref<GDScript>(base_type))); #endif // DEBUG_ENABLED OPCODE_BREAK; diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index 36806d2f73..ad7af34bf1 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -48,17 +48,17 @@ lsp::Position GodotPosition::to_lsp(const Vector<String> &p_lines) const { lsp::Position res; // Special case: `line = 0` -> root class (range covers everything). - if (this->line <= 0) { + if (line <= 0) { return res; } // Special case: `line = p_lines.size() + 1` -> root class (range covers everything). - if (this->line >= p_lines.size() + 1) { + if (line >= p_lines.size() + 1) { res.line = p_lines.size(); return res; } - res.line = this->line - 1; + res.line = line - 1; // Note: character outside of `pos_line.length()-1` is valid. - res.character = this->column - 1; + res.character = column - 1; String pos_line = p_lines[res.line]; if (pos_line.contains("\t")) { @@ -67,7 +67,7 @@ lsp::Position GodotPosition::to_lsp(const Vector<String> &p_lines) const { int in_col = 1; int res_char = 0; - while (res_char < pos_line.size() && in_col < this->column) { + while (res_char < pos_line.size() && in_col < column) { if (pos_line[res_char] == '\t') { in_col += tab_size; res_char++; @@ -191,7 +191,7 @@ void ExtendGDScriptParser::update_symbols() { void ExtendGDScriptParser::update_document_links(const String &p_code) { document_links.clear(); - GDScriptTokenizer scr_tokenizer; + GDScriptTokenizerText scr_tokenizer; Ref<FileAccess> fs = FileAccess::create(FileAccess::ACCESS_RESOURCES); scr_tokenizer.set_source_code(p_code); while (true) { @@ -211,7 +211,7 @@ void ExtendGDScriptParser::update_document_links(const String &p_code) { String value = const_val; lsp::DocumentLink link; link.target = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(scr_path); - link.range = GodotRange(GodotPosition(token.start_line, token.start_column), GodotPosition(token.end_line, token.end_column)).to_lsp(this->lines); + link.range = GodotRange(GodotPosition(token.start_line, token.start_column), GodotPosition(token.end_line, token.end_column)).to_lsp(lines); document_links.push_back(link); } } @@ -222,7 +222,7 @@ void ExtendGDScriptParser::update_document_links(const String &p_code) { lsp::Range ExtendGDScriptParser::range_of_node(const GDScriptParser::Node *p_node) const { GodotPosition start(p_node->start_line, p_node->start_column); GodotPosition end(p_node->end_line, p_node->end_column); - return GodotRange(start, end).to_lsp(this->lines); + return GodotRange(start, end).to_lsp(lines); } void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p_class, lsp::DocumentSymbol &r_symbol) { @@ -394,8 +394,8 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p symbol.name = m.enum_value.identifier->name; symbol.kind = lsp::SymbolKind::EnumMember; symbol.deprecated = false; - symbol.range.start = GodotPosition(m.enum_value.line, m.enum_value.leftmost_column).to_lsp(this->lines); - symbol.range.end = GodotPosition(m.enum_value.line, m.enum_value.rightmost_column).to_lsp(this->lines); + symbol.range.start = GodotPosition(m.enum_value.line, m.enum_value.leftmost_column).to_lsp(lines); + symbol.range.end = GodotPosition(m.enum_value.line, m.enum_value.rightmost_column).to_lsp(lines); symbol.selectionRange = range_of_node(m.enum_value.identifier); symbol.documentation = m.enum_value.doc_data.description; symbol.uri = uri; @@ -430,8 +430,8 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p child.name = value.identifier->name; child.kind = lsp::SymbolKind::EnumMember; child.deprecated = false; - child.range.start = GodotPosition(value.line, value.leftmost_column).to_lsp(this->lines); - child.range.end = GodotPosition(value.line, value.rightmost_column).to_lsp(this->lines); + child.range.start = GodotPosition(value.line, value.leftmost_column).to_lsp(lines); + child.range.end = GodotPosition(value.line, value.rightmost_column).to_lsp(lines); child.selectionRange = range_of_node(value.identifier); child.documentation = value.doc_data.description; child.uri = uri; diff --git a/modules/gdscript/language_server/gdscript_language_server.cpp b/modules/gdscript/language_server/gdscript_language_server.cpp index 053be7eec2..9ba41352f2 100644 --- a/modules/gdscript/language_server/gdscript_language_server.cpp +++ b/modules/gdscript/language_server/gdscript_language_server.cpp @@ -63,6 +63,10 @@ void GDScriptLanguageServer::_notification(int p_what) { } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!EditorSettings::get_singleton()->check_changed_settings_in_group("network/language_server")) { + break; + } + String remote_host = String(_EDITOR_GET("network/language_server/remote_host")); int remote_port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); bool remote_use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index 44f605232d..9bf458e031 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -114,7 +114,7 @@ void GDScriptTextDocument::didSave(const Variant &p_param) { scr->update_exports(); ScriptEditor::get_singleton()->reload_scripts(true); ScriptEditor::get_singleton()->update_docs_from_script(scr); - ScriptEditor::get_singleton()->trigger_live_script_reload(); + ScriptEditor::get_singleton()->trigger_live_script_reload(scr->get_path()); } } @@ -315,9 +315,8 @@ Dictionary GDScriptTextDocument::resolve(const Dictionary &p_params) { Vector<String> param_symbols = query.split(SYMBOL_SEPERATOR, false); if (param_symbols.size() >= 2) { - String class_ = param_symbols[0]; - StringName class_name = class_; - String member_name = param_symbols[param_symbols.size() - 1]; + StringName class_name = param_symbols[0]; + const String &member_name = param_symbols[param_symbols.size() - 1]; String inner_class_name; if (param_symbols.size() >= 3) { inner_class_name = param_symbols[1]; @@ -422,7 +421,7 @@ Array GDScriptTextDocument::definition(const Dictionary &p_params) { lsp::TextDocumentPositionParams params; params.load(p_params); List<const lsp::DocumentSymbol *> symbols; - Array arr = this->find_symbols(params, symbols); + Array arr = find_symbols(params, symbols); return arr; } @@ -430,7 +429,7 @@ Variant GDScriptTextDocument::declaration(const Dictionary &p_params) { lsp::TextDocumentPositionParams params; params.load(p_params); List<const lsp::DocumentSymbol *> symbols; - Array arr = this->find_symbols(params, symbols); + Array arr = find_symbols(params, symbols); if (arr.is_empty() && !symbols.is_empty() && !symbols.front()->get()->native_class.is_empty()) { // Find a native symbol const lsp::DocumentSymbol *symbol = symbols.front()->get(); if (GDScriptLanguageProtocol::get_singleton()->is_goto_native_symbols_enabled()) { @@ -457,7 +456,7 @@ Variant GDScriptTextDocument::declaration(const Dictionary &p_params) { id = "class_global:" + symbol->native_class + ":" + symbol->name; break; } - call_deferred(SNAME("show_native_symbol_in_editor"), id); + callable_mp(this, &GDScriptTextDocument::show_native_symbol_in_editor).call_deferred(id); } else { notify_client_show_symbol(symbol); } diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index 81933c8c87..853a8e0f19 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -641,7 +641,7 @@ void GDScriptWorkspace::completion(const lsp::CompletionParams &p_params, List<S while (!stack.is_empty()) { current = Object::cast_to<Node>(stack.pop_back()); Ref<GDScript> scr = current->get_script(); - if (scr.is_valid() && scr->get_path() == path) { + if (scr.is_valid() && GDScript::is_canonically_equal_paths(scr->get_path(), path)) { break; } for (int i = 0; i < current->get_child_count(); ++i) { @@ -650,7 +650,7 @@ void GDScriptWorkspace::completion(const lsp::CompletionParams &p_params, List<S } Ref<GDScript> scr = current->get_script(); - if (!scr.is_valid() || scr->get_path() != path) { + if (!scr.is_valid() || !GDScript::is_canonically_equal_paths(scr->get_path(), path)) { current = owner_scene_node; } } diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index e09adb74bd..284762018f 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -200,7 +200,7 @@ struct LocationLink { /** * The range that should be selected and revealed when this link is being followed, e.g the name of a function. - * Must be contained by the the `targetRange`. See also `DocumentSymbol#range` + * Must be contained by the `targetRange`. See also `DocumentSymbol#range` */ Range targetSelectionRange; }; diff --git a/modules/gdscript/register_types.cpp b/modules/gdscript/register_types.cpp index 605e82be6e..59e387eece 100644 --- a/modules/gdscript/register_types.cpp +++ b/modules/gdscript/register_types.cpp @@ -34,6 +34,7 @@ #include "gdscript_analyzer.h" #include "gdscript_cache.h" #include "gdscript_tokenizer.h" +#include "gdscript_tokenizer_buffer.h" #include "gdscript_utility_functions.h" #ifdef TOOLS_ENABLED @@ -81,23 +82,41 @@ Ref<GDScriptEditorTranslationParserPlugin> gdscript_translation_parser_plugin; class EditorExportGDScript : public EditorExportPlugin { GDCLASS(EditorExportGDScript, EditorExportPlugin); -public: - virtual void _export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) override { - String script_key; + static constexpr int DEFAULT_SCRIPT_MODE = EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED; + int script_mode = DEFAULT_SCRIPT_MODE; - const Ref<EditorExportPreset> &preset = get_export_preset(); +protected: + virtual void _export_begin(const HashSet<String> &p_features, bool p_debug, const String &p_path, int p_flags) override { + script_mode = DEFAULT_SCRIPT_MODE; + const Ref<EditorExportPreset> &preset = get_export_preset(); if (preset.is_valid()) { - script_key = preset->get_script_encryption_key().to_lower(); + script_mode = preset->get_script_export_mode(); } + } - if (!p_path.ends_with(".gd")) { + virtual void _export_file(const String &p_path, const String &p_type, const HashSet<String> &p_features) override { + if (p_path.get_extension() != "gd" || script_mode == EditorExportPreset::MODE_SCRIPT_TEXT) { return; } - return; + Vector<uint8_t> file = FileAccess::get_file_as_bytes(p_path); + if (file.is_empty()) { + return; + } + + String source; + source.parse_utf8(reinterpret_cast<const char *>(file.ptr()), file.size()); + GDScriptTokenizerBuffer::CompressMode compress_mode = script_mode == EditorExportPreset::MODE_SCRIPT_BINARY_TOKENS_COMPRESSED ? GDScriptTokenizerBuffer::COMPRESS_ZSTD : GDScriptTokenizerBuffer::COMPRESS_NONE; + file = GDScriptTokenizerBuffer::parse_code_string(source, compress_mode); + if (file.is_empty()) { + return; + } + + add_file(p_path.get_basename() + ".gdc", file, true); } +public: virtual String get_name() const override { return "GDScript"; } }; @@ -185,6 +204,10 @@ void test_tokenizer() { GDScriptTests::test(GDScriptTests::TestType::TEST_TOKENIZER); } +void test_tokenizer_buffer() { + GDScriptTests::test(GDScriptTests::TestType::TEST_TOKENIZER_BUFFER); +} + void test_parser() { GDScriptTests::test(GDScriptTests::TestType::TEST_PARSER); } @@ -198,6 +221,7 @@ void test_bytecode() { } REGISTER_TEST_COMMAND("gdscript-tokenizer", &test_tokenizer); +REGISTER_TEST_COMMAND("gdscript-tokenizer-buffer", &test_tokenizer_buffer); REGISTER_TEST_COMMAND("gdscript-parser", &test_parser); REGISTER_TEST_COMMAND("gdscript-compiler", &test_compiler); REGISTER_TEST_COMMAND("gdscript-bytecode", &test_bytecode); diff --git a/modules/gdscript/tests/README.md b/modules/gdscript/tests/README.md index 361d586d32..72b5316532 100644 --- a/modules/gdscript/tests/README.md +++ b/modules/gdscript/tests/README.md @@ -6,3 +6,44 @@ and output files. See the [Integration tests for GDScript documentation](https://docs.godotengine.org/en/latest/contributing/development/core_and_modules/unit_testing.html#integration-tests-for-gdscript) for information about creating and running GDScript integration tests. + +# GDScript Autocompletion tests + +The `script/completion` folder contains test for the GDScript autocompletion. + +Each test case consists of at least one `.gd` file, which contains the code, and one `.cfg` file, which contains expected results and configuration. Inside of the GDScript file the character `➡` represents the cursor position, at which autocompletion is invoked. + +The config file contains two section: + +`[input]` contains keys that configure the test environment. The following keys are possible: + +- `cs: boolean = false`: If `true`, the test will be skipped when running a non C# build. +- `use_single_quotes: boolean = false`: Configures the corresponding editor setting for the test. +- `scene: String`: Allows to specify a scene which is opened while autocompletion is performed. If this is not set the test runner will search for a `.tscn` file with the same basename as the GDScript file. If that isn't found either, autocompletion will behave as if no scene was opened. + +`[output]` specifies the expected results for the test. The following key are supported: + +- `include: Array`: An unordered list of suggestions that should be in the result. Each entry is one dictionary with the following keys: `display`, `insert_text`, `kind`, `location`, which correspond to the suggestion struct which is used in the code. The runner only tests against specified keys, so in most cases `display` will suffice. +- `exclude: Array`: An array of suggestions which should not be in the result. The entries take the same form as for `include`. +- `call_hint: String`: The expected call hint returned by autocompletion. +- `forced: boolean`: Whether autocompletion is expected to force opening a completion window. + +Tests will only test against entries in `[output]` that were specified. + +## Writing autocompletion tests + +To avoid failing edge cases a certain behavior needs to be tested multiple times. Some things that tests should account for: + +- All possible types: Test with all possible types that apply to the tested behavior. (For the last points testing against `SCRIPT` and `CLASS` should suffice. `CLASS` can be obtained through C#, `SCRIPT` through GDScript. Relying on autoloads to be of type `SCRIPT` is not good, since this might change in the future.) + + - `BUILTIN` + - `NATIVE` + - GDScripts (with `class_name` as well as `preload`ed) + - C# (as standin for all other language bindings) (with `class_name` as well as `preload`ed) + - Autoloads + +- Possible contexts: the completion might be placed in different places of the program. e.g: + - initializers of class members + - directly inside a suite + - assignments inside a suite + - as parameter to a call diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index f91dc83f2c..a0329eb8d2 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -34,6 +34,7 @@ #include "../gdscript_analyzer.h" #include "../gdscript_compiler.h" #include "../gdscript_parser.h" +#include "../gdscript_tokenizer_buffer.h" #include "core/config/project_settings.h" #include "core/core_globals.h" @@ -78,31 +79,30 @@ void init_autoloads() { scn.instantiate(); scn->set_path(info.path); scn->reload_from_file(); - ERR_CONTINUE_MSG(!scn.is_valid(), vformat("Can't autoload: %s.", info.path)); + ERR_CONTINUE_MSG(!scn.is_valid(), vformat("Failed to instantiate an autoload, can't load from path: %s.", info.path)); if (scn.is_valid()) { n = scn->instantiate(); } } else { Ref<Resource> res = ResourceLoader::load(info.path); - ERR_CONTINUE_MSG(res.is_null(), vformat("Can't autoload: %s.", info.path)); + ERR_CONTINUE_MSG(res.is_null(), vformat("Failed to instantiate an autoload, can't load from path: %s.", info.path)); Ref<Script> scr = res; if (scr.is_valid()) { StringName ibt = scr->get_instance_base_type(); bool valid_type = ClassDB::is_parent_class(ibt, "Node"); - ERR_CONTINUE_MSG(!valid_type, vformat("Script does not inherit from Node: %s.", info.path)); + ERR_CONTINUE_MSG(!valid_type, vformat("Failed to instantiate an autoload, script '%s' does not inherit from 'Node'.", info.path)); Object *obj = ClassDB::instantiate(ibt); - - ERR_CONTINUE_MSG(!obj, vformat("Cannot instance script for Autoload, expected 'Node' inheritance, got: %s.", ibt)); + ERR_CONTINUE_MSG(!obj, vformat("Failed to instantiate an autoload, cannot instantiate '%s'.", ibt)); n = Object::cast_to<Node>(obj); n->set_script(scr); } } - ERR_CONTINUE_MSG(!n, vformat("Path in autoload not a node or script: %s.", info.path)); + ERR_CONTINUE_MSG(!n, vformat("Failed to instantiate an autoload, path is not pointing to a scene or a script: %s.", info.path)); n->set_name(info.name); for (int i = 0; i < ScriptServer::get_language_count(); i++) { @@ -132,10 +132,11 @@ void finish_language() { StringName GDScriptTestRunner::test_function_name; -GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames) { +GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames, bool p_use_binary_tokens) { test_function_name = StaticCString::create("test"); do_init_languages = p_init_language; print_filenames = p_print_filenames; + binary_tokens = p_use_binary_tokens; source_dir = p_source_dir; if (!source_dir.ends_with("/")) { @@ -267,7 +268,7 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { while (!next.is_empty()) { if (dir->current_is_dir()) { - if (next == "." || next == "..") { + if (next == "." || next == ".." || next == "completion" || next == "lsp") { next = dir->get_next(); continue; } @@ -278,6 +279,9 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { if (next.ends_with(".notest.gd")) { next = dir->get_next(); continue; + } else if (binary_tokens && next.ends_with(".textonly.gd")) { + next = dir->get_next(); + continue; } else if (next.get_extension().to_lower() == "gd") { #ifndef DEBUG_ENABLED // On release builds, skip tests marked as debug only. @@ -300,6 +304,9 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { ERR_FAIL_V_MSG(false, "Could not find output file for " + next); } GDScriptTest test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir); + if (binary_tokens) { + test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER); + } tests.push_back(test); } } @@ -322,24 +329,65 @@ bool GDScriptTestRunner::make_tests() { return make_tests_for_dir(dir->get_current_dir()); } -bool GDScriptTestRunner::generate_class_index() { +static bool generate_class_index_recursive(const String &p_dir) { + Error err = OK; + Ref<DirAccess> dir(DirAccess::open(p_dir, &err)); + + if (err != OK) { + return false; + } + + String current_dir = dir->get_current_dir(); + + dir->list_dir_begin(); + String next = dir->get_next(); + StringName gdscript_name = GDScriptLanguage::get_singleton()->get_name(); - for (int i = 0; i < tests.size(); i++) { - GDScriptTest test = tests[i]; - String base_type; + while (!next.is_empty()) { + if (dir->current_is_dir()) { + if (next == "." || next == ".." || next == "completion" || next == "lsp") { + next = dir->get_next(); + continue; + } + if (!generate_class_index_recursive(current_dir.path_join(next))) { + return false; + } + } else { + if (!next.ends_with(".gd")) { + next = dir->get_next(); + continue; + } + String base_type; + String source_file = current_dir.path_join(next); + String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type); + if (class_name.is_empty()) { + next = dir->get_next(); + continue; + } + ERR_FAIL_COND_V_MSG(ScriptServer::is_global_class(class_name), false, + "Class name '" + class_name + "' from " + source_file + " is already used in " + ScriptServer::get_global_class_path(class_name)); - String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(test.get_source_file(), &base_type); - if (class_name.is_empty()) { - continue; + ScriptServer::add_global_class(class_name, base_type, gdscript_name, source_file); } - ERR_FAIL_COND_V_MSG(ScriptServer::is_global_class(class_name), false, - "Class name '" + class_name + "' from " + test.get_source_file() + " is already used in " + ScriptServer::get_global_class_path(class_name)); - ScriptServer::add_global_class(class_name, base_type, gdscript_name, test.get_source_file()); + next = dir->get_next(); } + + dir->list_dir_end(); + return true; } +bool GDScriptTestRunner::generate_class_index() { + Error err = OK; + Ref<DirAccess> dir(DirAccess::open(source_dir, &err)); + + ERR_FAIL_COND_V_MSG(err != OK, false, "Could not open specified test directory."); + + source_dir = dir->get_current_dir() + "/"; // Make it absolute path. + return generate_class_index_recursive(dir->get_current_dir()); +} + GDScriptTest::GDScriptTest(const String &p_source_path, const String &p_output_path, const String &p_base_dir) { source_file = p_source_path; output_file = p_output_path; @@ -485,7 +533,15 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { Ref<GDScript> script; script.instantiate(); script->set_path(source_file); - err = script->load_source_code(source_file); + if (tokenizer_mode == TOKENIZER_TEXT) { + err = script->load_source_code(source_file); + } else { + String code = FileAccess::get_file_as_string(source_file, &err); + if (!err) { + Vector<uint8_t> buffer = GDScriptTokenizerBuffer::parse_code_string(code, GDScriptTokenizerBuffer::COMPRESS_ZSTD); + script->set_binary_tokens_source(buffer); + } + } if (err != OK) { enable_stdout(); result.status = GDTEST_LOAD_ERROR; @@ -495,7 +551,11 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { // Test parsing. GDScriptParser parser; - err = parser.parse(script->get_source_code(), source_file, false); + if (tokenizer_mode == TOKENIZER_TEXT) { + err = parser.parse(script->get_source_code(), source_file, false); + } else { + err = parser.parse_binary(script->get_binary_tokens_source(), source_file); + } if (err != OK) { enable_stdout(); result.status = GDTEST_PARSER_ERROR; @@ -584,7 +644,14 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) { add_print_handler(&_print_handler); add_error_handler(&_error_handler); - script->reload(); + err = script->reload(); + if (err) { + enable_stdout(); + result.status = GDTEST_LOAD_ERROR; + result.output = ""; + result.passed = false; + ERR_FAIL_V_MSG(result, "\nCould not reload script: '" + source_file + "'"); + } // Create object instance for test. Object *obj = ClassDB::instantiate(script->get_native()->get_name()); diff --git a/modules/gdscript/tests/gdscript_test_runner.h b/modules/gdscript/tests/gdscript_test_runner.h index b1190604ad..57e3ac86f9 100644 --- a/modules/gdscript/tests/gdscript_test_runner.h +++ b/modules/gdscript/tests/gdscript_test_runner.h @@ -62,6 +62,11 @@ public: bool passed; }; + enum TokenizerMode { + TOKENIZER_TEXT, + TOKENIZER_BUFFER, + }; + private: struct ErrorHandlerData { TestResult *result = nullptr; @@ -79,6 +84,8 @@ private: PrintHandlerList _print_handler; ErrorHandlerList _error_handler; + TokenizerMode tokenizer_mode = TOKENIZER_TEXT; + void enable_stdout(); void disable_stdout(); bool check_output(const String &p_output) const; @@ -96,6 +103,9 @@ public: const String get_source_relative_filepath() const { return source_file.trim_prefix(base_dir); } const String &get_output_file() const { return output_file; } + void set_tokenizer_mode(TokenizerMode p_tokenizer_mode) { tokenizer_mode = p_tokenizer_mode; } + TokenizerMode get_tokenizer_mode() const { return tokenizer_mode; } + GDScriptTest(const String &p_source_path, const String &p_output_path, const String &p_base_dir); GDScriptTest() : GDScriptTest(String(), String(), String()) {} // Needed to use in Vector. @@ -108,6 +118,7 @@ class GDScriptTestRunner { bool is_generating = false; bool do_init_languages = false; bool print_filenames; // Whether filenames should be printed when generated/running tests + bool binary_tokens; // Test with buffer tokenizer. bool make_tests(); bool make_tests_for_dir(const String &p_dir); @@ -120,7 +131,7 @@ public: int run_tests(); bool generate_outputs(); - GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames = false); + GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames = false, bool p_use_binary_tokens = false); ~GDScriptTestRunner(); }; diff --git a/modules/gdscript/tests/gdscript_test_runner_suite.h b/modules/gdscript/tests/gdscript_test_runner_suite.h index 5fd7d942d2..b2289ef9cc 100644 --- a/modules/gdscript/tests/gdscript_test_runner_suite.h +++ b/modules/gdscript/tests/gdscript_test_runner_suite.h @@ -37,13 +37,13 @@ namespace GDScriptTests { +// TODO: Handle some cases failing on release builds. See: https://github.com/godotengine/godot/pull/88452 +#ifdef TOOLS_ENABLED TEST_SUITE("[Modules][GDScript]") { - // GDScript 2.0 is still under heavy construction. - // Allow the tests to fail, but do not ignore errors during development. - // Update the scripts and expected output as needed. TEST_CASE("Script compilation and runtime") { bool print_filenames = OS::get_singleton()->get_cmdline_args().find("--print-filenames") != nullptr; - GDScriptTestRunner runner("modules/gdscript/tests/scripts", true, print_filenames); + bool use_binary_tokens = OS::get_singleton()->get_cmdline_args().find("--use-binary-tokens") != nullptr; + GDScriptTestRunner runner("modules/gdscript/tests/scripts", true, print_filenames, use_binary_tokens); int fail_count = runner.run_tests(); INFO("Make sure `*.out` files have expected results."); REQUIRE_MESSAGE(fail_count == 0, "All GDScript tests should pass."); @@ -70,6 +70,7 @@ func _init(): ref_counted->set_script(gdscript); CHECK_MESSAGE(int(ref_counted->get_meta("result")) == 42, "The script should assign object metadata successfully."); } +#endif // TOOLS_ENABLED TEST_CASE("[Modules][GDScript] Validate built-in API") { GDScriptLanguage *lang = GDScriptLanguage::get_singleton(); diff --git a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants.out b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants.out index 73a54d7820..a6a3973255 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> analyzer/errors/outer_class_constants.gd >> 8 ->> Invalid get index 'OUTER_CONST' (on base: 'GDScript'). +>> Invalid access to property or key 'OUTER_CONST' on a base object of type 'GDScript'. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants_as_variant.out b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants_as_variant.out index 92e7b9316e..70fdc5b62c 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants_as_variant.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_constants_as_variant.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> analyzer/errors/outer_class_constants_as_variant.gd >> 9 ->> Invalid get index 'OUTER_CONST' (on base: 'GDScript'). +>> Invalid access to property or key 'OUTER_CONST' on a base object of type 'GDScript'. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants.out b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants.out index 892f8e2c3f..6632f056bd 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> analyzer/errors/outer_class_instance_constants.gd >> 8 ->> Invalid get index 'OUTER_CONST' (on base: 'RefCounted (Inner)'). +>> Invalid access to property or key 'OUTER_CONST' on a base object of type 'RefCounted (Inner)'. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants_as_variant.out b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants_as_variant.out index 8257e74f57..0459b756d1 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants_as_variant.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/outer_class_instance_constants_as_variant.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> analyzer/errors/outer_class_instance_constants_as_variant.gd >> 9 ->> Invalid get index 'OUTER_CONST' (on base: 'RefCounted (Inner)'). +>> Invalid access to property or key 'OUTER_CONST' on a base object of type 'RefCounted (Inner)'. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.gd new file mode 100644 index 0000000000..a98f69f3ac --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.gd @@ -0,0 +1,14 @@ +# GH-83468 + +func non_static_func(): + pass + +static func static_func(): + var f := func (): + var g := func (): + non_static_func() + g.call() + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out new file mode 100644 index 0000000000..b78f131345 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot call non-static function "non_static_func()" from static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.gd new file mode 100644 index 0000000000..7af9ff274c --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.gd @@ -0,0 +1,15 @@ +# GH-83468 + +func non_static_func(): + pass + +static func static_func( + f := func (): + var g := func (): + non_static_func() + g.call() +): + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out new file mode 100644 index 0000000000..b78f131345 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_func_call_non_static_in_lambda_param.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot call non-static function "non_static_func()" from static function "static_func()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.gd new file mode 100644 index 0000000000..5130973bd2 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.gd @@ -0,0 +1,14 @@ +# GH-83468 + +func non_static_func(): + pass + +static var static_var = func (): + var f := func (): + var g := func (): + non_static_func() + g.call() + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.out new file mode 100644 index 0000000000..c0308c81f3 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot call non-static function "non_static_func()" from a static variable initializer. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.gd new file mode 100644 index 0000000000..2d15b4e3e5 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.gd @@ -0,0 +1,15 @@ +# GH-83468 + +func non_static_func(): + pass + +static var static_var: + set(_value): + var f := func (): + var g := func (): + non_static_func() + g.call() + f.call() + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out new file mode 100644 index 0000000000..cdf3ab2aeb --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_call_non_static_in_lambda_setter.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot call non-static function "non_static_func()" from static function "@static_var_setter()". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out index f1e9ec34f2..81554ec707 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot call non-static function "non_static()" for static variable initializer. +Cannot call non-static function "non_static()" from a static variable initializer. diff --git a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd index dafd2ec0c8..39f490c4b3 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd @@ -4,14 +4,13 @@ enum MyEnum {A, B, C} const Utils = preload("../../utils.notest.gd") -@export var x1 = MyEnum -@export var x2 = MyEnum.A -@export var x3 := MyEnum -@export var x4 := MyEnum.A -@export var x5: MyEnum +@export var test_1 = MyEnum +@export var test_2 = MyEnum.A +@export var test_3 := MyEnum +@export var test_4 := MyEnum.A +@export var test_5: MyEnum func test(): for property in get_property_list(): - if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE: - print(Utils.get_property_signature(property)) - print(" ", Utils.get_property_additional_info(property)) + if str(property.name).begins_with("test_"): + Utils.print_property_extended_info(property) diff --git a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out index f1a13f1045..505af5f1f3 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out +++ b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.out @@ -1,11 +1,11 @@ GDTEST_OK -@export var x1: Dictionary +var test_1: Dictionary hint=NONE hint_string="" usage=DEFAULT|SCRIPT_VARIABLE -@export var x2: TestExportEnumAsDictionary.MyEnum +var test_2: TestExportEnumAsDictionary.MyEnum hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM -@export var x3: Dictionary +var test_3: Dictionary hint=NONE hint_string="" usage=DEFAULT|SCRIPT_VARIABLE -@export var x4: TestExportEnumAsDictionary.MyEnum +var test_4: TestExportEnumAsDictionary.MyEnum hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM -@export var x5: TestExportEnumAsDictionary.MyEnum +var test_5: TestExportEnumAsDictionary.MyEnum hint=ENUM hint_string="A:0,B:1,C:2" usage=DEFAULT|SCRIPT_VARIABLE|CLASS_IS_ENUM diff --git a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg new file mode 100644 index 0000000000..27e695d245 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg @@ -0,0 +1,4 @@ +[output] +include=[ + {"display": "autoplay"}, +] diff --git a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd new file mode 100644 index 0000000000..d41bbb970c --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd @@ -0,0 +1,6 @@ +extends Node + +var test: AnimationPlayer = $AnimationPlayer + +func _ready(): + test.➡ diff --git a/modules/gdscript/tests/scripts/lsp/class.notest.gd b/modules/gdscript/tests/scripts/lsp/class.gd index 53d0b14d72..53d0b14d72 100644 --- a/modules/gdscript/tests/scripts/lsp/class.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/class.gd diff --git a/modules/gdscript/tests/scripts/lsp/enums.notest.gd b/modules/gdscript/tests/scripts/lsp/enums.gd index 38b9ec110a..38b9ec110a 100644 --- a/modules/gdscript/tests/scripts/lsp/enums.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/enums.gd diff --git a/modules/gdscript/tests/scripts/lsp/indentation.notest.gd b/modules/gdscript/tests/scripts/lsp/indentation.gd index c25d73a719..c25d73a719 100644 --- a/modules/gdscript/tests/scripts/lsp/indentation.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/indentation.gd diff --git a/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd b/modules/gdscript/tests/scripts/lsp/lambdas.gd index 6f5d468eea..6f5d468eea 100644 --- a/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/lambdas.gd diff --git a/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd b/modules/gdscript/tests/scripts/lsp/local_variables.gd index b6cc46f7da..b6cc46f7da 100644 --- a/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/local_variables.gd diff --git a/modules/gdscript/tests/scripts/lsp/properties.notest.gd b/modules/gdscript/tests/scripts/lsp/properties.gd index 8dfaee2e5b..8dfaee2e5b 100644 --- a/modules/gdscript/tests/scripts/lsp/properties.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/properties.gd diff --git a/modules/gdscript/tests/scripts/lsp/scopes.notest.gd b/modules/gdscript/tests/scripts/lsp/scopes.gd index 20b8fb9bd7..20b8fb9bd7 100644 --- a/modules/gdscript/tests/scripts/lsp/scopes.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/scopes.gd diff --git a/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd index 338000fa0e..338000fa0e 100644 --- a/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd +++ b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd diff --git a/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.gd b/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.textonly.gd index 9ad77f1432..9ad77f1432 100644 --- a/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.gd +++ b/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.textonly.gd diff --git a/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.out b/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.textonly.out index 31bed2dbc7..31bed2dbc7 100644 --- a/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.out +++ b/modules/gdscript/tests/scripts/parser/errors/mixing_tabs_spaces.textonly.out diff --git a/modules/gdscript/tests/scripts/parser/features/annotations.gd b/modules/gdscript/tests/scripts/parser/features/annotations.gd index 13c89a0a09..7a7d6d953e 100644 --- a/modules/gdscript/tests/scripts/parser/features/annotations.gd +++ b/modules/gdscript/tests/scripts/parser/features/annotations.gd @@ -1,48 +1,49 @@ extends Node -@export_enum("A", "B", "C") var a0 -@export_enum("A", "B", "C",) var a1 +const Utils = preload("../../utils.notest.gd") + +@export_enum("A", "B", "C") var test_1 +@export_enum("A", "B", "C",) var test_2 @export_enum( "A", "B", "C" -) var a2 +) var test_3 @export_enum( "A", "B", "C", -) var a3 +) var test_4 @export -var a4: int +var test_5: int @export() -var a5: int +var test_6: int -@export() var a6: int -@warning_ignore("onready_with_export") @onready @export var a7: int -@warning_ignore("onready_with_export") @onready() @export() var a8: int +@export() var test_7: int = 42 +@warning_ignore("onready_with_export") @onready @export var test_8: int = 42 +@warning_ignore("onready_with_export") @onready() @export() var test_9: int = 42 @warning_ignore("onready_with_export") @onready @export -var a9: int +var test_10: int = 42 @warning_ignore("onready_with_export") @onready() @export() -var a10: int +var test_11: int = 42 @warning_ignore("onready_with_export") @onready() @export() -var a11: int - +var test_12: int = 42 func test(): for property in get_property_list(): - if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE: - print(property) + if str(property.name).begins_with("test_"): + Utils.print_property_extended_info(property, self) diff --git a/modules/gdscript/tests/scripts/parser/features/annotations.out b/modules/gdscript/tests/scripts/parser/features/annotations.out index 3af0436c53..2ba9dd7496 100644 --- a/modules/gdscript/tests/scripts/parser/features/annotations.out +++ b/modules/gdscript/tests/scripts/parser/features/annotations.out @@ -1,13 +1,25 @@ GDTEST_OK -{ "name": "a0", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 } -{ "name": "a1", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 } -{ "name": "a2", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 } -{ "name": "a3", "class_name": &"", "type": 2, "hint": 2, "hint_string": "A,B,C", "usage": 4102 } -{ "name": "a4", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a5", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a6", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a7", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a8", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a9", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a10", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "a11", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } +var test_1: int = null + hint=ENUM hint_string="A,B,C" usage=DEFAULT|SCRIPT_VARIABLE +var test_2: int = null + hint=ENUM hint_string="A,B,C" usage=DEFAULT|SCRIPT_VARIABLE +var test_3: int = null + hint=ENUM hint_string="A,B,C" usage=DEFAULT|SCRIPT_VARIABLE +var test_4: int = null + hint=ENUM hint_string="A,B,C" usage=DEFAULT|SCRIPT_VARIABLE +var test_5: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_6: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_7: int = 42 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_8: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_9: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_10: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_11: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_12: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE diff --git a/modules/gdscript/tests/scripts/parser/features/export_enum.gd b/modules/gdscript/tests/scripts/parser/features/export_enum.gd index 9b2c22dea1..4f2a43f4fe 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_enum.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_enum.gd @@ -1,15 +1,16 @@ -@export_enum("Red", "Green", "Blue") var untyped +const Utils = preload("../../utils.notest.gd") -@export_enum("Red", "Green", "Blue") var weak_int = 0 -@export_enum("Red", "Green", "Blue") var weak_string = "" +@export_enum("Red", "Green", "Blue") var test_untyped -@export_enum("Red", "Green", "Blue") var hard_int: int -@export_enum("Red", "Green", "Blue") var hard_string: String +@export_enum("Red", "Green", "Blue") var test_weak_int = 0 +@export_enum("Red", "Green", "Blue") var test_weak_string = "" -@export_enum("Red:10", "Green:20", "Blue:30") var with_values +@export_enum("Red", "Green", "Blue") var test_hard_int: int +@export_enum("Red", "Green", "Blue") var test_hard_string: String + +@export_enum("Red:10", "Green:20", "Blue:30") var test_with_values func test(): for property in get_property_list(): - if property.name in ["untyped", "weak_int", "weak_string", "hard_int", - "hard_string", "with_values"]: - prints(property.name, property.type, property.hint_string) + if str(property.name).begins_with("test_"): + Utils.print_property_extended_info(property, self) diff --git a/modules/gdscript/tests/scripts/parser/features/export_enum.out b/modules/gdscript/tests/scripts/parser/features/export_enum.out index 330b7eaf01..43f5e197ad 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_enum.out +++ b/modules/gdscript/tests/scripts/parser/features/export_enum.out @@ -1,7 +1,13 @@ GDTEST_OK -untyped 2 Red,Green,Blue -weak_int 2 Red,Green,Blue -weak_string 4 Red,Green,Blue -hard_int 2 Red,Green,Blue -hard_string 4 Red,Green,Blue -with_values 2 Red:10,Green:20,Blue:30 +var test_untyped: int = null + hint=ENUM hint_string="Red,Green,Blue" usage=DEFAULT|SCRIPT_VARIABLE +var test_weak_int: int = 0 + hint=ENUM hint_string="Red,Green,Blue" usage=DEFAULT|SCRIPT_VARIABLE +var test_weak_string: String = "" + hint=ENUM hint_string="Red,Green,Blue" usage=DEFAULT|SCRIPT_VARIABLE +var test_hard_int: int = 0 + hint=ENUM hint_string="Red,Green,Blue" usage=DEFAULT|SCRIPT_VARIABLE +var test_hard_string: String = "" + hint=ENUM hint_string="Red,Green,Blue" usage=DEFAULT|SCRIPT_VARIABLE +var test_with_values: int = null + hint=ENUM hint_string="Red:10,Green:20,Blue:30" usage=DEFAULT|SCRIPT_VARIABLE diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.gd b/modules/gdscript/tests/scripts/parser/features/export_variable.gd index c9d05a7e68..2a218774de 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_variable.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_variable.gd @@ -1,23 +1,22 @@ extends Node -@export var example = 99 -@export_range(0, 100) var example_range = 100 -@export_range(0, 100, 1) var example_range_step = 101 -@export_range(0, 100, 1, "or_greater") var example_range_step_or_greater = 102 +const Utils = preload("../../utils.notest.gd") -@export var color: Color -@export_color_no_alpha var color_no_alpha: Color -@export_node_path("Sprite2D", "Sprite3D", "Control", "Node") var nodepath := ^"hello" -@export var node: Node -@export var node_array: Array[Node] +@export var test_weak_int = 1 +@export var test_hard_int: int = 2 +@export_storage var test_storage_untyped +@export_storage var test_storage_weak_int = 3 # Property info still `Variant`, unlike `@export`. +@export_storage var test_storage_hard_int: int = 4 +@export_range(0, 100) var test_range = 100 +@export_range(0, 100, 1) var test_range_step = 101 +@export_range(0, 100, 1, "or_greater") var test_range_step_or_greater = 102 +@export var test_color: Color +@export_color_no_alpha var test_color_no_alpha: Color +@export_node_path("Sprite2D", "Sprite3D", "Control", "Node") var test_node_path := ^"hello" +@export var test_node: Node +@export var test_node_array: Array[Node] func test(): - print(example) - print(example_range) - print(example_range_step) - print(example_range_step_or_greater) - print(color) - print(color_no_alpha) - print(nodepath) - print(node) - print(var_to_str(node_array)) + for property in get_property_list(): + if str(property.name).begins_with("test_"): + Utils.print_property_extended_info(property, self) diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.out b/modules/gdscript/tests/scripts/parser/features/export_variable.out index 5430c975f4..baadcd4ee8 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_variable.out +++ b/modules/gdscript/tests/scripts/parser/features/export_variable.out @@ -1,10 +1,27 @@ GDTEST_OK -99 -100 -101 -102 -(0, 0, 0, 1) -(0, 0, 0, 1) -hello -<null> -Array[Node]([]) +var test_weak_int: int = 1 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_hard_int: int = 2 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +var test_storage_untyped: Variant = null + hint=NONE hint_string="" usage=STORAGE|SCRIPT_VARIABLE|NIL_IS_VARIANT +var test_storage_weak_int: Variant = 3 + hint=NONE hint_string="" usage=STORAGE|SCRIPT_VARIABLE|NIL_IS_VARIANT +var test_storage_hard_int: int = 4 + hint=NONE hint_string="" usage=STORAGE|SCRIPT_VARIABLE +var test_range: int = 100 + hint=RANGE hint_string="0,100" usage=DEFAULT|SCRIPT_VARIABLE +var test_range_step: int = 101 + hint=RANGE hint_string="0,100,1" usage=DEFAULT|SCRIPT_VARIABLE +var test_range_step_or_greater: int = 102 + hint=RANGE hint_string="0,100,1,or_greater" usage=DEFAULT|SCRIPT_VARIABLE +var test_color: Color = Color(0, 0, 0, 1) + hint=NONE hint_string="Color" usage=DEFAULT|SCRIPT_VARIABLE +var test_color_no_alpha: Color = Color(0, 0, 0, 1) + hint=COLOR_NO_ALPHA hint_string="" usage=DEFAULT|SCRIPT_VARIABLE +var test_node_path: NodePath = NodePath("hello") + hint=NODE_PATH_VALID_TYPES hint_string="Sprite2D,Sprite3D,Control,Node" usage=DEFAULT|SCRIPT_VARIABLE +var test_node: Node = null + hint=NODE_TYPE hint_string="Node" usage=DEFAULT|SCRIPT_VARIABLE +var test_node_array: Array = Array[Node]([]) + hint=TYPE_STRING hint_string="24/34:Node" usage=DEFAULT|SCRIPT_VARIABLE diff --git a/modules/gdscript/tests/scripts/parser/features/is_not_operator.gd b/modules/gdscript/tests/scripts/parser/features/is_not_operator.gd new file mode 100644 index 0000000000..b744e6170b --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/is_not_operator.gd @@ -0,0 +1,11 @@ +func test(): + var i: Variant = 123 + var s: Variant = "str" + prints(i is int, i is not int) + prints(s is int, s is not int) + + var a: Variant = false + var b: Variant = true + prints(a == b is int, a == b is not int) + prints(a == (b is int), a == (b is not int)) + prints((a == b) is int, (a == b) is not int) diff --git a/modules/gdscript/tests/scripts/parser/features/is_not_operator.out b/modules/gdscript/tests/scripts/parser/features/is_not_operator.out new file mode 100644 index 0000000000..f0535f9c83 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/is_not_operator.out @@ -0,0 +1,6 @@ +GDTEST_OK +true false +false true +true false +true false +false true diff --git a/modules/gdscript/tests/scripts/parser/features/multiline_if.gd b/modules/gdscript/tests/scripts/parser/features/multiline_if.gd index 86152f4543..7b82d9b1da 100644 --- a/modules/gdscript/tests/scripts/parser/features/multiline_if.gd +++ b/modules/gdscript/tests/scripts/parser/features/multiline_if.gd @@ -9,6 +9,7 @@ func test(): # Alternatively, backslashes can be used. if 1 == 1 \ + \ and 2 == 2 and \ 3 == 3: pass diff --git a/modules/gdscript/tests/scripts/parser/features/property_setter_getter.gd b/modules/gdscript/tests/scripts/parser/features/property_setter_getter.gd index 9e4b360fb2..82616ee3cf 100644 --- a/modules/gdscript/tests/scripts/parser/features/property_setter_getter.gd +++ b/modules/gdscript/tests/scripts/parser/features/property_setter_getter.gd @@ -6,6 +6,9 @@ var property: set(value): _backing = value - 1000 +var property_2: + get(): # Allow parentheses. + return 123 func test(): print("Not using self:") @@ -35,3 +38,5 @@ func test(): self.property = 5000 print(self.property) print(self._backing) + + print(property_2) diff --git a/modules/gdscript/tests/scripts/parser/features/property_setter_getter.out b/modules/gdscript/tests/scripts/parser/features/property_setter_getter.out index 560e0c3bd7..23f98f44ab 100644 --- a/modules/gdscript/tests/scripts/parser/features/property_setter_getter.out +++ b/modules/gdscript/tests/scripts/parser/features/property_setter_getter.out @@ -17,3 +17,4 @@ Using self: -50 5000 4000 +123 diff --git a/modules/gdscript/tests/scripts/project.godot b/modules/gdscript/tests/scripts/project.godot index 25b49c0abd..c500ef443d 100644 --- a/modules/gdscript/tests/scripts/project.godot +++ b/modules/gdscript/tests/scripts/project.godot @@ -3,7 +3,7 @@ ; It also helps for opening Godot to edit the scripts, but please don't ; let the editor changes be saved. -config_version=4 +config_version=5 [application] diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out index 2a97eaea44..c524a1ae6b 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out +++ b/modules/gdscript/tests/scripts/runtime/errors/constant_array_is_deep.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> runtime/errors/constant_array_is_deep.gd >> 6 ->> Invalid set index '0' (on base: 'Dictionary') with value of type 'int' +>> Invalid assignment of property or key '0' with value of type 'int' on a base object of type 'Dictionary'. diff --git a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out index c807db6b0c..cf51b0262d 100644 --- a/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out +++ b/modules/gdscript/tests/scripts/runtime/errors/constant_dictionary_is_deep.out @@ -3,4 +3,4 @@ GDTEST_RUNTIME_ERROR >> on function: test() >> runtime/errors/constant_dictionary_is_deep.gd >> 6 ->> Invalid set index '0' (on base: 'Array') with value of type 'int' +>> Invalid assignment of index '0' (on base: 'Array') with value of type 'int'. diff --git a/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.gd b/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.gd new file mode 100644 index 0000000000..cb5ea827f6 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.gd @@ -0,0 +1,13 @@ +func test(): + var array: Array = [1, 2, 3] + print(array) + var array_clear: Callable = array.clear + array_clear.call() + print(array) + + var dictionary: Dictionary = {1: 2, 3: 4} + print(dictionary) + # `dictionary.clear` is treated as a key. + var dictionary_clear := Callable.create(dictionary, &"clear") + dictionary_clear.call() + print(dictionary) diff --git a/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.out b/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.out new file mode 100644 index 0000000000..c12984ca37 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/builtin_method_as_callable.out @@ -0,0 +1,5 @@ +GDTEST_OK +[1, 2, 3] +[] +{ 1: 2, 3: 4 } +{ } diff --git a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd index e46f24cc5f..0133d7fcfc 100644 --- a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd +++ b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd @@ -1,17 +1,17 @@ -extends RefCounted # TODO: Fix standalone annotations parsing. +const Utils = preload("../../utils.notest.gd") # GH-73843 @export_group("Resource") # GH-78252 -@export var prop_1: int -@export_category("prop_1") -@export var prop_2: int +@export var test_1: int +@export_category("test_1") +@export var test_2: int func test(): var resource := Resource.new() prints("Not shadowed:", resource.get_class()) for property in get_property_list(): - if property.name in ["prop_1", "prop_2"]: - print(property) + if str(property.name).begins_with("test_"): + Utils.print_property_extended_info(property, self) diff --git a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.out b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.out index 96ae84e986..9387ec50d7 100644 --- a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.out +++ b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.out @@ -1,5 +1,8 @@ GDTEST_OK Not shadowed: Resource -{ "name": "prop_1", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } -{ "name": "prop_1", "class_name": &"", "type": 0, "hint": 0, "hint_string": "", "usage": 128 } -{ "name": "prop_2", "class_name": &"", "type": 2, "hint": 0, "hint_string": "int", "usage": 4102 } +var test_1: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE +@export_category("test_1") + hint=NONE hint_string="" usage=CATEGORY +var test_2: int = 0 + hint=NONE hint_string="int" usage=DEFAULT|SCRIPT_VARIABLE diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd new file mode 100644 index 0000000000..b9746a8207 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd @@ -0,0 +1,10 @@ +func test(): + var node := Node.new() + var callable: Callable = node.free + callable.call() + print(node) + + node = Node.new() + callable = node["free"] + callable.call() + print(node) diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out new file mode 100644 index 0000000000..97bfc46d96 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out @@ -0,0 +1,3 @@ +GDTEST_OK +<Freed Object> +<Freed Object> diff --git a/modules/gdscript/tests/scripts/runtime/features/match_test_null.gd b/modules/gdscript/tests/scripts/runtime/features/match_test_null.gd new file mode 100644 index 0000000000..9bb57b68ee --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/match_test_null.gd @@ -0,0 +1,6 @@ +func test(): + match null: + null: + print('null matched') + _: + pass diff --git a/modules/gdscript/tests/scripts/runtime/features/match_test_null.out b/modules/gdscript/tests/scripts/runtime/features/match_test_null.out new file mode 100644 index 0000000000..7640cf42ab --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/match_test_null.out @@ -0,0 +1,2 @@ +GDTEST_OK +null matched diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.gd b/modules/gdscript/tests/scripts/runtime/features/member_info.gd index 805ea42455..6fe9647b4d 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info.gd @@ -60,7 +60,7 @@ func test(): var script: Script = get_script() for property in script.get_property_list(): if str(property.name).begins_with("test_"): - print(Utils.get_property_signature(property, true)) + print(Utils.get_property_signature(property, null, true)) for property in get_property_list(): if str(property.name).begins_with("test_"): print(Utils.get_property_signature(property)) diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.out b/modules/gdscript/tests/scripts/runtime/features/member_info.out index 3a91507da9..7c826ac05a 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info.out +++ b/modules/gdscript/tests/scripts/runtime/features/member_info.out @@ -6,13 +6,13 @@ static var test_static_var_hard_int: int var test_var_untyped: Variant var test_var_weak_null: Variant var test_var_weak_int: Variant -@export var test_var_weak_int_exported: int +var test_var_weak_int_exported: int var test_var_weak_variant_type: Variant -@export var test_var_weak_variant_type_exported: Variant.Type +var test_var_weak_variant_type_exported: Variant.Type var test_var_hard_variant: Variant var test_var_hard_int: int var test_var_hard_variant_type: Variant.Type -@export var test_var_hard_variant_type_exported: Variant.Type +var test_var_hard_variant_type_exported: Variant.Type var test_var_hard_node_process_mode: Node.ProcessMode var test_var_hard_my_enum: TestMemberInfo.MyEnum var test_var_hard_array: Array diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd index d0cbeeab85..563c6ce569 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd @@ -30,7 +30,7 @@ func test(): var b := B.new() for property in (B as GDScript).get_property_list(): if str(property.name).begins_with("test_"): - print(Utils.get_property_signature(property, true)) + print(Utils.get_property_signature(property, null, true)) print("---") for property in b.get_property_list(): if str(property.name).begins_with("test_"): diff --git a/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.gd b/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.gd index f6aa58737f..97e9da3b26 100644 --- a/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.gd +++ b/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.gd @@ -1,12 +1,18 @@ -# GH-79521 +# GH-79521, GH-86032 class_name TestStaticMethodAsCallable static func static_func() -> String: return "Test" +static func another_static_func(): + prints("another_static_func:", static_func.call(), static_func.is_valid()) + func test(): var a: Callable = TestStaticMethodAsCallable.static_func var b: Callable = static_func prints(a.call(), a.is_valid()) prints(b.call(), b.is_valid()) + @warning_ignore("static_called_on_instance") + another_static_func() + TestStaticMethodAsCallable.another_static_func() diff --git a/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.out b/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.out index e6d461b8f9..2b773ce8ee 100644 --- a/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.out +++ b/modules/gdscript/tests/scripts/runtime/features/static_method_as_callable.out @@ -1,3 +1,5 @@ GDTEST_OK Test true Test true +another_static_func: Test true +another_static_func: Test true diff --git a/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.gd b/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.gd new file mode 100644 index 0000000000..11f064bb83 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.gd @@ -0,0 +1,10 @@ +func test(): + print(print) + print(len) + + prints.callv([1, 2, 3]) + print(mini.call(1, 2)) + print(len.bind("abc").call()) + + const ABSF = absf + print(ABSF.call(-1.2)) diff --git a/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.out b/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.out new file mode 100644 index 0000000000..91549b9345 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/utility_func_as_callable.out @@ -0,0 +1,7 @@ +GDTEST_OK +@GlobalScope::print (Callable) +@GDScript::len (Callable) +1 2 3 +1 +3 +1.2 diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd index 781843b8e2..31818c9d01 100644 --- a/modules/gdscript/tests/scripts/utils.notest.gd +++ b/modules/gdscript/tests/scripts/utils.notest.gd @@ -20,24 +20,32 @@ static func get_type(property: Dictionary, is_return: bool = false) -> String: return type_string(property.type) -static func get_property_signature(property: Dictionary, is_static: bool = false) -> String: +static func get_property_signature(property: Dictionary, base: Object = null, is_static: bool = false) -> String: + if property.usage & PROPERTY_USAGE_CATEGORY: + return '@export_category("%s")' % str(property.name).c_escape() + if property.usage & PROPERTY_USAGE_GROUP: + return '@export_group("%s")' % str(property.name).c_escape() + if property.usage & PROPERTY_USAGE_SUBGROUP: + return '@export_subgroup("%s")' % str(property.name).c_escape() + var result: String = "" if not (property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE): printerr("Missing `PROPERTY_USAGE_SCRIPT_VARIABLE` flag.") - if property.usage & PROPERTY_USAGE_DEFAULT: - result += "@export " if is_static: result += "static " result += "var " + property.name + ": " + get_type(property) + if is_instance_valid(base): + result += " = " + var_to_str(base.get(property.name)) return result -static func get_property_additional_info(property: Dictionary) -> String: - return 'hint=%s hint_string="%s" usage=%s' % [ +static func print_property_extended_info(property: Dictionary, base: Object = null, is_static: bool = false) -> void: + print(get_property_signature(property, base, is_static)) + print(' hint=%s hint_string="%s" usage=%s' % [ get_property_hint_name(property.hint).trim_prefix("PROPERTY_HINT_"), str(property.hint_string).c_escape(), get_property_usage_string(property.usage).replace("PROPERTY_USAGE_", ""), - ] + ]) static func get_method_signature(method: Dictionary, is_signal: bool = false) -> String: @@ -153,7 +161,6 @@ static func get_property_usage_string(usage: int) -> String: return "PROPERTY_USAGE_NONE" const FLAGS: Array[Array] = [ - [PROPERTY_USAGE_DEFAULT, "PROPERTY_USAGE_DEFAULT"], [PROPERTY_USAGE_STORAGE, "PROPERTY_USAGE_STORAGE"], [PROPERTY_USAGE_EDITOR, "PROPERTY_USAGE_EDITOR"], [PROPERTY_USAGE_INTERNAL, "PROPERTY_USAGE_INTERNAL"], @@ -187,6 +194,10 @@ static func get_property_usage_string(usage: int) -> String: var result: String = "" + if (usage & PROPERTY_USAGE_DEFAULT) == PROPERTY_USAGE_DEFAULT: + result += "PROPERTY_USAGE_DEFAULT|" + usage &= ~PROPERTY_USAGE_DEFAULT + for flag in FLAGS: if usage & flag[0]: result += flag[1] + "|" diff --git a/modules/gdscript/tests/test_completion.h b/modules/gdscript/tests/test_completion.h new file mode 100644 index 0000000000..fd6b5321e6 --- /dev/null +++ b/modules/gdscript/tests/test_completion.h @@ -0,0 +1,203 @@ +/**************************************************************************/ +/* test_completion.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_COMPLETION_H +#define TEST_COMPLETION_H + +#ifdef TOOLS_ENABLED + +#include "core/io/config_file.h" +#include "core/io/dir_access.h" +#include "core/io/file_access.h" +#include "core/object/script_language.h" +#include "core/variant/dictionary.h" +#include "core/variant/variant.h" +#include "gdscript_test_runner.h" +#include "modules/modules_enabled.gen.h" // For mono. +#include "scene/resources/packed_scene.h" + +#include "../gdscript.h" +#include "tests/test_macros.h" + +#include "editor/editor_settings.h" +#include "scene/theme/theme_db.h" + +namespace GDScriptTests { + +static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) { + if (p_expected.get("display", p_got.display) != p_got.display) { + return false; + } + if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) { + return false; + } + if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) { + return false; + } + if (p_expected.get("location", p_got.location) != Variant(p_got.location)) { + return false; + } + return true; +} + +static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) { + ERR_FAIL_COND(!p_variant.is_array()); + + Array arr = p_variant; + for (int i = 0; i < arr.size(); i++) { + if (arr[i].get_type() == Variant::DICTIONARY) { + p_list.push_back(arr[i]); + } + } +} + +static void test_directory(const String &p_dir) { + Error err = OK; + Ref<DirAccess> dir = DirAccess::open(p_dir, &err); + + if (err != OK) { + FAIL("Invalid test directory."); + return; + } + + String path = dir->get_current_dir(); + + dir->list_dir_begin(); + String next = dir->get_next(); + + while (!next.is_empty()) { + if (dir->current_is_dir()) { + if (next == "." || next == "..") { + next = dir->get_next(); + continue; + } + test_directory(path.path_join(next)); + } else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) { + Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err); + + if (err != OK) { + next = dir->get_next(); + continue; + } + + String code = acc->get_as_utf8_string(); + // For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files. + code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF)); + // Require pointer sentinel char in scripts. + CHECK(code.find_char(0xFFFF) != -1); + + ConfigFile conf; + if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) { + FAIL("No config file found."); + } + +#ifndef MODULE_MONO_ENABLED + if (conf.get_value("input", "cs", false)) { + next = dir->get_next(); + continue; + } +#endif + + EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false)); + + List<Dictionary> include; + to_dict_list(conf.get_value("output", "include", Array()), include); + + List<Dictionary> exclude; + to_dict_list(conf.get_value("output", "exclude", Array()), exclude); + + List<ScriptLanguage::CodeCompletionOption> options; + String call_hint; + bool forced; + + Node *owner = nullptr; + if (conf.has_section_key("input", "scene")) { + Ref<PackedScene> scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene"); + if (scene.is_valid()) { + owner = scene->instantiate(); + } + } else if (dir->file_exists(next.get_basename() + ".tscn")) { + Ref<PackedScene> scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene"); + if (scene.is_valid()) { + owner = scene->instantiate(); + } + } + + GDScriptLanguage::get_singleton()->complete_code(code, path.path_join(next), owner, &options, forced, call_hint); + String contains_excluded; + for (ScriptLanguage::CodeCompletionOption &option : options) { + for (const Dictionary &E : exclude) { + if (match_option(E, option)) { + contains_excluded = option.display; + break; + } + } + if (!contains_excluded.is_empty()) { + break; + } + + for (const Dictionary &E : include) { + if (match_option(E, option)) { + include.erase(E); + break; + } + } + } + CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'."); + CHECK(include.is_empty()); + + String expected_call_hint = conf.get_value("output", "call_hint", call_hint); + bool expected_forced = conf.get_value("output", "forced", forced); + + CHECK(expected_call_hint == call_hint); + CHECK(expected_forced == forced); + + if (owner) { + memdelete(owner); + } + } + next = dir->get_next(); + } +} + +TEST_SUITE("[Modules][GDScript][Completion]") { + TEST_CASE("[Editor] Check suggestion list") { + // Set all editor settings that code completion relies on. + EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false); + init_language("modules/gdscript/tests/scripts"); + + test_directory("modules/gdscript/tests/scripts/completion"); + } +} +} // namespace GDScriptTests + +#endif + +#endif // TEST_COMPLETION_H diff --git a/modules/gdscript/tests/test_gdscript.cpp b/modules/gdscript/tests/test_gdscript.cpp index 467bedc4b2..f6965cf7cf 100644 --- a/modules/gdscript/tests/test_gdscript.cpp +++ b/modules/gdscript/tests/test_gdscript.cpp @@ -34,6 +34,7 @@ #include "../gdscript_compiler.h" #include "../gdscript_parser.h" #include "../gdscript_tokenizer.h" +#include "../gdscript_tokenizer_buffer.h" #include "core/config/project_settings.h" #include "core/io/file_access.h" @@ -50,7 +51,7 @@ namespace GDScriptTests { static void test_tokenizer(const String &p_code, const Vector<String> &p_lines) { - GDScriptTokenizer tokenizer; + GDScriptTokenizerText tokenizer; tokenizer.set_source_code(p_code); int tab_size = 4; @@ -107,6 +108,53 @@ static void test_tokenizer(const String &p_code, const Vector<String> &p_lines) print_line(current.get_name()); // Should be EOF } +static void test_tokenizer_buffer(const Vector<uint8_t> &p_buffer, const Vector<String> &p_lines); + +static void test_tokenizer_buffer(const String &p_code, const Vector<String> &p_lines) { + Vector<uint8_t> binary = GDScriptTokenizerBuffer::parse_code_string(p_code, GDScriptTokenizerBuffer::COMPRESS_NONE); + test_tokenizer_buffer(binary, p_lines); +} + +static void test_tokenizer_buffer(const Vector<uint8_t> &p_buffer, const Vector<String> &p_lines) { + GDScriptTokenizerBuffer tokenizer; + tokenizer.set_code_buffer(p_buffer); + + int tab_size = 4; +#ifdef TOOLS_ENABLED + if (EditorSettings::get_singleton()) { + tab_size = EditorSettings::get_singleton()->get_setting("text_editor/behavior/indent/size"); + } +#endif // TOOLS_ENABLED + String tab = String(" ").repeat(tab_size); + + GDScriptTokenizer::Token current = tokenizer.scan(); + while (current.type != GDScriptTokenizer::Token::TK_EOF) { + StringBuilder token; + token += " --> "; // Padding for line number. + + for (int l = current.start_line; l <= current.end_line && l <= p_lines.size(); l++) { + print_line(vformat("%04d %s", l, p_lines[l - 1]).replace("\t", tab)); + } + + token += current.get_name(); + + if (current.type == GDScriptTokenizer::Token::ERROR || current.type == GDScriptTokenizer::Token::LITERAL || current.type == GDScriptTokenizer::Token::IDENTIFIER || current.type == GDScriptTokenizer::Token::ANNOTATION) { + token += "("; + token += Variant::get_type_name(current.literal.get_type()); + token += ") "; + token += current.literal; + } + + print_line(token.as_string()); + + print_line("-------------------------------------------------------"); + + current = tokenizer.scan(); + } + + print_line(current.get_name()); // Should be EOF +} + static void test_parser(const String &p_code, const String &p_script_path, const Vector<String> &p_lines) { GDScriptParser parser; Error err = parser.parse(p_code, p_script_path, false); @@ -119,7 +167,7 @@ static void test_parser(const String &p_code, const String &p_script_path, const } GDScriptAnalyzer analyzer(&parser); - analyzer.analyze(); + err = analyzer.analyze(); if (err != OK) { const List<GDScriptParser::ParserError> &errors = parser.get_errors(); @@ -212,7 +260,7 @@ void test(TestType p_type) { } String test = cmdlargs.back()->get(); - if (!test.ends_with(".gd")) { + if (!test.ends_with(".gd") && !test.ends_with(".gdc")) { print_line("This test expects a path to a GDScript file as its last parameter. Got: " + test); return; } @@ -255,6 +303,13 @@ void test(TestType p_type) { case TEST_TOKENIZER: test_tokenizer(code, lines); break; + case TEST_TOKENIZER_BUFFER: + if (test.ends_with(".gdc")) { + test_tokenizer_buffer(buf, lines); + } else { + test_tokenizer_buffer(code, lines); + } + break; case TEST_PARSER: test_parser(code, test, lines); break; diff --git a/modules/gdscript/tests/test_gdscript.h b/modules/gdscript/tests/test_gdscript.h index b39dfe2b5a..32f278d5ce 100644 --- a/modules/gdscript/tests/test_gdscript.h +++ b/modules/gdscript/tests/test_gdscript.h @@ -39,6 +39,7 @@ namespace GDScriptTests { enum TestType { TEST_TOKENIZER, + TEST_TOKENIZER_BUFFER, TEST_PARSER, TEST_COMPILER, TEST_BYTECODE, diff --git a/modules/gdscript/tests/test_lsp.h b/modules/gdscript/tests/test_lsp.h index e57df00e2d..6192272f80 100644 --- a/modules/gdscript/tests/test_lsp.h +++ b/modules/gdscript/tests/test_lsp.h @@ -76,7 +76,7 @@ namespace GDScriptTests { // LSP GDScript test scripts are located inside project of other GDScript tests: // Cannot reset `ProjectSettings` (singleton) -> Cannot load another workspace and resources in there. // -> Reuse GDScript test project. LSP specific scripts are then placed inside `lsp` folder. -// Access via `res://lsp/my_script.notest.gd`. +// Access via `res://lsp/my_script.gd`. const String root = "modules/gdscript/tests/scripts/"; /* @@ -394,7 +394,7 @@ func f(): Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace(); { - String path = "res://lsp/local_variables.notest.gd"; + String path = "res://lsp/local_variables.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -413,7 +413,7 @@ func f(): } SUBCASE("Can get correct ranges for indented variables") { - String path = "res://lsp/indentation.notest.gd"; + String path = "res://lsp/indentation.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -421,7 +421,7 @@ func f(): } SUBCASE("Can get correct ranges for scopes") { - String path = "res://lsp/scopes.notest.gd"; + String path = "res://lsp/scopes.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -429,7 +429,7 @@ func f(): } SUBCASE("Can get correct ranges for lambda") { - String path = "res://lsp/lambdas.notest.gd"; + String path = "res://lsp/lambdas.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -437,7 +437,7 @@ func f(): } SUBCASE("Can get correct ranges for inner class") { - String path = "res://lsp/class.notest.gd"; + String path = "res://lsp/class.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -445,7 +445,7 @@ func f(): } SUBCASE("Can get correct ranges for inner class") { - String path = "res://lsp/enums.notest.gd"; + String path = "res://lsp/enums.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -453,7 +453,7 @@ func f(): } SUBCASE("Can get correct ranges for shadowing & shadowed variables") { - String path = "res://lsp/shadowing_initializer.notest.gd"; + String path = "res://lsp/shadowing_initializer.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); @@ -461,7 +461,7 @@ func f(): } SUBCASE("Can get correct ranges for properties and getter/setter") { - String path = "res://lsp/properties.notest.gd"; + String path = "res://lsp/properties.gd"; assert_no_errors_in(path); String uri = workspace->get_file_uri(path); Vector<InlineTestData> all_test_data = read_tests(path); diff --git a/modules/glslang/SCsub b/modules/glslang/SCsub index a34a97d230..3068377e60 100644 --- a/modules/glslang/SCsub +++ b/modules/glslang/SCsub @@ -43,7 +43,6 @@ if env["builtin_glslang"]: "glslang/MachineIndependent/SymbolTable.cpp", "glslang/MachineIndependent/Versions.cpp", "glslang/ResourceLimits/ResourceLimits.cpp", - "OGLCompilersDLL/InitializeDll.cpp", "SPIRV/disassemble.cpp", "SPIRV/doc.cpp", "SPIRV/GlslangToSpv.cpp", diff --git a/modules/glslang/config.py b/modules/glslang/config.py index d22f9454ed..1169776a73 100644 --- a/modules/glslang/config.py +++ b/modules/glslang/config.py @@ -1,5 +1,7 @@ def can_build(env, platform): - return True + # glslang is only needed when Vulkan or Direct3D 12-based renderers are available, + # as OpenGL doesn't use glslang. + return env["vulkan"] or env["d3d12"] def configure(env): diff --git a/modules/glslang/register_types.cpp b/modules/glslang/register_types.cpp index 7fe3a57880..09ad1d6777 100644 --- a/modules/glslang/register_types.cpp +++ b/modules/glslang/register_types.cpp @@ -39,7 +39,7 @@ #include <glslang/SPIRV/GlslangToSpv.h> static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage, const String &p_source_code, RenderingDevice::ShaderLanguage p_language, String *r_error, const RenderingDevice *p_render_device) { - const RD::Capabilities *capabilities = p_render_device->get_device_capabilities(); + const RDD::Capabilities &capabilities = p_render_device->get_device_capabilities(); Vector<uint8_t> ret; ERR_FAIL_COND_V(p_language == RenderingDevice::SHADER_LANGUAGE_HLSL, ret); @@ -57,16 +57,23 @@ static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage glslang::EShTargetClientVersion ClientVersion = glslang::EShTargetVulkan_1_2; glslang::EShTargetLanguageVersion TargetVersion = glslang::EShTargetSpv_1_5; - if (capabilities->device_family == RenderingDevice::DeviceFamily::DEVICE_VULKAN) { - if (capabilities->version_major == 1 && capabilities->version_minor == 0) { + if (capabilities.device_family == RDD::DEVICE_VULKAN) { + if (capabilities.version_major == 1 && capabilities.version_minor == 0) { ClientVersion = glslang::EShTargetVulkan_1_0; TargetVersion = glslang::EShTargetSpv_1_0; - } else if (capabilities->version_major == 1 && capabilities->version_minor == 1) { + } else if (capabilities.version_major == 1 && capabilities.version_minor == 1) { ClientVersion = glslang::EShTargetVulkan_1_1; TargetVersion = glslang::EShTargetSpv_1_3; } else { // use defaults } + } else if (capabilities.device_family == RDD::DEVICE_DIRECTX) { + // NIR-DXIL is Vulkan 1.1-conformant. + ClientVersion = glslang::EShTargetVulkan_1_1; + // The SPIR-V part of Mesa supports 1.6, but: + // - SPIRV-Reflect won't be able to parse the compute workgroup size. + // - We want to play it safe with NIR-DXIL. + TargetVersion = glslang::EShTargetSpv_1_3; } else { // once we support other backends we'll need to do something here if (r_error) { @@ -179,9 +186,9 @@ static Vector<uint8_t> _compile_shader_glsl(RenderingDevice::ShaderStage p_stage } static String _get_cache_key_function_glsl(const RenderingDevice *p_render_device) { - const RD::Capabilities *capabilities = p_render_device->get_device_capabilities(); + const RenderingDeviceDriver::Capabilities &capabilities = p_render_device->get_device_capabilities(); String version; - version = "SpirVGen=" + itos(glslang::GetSpirvGeneratorVersion()) + ", major=" + itos(capabilities->version_major) + ", minor=" + itos(capabilities->version_minor) + " , subgroup_size=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_SIZE)) + " , subgroup_ops=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_OPERATIONS)) + " , subgroup_in_shaders=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_IN_SHADERS)) + " , debug=" + itos(Engine::get_singleton()->is_generate_spirv_debug_info_enabled()); + version = "SpirVGen=" + itos(glslang::GetSpirvGeneratorVersion()) + ", major=" + itos(capabilities.version_major) + ", minor=" + itos(capabilities.version_minor) + " , subgroup_size=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_SIZE)) + " , subgroup_ops=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_OPERATIONS)) + " , subgroup_in_shaders=" + itos(p_render_device->limit_get(RD::LIMIT_SUBGROUP_IN_SHADERS)) + " , debug=" + itos(Engine::get_singleton()->is_generate_spirv_debug_info_enabled()); return version; } diff --git a/modules/gltf/SCsub b/modules/gltf/SCsub index d1f337715f..9d263cccac 100644 --- a/modules/gltf/SCsub +++ b/modules/gltf/SCsub @@ -6,8 +6,11 @@ Import("env_modules") env_gltf = env_modules.Clone() # Godot source files + env_gltf.add_source_files(env.modules_sources, "*.cpp") env_gltf.add_source_files(env.modules_sources, "structures/*.cpp") + SConscript("extensions/SCsub") + if env.editor_build: env_gltf.add_source_files(env.modules_sources, "editor/*.cpp") diff --git a/modules/gltf/config.py b/modules/gltf/config.py index f8de431fae..67233db579 100644 --- a/modules/gltf/config.py +++ b/modules/gltf/config.py @@ -9,7 +9,6 @@ def configure(env): def get_doc_classes(): return [ "EditorSceneFormatImporterBlend", - "EditorSceneFormatImporterFBX", "EditorSceneFormatImporterGLTF", "GLTFAccessor", "GLTFAnimation", diff --git a/modules/gltf/doc_classes/EditorSceneFormatImporterBlend.xml b/modules/gltf/doc_classes/EditorSceneFormatImporterBlend.xml index 24f6dbd887..e3433e6e29 100644 --- a/modules/gltf/doc_classes/EditorSceneFormatImporterBlend.xml +++ b/modules/gltf/doc_classes/EditorSceneFormatImporterBlend.xml @@ -5,7 +5,7 @@ </brief_description> <description> Imports Blender scenes in the [code].blend[/code] file format through the glTF 2.0 3D import pipeline. This importer requires Blender to be installed by the user, so that it can be used to export the scene as glTF 2.0. - The location of the Blender binary is set via the [code]filesystem/import/blender/blender3_path[/code] editor setting. + The location of the Blender binary is set via the [code]filesystem/import/blender/blender_path[/code] editor setting. This importer is only used if [member ProjectSettings.filesystem/import/blender/enabled] is enabled, otherwise [code].blend[/code] files present in the project folder are not imported. Blend import requires Blender 3.0. Internally, the EditorSceneFormatImporterBlend uses the Blender glTF "Use Original" mode to reference external textures. diff --git a/modules/gltf/doc_classes/EditorSceneFormatImporterFBX.xml b/modules/gltf/doc_classes/EditorSceneFormatImporterFBX.xml deleted file mode 100644 index b33735f4ad..0000000000 --- a/modules/gltf/doc_classes/EditorSceneFormatImporterFBX.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> -<class name="EditorSceneFormatImporterFBX" inherits="EditorSceneFormatImporter" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> - <brief_description> - Importer for the [code].fbx[/code] scene file format. - </brief_description> - <description> - Imports Autodesk FBX 3D scenes by way of converting them to glTF 2.0 using the FBX2glTF command line tool. - The location of the FBX2glTF binary is set via the [code]filesystem/import/fbx/fbx2gltf_path[/code] editor setting. - This importer is only used if [member ProjectSettings.filesystem/import/fbx/enabled] is enabled, otherwise [code].fbx[/code] files present in the project folder are not imported. - </description> - <tutorials> - </tutorials> -</class> diff --git a/modules/gltf/doc_classes/GLTFAccessor.xml b/modules/gltf/doc_classes/GLTFAccessor.xml index 8e4a72e6ae..ba7323b7cd 100644 --- a/modules/gltf/doc_classes/GLTFAccessor.xml +++ b/modules/gltf/doc_classes/GLTFAccessor.xml @@ -1,13 +1,19 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="GLTFAccessor" inherits="Resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> + Represents a GLTF accessor. </brief_description> <description> + GLTFAccessor is a data structure representing GLTF a [code]accessor[/code] that would be found in the [code]"accessors"[/code] array. A buffer is a blob of binary data. A buffer view is a slice of a buffer. An accessor is a typed interpretation of the data in a buffer view. + Most custom data stored in GLTF does not need accessors, only buffer views (see [GLTFBufferView]). Accessors are for more advanced use cases such as interleaved mesh data encoded for the GPU. </description> <tutorials> + <link title="Buffers, BufferViews, and Accessors in Khronos glTF specification">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_005_BuffersBufferViewsAccessors.md</link> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <members> <member name="buffer_view" type="int" setter="set_buffer_view" getter="get_buffer_view" default="-1"> + The index of the buffer view this accessor is referencing. If [code]-1[/code], this accessor is not referencing any buffer view. </member> <member name="byte_offset" type="int" setter="set_byte_offset" getter="get_byte_offset" default="0"> </member> diff --git a/modules/gltf/doc_classes/GLTFAnimation.xml b/modules/gltf/doc_classes/GLTFAnimation.xml index c1fe85c1c0..2c70f4461f 100644 --- a/modules/gltf/doc_classes/GLTFAnimation.xml +++ b/modules/gltf/doc_classes/GLTFAnimation.xml @@ -5,9 +5,32 @@ <description> </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> + <methods> + <method name="get_additional_data"> + <return type="Variant" /> + <param index="0" name="extension_name" type="StringName" /> + <description> + Gets additional arbitrary data in this [GLTFAnimation] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the return value can be anything you set. If nothing was set, the return value is null. + </description> + </method> + <method name="set_additional_data"> + <return type="void" /> + <param index="0" name="extension_name" type="StringName" /> + <param index="1" name="additional_data" type="Variant" /> + <description> + Sets additional arbitrary data in this [GLTFAnimation] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The first argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the second argument can be anything you want. + </description> + </method> + </methods> <members> <member name="loop" type="bool" setter="set_loop" getter="get_loop" default="false"> </member> + <member name="original_name" type="String" setter="set_original_name" getter="get_original_name" default=""""> + The original name of the animation. + </member> </members> </class> diff --git a/modules/gltf/doc_classes/GLTFBufferView.xml b/modules/gltf/doc_classes/GLTFBufferView.xml index ce19650b5b..11d58bda72 100644 --- a/modules/gltf/doc_classes/GLTFBufferView.xml +++ b/modules/gltf/doc_classes/GLTFBufferView.xml @@ -1,21 +1,40 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="GLTFBufferView" inherits="Resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> + Represents a GLTF buffer view. </brief_description> <description> + GLTFBufferView is a data structure representing GLTF a [code]bufferView[/code] that would be found in the [code]"bufferViews"[/code] array. A buffer is a blob of binary data. A buffer view is a slice of a buffer that can be used to identify and extract data from the buffer. + Most custom uses of buffers only need to use the [member buffer], [member byte_length], and [member byte_offset]. The [member byte_stride] and [member indices] properties are for more advanced use cases such as interleaved mesh data encoded for the GPU. </description> <tutorials> + <link title="Buffers, BufferViews, and Accessors in Khronos glTF specification">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_005_BuffersBufferViewsAccessors.md</link> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> + <methods> + <method name="load_buffer_view_data" qualifiers="const"> + <return type="PackedByteArray" /> + <param index="0" name="state" type="GLTFState" /> + <description> + Loads the buffer view data from the buffer referenced by this buffer view in the given [GLTFState]. Interleaved data with a byte stride is not yet supported by this method. The data is returned as a [PackedByteArray]. + </description> + </method> + </methods> <members> <member name="buffer" type="int" setter="set_buffer" getter="get_buffer" default="-1"> + The index of the buffer this buffer view is referencing. If [code]-1[/code], this buffer view is not referencing any buffer. </member> <member name="byte_length" type="int" setter="set_byte_length" getter="get_byte_length" default="0"> + The length, in bytes, of this buffer view. If [code]0[/code], this buffer view is empty. </member> <member name="byte_offset" type="int" setter="set_byte_offset" getter="get_byte_offset" default="0"> + The offset, in bytes, from the start of the buffer to the start of this buffer view. </member> <member name="byte_stride" type="int" setter="set_byte_stride" getter="get_byte_stride" default="-1"> + The stride, in bytes, between interleaved data. If [code]-1[/code], this buffer view is not interleaved. </member> <member name="indices" type="bool" setter="set_indices" getter="get_indices" default="false"> + True if the GLTFBufferView's OpenGL GPU buffer type is an [code]ELEMENT_ARRAY_BUFFER[/code] used for vertex indices (integer constant [code]34963[/code]). False if the buffer type is [code]ARRAY_BUFFER[/code] used for vertex attributes (integer constant [code]34962[/code]) or when any other value. See [url=https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_005_BuffersBufferViewsAccessors.md]Buffers, BufferViews, and Accessors[/url] for possible values. This property is set but never used, setting this property will do nothing. </member> </members> </class> diff --git a/modules/gltf/doc_classes/GLTFCamera.xml b/modules/gltf/doc_classes/GLTFCamera.xml index c2ed4d08e3..b334bf2867 100644 --- a/modules/gltf/doc_classes/GLTFCamera.xml +++ b/modules/gltf/doc_classes/GLTFCamera.xml @@ -7,6 +7,7 @@ Represents a camera as defined by the base GLTF spec. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="GLTF camera detailed specification">https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-camera</link> <link title="GLTF camera spec and example file">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_015_SimpleCameras.md</link> </tutorials> diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index 5c10b76e0a..1f172633da 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -9,7 +9,8 @@ GLTFDocument can be extended with arbitrary functionality by extending the [GLTFDocumentExtension] class and registering it with GLTFDocument via [method register_gltf_document_extension]. This allows for custom data to be imported and exported. </description> <tutorials> - <link title="glTF 'What the duck?' guide">https://www.khronos.org/files/gltf20-reference-guide.pdf</link> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> + <link title="glTF 'What the duck?' guide">https://www.khronos.org/files/gltf20-reference-guide.pdf</link> <link title="Khronos glTF specification">https://registry.khronos.org/glTF/</link> </tutorials> <methods> @@ -83,7 +84,7 @@ <param index="1" name="path" type="String" /> <description> Takes a [GLTFState] object through the [param state] parameter and writes a glTF file to the filesystem. - [b]Note:[/b] The extension of the glTF file determines if it is a .glb binary file or a .gltf file. + [b]Note:[/b] The extension of the glTF file determines if it is a .glb binary file or a .gltf text file. </description> </method> </methods> diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml index eee62845ca..0eabcb5022 100644 --- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml +++ b/modules/gltf/doc_classes/GLTFDocumentExtension.xml @@ -9,6 +9,7 @@ [b]Note:[/b] Like GLTFDocument itself, all GLTFDocumentExtension classes must be stateless in order to function properly. If you need to store data, use the [code]set_additional_data[/code] and [code]get_additional_data[/code] methods in [GLTFState] or [GLTFNode]. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <methods> <method name="_convert_scene_node" qualifiers="virtual"> @@ -29,7 +30,7 @@ <param index="3" name="node" type="Node" /> <description> Part of the export process. This method is run after [method _get_saveable_image_formats] and before [method _export_post]. If this [GLTFDocumentExtension] is used for exporting images, this runs after [method _serialize_texture_json]. - This method can be used to modify the final JSON of each node. + This method can be used to modify the final JSON of each node. Data should be primarily stored in [param gltf_node] prior to serializing the JSON, but the original Godot [param node] is also provided if available. The node may be null if not available, such as when exporting GLTF data not generated from a Godot scene. </description> </method> <method name="_export_post" qualifiers="virtual"> @@ -113,7 +114,7 @@ <param index="0" name="state" type="GLTFState" /> <description> Part of the import process. This method is run after [method _parse_node_extensions] and before [method _generate_scene_node]. - This method can be used to modify any of the data imported so far, including any scene nodes, before running the final per-node import step. + This method can be used to modify any of the data imported so far after parsing, before generating the nodes and then running the final per-node import step. </description> </method> <method name="_import_preflight" qualifiers="virtual"> diff --git a/modules/gltf/doc_classes/GLTFDocumentExtensionConvertImporterMesh.xml b/modules/gltf/doc_classes/GLTFDocumentExtensionConvertImporterMesh.xml index 3ca0359311..04075caba5 100644 --- a/modules/gltf/doc_classes/GLTFDocumentExtensionConvertImporterMesh.xml +++ b/modules/gltf/doc_classes/GLTFDocumentExtensionConvertImporterMesh.xml @@ -5,5 +5,6 @@ <description> </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> </class> diff --git a/modules/gltf/doc_classes/GLTFLight.xml b/modules/gltf/doc_classes/GLTFLight.xml index c55962eeaa..87ea159e7c 100644 --- a/modules/gltf/doc_classes/GLTFLight.xml +++ b/modules/gltf/doc_classes/GLTFLight.xml @@ -7,6 +7,7 @@ Represents a light as defined by the [code]KHR_lights_punctual[/code] GLTF extension. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="KHR_lights_punctual GLTF extension spec">https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual</link> </tutorials> <methods> @@ -24,6 +25,19 @@ Create a new GLTFLight instance from the given Godot [Light3D] node. </description> </method> + <method name="get_additional_data"> + <return type="Variant" /> + <param index="0" name="extension_name" type="StringName" /> + <description> + </description> + </method> + <method name="set_additional_data"> + <return type="void" /> + <param index="0" name="extension_name" type="StringName" /> + <param index="1" name="additional_data" type="Variant" /> + <description> + </description> + </method> <method name="to_dictionary" qualifiers="const"> <return type="Dictionary" /> <description> @@ -38,7 +52,7 @@ </method> </methods> <members> - <member name="color" type="Color" setter="set_color" getter="get_color" default="Color(1, 1, 1, 1)"> + <member name="color" type="Color" setter="set_color" getter="get_color" default="Color(1, 1, 1, 1)" keywords="colour"> The [Color] of the light. Defaults to white. A black color causes the light to have no effect. </member> <member name="inner_cone_angle" type="float" setter="set_inner_cone_angle" getter="get_inner_cone_angle" default="0.0"> diff --git a/modules/gltf/doc_classes/GLTFMesh.xml b/modules/gltf/doc_classes/GLTFMesh.xml index df4d436f5c..8234ed9eac 100644 --- a/modules/gltf/doc_classes/GLTFMesh.xml +++ b/modules/gltf/doc_classes/GLTFMesh.xml @@ -1,17 +1,45 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="GLTFMesh" inherits="Resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> + GLTFMesh represents an GLTF mesh. </brief_description> <description> + GLTFMesh handles 3D mesh data imported from GLTF files. It includes properties for blend channels, blend weights, instance materials, and the mesh itself. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> + <methods> + <method name="get_additional_data"> + <return type="Variant" /> + <param index="0" name="extension_name" type="StringName" /> + <description> + Gets additional arbitrary data in this [GLTFMesh] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the return value can be anything you set. If nothing was set, the return value is null. + </description> + </method> + <method name="set_additional_data"> + <return type="void" /> + <param index="0" name="extension_name" type="StringName" /> + <param index="1" name="additional_data" type="Variant" /> + <description> + Sets additional arbitrary data in this [GLTFMesh] instance. This can be used to keep per-node state data in [GLTFDocumentExtension] classes, which is important because they are stateless. + The first argument should be the [GLTFDocumentExtension] name (does not have to match the extension name in the GLTF file), and the second argument can be anything you want. + </description> + </method> + </methods> <members> <member name="blend_weights" type="PackedFloat32Array" setter="set_blend_weights" getter="get_blend_weights" default="PackedFloat32Array()"> + An array of floats representing the blend weights of the mesh. </member> <member name="instance_materials" type="Material[]" setter="set_instance_materials" getter="get_instance_materials" default="[]"> + An array of Material objects representing the materials used in the mesh. </member> <member name="mesh" type="ImporterMesh" setter="set_mesh" getter="get_mesh"> + The [ImporterMesh] object representing the mesh itself. + </member> + <member name="original_name" type="String" setter="set_original_name" getter="get_original_name" default=""""> + The original name of the mesh. </member> </members> </class> diff --git a/modules/gltf/doc_classes/GLTFNode.xml b/modules/gltf/doc_classes/GLTFNode.xml index 2ec39801f3..4a7570e4bc 100644 --- a/modules/gltf/doc_classes/GLTFNode.xml +++ b/modules/gltf/doc_classes/GLTFNode.xml @@ -8,6 +8,7 @@ GLTF nodes generally exist inside of [GLTFState] which represents all data of a GLTF file. Most of GLTFNode's properties are indices of other data in the GLTF file. You can extend a GLTF node with additional properties by using [method get_additional_data] and [method set_additional_data]. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="GLTF scene and node spec">https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_004_ScenesNodes.md"</link> </tutorials> <methods> @@ -34,7 +35,7 @@ If this GLTF node is a camera, the index of the [GLTFCamera] in the [GLTFState] that describes the camera's properties. If -1, this node is not a camera. </member> <member name="children" type="PackedInt32Array" setter="set_children" getter="get_children" default="PackedInt32Array()"> - The indices of the children nodes in the [GLTFState]. If this GLTF node has no children, this will be an empty array. + The indices of the child nodes in the [GLTFState]. If this GLTF node has no children, this will be an empty array. </member> <member name="height" type="int" setter="set_height" getter="get_height" default="-1"> How deep into the node hierarchy this node is. A root node will have a height of 0, its children will have a height of 1, and so on. If -1, the height has not been calculated. @@ -45,6 +46,9 @@ <member name="mesh" type="int" setter="set_mesh" getter="get_mesh" default="-1"> If this GLTF node is a mesh, the index of the [GLTFMesh] in the [GLTFState] that describes the mesh's properties. If -1, this node is not a mesh. </member> + <member name="original_name" type="String" setter="set_original_name" getter="get_original_name" default=""""> + The original name of the node. + </member> <member name="parent" type="int" setter="set_parent" getter="get_parent" default="-1"> The index of the parent node in the [GLTFState]. If -1, this node is a root node. </member> diff --git a/modules/gltf/doc_classes/GLTFPhysicsBody.xml b/modules/gltf/doc_classes/GLTFPhysicsBody.xml index d364069193..5cfc22f6b2 100644 --- a/modules/gltf/doc_classes/GLTFPhysicsBody.xml +++ b/modules/gltf/doc_classes/GLTFPhysicsBody.xml @@ -4,9 +4,10 @@ Represents a GLTF physics body. </brief_description> <description> - Represents a physics body as defined by the [code]OMI_physics_body[/code] GLTF extension. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future. + Represents a physics body as an intermediary between the [code]OMI_physics_body[/code] GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="OMI_physics_body GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_body</link> </tutorials> <methods> @@ -14,7 +15,7 @@ <return type="GLTFPhysicsBody" /> <param index="0" name="dictionary" type="Dictionary" /> <description> - Creates a new GLTFPhysicsBody instance by parsing the given [Dictionary]. + Creates a new GLTFPhysicsBody instance by parsing the given [Dictionary] in the [code]OMI_physics_body[/code] GLTF extension format. </description> </method> <method name="from_node" qualifiers="static"> @@ -27,7 +28,7 @@ <method name="to_dictionary" qualifiers="const"> <return type="Dictionary" /> <description> - Serializes this GLTFPhysicsBody instance into a [Dictionary]. + Serializes this GLTFPhysicsBody instance into a [Dictionary]. It will be in the format expected by the [code]OMI_physics_body[/code] GLTF extension. </description> </method> <method name="to_node" qualifiers="const"> @@ -41,13 +42,20 @@ <member name="angular_velocity" type="Vector3" setter="set_angular_velocity" getter="get_angular_velocity" default="Vector3(0, 0, 0)"> The angular velocity of the physics body, in radians per second. This is only used when the body type is "rigid" or "vehicle". </member> - <member name="body_type" type="String" setter="set_body_type" getter="get_body_type" default=""static""> - The type of the body. When importing, this controls what type of [CollisionObject3D] node Godot should generate. Valid values are "static", "kinematic", "character", "rigid", "vehicle", and "trigger". + <member name="body_type" type="String" setter="set_body_type" getter="get_body_type" default=""rigid""> + The type of the body. When importing, this controls what type of [CollisionObject3D] node Godot should generate. Valid values are "static", "animatable", "character", "rigid", "vehicle", and "trigger". When exporting, this will be squashed down to one of "static", "kinematic", or "dynamic" motion types, or the "trigger" property. </member> <member name="center_of_mass" type="Vector3" setter="set_center_of_mass" getter="get_center_of_mass" default="Vector3(0, 0, 0)"> The center of mass of the body, in meters. This is in local space relative to the body. By default, the center of the mass is the body's origin. </member> - <member name="inertia_tensor" type="Basis" setter="set_inertia_tensor" getter="get_inertia_tensor" default="Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)"> + <member name="inertia_diagonal" type="Vector3" setter="set_inertia_diagonal" getter="get_inertia_diagonal" default="Vector3(0, 0, 0)"> + The inertia strength of the physics body, in kilogram meter squared (kg⋅m²). This represents the inertia around the principle axes, the diagonal of the inertia tensor matrix. This is only used when the body type is "rigid" or "vehicle". + When converted to a Godot [RigidBody3D] node, if this value is zero, then the inertia will be calculated automatically. + </member> + <member name="inertia_orientation" type="Quaternion" setter="set_inertia_orientation" getter="get_inertia_orientation" default="Quaternion(0, 0, 0, 1)"> + The inertia orientation of the physics body. This defines the rotation of the inertia's principle axes relative to the object's local axes. This is only used when the body type is "rigid" or "vehicle" and [member inertia_diagonal] is set to a non-zero value. + </member> + <member name="inertia_tensor" type="Basis" setter="set_inertia_tensor" getter="get_inertia_tensor" default="Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)" deprecated=""> The inertia tensor of the physics body, in kilogram meter squared (kg⋅m²). This is only used when the body type is "rigid" or "vehicle". When converted to a Godot [RigidBody3D] node, if this value is zero, then the inertia will be calculated automatically. </member> diff --git a/modules/gltf/doc_classes/GLTFPhysicsShape.xml b/modules/gltf/doc_classes/GLTFPhysicsShape.xml index 2891fab115..c397c660d9 100644 --- a/modules/gltf/doc_classes/GLTFPhysicsShape.xml +++ b/modules/gltf/doc_classes/GLTFPhysicsShape.xml @@ -4,10 +4,12 @@ Represents a GLTF physics shape. </brief_description> <description> - Represents a physics shape as defined by the [code]OMI_collider[/code] GLTF extension. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future. + Represents a physics shape as defined by the [code]OMI_physics_shape[/code] or [code]OMI_collider[/code] GLTF extensions. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future. </description> <tutorials> - <link title="OMI_collider GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_collider</link> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> + <link title="OMI_physics_shape GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_shape</link> + <link title="OMI_collider GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/Archived/OMI_collider</link> </tutorials> <methods> <method name="from_dictionary" qualifiers="static"> @@ -27,7 +29,7 @@ <method name="to_dictionary" qualifiers="const"> <return type="Dictionary" /> <description> - Serializes this GLTFPhysicsShape instance into a [Dictionary]. + Serializes this GLTFPhysicsShape instance into a [Dictionary] in the format defined by [code]OMI_physics_shape[/code]. </description> </method> <method name="to_node"> diff --git a/modules/gltf/doc_classes/GLTFSkeleton.xml b/modules/gltf/doc_classes/GLTFSkeleton.xml index c7b8cb2ac3..ac03a6ee9e 100644 --- a/modules/gltf/doc_classes/GLTFSkeleton.xml +++ b/modules/gltf/doc_classes/GLTFSkeleton.xml @@ -5,6 +5,7 @@ <description> </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <methods> <method name="get_bone_attachment"> diff --git a/modules/gltf/doc_classes/GLTFSkin.xml b/modules/gltf/doc_classes/GLTFSkin.xml index 98436ea2e7..0835a2d510 100644 --- a/modules/gltf/doc_classes/GLTFSkin.xml +++ b/modules/gltf/doc_classes/GLTFSkin.xml @@ -5,6 +5,7 @@ <description> </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <methods> <method name="get_inverse_binds"> diff --git a/modules/gltf/doc_classes/GLTFSpecGloss.xml b/modules/gltf/doc_classes/GLTFSpecGloss.xml index d64a918920..722fa5e9ae 100644 --- a/modules/gltf/doc_classes/GLTFSpecGloss.xml +++ b/modules/gltf/doc_classes/GLTFSpecGloss.xml @@ -7,6 +7,7 @@ KHR_materials_pbrSpecularGlossiness is an archived GLTF extension. This means that it is deprecated and not recommended for new files. However, it is still supported for loading old files. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="KHR_materials_pbrSpecularGlossiness GLTF extension spec">https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Archived/KHR_materials_pbrSpecularGlossiness</link> </tutorials> <members> diff --git a/modules/gltf/doc_classes/GLTFState.xml b/modules/gltf/doc_classes/GLTFState.xml index ba1c531283..6c7c5ee0e6 100644 --- a/modules/gltf/doc_classes/GLTFState.xml +++ b/modules/gltf/doc_classes/GLTFState.xml @@ -8,6 +8,7 @@ GLTFState can be populated by [GLTFDocument] reading a file or by converting a Godot scene. Then the data can either be used to create a Godot scene or save to a GLTF file. The code that converts to/from a Godot scene can be intercepted at arbitrary points by [GLTFDocumentExtension] classes. This allows for custom data to be stored in the GLTF file or for custom data to be converted to/from Godot nodes. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> <link title="GLTF asset header schema">https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/schema/asset.schema.json"</link> </tutorials> <methods> @@ -19,6 +20,14 @@ Appends an extension to the list of extensions used by this GLTF file during serialization. If [param required] is true, the extension will also be added to the list of required extensions. Do not run this in [method GLTFDocumentExtension._export_post], as that stage is too late to add extensions. The final list is sorted alphabetically. </description> </method> + <method name="append_data_to_buffers"> + <return type="int" /> + <param index="0" name="data" type="PackedByteArray" /> + <param index="1" name="deduplication" type="bool" /> + <description> + Appends the given byte array data to the buffers and creates a [GLTFBufferView] for it. The index of the destination [GLTFBufferView] is returned. If [param deduplication] is true, the buffers will first be searched for duplicate data, otherwise new bytes will always be appended. + </description> + </method> <method name="get_accessors"> <return type="GLTFAccessor[]" /> <description> @@ -280,8 +289,13 @@ The file name associated with this GLTF data. If it ends with [code].gltf[/code], this is text-based GLTF, otherwise this is binary GLB. This will be set during import when appending from a file, and will be set during export when writing to a file. If writing to a buffer, this will be an empty string. </member> <member name="glb_data" type="PackedByteArray" setter="set_glb_data" getter="get_glb_data" default="PackedByteArray()"> + The binary buffer attached to a .glb file. + </member> + <member name="import_as_skeleton_bones" type="bool" setter="set_import_as_skeleton_bones" getter="get_import_as_skeleton_bones" default="false"> + True to force all GLTFNodes in the document to be bones of a single Skeleton3D godot node. </member> <member name="json" type="Dictionary" setter="set_json" getter="get_json" default="{}"> + The original raw JSON document corresponding to this GLTFState. </member> <member name="major_version" type="int" setter="set_major_version" getter="get_major_version" default="0"> </member> diff --git a/modules/gltf/doc_classes/GLTFTexture.xml b/modules/gltf/doc_classes/GLTFTexture.xml index 41c20439ea..50789a0ebf 100644 --- a/modules/gltf/doc_classes/GLTFTexture.xml +++ b/modules/gltf/doc_classes/GLTFTexture.xml @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="GLTFTexture" inherits="Resource" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> + GLTFTexture represents a texture in an GLTF file. </brief_description> <description> </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <members> <member name="sampler" type="int" setter="set_sampler" getter="get_sampler" default="-1"> diff --git a/modules/gltf/doc_classes/GLTFTextureSampler.xml b/modules/gltf/doc_classes/GLTFTextureSampler.xml index 027d35e9d2..2b5bad6724 100644 --- a/modules/gltf/doc_classes/GLTFTextureSampler.xml +++ b/modules/gltf/doc_classes/GLTFTextureSampler.xml @@ -7,6 +7,7 @@ Represents a texture sampler as defined by the base GLTF spec. Texture samplers in GLTF specify how to sample data from the texture's base image, when rendering the texture on an object. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <members> <member name="mag_filter" type="int" setter="set_mag_filter" getter="get_mag_filter" default="9729"> diff --git a/modules/gltf/editor/editor_import_blend_runner.cpp b/modules/gltf/editor/editor_import_blend_runner.cpp index 659a60e6a1..330310d92a 100644 --- a/modules/gltf/editor/editor_import_blend_runner.cpp +++ b/modules/gltf/editor/editor_import_blend_runner.cpp @@ -153,13 +153,7 @@ String dict_to_xmlrpc(const Dictionary &p_dict) { } Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) { - String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path"); - -#ifdef WINDOWS_ENABLED - blender_path = blender_path.path_join("blender.exe"); -#else - blender_path = blender_path.path_join("blender"); -#endif + String blender_path = EDITOR_GET("filesystem/import/blender/blender_path"); List<String> args; args.push_back("--background"); @@ -198,6 +192,40 @@ Error EditorImportBlendRunner::do_import(const Dictionary &p_options) { } } +HTTPClient::Status EditorImportBlendRunner::connect_blender_rpc(const Ref<HTTPClient> &p_client, int p_timeout_usecs) { + p_client->connect_to_host("127.0.0.1", rpc_port); + HTTPClient::Status status = p_client->get_status(); + + int attempts = 1; + int wait_usecs = 1000; + + bool done = false; + while (!done) { + OS::get_singleton()->delay_usec(wait_usecs); + status = p_client->get_status(); + switch (status) { + case HTTPClient::STATUS_RESOLVING: + case HTTPClient::STATUS_CONNECTING: { + p_client->poll(); + break; + } + case HTTPClient::STATUS_CONNECTED: { + done = true; + break; + } + default: { + if (attempts * wait_usecs < p_timeout_usecs) { + p_client->connect_to_host("127.0.0.1", rpc_port); + } else { + return status; + } + } + } + } + + return status; +} + Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { kill_timer->stop(); @@ -217,25 +245,9 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { // Connect to RPC server. Ref<HTTPClient> client = HTTPClient::create(); - client->connect_to_host("127.0.0.1", rpc_port); - - bool done = false; - while (!done) { - HTTPClient::Status status = client->get_status(); - switch (status) { - case HTTPClient::STATUS_RESOLVING: - case HTTPClient::STATUS_CONNECTING: { - client->poll(); - break; - } - case HTTPClient::STATUS_CONNECTED: { - done = true; - break; - } - default: { - ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status)); - } - } + HTTPClient::Status status = connect_blender_rpc(client, 1000000); + if (status != HTTPClient::STATUS_CONNECTED) { + ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status)); } // Send XML request. @@ -246,9 +258,9 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { } // Wait for response. - done = false; + bool done = false; while (!done) { - HTTPClient::Status status = client->get_status(); + status = client->get_status(); switch (status) { case HTTPClient::STATUS_REQUESTING: { client->poll(); diff --git a/modules/gltf/editor/editor_import_blend_runner.h b/modules/gltf/editor/editor_import_blend_runner.h index b2b82394e1..626f3c9eba 100644 --- a/modules/gltf/editor/editor_import_blend_runner.h +++ b/modules/gltf/editor/editor_import_blend_runner.h @@ -33,6 +33,7 @@ #ifdef TOOLS_ENABLED +#include "core/io/http_client.h" #include "core/os/os.h" #include "scene/main/node.h" #include "scene/main/timer.h" @@ -60,6 +61,7 @@ public: bool is_running() { return blender_pid != 0 && OS::get_singleton()->is_process_running(blender_pid); } bool is_using_rpc() { return rpc_port != 0; } Error do_import(const Dictionary &p_options); + HTTPClient::Status connect_blender_rpc(const Ref<HTTPClient> &p_client, int p_timeout_usecs); EditorImportBlendRunner(); }; diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp index 83c7f463df..fee8156375 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp @@ -32,12 +32,14 @@ #ifdef TOOLS_ENABLED -#include "../gltf_document.h" +#include "editor_scene_exporter_gltf_settings.h" #include "editor/editor_file_system.h" +#include "editor/editor_inspector.h" #include "editor/editor_node.h" #include "editor/gui/editor_file_dialog.h" -#include "scene/gui/popup_menu.h" +#include "editor/import/3d/scene_import_settings.h" +#include "editor/themes/editor_scale.h" String SceneExporterGLTFPlugin::get_name() const { return "ConvertGLTF2"; @@ -48,59 +50,72 @@ bool SceneExporterGLTFPlugin::has_main_screen() const { } SceneExporterGLTFPlugin::SceneExporterGLTFPlugin() { - file_export_lib = memnew(EditorFileDialog); - EditorNode::get_singleton()->get_gui_base()->add_child(file_export_lib); - file_export_lib->connect("file_selected", callable_mp(this, &SceneExporterGLTFPlugin::_gltf2_dialog_action)); - file_export_lib->set_title(TTR("Export Library")); - file_export_lib->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); - file_export_lib->set_access(EditorFileDialog::ACCESS_FILESYSTEM); - file_export_lib->clear_filters(); - file_export_lib->add_filter("*.glb"); - file_export_lib->add_filter("*.gltf"); - file_export_lib->set_title(TTR("Export Scene to glTF 2.0 File")); - + _gltf_document.instantiate(); + // Set up the file dialog. + _file_dialog = memnew(EditorFileDialog); + _file_dialog->connect("file_selected", callable_mp(this, &SceneExporterGLTFPlugin::_export_scene_as_gltf)); + _file_dialog->set_title(TTR("Export Library")); + _file_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + _file_dialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + _file_dialog->clear_filters(); + _file_dialog->add_filter("*.glb"); + _file_dialog->add_filter("*.gltf"); + _file_dialog->set_title(TTR("Export Scene to glTF 2.0 File")); + EditorNode::get_singleton()->get_gui_base()->add_child(_file_dialog); + // Set up the export settings menu. + _export_settings.instantiate(); + _export_settings->generate_property_list(_gltf_document); + _settings_inspector = memnew(EditorInspector); + _settings_inspector->set_custom_minimum_size(Size2(350, 300) * EDSCALE); + _file_dialog->add_side_menu(_settings_inspector, TTR("Export Settings:")); + // Add a button to the Scene -> Export menu to pop up the settings dialog. PopupMenu *menu = get_export_as_menu(); int idx = menu->get_item_count(); menu->add_item(TTR("glTF 2.0 Scene...")); - menu->set_item_metadata(idx, callable_mp(this, &SceneExporterGLTFPlugin::convert_scene_to_gltf2)); + menu->set_item_metadata(idx, callable_mp(this, &SceneExporterGLTFPlugin::_popup_gltf_export_dialog)); +} + +void SceneExporterGLTFPlugin::_popup_gltf_export_dialog() { + Node *root = EditorNode::get_singleton()->get_tree()->get_edited_scene_root(); + if (!root) { + EditorNode::get_singleton()->show_accept(TTR("This operation can't be done without a scene."), TTR("OK")); + return; + } + // Set the file dialog's file name to the scene name. + String filename = String(root->get_scene_file_path().get_file().get_basename()); + if (filename.is_empty()) { + filename = root->get_name(); + } + _file_dialog->set_current_file(filename + String(".gltf")); + // Generate and refresh the export settings. + _export_settings->generate_property_list(_gltf_document); + _settings_inspector->edit(nullptr); + _settings_inspector->edit(_export_settings.ptr()); + // Show the file dialog. + _file_dialog->popup_centered_ratio(); } -void SceneExporterGLTFPlugin::_gltf2_dialog_action(String p_file) { +void SceneExporterGLTFPlugin::_export_scene_as_gltf(const String &p_file_path) { Node *root = EditorNode::get_singleton()->get_tree()->get_edited_scene_root(); if (!root) { EditorNode::get_singleton()->show_accept(TTR("This operation can't be done without a scene."), TTR("OK")); return; } List<String> deps; - Ref<GLTFDocument> doc; - doc.instantiate(); Ref<GLTFState> state; state.instantiate(); + state->set_copyright(_export_settings->get_copyright()); int32_t flags = 0; flags |= EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS; - Error err = doc->append_from_scene(root, state, flags); + Error err = _gltf_document->append_from_scene(root, state, flags); if (err != OK) { ERR_PRINT(vformat("glTF2 save scene error %s.", itos(err))); } - err = doc->write_to_filesystem(state, p_file); + err = _gltf_document->write_to_filesystem(state, p_file_path); if (err != OK) { ERR_PRINT(vformat("glTF2 save scene error %s.", itos(err))); } EditorFileSystem::get_singleton()->scan_changes(); } -void SceneExporterGLTFPlugin::convert_scene_to_gltf2() { - Node *root = EditorNode::get_singleton()->get_tree()->get_edited_scene_root(); - if (!root) { - EditorNode::get_singleton()->show_accept(TTR("This operation can't be done without a scene."), TTR("OK")); - return; - } - String filename = String(root->get_scene_file_path().get_file().get_basename()); - if (filename.is_empty()) { - filename = root->get_name(); - } - file_export_lib->set_current_file(filename + String(".gltf")); - file_export_lib->popup_centered_ratio(); -} - #endif // TOOLS_ENABLED diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h index f92b3c5180..683ff6d4f6 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h @@ -33,18 +33,23 @@ #ifdef TOOLS_ENABLED -#include "editor_scene_importer_gltf.h" +#include "../gltf_document.h" +#include "editor_scene_exporter_gltf_settings.h" #include "editor/editor_plugin.h" class EditorFileDialog; +class EditorInspector; class SceneExporterGLTFPlugin : public EditorPlugin { GDCLASS(SceneExporterGLTFPlugin, EditorPlugin); - EditorFileDialog *file_export_lib = nullptr; - void _gltf2_dialog_action(String p_file); - void convert_scene_to_gltf2(); + Ref<GLTFDocument> _gltf_document; + Ref<EditorSceneExporterGLTFSettings> _export_settings; + EditorInspector *_settings_inspector = nullptr; + EditorFileDialog *_file_dialog = nullptr; + void _popup_gltf_export_dialog(); + void _export_scene_as_gltf(const String &p_file_path); public: virtual String get_name() const override; diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp new file mode 100644 index 0000000000..b0283b0c55 --- /dev/null +++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp @@ -0,0 +1,176 @@ +/**************************************************************************/ +/* editor_scene_exporter_gltf_settings.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_scene_exporter_gltf_settings.h" + +const uint32_t PROP_EDITOR_SCRIPT_VAR = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_SCRIPT_VARIABLE; + +bool EditorSceneExporterGLTFSettings::_set(const StringName &p_name, const Variant &p_value) { + String name_str = String(p_name); + if (name_str.contains("/")) { + return _set_extension_setting(name_str, p_value); + } + if (p_name == StringName("image_format")) { + _document->set_image_format(p_value); + emit_signal("property_list_changed"); + return true; + } + if (p_name == StringName("lossy_quality")) { + _document->set_lossy_quality(p_value); + return true; + } + if (p_name == StringName("root_node_mode")) { + _document->set_root_node_mode((GLTFDocument::RootNodeMode)(int64_t)p_value); + return true; + } + return false; +} + +bool EditorSceneExporterGLTFSettings::_get(const StringName &p_name, Variant &r_ret) const { + String name_str = String(p_name); + if (name_str.contains("/")) { + return _get_extension_setting(name_str, r_ret); + } + if (p_name == StringName("image_format")) { + r_ret = _document->get_image_format(); + return true; + } + if (p_name == StringName("lossy_quality")) { + r_ret = _document->get_lossy_quality(); + return true; + } + if (p_name == StringName("root_node_mode")) { + r_ret = _document->get_root_node_mode(); + return true; + } + return false; +} + +void EditorSceneExporterGLTFSettings::_get_property_list(List<PropertyInfo> *p_list) const { + for (PropertyInfo prop : _property_list) { + if (prop.name == "lossy_quality") { + String image_format = get("image_format"); + bool is_image_format_lossy = image_format == "JPEG" || image_format.findn("Lossy") != -1; + prop.usage = is_image_format_lossy ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE; + } + p_list->push_back(prop); + } +} + +bool EditorSceneExporterGLTFSettings::_set_extension_setting(const String &p_name_str, const Variant &p_value) { + PackedStringArray split = String(p_name_str).split("/", true, 1); + if (!_config_name_to_extension_map.has(split[0])) { + return false; + } + Ref<GLTFDocumentExtension> extension = _config_name_to_extension_map[split[0]]; + bool valid; + extension->set(split[1], p_value, &valid); + return valid; +} + +bool EditorSceneExporterGLTFSettings::_get_extension_setting(const String &p_name_str, Variant &r_ret) const { + PackedStringArray split = String(p_name_str).split("/", true, 1); + if (!_config_name_to_extension_map.has(split[0])) { + return false; + } + Ref<GLTFDocumentExtension> extension = _config_name_to_extension_map[split[0]]; + bool valid; + r_ret = extension->get(split[1], &valid); + return valid; +} + +String get_friendly_config_prefix(Ref<GLTFDocumentExtension> p_extension) { + String config_prefix = p_extension->get_name(); + if (!config_prefix.is_empty()) { + return config_prefix; + } + const String class_name = p_extension->get_class_name(); + config_prefix = class_name.trim_prefix("GLTFDocumentExtension").capitalize(); + if (!config_prefix.is_empty()) { + return config_prefix; + } + PackedStringArray supported_extensions = p_extension->get_supported_extensions(); + if (supported_extensions.size() > 0) { + return supported_extensions[0]; + } + return "Unknown GLTFDocumentExtension"; +} + +// Run this before popping up the export settings, because the extensions may have changed. +void EditorSceneExporterGLTFSettings::generate_property_list(Ref<GLTFDocument> p_document) { + _property_list.clear(); + _document = p_document; + String image_format_hint_string = "None,PNG,JPEG"; + // Add properties from all document extensions. + for (Ref<GLTFDocumentExtension> &extension : GLTFDocument::get_all_gltf_document_extensions()) { + const String config_prefix = get_friendly_config_prefix(extension); + _config_name_to_extension_map[config_prefix] = extension; + // If the extension allows saving in different image formats, add to the enum. + PackedStringArray saveable_image_formats = extension->get_saveable_image_formats(); + for (int i = 0; i < saveable_image_formats.size(); i++) { + image_format_hint_string += "," + saveable_image_formats[i]; + } + // Look through the extension's properties and find the relevant ones. + List<PropertyInfo> ext_prop_list; + extension->get_property_list(&ext_prop_list); + for (const PropertyInfo &prop : ext_prop_list) { + // We only want properties that will show up in the exporter + // settings list. Exclude Resource's properties, as they are + // not relevant to the exporter. Include any user-defined script + // variables exposed to the editor (PROP_EDITOR_SCRIPT_VAR). + if ((prop.usage & PROP_EDITOR_SCRIPT_VAR) == PROP_EDITOR_SCRIPT_VAR) { + PropertyInfo ext_prop = prop; + ext_prop.name = config_prefix + "/" + prop.name; + _property_list.push_back(ext_prop); + } + } + } + // Add top-level properties (in addition to what _bind_methods registers). + PropertyInfo image_format_prop = PropertyInfo(Variant::STRING, "image_format", PROPERTY_HINT_ENUM, image_format_hint_string); + _property_list.push_back(image_format_prop); + PropertyInfo lossy_quality_prop = PropertyInfo(Variant::FLOAT, "lossy_quality", PROPERTY_HINT_RANGE, "0,1,0.01"); + _property_list.push_back(lossy_quality_prop); + PropertyInfo root_node_mode_prop = PropertyInfo(Variant::INT, "root_node_mode", PROPERTY_HINT_ENUM, "Single Root,Keep Root,Multi Root"); + _property_list.push_back(root_node_mode_prop); +} + +String EditorSceneExporterGLTFSettings::get_copyright() const { + return _copyright; +} + +void EditorSceneExporterGLTFSettings::set_copyright(const String &p_copyright) { + _copyright = p_copyright; +} + +void EditorSceneExporterGLTFSettings::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_copyright"), &EditorSceneExporterGLTFSettings::get_copyright); + ClassDB::bind_method(D_METHOD("set_copyright", "copyright"), &EditorSceneExporterGLTFSettings::set_copyright); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "copyright", PROPERTY_HINT_PLACEHOLDER_TEXT, "Example: 2014 Godette"), "set_copyright", "get_copyright"); +} diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.h b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h new file mode 100644 index 0000000000..6032932367 --- /dev/null +++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h @@ -0,0 +1,64 @@ +/**************************************************************************/ +/* editor_scene_exporter_gltf_settings.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_SCENE_EXPORTER_GLTF_SETTINGS_H +#define EDITOR_SCENE_EXPORTER_GLTF_SETTINGS_H + +#ifdef TOOLS_ENABLED + +#include "../gltf_document.h" + +class EditorSceneExporterGLTFSettings : public RefCounted { + GDCLASS(EditorSceneExporterGLTFSettings, RefCounted); + List<PropertyInfo> _property_list; + Ref<GLTFDocument> _document; + HashMap<String, Ref<GLTFDocumentExtension>> _config_name_to_extension_map; + + String _copyright; + +protected: + static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + + bool _set_extension_setting(const String &p_name_str, const Variant &p_value); + bool _get_extension_setting(const String &p_name_str, Variant &r_ret) const; + +public: + void generate_property_list(Ref<GLTFDocument> p_document); + + String get_copyright() const; + void set_copyright(const String &p_copyright); +}; + +#endif // TOOLS_ENABLED + +#endif // EDITOR_SCENE_EXPORTER_GLTF_SETTINGS_H diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index cb45a6589e..c6e92de762 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -38,32 +38,24 @@ #include "core/config/project_settings.h" #include "editor/editor_node.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/themes/editor_scale.h" #include "main/main.h" #include "scene/gui/line_edit.h" -#ifdef WINDOWS_ENABLED -#include <shlwapi.h> +#ifdef MINGW_ENABLED +#define near +#define far #endif -static bool _get_blender_version(const String &p_path, int &r_major, int &r_minor, String *r_err = nullptr) { - String path = p_path; #ifdef WINDOWS_ENABLED - path = path.path_join("blender.exe"); -#else - path = path.path_join("blender"); -#endif - -#if defined(MACOS_ENABLED) - if (!FileAccess::exists(path)) { - path = p_path.path_join("Blender"); - } +#include <shlwapi.h> #endif - if (!FileAccess::exists(path)) { +static bool _get_blender_version(const String &p_path, int &r_major, int &r_minor, String *r_err = nullptr) { + if (!FileAccess::exists(p_path)) { if (r_err) { *r_err = TTR("Path does not contain a Blender installation."); } @@ -72,7 +64,7 @@ static bool _get_blender_version(const String &p_path, int &r_major, int &r_mino List<String> args; args.push_back("--version"); String pipe; - Error err = OS::get_singleton()->execute(path, args, &pipe); + Error err = OS::get_singleton()->execute(p_path, args, &pipe); if (err != OK) { if (r_err) { *r_err = TTR("Can't execute Blender binary."); @@ -82,7 +74,7 @@ static bool _get_blender_version(const String &p_path, int &r_major, int &r_mino int bl = pipe.find("Blender "); if (bl == -1) { if (r_err) { - *r_err = vformat(TTR("Unexpected --version output from Blender binary at: %s."), path); + *r_err = vformat(TTR("Unexpected --version output from Blender binary at: %s."), p_path); } return false; } @@ -121,7 +113,7 @@ void EditorSceneFormatImporterBlend::get_extensions(List<String> *r_extensions) Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_t p_flags, const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err) { - String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path"); + String blender_path = EDITOR_GET("filesystem/import/blender/blender_path"); if (blender_major_version == -1 || blender_minor_version == -1) { _get_blender_version(blender_path, blender_major_version, blender_minor_version, nullptr); @@ -129,9 +121,22 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ // Get global paths for source and sink. // Escape paths to be valid Python strings to embed in the script. - const String source_global = ProjectSettings::get_singleton()->globalize_path(p_path).c_escape(); + String source_global = ProjectSettings::get_singleton()->globalize_path(p_path); + +#ifdef WINDOWS_ENABLED + // On Windows, when using a network share path, the above will return a path starting with "//" + // which once handed to Blender will be treated like a relative path. So we need to replace the + // first two characters with "\\" to make it absolute again. + if (source_global.is_network_share_path()) { + source_global = "\\\\" + source_global.substr(2); + } +#endif + + source_global = source_global.c_escape(); + + const String blend_basename = p_path.get_file().get_basename(); const String sink = ProjectSettings::get_singleton()->get_imported_files_path().path_join( - vformat("%s-%s.gltf", p_path.get_file().get_basename(), p_path.md5_text())); + vformat("%s-%s.gltf", blend_basename, p_path.md5_text())); const String sink_global = ProjectSettings::get_singleton()->globalize_path(sink).c_escape(); // Handle configuration options. @@ -282,6 +287,10 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ if (p_options.has(SNAME("blender/materials/unpack_enabled")) && p_options[SNAME("blender/materials/unpack_enabled")]) { base_dir = sink.get_base_dir(); } + if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { + state->set_import_as_skeleton_bones(true); + } + state->set_scene_name(blend_basename); err = gltf->append_from_file(sink.get_basename() + ".gltf", state, p_flags, base_dir); if (err != OK) { if (r_err) { @@ -350,7 +359,7 @@ static bool _test_blender_path(const String &p_path, String *r_err = nullptr) { bool EditorFileSystemImportFormatSupportQueryBlend::is_active() const { bool blend_enabled = GLOBAL_GET("filesystem/import/blender/enabled"); - if (blend_enabled && !_test_blender_path(EDITOR_GET("filesystem/import/blender/blender3_path").operator String())) { + if (blend_enabled && !_test_blender_path(EDITOR_GET("filesystem/import/blender/blender_path").operator String())) { // Intending to import Blender, but blend not configured. return true; } @@ -390,11 +399,59 @@ void EditorFileSystemImportFormatSupportQueryBlend::_validate_path(String p_path } } -bool EditorFileSystemImportFormatSupportQueryBlend::_autodetect_path(String p_path) { - if (_test_blender_path(p_path)) { - auto_detected_path = p_path; - return true; +bool EditorFileSystemImportFormatSupportQueryBlend::_autodetect_path() { + // Autodetect + auto_detected_path = ""; + +#if defined(MACOS_ENABLED) + Vector<String> find_paths = { + "/opt/homebrew/bin/blender", + "/opt/local/bin/blender", + "/usr/local/bin/blender", + "/usr/local/opt/blender", + "/Applications/Blender.app/Contents/MacOS/Blender", + }; + { + List<String> mdfind_args; + mdfind_args.push_back("kMDItemCFBundleIdentifier=org.blenderfoundation.blender"); + + String output; + Error err = OS::get_singleton()->execute("mdfind", mdfind_args, &output); + if (err == OK) { + for (const String &find_path : output.split("\n")) { + find_paths.push_back(find_path.path_join("Contents/MacOS/Blender")); + } + } + } +#elif defined(WINDOWS_ENABLED) + Vector<String> find_paths = { + "C:\\Program Files\\Blender Foundation\\blender.exe", + "C:\\Program Files (x86)\\Blender Foundation\\blender.exe", + }; + { + char blender_opener_path[MAX_PATH]; + DWORD path_len = MAX_PATH; + HRESULT res = AssocQueryString(0, ASSOCSTR_EXECUTABLE, ".blend", "open", blender_opener_path, &path_len); + if (res == S_OK) { + find_paths.push_back(String(blender_opener_path).get_base_dir().path_join("blender.exe")); + } + } + +#elif defined(UNIX_ENABLED) + Vector<String> find_paths = { + "/usr/bin/blender", + "/usr/local/bin/blender", + "/opt/blender/bin/blender", + }; +#endif + + for (const String &find_path : find_paths) { + if (_test_blender_path(find_path)) { + auto_detected_path = find_path; + return true; + } } + return false; } @@ -408,7 +465,7 @@ void EditorFileSystemImportFormatSupportQueryBlend::_select_install(String p_pat } void EditorFileSystemImportFormatSupportQueryBlend::_browse_install() { if (blender_path->get_text() != String()) { - browse_dialog->set_current_dir(blender_path->get_text()); + browse_dialog->set_current_file(blender_path->get_text()); } browse_dialog->popup_centered_ratio(); @@ -460,76 +517,10 @@ bool EditorFileSystemImportFormatSupportQueryBlend::query() { EditorNode::get_singleton()->get_gui_base()->add_child(browse_dialog); } - String path = EDITOR_GET("filesystem/import/blender/blender3_path"); - - if (path == "") { - // Autodetect - auto_detected_path = ""; - -#if defined(MACOS_ENABLED) - - { - Vector<String> mdfind_paths; - { - List<String> mdfind_args; - mdfind_args.push_back("kMDItemCFBundleIdentifier=org.blenderfoundation.blender"); - - String output; - Error err = OS::get_singleton()->execute("mdfind", mdfind_args, &output); - if (err == OK) { - mdfind_paths = output.split("\n"); - } - } + String path = EDITOR_GET("filesystem/import/blender/blender_path"); - bool found = false; - for (const String &found_path : mdfind_paths) { - found = _autodetect_path(found_path.path_join("Contents/MacOS")); - if (found) { - break; - } - } - if (!found) { - found = _autodetect_path("/opt/homebrew/bin"); - } - if (!found) { - found = _autodetect_path("/opt/local/bin"); - } - if (!found) { - found = _autodetect_path("/usr/local/bin"); - } - if (!found) { - found = _autodetect_path("/usr/local/opt"); - } - if (!found) { - found = _autodetect_path("/Applications/Blender.app/Contents/MacOS"); - } - } -#elif defined(WINDOWS_ENABLED) - { - char blender_opener_path[MAX_PATH]; - DWORD path_len = MAX_PATH; - HRESULT res = AssocQueryString(0, ASSOCSTR_EXECUTABLE, ".blend", "open", blender_opener_path, &path_len); - if (res == S_OK && _autodetect_path(String(blender_opener_path).get_base_dir())) { - // Good. - } else if (_autodetect_path("C:\\Program Files\\Blender Foundation")) { - // Good. - } else { - _autodetect_path("C:\\Program Files (x86)\\Blender Foundation"); - } - } - -#elif defined(UNIX_ENABLED) - if (_autodetect_path("/usr/bin")) { - // Good. - } else if (_autodetect_path("/usr/local/bin")) { - // Good - } else { - _autodetect_path("/opt/blender/bin"); - } -#endif - if (auto_detected_path != "") { - path = auto_detected_path; - } + if (path.is_empty() && _autodetect_path()) { + path = auto_detected_path; } blender_path->set_text(path); @@ -550,7 +541,7 @@ bool EditorFileSystemImportFormatSupportQueryBlend::query() { if (confirmed) { // Can only confirm a valid path. - EditorSettings::get_singleton()->set("filesystem/import/blender/blender3_path", blender_path->get_text()); + EditorSettings::get_singleton()->set("filesystem/import/blender/blender_path", blender_path->get_text()); EditorSettings::get_singleton()->save(); } else { // Disable Blender import diff --git a/modules/gltf/editor/editor_scene_importer_blend.h b/modules/gltf/editor/editor_scene_importer_blend.h index ec467db457..ed1b19eaf3 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.h +++ b/modules/gltf/editor/editor_scene_importer_blend.h @@ -34,7 +34,7 @@ #ifdef TOOLS_ENABLED #include "editor/editor_file_system.h" -#include "editor/import/resource_importer_scene.h" +#include "editor/import/3d/resource_importer_scene.h" class Animation; class Node; @@ -95,7 +95,7 @@ class EditorFileSystemImportFormatSupportQueryBlend : public EditorFileSystemImp String auto_detected_path; void _validate_path(String p_path); - bool _autodetect_path(String p_path); + bool _autodetect_path(); void _path_confirmed(); diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp index b63a938e64..78beaac3ed 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.cpp +++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp @@ -51,6 +51,10 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t gltf.instantiate(); Ref<GLTFState> state; state.instantiate(); + if (p_options.has("gltf/naming_version")) { + int naming_version = p_options["gltf/naming_version"]; + gltf->set_naming_version(naming_version); + } if (p_options.has("gltf/embedded_image_handling")) { int32_t enum_option = p_options["gltf/embedded_image_handling"]; state->set_handle_binary_image(enum_option); @@ -77,7 +81,25 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t void EditorSceneFormatImporterGLTF::get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) { - r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/embedded_image_handling", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed As Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), GLTFState::HANDLE_BINARY_EXTRACT_TEXTURES)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/naming_version", PROPERTY_HINT_ENUM, "Godot 4.1 or 4.0,Godot 4.2 or later"), 1)); + r_options->push_back(ResourceImporterScene::ImportOption(PropertyInfo(Variant::INT, "gltf/embedded_image_handling", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed as Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), GLTFState::HANDLE_BINARY_EXTRACT_TEXTURES)); +} + +void EditorSceneFormatImporterGLTF::handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const { + if (!p_import_params.has("gltf/naming_version")) { + // If an existing import file is missing the glTF + // compatibility version, we need to use version 0. + p_import_params["gltf/naming_version"] = 0; + } +} + +Variant EditorSceneFormatImporterGLTF::get_option_visibility(const String &p_path, bool p_for_animation, + const String &p_option, const HashMap<StringName, Variant> &p_options) { + String file_extension = p_path.get_extension().to_lower(); + if ((file_extension != "gltf" && file_extension != "glb") && p_option.begins_with("gltf/")) { + return false; + } + return true; } #endif // TOOLS_ENABLED diff --git a/modules/gltf/editor/editor_scene_importer_gltf.h b/modules/gltf/editor/editor_scene_importer_gltf.h index ed57ec8cdb..d0a9aaf05a 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.h +++ b/modules/gltf/editor/editor_scene_importer_gltf.h @@ -33,7 +33,7 @@ #ifdef TOOLS_ENABLED -#include "editor/import/resource_importer_scene.h" +#include "editor/import/3d/resource_importer_scene.h" class Animation; class Node; @@ -49,6 +49,9 @@ public: List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; + virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; + virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, + const String &p_option, const HashMap<StringName, Variant> &p_options) override; }; #endif // TOOLS_ENABLED diff --git a/modules/gltf/extensions/SCsub b/modules/gltf/extensions/SCsub index 105a1736de..fdf14300f1 100644 --- a/modules/gltf/extensions/SCsub +++ b/modules/gltf/extensions/SCsub @@ -6,5 +6,6 @@ Import("env_modules") env_gltf = env_modules.Clone() # Godot source files + env_gltf.add_source_files(env.modules_sources, "*.cpp") env_gltf.add_source_files(env.modules_sources, "physics/*.cpp") diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp index 582bcf466b..9fdd6034a9 100644 --- a/modules/gltf/extensions/gltf_document_extension.cpp +++ b/modules/gltf/extensions/gltf_document_extension.cpp @@ -185,7 +185,6 @@ Error GLTFDocumentExtension::serialize_texture_json(Ref<GLTFState> p_state, Dict Error GLTFDocumentExtension::export_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_dict, Node *p_node) { ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER); ERR_FAIL_NULL_V(p_gltf_node, ERR_INVALID_PARAMETER); - ERR_FAIL_NULL_V(p_node, ERR_INVALID_PARAMETER); Error err = OK; GDVIRTUAL_CALL(_export_node, p_state, p_gltf_node, r_dict, p_node, err); return err; diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h index 512b7aba91..761dff725c 100644 --- a/modules/gltf/extensions/gltf_document_extension.h +++ b/modules/gltf/extensions/gltf_document_extension.h @@ -33,6 +33,8 @@ #include "../gltf_state.h" +#include "scene/3d/node_3d.h" + class GLTFDocumentExtension : public Resource { GDCLASS(GLTFDocumentExtension, Resource); diff --git a/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp b/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp index 2af716b867..1e64a6daa4 100644 --- a/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp +++ b/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp @@ -32,7 +32,7 @@ #include "scene/3d/importer_mesh_instance_3d.h" #include "scene/3d/mesh_instance_3d.h" -#include "scene/resources/importer_mesh.h" +#include "scene/resources/3d/importer_mesh.h" void GLTFDocumentExtensionConvertImporterMesh::_bind_methods() { } diff --git a/modules/gltf/extensions/gltf_light.cpp b/modules/gltf/extensions/gltf_light.cpp index 435a1260ba..c1d2fea98b 100644 --- a/modules/gltf/extensions/gltf_light.cpp +++ b/modules/gltf/extensions/gltf_light.cpp @@ -51,6 +51,8 @@ void GLTFLight::_bind_methods() { ClassDB::bind_method(D_METHOD("set_inner_cone_angle", "inner_cone_angle"), &GLTFLight::set_inner_cone_angle); ClassDB::bind_method(D_METHOD("get_outer_cone_angle"), &GLTFLight::get_outer_cone_angle); ClassDB::bind_method(D_METHOD("set_outer_cone_angle", "outer_cone_angle"), &GLTFLight::set_outer_cone_angle); + ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFLight::get_additional_data); + ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFLight::set_additional_data); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); // Color ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "intensity"), "set_intensity", "get_intensity"); // float @@ -220,3 +222,11 @@ Dictionary GLTFLight::to_dictionary() const { d["range"] = range; return d; } + +Variant GLTFLight::get_additional_data(const StringName &p_extension_name) { + return additional_data[p_extension_name]; +} + +void GLTFLight::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { + additional_data[p_extension_name] = p_additional_data; +} diff --git a/modules/gltf/extensions/gltf_light.h b/modules/gltf/extensions/gltf_light.h index 2ace08202f..e0894fc8c6 100644 --- a/modules/gltf/extensions/gltf_light.h +++ b/modules/gltf/extensions/gltf_light.h @@ -51,6 +51,7 @@ private: float range = INFINITY; float inner_cone_angle = 0.0f; float outer_cone_angle = Math_TAU / 8.0f; + Dictionary additional_data; public: Color get_color(); @@ -76,6 +77,9 @@ public: static Ref<GLTFLight> from_dictionary(const Dictionary p_dictionary); Dictionary to_dictionary() const; + + Variant get_additional_data(const StringName &p_extension_name); + void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); }; #endif // GLTF_LIGHT_H diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp index 2ba5123c31..c6e34cc023 100644 --- a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp +++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp @@ -30,17 +30,31 @@ #include "gltf_document_extension_physics.h" -#include "scene/3d/area_3d.h" +#include "scene/3d/physics/area_3d.h" +#include "scene/3d/physics/static_body_3d.h" // Import process. Error GLTFDocumentExtensionPhysics::import_preflight(Ref<GLTFState> p_state, Vector<String> p_extensions) { - if (!p_extensions.has("OMI_collider") && !p_extensions.has("OMI_physics_body")) { + if (!p_extensions.has("OMI_collider") && !p_extensions.has("OMI_physics_body") && !p_extensions.has("OMI_physics_shape")) { return ERR_SKIP; } Dictionary state_json = p_state->get_json(); if (state_json.has("extensions")) { Dictionary state_extensions = state_json["extensions"]; - if (state_extensions.has("OMI_collider")) { + if (state_extensions.has("OMI_physics_shape")) { + Dictionary omi_physics_shape_ext = state_extensions["OMI_physics_shape"]; + if (omi_physics_shape_ext.has("shapes")) { + Array state_shape_dicts = omi_physics_shape_ext["shapes"]; + if (state_shape_dicts.size() > 0) { + Array state_shapes; + for (int i = 0; i < state_shape_dicts.size(); i++) { + state_shapes.push_back(GLTFPhysicsShape::from_dictionary(state_shape_dicts[i])); + } + p_state->set_additional_data(StringName("GLTFPhysicsShapes"), state_shapes); + } + } +#ifndef DISABLE_DEPRECATED + } else if (state_extensions.has("OMI_collider")) { Dictionary omi_collider_ext = state_extensions["OMI_collider"]; if (omi_collider_ext.has("colliders")) { Array state_collider_dicts = omi_collider_ext["colliders"]; @@ -49,9 +63,10 @@ Error GLTFDocumentExtensionPhysics::import_preflight(Ref<GLTFState> p_state, Vec for (int i = 0; i < state_collider_dicts.size(); i++) { state_colliders.push_back(GLTFPhysicsShape::from_dictionary(state_collider_dicts[i])); } - p_state->set_additional_data("GLTFPhysicsShapes", state_colliders); + p_state->set_additional_data(StringName("GLTFPhysicsShapes"), state_colliders); } } +#endif // DISABLE_DEPRECATED } } return OK; @@ -61,49 +76,87 @@ Vector<String> GLTFDocumentExtensionPhysics::get_supported_extensions() { Vector<String> ret; ret.push_back("OMI_collider"); ret.push_back("OMI_physics_body"); + ret.push_back("OMI_physics_shape"); return ret; } Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &p_extensions) { +#ifndef DISABLE_DEPRECATED if (p_extensions.has("OMI_collider")) { Dictionary node_collider_ext = p_extensions["OMI_collider"]; if (node_collider_ext.has("collider")) { // "collider" is the index of the collider in the state colliders array. int node_collider_index = node_collider_ext["collider"]; - Array state_colliders = p_state->get_additional_data("GLTFPhysicsShapes"); + Array state_colliders = p_state->get_additional_data(StringName("GLTFPhysicsShapes")); ERR_FAIL_INDEX_V_MSG(node_collider_index, state_colliders.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the collider index " + itos(node_collider_index) + " is not in the state colliders (size: " + itos(state_colliders.size()) + ")."); p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), state_colliders[node_collider_index]); } else { - p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), GLTFPhysicsShape::from_dictionary(p_extensions["OMI_collider"])); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), GLTFPhysicsShape::from_dictionary(node_collider_ext)); } } +#endif // DISABLE_DEPRECATED if (p_extensions.has("OMI_physics_body")) { - p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_dictionary(p_extensions["OMI_physics_body"])); + Dictionary physics_body_ext = p_extensions["OMI_physics_body"]; + if (physics_body_ext.has("collider")) { + Dictionary node_collider = physics_body_ext["collider"]; + // "shape" is the index of the shape in the state shapes array. + int node_shape_index = node_collider.get("shape", -1); + if (node_shape_index != -1) { + Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes")); + ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ")."); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), state_shapes[node_shape_index]); + } else { + // If this node is a collider but does not have a collider + // shape, then it only serves to combine together shapes. + p_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundCollider"), true); + } + } + if (physics_body_ext.has("trigger")) { + Dictionary node_trigger = physics_body_ext["trigger"]; + // "shape" is the index of the shape in the state shapes array. + int node_shape_index = node_trigger.get("shape", -1); + if (node_shape_index != -1) { + Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes")); + ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ")."); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), state_shapes[node_shape_index]); + } else { + // If this node is a trigger but does not have a trigger shape, + // then it's a trigger body, what Godot calls an Area3D node. + Ref<GLTFPhysicsBody> trigger_body; + trigger_body.instantiate(); + trigger_body->set_body_type("trigger"); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), trigger_body); + } + } + if (physics_body_ext.has("motion") || physics_body_ext.has("type")) { + p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_dictionary(physics_body_ext)); + } } return OK; } -void _setup_collider_mesh_resource_from_index_if_needed(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_collider) { - GLTFMeshIndex collider_mesh_index = p_collider->get_mesh_index(); - if (collider_mesh_index == -1) { - return; // No mesh for this collider. +void _setup_shape_mesh_resource_from_index_if_needed(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_gltf_shape) { + GLTFMeshIndex shape_mesh_index = p_gltf_shape->get_mesh_index(); + if (shape_mesh_index == -1) { + return; // No mesh for this shape. } - Ref<ImporterMesh> importer_mesh = p_collider->get_importer_mesh(); + Ref<ImporterMesh> importer_mesh = p_gltf_shape->get_importer_mesh(); if (importer_mesh.is_valid()) { return; // The mesh resource is already set up. } TypedArray<GLTFMesh> state_meshes = p_state->get_meshes(); - ERR_FAIL_INDEX_MSG(collider_mesh_index, state_meshes.size(), "GLTF Physics: When importing '" + p_state->get_scene_name() + "', the collider mesh index " + itos(collider_mesh_index) + " is not in the state meshes (size: " + itos(state_meshes.size()) + ")."); - Ref<GLTFMesh> gltf_mesh = state_meshes[collider_mesh_index]; + ERR_FAIL_INDEX_MSG(shape_mesh_index, state_meshes.size(), "GLTF Physics: When importing '" + p_state->get_scene_name() + "', the shape mesh index " + itos(shape_mesh_index) + " is not in the state meshes (size: " + itos(state_meshes.size()) + ")."); + Ref<GLTFMesh> gltf_mesh = state_meshes[shape_mesh_index]; ERR_FAIL_COND(gltf_mesh.is_null()); importer_mesh = gltf_mesh->get_mesh(); ERR_FAIL_COND(importer_mesh.is_null()); - p_collider->set_importer_mesh(importer_mesh); + p_gltf_shape->set_importer_mesh(importer_mesh); } -CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_collider, Ref<GLTFPhysicsBody> p_physics_body) { - print_verbose("glTF: Creating collision for: " + p_gltf_node->get_name()); - bool is_trigger = p_collider->get_is_trigger(); +#ifndef DISABLE_DEPRECATED +CollisionObject3D *_generate_shape_with_body(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_physics_shape, Ref<GLTFPhysicsBody> p_physics_body) { + print_verbose("glTF: Creating shape with body for: " + p_gltf_node->get_name()); + bool is_trigger = p_physics_shape->get_is_trigger(); // This method is used for the case where we must generate a parent body. // This is can happen for multiple reasons. One possibility is that this // GLTF file is using OMI_collider but not OMI_physics_body, or at least @@ -113,10 +166,10 @@ CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLT if (p_physics_body.is_valid()) { // This code is run when the physics body is on the same GLTF node. body = p_physics_body->to_node(); - if (is_trigger != (p_physics_body->get_body_type() == "trigger")) { + if (is_trigger && (p_physics_body->get_body_type() != "trigger")) { // Edge case: If the body's trigger and the collider's trigger // are in disagreement, we need to create another new body. - CollisionObject3D *child = _generate_collision_with_body(p_state, p_gltf_node, p_collider, nullptr); + CollisionObject3D *child = _generate_shape_with_body(p_state, p_gltf_node, p_physics_shape, nullptr); child->set_name(p_gltf_node->get_name() + (is_trigger ? String("Trigger") : String("Solid"))); body->add_child(child); return body; @@ -126,33 +179,131 @@ CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLT } else { body = memnew(StaticBody3D); } - CollisionShape3D *shape = p_collider->to_node(); + CollisionShape3D *shape = p_physics_shape->to_node(); shape->set_name(p_gltf_node->get_name() + "Shape"); body->add_child(shape); return body; } +#endif // DISABLE_DEPRECATED + +CollisionObject3D *_get_ancestor_collision_object(Node *p_scene_parent) { + // Note: Despite the name of the method, at the moment this only checks + // the direct parent. Only check more later if Godot adds support for it. + if (p_scene_parent) { + CollisionObject3D *co = Object::cast_to<CollisionObject3D>(p_scene_parent); + if (likely(co)) { + return co; + } + } + return nullptr; +} + +Node3D *_generate_shape_node_and_body_if_needed(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_physics_shape, CollisionObject3D *p_col_object, bool p_is_trigger) { + // If we need to generate a body node, do so. + CollisionObject3D *body_node = nullptr; + if (p_is_trigger || p_physics_shape->get_is_trigger()) { + // If the shape wants to be a trigger but it doesn't + // have an Area3D parent, we need to make one. + if (!Object::cast_to<Area3D>(p_col_object)) { + body_node = memnew(Area3D); + } + } else { + if (!Object::cast_to<PhysicsBody3D>(p_col_object)) { + body_node = memnew(StaticBody3D); + } + } + // Generate the shape node. + _setup_shape_mesh_resource_from_index_if_needed(p_state, p_physics_shape); + CollisionShape3D *shape_node = p_physics_shape->to_node(true); + if (body_node) { + shape_node->set_name(p_gltf_node->get_name() + "Shape"); + body_node->add_child(shape_node); + return body_node; + } + return shape_node; +} + +// Either add the child to the parent, or return the child if there is no parent. +Node3D *_add_physics_node_to_given_node(Node3D *p_current_node, Node3D *p_child, Ref<GLTFNode> p_gltf_node) { + if (!p_current_node) { + return p_child; + } + String suffix; + if (Object::cast_to<CollisionShape3D>(p_child)) { + suffix = "Shape"; + } else if (Object::cast_to<Area3D>(p_child)) { + suffix = "Trigger"; + } else { + suffix = "Collider"; + } + p_child->set_name(p_gltf_node->get_name() + suffix); + p_current_node->add_child(p_child); + return p_current_node; +} Node3D *GLTFDocumentExtensionPhysics::generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) { - Ref<GLTFPhysicsBody> physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); - Ref<GLTFPhysicsShape> collider = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape")); - if (collider.is_valid()) { - _setup_collider_mesh_resource_from_index_if_needed(p_state, collider); - // If the collider has the correct type of parent, we just return one node. - if (collider->get_is_trigger()) { - if (Object::cast_to<Area3D>(p_scene_parent)) { - return collider->to_node(true); + Ref<GLTFPhysicsBody> gltf_physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); +#ifndef DISABLE_DEPRECATED + // This deprecated code handles OMI_collider (which we internally name "GLTFPhysicsShape"). + Ref<GLTFPhysicsShape> gltf_physics_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape")); + if (gltf_physics_shape.is_valid()) { + _setup_shape_mesh_resource_from_index_if_needed(p_state, gltf_physics_shape); + // If this GLTF node specifies both a shape and a body, generate both. + if (gltf_physics_body.is_valid()) { + return _generate_shape_with_body(p_state, p_gltf_node, gltf_physics_shape, gltf_physics_body); + } + CollisionObject3D *ancestor_col_obj = _get_ancestor_collision_object(p_scene_parent); + if (gltf_physics_shape->get_is_trigger()) { + // If the shape wants to be a trigger and it already has a + // trigger parent, we only need to make the shape node. + if (Object::cast_to<Area3D>(ancestor_col_obj)) { + return gltf_physics_shape->to_node(true); } - } else { - if (Object::cast_to<PhysicsBody3D>(p_scene_parent)) { - return collider->to_node(true); + } else if (ancestor_col_obj != nullptr) { + // If the shape has a valid parent, only make the shape node. + return gltf_physics_shape->to_node(true); + } + // Otherwise, we need to create a new body. + return _generate_shape_with_body(p_state, p_gltf_node, gltf_physics_shape, nullptr); + } +#endif // DISABLE_DEPRECATED + Node3D *ret = nullptr; + CollisionObject3D *ancestor_col_obj = nullptr; + if (gltf_physics_body.is_valid()) { + ancestor_col_obj = gltf_physics_body->to_node(); + ret = ancestor_col_obj; + } else { + ancestor_col_obj = _get_ancestor_collision_object(p_scene_parent); + if (!Object::cast_to<PhysicsBody3D>(ancestor_col_obj)) { + if (p_gltf_node->get_additional_data(StringName("GLTFPhysicsCompoundCollider"))) { + // If the GLTF file wants this node to group solid shapes together, + // and there is no parent body, we need to create a static body. + ancestor_col_obj = memnew(StaticBody3D); + ret = ancestor_col_obj; } } - return _generate_collision_with_body(p_state, p_gltf_node, collider, physics_body); } - if (physics_body.is_valid()) { - return physics_body->to_node(); + // Add the shapes to the tree. When an ancestor body is present, use it. + // If an explicit body was specified, it has already been generated and + // set above. If there is no ancestor body, we will either generate an + // Area3D or StaticBody3D implicitly, so prefer an Area3D as the base + // node for best compatibility with signal connections to this node. + Ref<GLTFPhysicsShape> gltf_physics_collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); + Ref<GLTFPhysicsShape> gltf_physics_trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); + bool is_ancestor_col_obj_solid = Object::cast_to<PhysicsBody3D>(ancestor_col_obj); + if (is_ancestor_col_obj_solid && gltf_physics_collider_shape.is_valid()) { + Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_collider_shape, ancestor_col_obj, false); + ret = _add_physics_node_to_given_node(ret, child, p_gltf_node); } - return nullptr; + if (gltf_physics_trigger_shape.is_valid()) { + Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_trigger_shape, ancestor_col_obj, true); + ret = _add_physics_node_to_given_node(ret, child, p_gltf_node); + } + if (!is_ancestor_col_obj_solid && gltf_physics_collider_shape.is_valid()) { + Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_collider_shape, ancestor_col_obj, false); + ret = _add_physics_node_to_given_node(ret, child, p_gltf_node); + } + return ret; } // Export process. @@ -202,22 +353,26 @@ GLTFMeshIndex _get_or_insert_mesh_in_state(Ref<GLTFState> p_state, Ref<ImporterM void GLTFDocumentExtensionPhysics::convert_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_node) { if (cast_to<CollisionShape3D>(p_scene_node)) { - CollisionShape3D *shape = Object::cast_to<CollisionShape3D>(p_scene_node); - Ref<GLTFPhysicsShape> collider = GLTFPhysicsShape::from_node(shape); + CollisionShape3D *godot_shape = Object::cast_to<CollisionShape3D>(p_scene_node); + Ref<GLTFPhysicsShape> gltf_shape = GLTFPhysicsShape::from_node(godot_shape); { - Ref<ImporterMesh> importer_mesh = collider->get_importer_mesh(); + Ref<ImporterMesh> importer_mesh = gltf_shape->get_importer_mesh(); if (importer_mesh.is_valid()) { - collider->set_mesh_index(_get_or_insert_mesh_in_state(p_state, importer_mesh)); + gltf_shape->set_mesh_index(_get_or_insert_mesh_in_state(p_state, importer_mesh)); } } - p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), collider); + if (cast_to<Area3D>(_get_ancestor_collision_object(p_scene_node->get_parent()))) { + p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), gltf_shape); + } else { + p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), gltf_shape); + } } else if (cast_to<CollisionObject3D>(p_scene_node)) { - CollisionObject3D *body = Object::cast_to<CollisionObject3D>(p_scene_node); - p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_node(body)); + CollisionObject3D *godot_body = Object::cast_to<CollisionObject3D>(p_scene_node); + p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_node(godot_body)); } } -Array _get_or_create_state_colliders_in_state(Ref<GLTFState> p_state) { +Array _get_or_create_state_shapes_in_state(Ref<GLTFState> p_state) { Dictionary state_json = p_state->get_json(); Dictionary state_extensions; if (state_json.has("extensions")) { @@ -225,48 +380,60 @@ Array _get_or_create_state_colliders_in_state(Ref<GLTFState> p_state) { } else { state_json["extensions"] = state_extensions; } - Dictionary omi_collider_ext; - if (state_extensions.has("OMI_collider")) { - omi_collider_ext = state_extensions["OMI_collider"]; + Dictionary omi_physics_shape_ext; + if (state_extensions.has("OMI_physics_shape")) { + omi_physics_shape_ext = state_extensions["OMI_physics_shape"]; } else { - state_extensions["OMI_collider"] = omi_collider_ext; - p_state->add_used_extension("OMI_collider"); + state_extensions["OMI_physics_shape"] = omi_physics_shape_ext; + p_state->add_used_extension("OMI_physics_shape"); } - Array state_colliders; - if (omi_collider_ext.has("colliders")) { - state_colliders = omi_collider_ext["colliders"]; + Array state_shapes; + if (omi_physics_shape_ext.has("shapes")) { + state_shapes = omi_physics_shape_ext["shapes"]; } else { - omi_collider_ext["colliders"] = state_colliders; + omi_physics_shape_ext["shapes"] = state_shapes; + } + return state_shapes; +} + +Dictionary _export_node_shape(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_physics_shape) { + Array state_shapes = _get_or_create_state_shapes_in_state(p_state); + int size = state_shapes.size(); + Dictionary shape_property; + Dictionary shape_dict = p_physics_shape->to_dictionary(); + for (int i = 0; i < size; i++) { + Dictionary other = state_shapes[i]; + if (other == shape_dict) { + // De-duplication: If we already have an identical shape, + // set the shape index to the existing one and return. + shape_property["shape"] = i; + return shape_property; + } } - return state_colliders; + // If we don't have an identical shape, add it to the array. + state_shapes.push_back(shape_dict); + shape_property["shape"] = size; + return shape_property; } Error GLTFDocumentExtensionPhysics::export_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_node_json, Node *p_node) { - Dictionary node_extensions = r_node_json["extensions"]; + Dictionary physics_body_ext; Ref<GLTFPhysicsBody> physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody")); if (physics_body.is_valid()) { - node_extensions["OMI_physics_body"] = physics_body->to_dictionary(); - p_state->add_used_extension("OMI_physics_body"); + physics_body_ext = physics_body->to_dictionary(); } - Ref<GLTFPhysicsShape> collider = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape")); - if (collider.is_valid()) { - Array state_colliders = _get_or_create_state_colliders_in_state(p_state); - int size = state_colliders.size(); - Dictionary omi_collider_ext; - node_extensions["OMI_collider"] = omi_collider_ext; - Dictionary collider_dict = collider->to_dictionary(); - for (int i = 0; i < size; i++) { - Dictionary other = state_colliders[i]; - if (other == collider_dict) { - // De-duplication: If we already have an identical collider, - // set the collider index to the existing one and return. - omi_collider_ext["collider"] = i; - return OK; - } - } - // If we don't have an identical collider, add it to the array. - state_colliders.push_back(collider_dict); - omi_collider_ext["collider"] = size; + Ref<GLTFPhysicsShape> collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape")); + if (collider_shape.is_valid()) { + physics_body_ext["collider"] = _export_node_shape(p_state, collider_shape); + } + Ref<GLTFPhysicsShape> trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape")); + if (trigger_shape.is_valid()) { + physics_body_ext["trigger"] = _export_node_shape(p_state, trigger_shape); + } + if (!physics_body_ext.is_empty()) { + Dictionary node_extensions = r_node_json["extensions"]; + node_extensions["OMI_physics_body"] = physics_body_ext; + p_state->add_used_extension("OMI_physics_body"); } return OK; } diff --git a/modules/gltf/extensions/physics/gltf_physics_body.cpp b/modules/gltf/extensions/physics/gltf_physics_body.cpp index b80f4348c2..7929b46542 100644 --- a/modules/gltf/extensions/physics/gltf_physics_body.cpp +++ b/modules/gltf/extensions/physics/gltf_physics_body.cpp @@ -30,8 +30,11 @@ #include "gltf_physics_body.h" -#include "scene/3d/area_3d.h" -#include "scene/3d/vehicle_body_3d.h" +#include "scene/3d/physics/animatable_body_3d.h" +#include "scene/3d/physics/area_3d.h" +#include "scene/3d/physics/character_body_3d.h" +#include "scene/3d/physics/static_body_3d.h" +#include "scene/3d/physics/vehicle_body_3d.h" void GLTFPhysicsBody::_bind_methods() { ClassDB::bind_static_method("GLTFPhysicsBody", D_METHOD("from_node", "body_node"), &GLTFPhysicsBody::from_node); @@ -50,22 +53,70 @@ void GLTFPhysicsBody::_bind_methods() { ClassDB::bind_method(D_METHOD("set_angular_velocity", "angular_velocity"), &GLTFPhysicsBody::set_angular_velocity); ClassDB::bind_method(D_METHOD("get_center_of_mass"), &GLTFPhysicsBody::get_center_of_mass); ClassDB::bind_method(D_METHOD("set_center_of_mass", "center_of_mass"), &GLTFPhysicsBody::set_center_of_mass); + ClassDB::bind_method(D_METHOD("get_inertia_diagonal"), &GLTFPhysicsBody::get_inertia_diagonal); + ClassDB::bind_method(D_METHOD("set_inertia_diagonal", "inertia_diagonal"), &GLTFPhysicsBody::set_inertia_diagonal); + ClassDB::bind_method(D_METHOD("get_inertia_orientation"), &GLTFPhysicsBody::get_inertia_orientation); + ClassDB::bind_method(D_METHOD("set_inertia_orientation", "inertia_orientation"), &GLTFPhysicsBody::set_inertia_orientation); +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_inertia_tensor"), &GLTFPhysicsBody::get_inertia_tensor); ClassDB::bind_method(D_METHOD("set_inertia_tensor", "inertia_tensor"), &GLTFPhysicsBody::set_inertia_tensor); +#endif // DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::STRING, "body_type"), "set_body_type", "get_body_type"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mass"), "set_mass", "get_mass"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "linear_velocity"), "set_linear_velocity", "get_linear_velocity"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "angular_velocity"), "set_angular_velocity", "get_angular_velocity"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "center_of_mass"), "set_center_of_mass", "get_center_of_mass"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "inertia_diagonal"), "set_inertia_diagonal", "get_inertia_diagonal"); + ADD_PROPERTY(PropertyInfo(Variant::QUATERNION, "inertia_orientation"), "set_inertia_orientation", "get_inertia_orientation"); +#ifndef DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::BASIS, "inertia_tensor"), "set_inertia_tensor", "get_inertia_tensor"); +#endif // DISABLE_DEPRECATED } String GLTFPhysicsBody::get_body_type() const { - return body_type; + switch (body_type) { + case PhysicsBodyType::STATIC: + return "static"; + case PhysicsBodyType::ANIMATABLE: + return "animatable"; + case PhysicsBodyType::CHARACTER: + return "character"; + case PhysicsBodyType::RIGID: + return "rigid"; + case PhysicsBodyType::VEHICLE: + return "vehicle"; + case PhysicsBodyType::TRIGGER: + return "trigger"; + } + // Unreachable, the switch cases handle all values the enum can take. + // Omitting this works on Clang but not GCC or MSVC. If reached, it's UB. + return "rigid"; } void GLTFPhysicsBody::set_body_type(String p_body_type) { + if (p_body_type == "static") { + body_type = PhysicsBodyType::STATIC; + } else if (p_body_type == "animatable") { + body_type = PhysicsBodyType::ANIMATABLE; + } else if (p_body_type == "character") { + body_type = PhysicsBodyType::CHARACTER; + } else if (p_body_type == "rigid") { + body_type = PhysicsBodyType::RIGID; + } else if (p_body_type == "vehicle") { + body_type = PhysicsBodyType::VEHICLE; + } else if (p_body_type == "trigger") { + body_type = PhysicsBodyType::TRIGGER; + } else { + ERR_PRINT("Error setting GLTF physics body type: The body type must be one of \"static\", \"animatable\", \"character\", \"rigid\", \"vehicle\", or \"trigger\"."); + } +} + +GLTFPhysicsBody::PhysicsBodyType GLTFPhysicsBody::get_physics_body_type() const { + return body_type; +} + +void GLTFPhysicsBody::set_physics_body_type(PhysicsBodyType p_body_type) { body_type = p_body_type; } @@ -101,140 +152,215 @@ void GLTFPhysicsBody::set_center_of_mass(const Vector3 &p_center_of_mass) { center_of_mass = p_center_of_mass; } +Vector3 GLTFPhysicsBody::get_inertia_diagonal() const { + return inertia_diagonal; +} + +void GLTFPhysicsBody::set_inertia_diagonal(const Vector3 &p_inertia_diagonal) { + inertia_diagonal = p_inertia_diagonal; +} + +Quaternion GLTFPhysicsBody::get_inertia_orientation() const { + return inertia_orientation; +} + +void GLTFPhysicsBody::set_inertia_orientation(const Quaternion &p_inertia_orientation) { + inertia_orientation = p_inertia_orientation; +} + +#ifndef DISABLE_DEPRECATED Basis GLTFPhysicsBody::get_inertia_tensor() const { - return inertia_tensor; + return Basis::from_scale(inertia_diagonal); } void GLTFPhysicsBody::set_inertia_tensor(Basis p_inertia_tensor) { - inertia_tensor = p_inertia_tensor; + inertia_diagonal = p_inertia_tensor.get_main_diagonal(); } +#endif // DISABLE_DEPRECATED Ref<GLTFPhysicsBody> GLTFPhysicsBody::from_node(const CollisionObject3D *p_body_node) { Ref<GLTFPhysicsBody> physics_body; physics_body.instantiate(); ERR_FAIL_NULL_V_MSG(p_body_node, physics_body, "Tried to create a GLTFPhysicsBody from a CollisionObject3D node, but the given node was null."); if (cast_to<CharacterBody3D>(p_body_node)) { - physics_body->body_type = "character"; + physics_body->body_type = PhysicsBodyType::CHARACTER; } else if (cast_to<AnimatableBody3D>(p_body_node)) { - physics_body->body_type = "kinematic"; + physics_body->body_type = PhysicsBodyType::ANIMATABLE; } else if (cast_to<RigidBody3D>(p_body_node)) { const RigidBody3D *body = cast_to<const RigidBody3D>(p_body_node); physics_body->mass = body->get_mass(); physics_body->linear_velocity = body->get_linear_velocity(); physics_body->angular_velocity = body->get_angular_velocity(); physics_body->center_of_mass = body->get_center_of_mass(); - Vector3 inertia_diagonal = body->get_inertia(); - physics_body->inertia_tensor = Basis(inertia_diagonal.x, 0, 0, 0, inertia_diagonal.y, 0, 0, 0, inertia_diagonal.z); + physics_body->inertia_diagonal = body->get_inertia(); if (body->get_center_of_mass() != Vector3()) { WARN_PRINT("GLTFPhysicsBody: This rigid body has a center of mass offset from the origin, which will be ignored when exporting to GLTF."); } if (cast_to<VehicleBody3D>(p_body_node)) { - physics_body->body_type = "vehicle"; + physics_body->body_type = PhysicsBodyType::VEHICLE; } else { - physics_body->body_type = "rigid"; + physics_body->body_type = PhysicsBodyType::RIGID; } } else if (cast_to<StaticBody3D>(p_body_node)) { - physics_body->body_type = "static"; + physics_body->body_type = PhysicsBodyType::STATIC; } else if (cast_to<Area3D>(p_body_node)) { - physics_body->body_type = "trigger"; + physics_body->body_type = PhysicsBodyType::TRIGGER; } return physics_body; } CollisionObject3D *GLTFPhysicsBody::to_node() const { - if (body_type == "character") { - CharacterBody3D *body = memnew(CharacterBody3D); - return body; - } - if (body_type == "kinematic") { - AnimatableBody3D *body = memnew(AnimatableBody3D); - return body; - } - if (body_type == "vehicle") { - VehicleBody3D *body = memnew(VehicleBody3D); - body->set_mass(mass); - body->set_linear_velocity(linear_velocity); - body->set_angular_velocity(angular_velocity); - body->set_inertia(inertia_tensor.get_main_diagonal()); - body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM); - body->set_center_of_mass(center_of_mass); - return body; - } - if (body_type == "rigid") { - RigidBody3D *body = memnew(RigidBody3D); - body->set_mass(mass); - body->set_linear_velocity(linear_velocity); - body->set_angular_velocity(angular_velocity); - body->set_inertia(inertia_tensor.get_main_diagonal()); - body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM); - body->set_center_of_mass(center_of_mass); - return body; - } - if (body_type == "static") { - StaticBody3D *body = memnew(StaticBody3D); - return body; - } - if (body_type == "trigger") { - Area3D *body = memnew(Area3D); - return body; + switch (body_type) { + case PhysicsBodyType::CHARACTER: { + CharacterBody3D *body = memnew(CharacterBody3D); + return body; + } + case PhysicsBodyType::ANIMATABLE: { + AnimatableBody3D *body = memnew(AnimatableBody3D); + return body; + } + case PhysicsBodyType::VEHICLE: { + VehicleBody3D *body = memnew(VehicleBody3D); + body->set_mass(mass); + body->set_linear_velocity(linear_velocity); + body->set_angular_velocity(angular_velocity); + body->set_inertia(inertia_diagonal); + body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM); + body->set_center_of_mass(center_of_mass); + return body; + } + case PhysicsBodyType::RIGID: { + RigidBody3D *body = memnew(RigidBody3D); + body->set_mass(mass); + body->set_linear_velocity(linear_velocity); + body->set_angular_velocity(angular_velocity); + body->set_inertia(inertia_diagonal); + body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM); + body->set_center_of_mass(center_of_mass); + return body; + } + case PhysicsBodyType::STATIC: { + StaticBody3D *body = memnew(StaticBody3D); + return body; + } + case PhysicsBodyType::TRIGGER: { + Area3D *body = memnew(Area3D); + return body; + } } - ERR_FAIL_V_MSG(nullptr, "Error converting GLTFPhysicsBody to a node: Body type '" + body_type + "' is unknown."); + // Unreachable, the switch cases handle all values the enum can take. + // Omitting this works on Clang but not GCC or MSVC. If reached, it's UB. + return nullptr; } Ref<GLTFPhysicsBody> GLTFPhysicsBody::from_dictionary(const Dictionary p_dictionary) { Ref<GLTFPhysicsBody> physics_body; physics_body.instantiate(); - ERR_FAIL_COND_V_MSG(!p_dictionary.has("type"), physics_body, "Failed to parse GLTF physics body, missing required field 'type'."); - const String &body_type = p_dictionary["type"]; - physics_body->body_type = body_type; - - if (p_dictionary.has("mass")) { - physics_body->mass = p_dictionary["mass"]; + Dictionary motion; + if (p_dictionary.has("motion")) { + motion = p_dictionary["motion"]; +#ifndef DISABLE_DEPRECATED + } else { + motion = p_dictionary; +#endif // DISABLE_DEPRECATED } - if (p_dictionary.has("linearVelocity")) { - const Array &arr = p_dictionary["linearVelocity"]; + if (motion.has("type")) { + // Read the body type. This representation sits between glTF's and Godot's physics nodes. + // While we may only read "static", "kinematic", or "dynamic" from a valid glTF file, we + // want to allow another extension to override this to another Godot node type mid-import. + // For example, a vehicle extension may want to override the body type to "vehicle" + // so Godot generates a VehicleBody3D node. Therefore we distinguish by importing + // "dynamic" as "rigid", and "kinematic" as "animatable", in the GLTFPhysicsBody code. + String body_type_string = motion["type"]; + if (body_type_string == "static") { + physics_body->body_type = PhysicsBodyType::STATIC; + } else if (body_type_string == "kinematic") { + physics_body->body_type = PhysicsBodyType::ANIMATABLE; + } else if (body_type_string == "dynamic") { + physics_body->body_type = PhysicsBodyType::RIGID; +#ifndef DISABLE_DEPRECATED + } else if (body_type_string == "character") { + physics_body->body_type = PhysicsBodyType::CHARACTER; + } else if (body_type_string == "rigid") { + physics_body->body_type = PhysicsBodyType::RIGID; + } else if (body_type_string == "vehicle") { + physics_body->body_type = PhysicsBodyType::VEHICLE; + } else if (body_type_string == "trigger") { + physics_body->body_type = PhysicsBodyType::TRIGGER; +#endif // DISABLE_DEPRECATED + } else { + ERR_PRINT("Error parsing GLTF physics body: The body type in the GLTF file \"" + body_type_string + "\" was not recognized."); + } + } + if (motion.has("mass")) { + physics_body->mass = motion["mass"]; + } + if (motion.has("linearVelocity")) { + const Array &arr = motion["linearVelocity"]; if (arr.size() == 3) { physics_body->set_linear_velocity(Vector3(arr[0], arr[1], arr[2])); } else { ERR_PRINT("Error parsing GLTF physics body: The linear velocity vector must have exactly 3 numbers."); } } - if (p_dictionary.has("angularVelocity")) { - const Array &arr = p_dictionary["angularVelocity"]; + if (motion.has("angularVelocity")) { + const Array &arr = motion["angularVelocity"]; if (arr.size() == 3) { physics_body->set_angular_velocity(Vector3(arr[0], arr[1], arr[2])); } else { ERR_PRINT("Error parsing GLTF physics body: The angular velocity vector must have exactly 3 numbers."); } } - if (p_dictionary.has("centerOfMass")) { - const Array &arr = p_dictionary["centerOfMass"]; + if (motion.has("centerOfMass")) { + const Array &arr = motion["centerOfMass"]; if (arr.size() == 3) { physics_body->set_center_of_mass(Vector3(arr[0], arr[1], arr[2])); } else { ERR_PRINT("Error parsing GLTF physics body: The center of mass vector must have exactly 3 numbers."); } } - if (p_dictionary.has("inertiaTensor")) { - const Array &arr = p_dictionary["inertiaTensor"]; - if (arr.size() == 9) { - // Only use the diagonal elements of the inertia tensor matrix (principal axes). - physics_body->set_inertia_tensor(Basis(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7], arr[8])); + if (motion.has("inertiaDiagonal")) { + const Array &arr = motion["inertiaDiagonal"]; + if (arr.size() == 3) { + physics_body->set_inertia_diagonal(Vector3(arr[0], arr[1], arr[2])); } else { - ERR_PRINT("Error parsing GLTF physics body: The inertia tensor must be a 3x3 matrix (9 number array)."); + ERR_PRINT("Error parsing GLTF physics body: The inertia diagonal vector must have exactly 3 numbers."); } } - if (body_type != "character" && body_type != "kinematic" && body_type != "rigid" && body_type != "static" && body_type != "trigger" && body_type != "vehicle") { - ERR_PRINT("Error parsing GLTF physics body: Body type '" + body_type + "' is unknown."); + if (motion.has("inertiaOrientation")) { + const Array &arr = motion["inertiaOrientation"]; + if (arr.size() == 4) { + physics_body->set_inertia_orientation(Quaternion(arr[0], arr[1], arr[2], arr[3])); + } else { + ERR_PRINT("Error parsing GLTF physics body: The inertia orientation quaternion must have exactly 4 numbers."); + } } return physics_body; } Dictionary GLTFPhysicsBody::to_dictionary() const { - Dictionary d; - d["type"] = body_type; + Dictionary ret; + if (body_type == PhysicsBodyType::TRIGGER) { + // The equivalent of a Godot Area3D node in glTF is a node that + // defines that it is a trigger, but does not have a shape. + Dictionary trigger; + ret["trigger"] = trigger; + return ret; + } + // All non-trigger body types are defined using the motion property. + Dictionary motion; + // When stored in memory, the body type can correspond to a Godot + // node type. However, when exporting to glTF, we need to squash + // this down to one of "static", "kinematic", or "dynamic". + if (body_type == PhysicsBodyType::STATIC) { + motion["type"] = "static"; + } else if (body_type == PhysicsBodyType::ANIMATABLE || body_type == PhysicsBodyType::CHARACTER) { + motion["type"] = "kinematic"; + } else { + motion["type"] = "dynamic"; + } if (mass != 1.0) { - d["mass"] = mass; + motion["mass"] = mass; } if (linear_velocity != Vector3()) { Array velocity_array; @@ -242,7 +368,7 @@ Dictionary GLTFPhysicsBody::to_dictionary() const { velocity_array[0] = linear_velocity.x; velocity_array[1] = linear_velocity.y; velocity_array[2] = linear_velocity.z; - d["linearVelocity"] = velocity_array; + motion["linearVelocity"] = velocity_array; } if (angular_velocity != Vector3()) { Array velocity_array; @@ -250,7 +376,7 @@ Dictionary GLTFPhysicsBody::to_dictionary() const { velocity_array[0] = angular_velocity.x; velocity_array[1] = angular_velocity.y; velocity_array[2] = angular_velocity.z; - d["angularVelocity"] = velocity_array; + motion["angularVelocity"] = velocity_array; } if (center_of_mass != Vector3()) { Array center_of_mass_array; @@ -258,22 +384,25 @@ Dictionary GLTFPhysicsBody::to_dictionary() const { center_of_mass_array[0] = center_of_mass.x; center_of_mass_array[1] = center_of_mass.y; center_of_mass_array[2] = center_of_mass.z; - d["centerOfMass"] = center_of_mass_array; + motion["centerOfMass"] = center_of_mass_array; + } + if (inertia_diagonal != Vector3()) { + Array inertia_array; + inertia_array.resize(3); + inertia_array[0] = inertia_diagonal[0]; + inertia_array[1] = inertia_diagonal[1]; + inertia_array[2] = inertia_diagonal[2]; + motion["inertiaDiagonal"] = inertia_array; } - if (inertia_tensor != Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)) { + if (inertia_orientation != Quaternion()) { Array inertia_array; - inertia_array.resize(9); - inertia_array.fill(0.0); - inertia_array[0] = inertia_tensor[0][0]; - inertia_array[1] = inertia_tensor[0][1]; - inertia_array[2] = inertia_tensor[0][2]; - inertia_array[3] = inertia_tensor[1][0]; - inertia_array[4] = inertia_tensor[1][1]; - inertia_array[5] = inertia_tensor[1][2]; - inertia_array[6] = inertia_tensor[2][0]; - inertia_array[7] = inertia_tensor[2][1]; - inertia_array[8] = inertia_tensor[2][2]; - d["inertiaTensor"] = inertia_array; + inertia_array.resize(4); + inertia_array[0] = inertia_orientation[0]; + inertia_array[1] = inertia_orientation[1]; + inertia_array[2] = inertia_orientation[2]; + inertia_array[3] = inertia_orientation[3]; + motion["inertiaDiagonal"] = inertia_array; } - return d; + ret["motion"] = motion; + return ret; } diff --git a/modules/gltf/extensions/physics/gltf_physics_body.h b/modules/gltf/extensions/physics/gltf_physics_body.h index 391b4b873f..ec139054ff 100644 --- a/modules/gltf/extensions/physics/gltf_physics_body.h +++ b/modules/gltf/extensions/physics/gltf_physics_body.h @@ -31,29 +31,49 @@ #ifndef GLTF_PHYSICS_BODY_H #define GLTF_PHYSICS_BODY_H -#include "scene/3d/physics_body_3d.h" +#include "scene/3d/physics/physics_body_3d.h" -// GLTFPhysicsBody is an intermediary between OMI_physics_body and Godot's physics body nodes. +// GLTFPhysicsBody is an intermediary between Godot's physics body nodes +// and the OMI_physics_body extension. // https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_body class GLTFPhysicsBody : public Resource { GDCLASS(GLTFPhysicsBody, Resource) +public: + // These values map to Godot's physics body types. + // When importing, the body type will be set to the closest match, and + // user code can change this to make Godot generate a different node type. + // When exporting, this will be squashed down to one of "static", + // "kinematic", or "dynamic" motion types, or the "trigger" property. + enum class PhysicsBodyType { + STATIC, + ANIMATABLE, + CHARACTER, + RIGID, + VEHICLE, + TRIGGER, + }; + protected: static void _bind_methods(); private: - String body_type = "static"; + PhysicsBodyType body_type = PhysicsBodyType::RIGID; real_t mass = 1.0; Vector3 linear_velocity; Vector3 angular_velocity; Vector3 center_of_mass; - Basis inertia_tensor = Basis(0, 0, 0, 0, 0, 0, 0, 0, 0); + Vector3 inertia_diagonal; + Quaternion inertia_orientation; public: String get_body_type() const; void set_body_type(String p_body_type); + PhysicsBodyType get_physics_body_type() const; + void set_physics_body_type(PhysicsBodyType p_body_type); + real_t get_mass() const; void set_mass(real_t p_mass); @@ -66,8 +86,16 @@ public: Vector3 get_center_of_mass() const; void set_center_of_mass(const Vector3 &p_center_of_mass); + Vector3 get_inertia_diagonal() const; + void set_inertia_diagonal(const Vector3 &p_inertia_diagonal); + + Quaternion get_inertia_orientation() const; + void set_inertia_orientation(const Quaternion &p_inertia_orientation); + +#ifndef DISABLE_DEPRECATED Basis get_inertia_tensor() const; void set_inertia_tensor(Basis p_inertia_tensor); +#endif // DISABLE_DEPRECATED static Ref<GLTFPhysicsBody> from_node(const CollisionObject3D *p_body_node); CollisionObject3D *to_node() const; diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.cpp b/modules/gltf/extensions/physics/gltf_physics_shape.cpp index d3c56c0da9..35c99adbe5 100644 --- a/modules/gltf/extensions/physics/gltf_physics_shape.cpp +++ b/modules/gltf/extensions/physics/gltf_physics_shape.cpp @@ -33,14 +33,14 @@ #include "../../gltf_state.h" #include "core/math/convex_hull.h" -#include "scene/3d/area_3d.h" -#include "scene/resources/box_shape_3d.h" -#include "scene/resources/capsule_shape_3d.h" -#include "scene/resources/concave_polygon_shape_3d.h" -#include "scene/resources/convex_polygon_shape_3d.h" -#include "scene/resources/cylinder_shape_3d.h" -#include "scene/resources/importer_mesh.h" -#include "scene/resources/sphere_shape_3d.h" +#include "scene/3d/physics/area_3d.h" +#include "scene/resources/3d/box_shape_3d.h" +#include "scene/resources/3d/capsule_shape_3d.h" +#include "scene/resources/3d/concave_polygon_shape_3d.h" +#include "scene/resources/3d/convex_polygon_shape_3d.h" +#include "scene/resources/3d/cylinder_shape_3d.h" +#include "scene/resources/3d/importer_mesh.h" +#include "scene/resources/3d/sphere_shape_3d.h" void GLTFPhysicsShape::_bind_methods() { ClassDB::bind_static_method("GLTFPhysicsShape", D_METHOD("from_node", "shape_node"), &GLTFPhysicsShape::from_node); @@ -129,16 +129,16 @@ void GLTFPhysicsShape::set_importer_mesh(Ref<ImporterMesh> p_importer_mesh) { importer_mesh = p_importer_mesh; } -Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_collider_node) { +Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godot_shape_node) { Ref<GLTFPhysicsShape> gltf_shape; gltf_shape.instantiate(); - ERR_FAIL_NULL_V_MSG(p_collider_node, gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node was null."); - Node *parent = p_collider_node->get_parent(); + ERR_FAIL_NULL_V_MSG(p_godot_shape_node, gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node was null."); + Node *parent = p_godot_shape_node->get_parent(); if (cast_to<const Area3D>(parent)) { gltf_shape->set_is_trigger(true); } // All the code for working with the shape is below this comment. - Ref<Shape3D> shape_resource = p_collider_node->get_shape(); + Ref<Shape3D> shape_resource = p_godot_shape_node->get_shape(); ERR_FAIL_COND_V_MSG(shape_resource.is_null(), gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node had a null shape."); gltf_shape->_shape_cache = shape_resource; if (cast_to<BoxShape3D>(shape_resource.ptr())) { @@ -160,7 +160,7 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_coll Ref<SphereShape3D> sphere = shape_resource; gltf_shape->set_radius(sphere->get_radius()); } else if (cast_to<const ConvexPolygonShape3D>(shape_resource.ptr())) { - gltf_shape->shape_type = "hull"; + gltf_shape->shape_type = "convex"; Ref<ConvexPolygonShape3D> convex = shape_resource; Vector<Vector3> hull_points = convex->get_points(); ERR_FAIL_COND_V_MSG(hull_points.size() < 3, gltf_shape, "GLTFPhysicsShape: Convex hull has fewer points (" + itos(hull_points.size()) + ") than the minimum of 3. At least 3 points are required in order to save to GLTF, since it uses a mesh to represent convex hulls."); @@ -206,7 +206,7 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_coll } CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) { - CollisionShape3D *gltf_shape = memnew(CollisionShape3D); + CollisionShape3D *godot_shape_node = memnew(CollisionShape3D); if (!p_cache_shapes || _shape_cache == nullptr) { if (shape_type == "box") { Ref<BoxShape3D> box; @@ -230,80 +230,88 @@ CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) { sphere.instantiate(); sphere->set_radius(radius); _shape_cache = sphere; - } else if (shape_type == "hull") { - ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Error converting convex hull shape to a node: The mesh resource is null."); + } else if (shape_type == "convex") { + ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting convex hull shape to a node: The mesh resource is null."); Ref<ConvexPolygonShape3D> convex = importer_mesh->get_mesh()->create_convex_shape(); _shape_cache = convex; } else if (shape_type == "trimesh") { - ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Error converting concave mesh shape to a node: The mesh resource is null."); + ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting concave mesh shape to a node: The mesh resource is null."); Ref<ConcavePolygonShape3D> concave = importer_mesh->create_trimesh_shape(); _shape_cache = concave; } else { ERR_PRINT("GLTFPhysicsShape: Error converting to a node: Shape type '" + shape_type + "' is unknown."); } } - gltf_shape->set_shape(_shape_cache); - return gltf_shape; + godot_shape_node->set_shape(_shape_cache); + return godot_shape_node; } Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_dictionary(const Dictionary p_dictionary) { ERR_FAIL_COND_V_MSG(!p_dictionary.has("type"), Ref<GLTFPhysicsShape>(), "Failed to parse GLTFPhysicsShape, missing required field 'type'."); Ref<GLTFPhysicsShape> gltf_shape; gltf_shape.instantiate(); - const String &shape_type = p_dictionary["type"]; + String shape_type = p_dictionary["type"]; + if (shape_type == "hull") { + shape_type = "convex"; + } gltf_shape->shape_type = shape_type; - if (shape_type != "box" && shape_type != "capsule" && shape_type != "cylinder" && shape_type != "sphere" && shape_type != "hull" && shape_type != "trimesh") { - ERR_PRINT("GLTFPhysicsShape: Error parsing unknown shape type '" + shape_type + "'. Only box, capsule, cylinder, sphere, hull, and trimesh are supported."); + if (shape_type != "box" && shape_type != "capsule" && shape_type != "cylinder" && shape_type != "sphere" && shape_type != "convex" && shape_type != "trimesh") { + ERR_PRINT("GLTFPhysicsShape: Error parsing unknown shape type '" + shape_type + "'. Only box, capsule, cylinder, sphere, convex, and trimesh are supported."); + } + Dictionary properties; + if (p_dictionary.has(shape_type)) { + properties = p_dictionary[shape_type]; + } else { + properties = p_dictionary; } - if (p_dictionary.has("radius")) { - gltf_shape->set_radius(p_dictionary["radius"]); + if (properties.has("radius")) { + gltf_shape->set_radius(properties["radius"]); } - if (p_dictionary.has("height")) { - gltf_shape->set_height(p_dictionary["height"]); + if (properties.has("height")) { + gltf_shape->set_height(properties["height"]); } - if (p_dictionary.has("size")) { - const Array &arr = p_dictionary["size"]; + if (properties.has("size")) { + const Array &arr = properties["size"]; if (arr.size() == 3) { gltf_shape->set_size(Vector3(arr[0], arr[1], arr[2])); } else { ERR_PRINT("GLTFPhysicsShape: Error parsing the size, it must have exactly 3 numbers."); } } - if (p_dictionary.has("isTrigger")) { - gltf_shape->set_is_trigger(p_dictionary["isTrigger"]); + if (properties.has("isTrigger")) { + gltf_shape->set_is_trigger(properties["isTrigger"]); } - if (p_dictionary.has("mesh")) { - gltf_shape->set_mesh_index(p_dictionary["mesh"]); + if (properties.has("mesh")) { + gltf_shape->set_mesh_index(properties["mesh"]); } - if (unlikely(gltf_shape->get_mesh_index() < 0 && (shape_type == "hull" || shape_type == "trimesh"))) { + if (unlikely(gltf_shape->get_mesh_index() < 0 && (shape_type == "convex" || shape_type == "trimesh"))) { ERR_PRINT("Error parsing GLTFPhysicsShape: The mesh-based shape type '" + shape_type + "' does not have a valid mesh index."); } return gltf_shape; } Dictionary GLTFPhysicsShape::to_dictionary() const { - Dictionary d; - d["type"] = shape_type; + Dictionary gltf_shape; + gltf_shape["type"] = shape_type; + Dictionary sub; if (shape_type == "box") { Array size_array; size_array.resize(3); size_array[0] = size.x; size_array[1] = size.y; size_array[2] = size.z; - d["size"] = size_array; + sub["size"] = size_array; } else if (shape_type == "capsule") { - d["radius"] = get_radius(); - d["height"] = get_height(); + sub["radius"] = get_radius(); + sub["height"] = get_height(); } else if (shape_type == "cylinder") { - d["radius"] = get_radius(); - d["height"] = get_height(); + sub["radius"] = get_radius(); + sub["height"] = get_height(); } else if (shape_type == "sphere") { - d["radius"] = get_radius(); - } else if (shape_type == "trimesh" || shape_type == "hull") { - d["mesh"] = get_mesh_index(); - } - if (is_trigger) { - d["isTrigger"] = is_trigger; + sub["radius"] = get_radius(); + } else if (shape_type == "trimesh" || shape_type == "convex") { + sub["mesh"] = get_mesh_index(); } - return d; + gltf_shape[shape_type] = sub; + return gltf_shape; } diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.h b/modules/gltf/extensions/physics/gltf_physics_shape.h index efecf27e1b..ec0a8931f1 100644 --- a/modules/gltf/extensions/physics/gltf_physics_shape.h +++ b/modules/gltf/extensions/physics/gltf_physics_shape.h @@ -33,12 +33,13 @@ #include "../../gltf_defines.h" -#include "scene/3d/collision_shape_3d.h" +#include "scene/3d/physics/collision_shape_3d.h" class ImporterMesh; -// GLTFPhysicsShape is an intermediary between OMI_collider and Godot's collision shape nodes. -// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_collider +// GLTFPhysicsShape is an intermediary between Godot's collision shape nodes +// and the OMI_physics_shape extension. +// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_shape class GLTFPhysicsShape : public Resource { GDCLASS(GLTFPhysicsShape, Resource) diff --git a/modules/gltf/gltf_defines.h b/modules/gltf/gltf_defines.h index ca9a4d04e4..d044105b42 100644 --- a/modules/gltf/gltf_defines.h +++ b/modules/gltf/gltf_defines.h @@ -33,17 +33,6 @@ // This file should only be included by other headers. -// Godot classes used by GLTF headers. -class BoneAttachment3D; -class CSGShape3D; -class GridMap; -class ImporterMeshInstance3D; -class Light3D; -class MeshInstance3D; -class MultiMeshInstance3D; -class Skeleton3D; -class Skin; - // GLTF classes. struct GLTFAccessor; class GLTFAnimation; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 324eb79ea2..eece6afdcc 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -31,6 +31,8 @@ #include "gltf_document.h" #include "extensions/gltf_spec_gloss.h" +#include "gltf_state.h" +#include "skin_tool.h" #include "core/config/project_settings.h" #include "core/crypto/crypto_core.h" @@ -40,7 +42,7 @@ #include "core/io/file_access_memory.h" #include "core/io/json.h" #include "core/io/stream_peer.h" -#include "core/math/disjoint_set.h" +#include "core/object/object_id.h" #include "core/version.h" #include "scene/3d/bone_attachment_3d.h" #include "scene/3d/camera_3d.h" @@ -48,22 +50,14 @@ #include "scene/3d/light_3d.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/3d/multimesh_instance_3d.h" +#include "scene/resources/3d/skin.h" #include "scene/resources/image_texture.h" #include "scene/resources/portable_compressed_texture.h" -#include "scene/resources/skin.h" #include "scene/resources/surface_tool.h" -#include "modules/modules_enabled.gen.h" // For csg, gridmap. - #ifdef TOOLS_ENABLED #include "editor/editor_file_system.h" #endif -#ifdef MODULE_CSG_ENABLED -#include "modules/csg/csg_shape.h" -#endif // MODULE_CSG_ENABLED -#ifdef MODULE_GRIDMAP_ENABLED -#include "modules/gridmap/grid_map.h" -#endif // MODULE_GRIDMAP_ENABLED // FIXME: Hardcoded to avoid editor dependency. #define GLTF_IMPORT_GENERATE_TANGENT_ARRAYS 8 @@ -74,7 +68,6 @@ #include <stdio.h> #include <stdlib.h> #include <cstdint> -#include <limits> static Ref<ImporterMesh> _mesh_to_importer_mesh(Ref<Mesh> p_mesh) { Ref<ImporterMesh> importer_mesh; @@ -250,7 +243,7 @@ Error GLTFDocument::_serialize_gltf_extensions(Ref<GLTFState> p_state) const { } Error GLTFDocument::_serialize_scenes(Ref<GLTFState> p_state) { - ERR_FAIL_COND_V_MSG(p_state->root_nodes.size() == 0, ERR_INVALID_DATA, "GLTF export: The scene must have at least one root node."); + ERR_FAIL_COND_V_MSG(p_state->root_nodes.is_empty(), ERR_INVALID_DATA, "GLTF export: The scene must have at least one root node."); // Godot only supports one scene per glTF file. Array scenes; Dictionary scene_dict; @@ -405,6 +398,7 @@ static Vector<real_t> _xform_to_array(const Transform3D p_transform) { Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { Array nodes; + const int scene_node_count = p_state->scene_nodes.size(); for (int i = 0; i < p_state->nodes.size(); i++) { Dictionary node; Ref<GLTFNode> gltf_node = p_state->nodes[i]; @@ -429,20 +423,22 @@ Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { } if (gltf_node->skeleton != -1 && gltf_node->skin < 0) { } - if (gltf_node->xform != Transform3D()) { - node["matrix"] = _xform_to_array(gltf_node->xform); - } - - if (!gltf_node->rotation.is_equal_approx(Quaternion())) { - node["rotation"] = _quaternion_to_array(gltf_node->rotation); - } - - if (!gltf_node->scale.is_equal_approx(Vector3(1.0f, 1.0f, 1.0f))) { - node["scale"] = _vec3_to_arr(gltf_node->scale); - } - - if (!gltf_node->position.is_zero_approx()) { - node["translation"] = _vec3_to_arr(gltf_node->position); + if (gltf_node->transform.basis.is_orthogonal()) { + // An orthogonal transform is decomposable into TRS, so prefer that. + const Vector3 position = gltf_node->get_position(); + if (!position.is_zero_approx()) { + node["translation"] = _vec3_to_arr(position); + } + const Quaternion rotation = gltf_node->get_rotation(); + if (!rotation.is_equal_approx(Quaternion())) { + node["rotation"] = _quaternion_to_array(rotation); + } + const Vector3 scale = gltf_node->get_scale(); + if (!scale.is_equal_approx(Vector3(1.0f, 1.0f, 1.0f))) { + node["scale"] = _vec3_to_arr(scale); + } + } else { + node["matrix"] = _xform_to_array(gltf_node->transform); } if (gltf_node->children.size()) { Array children; @@ -452,10 +448,13 @@ Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { node["children"] = children; } + Node *scene_node = nullptr; + if (i < scene_node_count) { + scene_node = p_state->scene_nodes[i]; + } for (Ref<GLTFDocumentExtension> ext : document_extensions) { ERR_CONTINUE(ext.is_null()); - ERR_CONTINUE(!p_state->scene_nodes.find(i)); - Error err = ext->export_node(p_state, gltf_node, node, p_state->scene_nodes[i]); + Error err = ext->export_node(p_state, gltf_node, node, scene_node); ERR_CONTINUE(err != OK); } @@ -466,25 +465,7 @@ Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { } String GLTFDocument::_gen_unique_name(Ref<GLTFState> p_state, const String &p_name) { - const String s_name = p_name.validate_node_name(); - - String u_name; - int index = 1; - while (true) { - u_name = s_name; - - if (index > 1) { - u_name += itos(index); - } - if (!p_state->unique_names.has(u_name)) { - break; - } - index++; - } - - p_state->unique_names.insert(u_name); - - return u_name; + return _gen_unique_name_static(p_state->unique_names, p_name); } String GLTFDocument::_sanitize_animation_name(const String &p_name) { @@ -573,9 +554,12 @@ Error GLTFDocument::_parse_scenes(Ref<GLTFState> p_state) { // Determine what to use for the scene name. if (scene_dict.has("name") && !String(scene_dict["name"]).is_empty() && !((String)scene_dict["name"]).begins_with("Scene")) { p_state->scene_name = scene_dict["name"]; - } else { + } else if (p_state->scene_name.is_empty()) { p_state->scene_name = p_state->filename; } + if (_naming_version == 0) { + p_state->scene_name = _gen_unique_name(p_state, p_state->scene_name); + } } return OK; @@ -590,6 +574,7 @@ Error GLTFDocument::_parse_nodes(Ref<GLTFState> p_state) { const Dictionary &n = nodes[i]; if (n.has("name")) { + node->set_original_name(n["name"]); node->set_name(n["name"]); } if (n.has("camera")) { @@ -602,20 +587,22 @@ Error GLTFDocument::_parse_nodes(Ref<GLTFState> p_state) { node->skin = n["skin"]; } if (n.has("matrix")) { - node->xform = _arr_to_xform(n["matrix"]); + node->transform = _arr_to_xform(n["matrix"]); } else { if (n.has("translation")) { - node->position = _arr_to_vec3(n["translation"]); + node->set_position(_arr_to_vec3(n["translation"])); } if (n.has("rotation")) { - node->rotation = _arr_to_quaternion(n["rotation"]); + node->set_rotation(_arr_to_quaternion(n["rotation"])); } if (n.has("scale")) { - node->scale = _arr_to_vec3(n["scale"]); + node->set_scale(_arr_to_vec3(n["scale"])); } - node->xform.basis.set_quaternion_scale(node->rotation, node->scale); - node->xform.origin = node->position; + Transform3D godot_rest_transform; + godot_rest_transform.basis.set_quaternion_scale(node->transform.basis.get_rotation_quaternion(), node->transform.basis.get_scale()); + godot_rest_transform.origin = node->transform.origin; + node->set_additional_data("GODOT_rest_transform", godot_rest_transform); } if (n.has("extensions")) { @@ -800,7 +787,7 @@ Error GLTFDocument::_parse_buffers(Ref<GLTFState> p_state, const String &p_base_ uri = uri.uri_decode(); uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows. buffer_data = FileAccess::get_file_as_bytes(uri); - ERR_FAIL_COND_V_MSG(buffer.size() == 0, ERR_PARSE_ERROR, "glTF: Couldn't load binary file as an array: " + uri); + ERR_FAIL_COND_V_MSG(buffer.is_empty(), ERR_PARSE_ERROR, "glTF: Couldn't load binary file as an array: " + uri); } ERR_FAIL_COND_V(!buffer.has("byteLength"), ERR_PARSE_ERROR); @@ -1538,7 +1525,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_ints(Ref<GLTFState> p_state, } } - ERR_FAIL_COND_V(attribs.size() == 0, -1); + ERR_FAIL_COND_V(attribs.is_empty(), -1); Ref<GLTFAccessor> accessor; accessor.instantiate(); @@ -1897,7 +1884,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref<GLTFState> p_stat _calc_accessor_min_max(i, element_count, type_max, attribs, type_min); } - ERR_FAIL_COND_V(!attribs.size(), -1); + ERR_FAIL_COND_V(attribs.is_empty(), -1); Ref<GLTFAccessor> accessor; accessor.instantiate(); @@ -2215,7 +2202,7 @@ Error GLTFDocument::_serialize_meshes(Ref<GLTFState> p_state) { Dictionary attributes; { Vector<Vector3> a = array[Mesh::ARRAY_VERTEX]; - ERR_FAIL_COND_V(!a.size(), ERR_INVALID_DATA); + ERR_FAIL_COND_V(a.is_empty(), ERR_INVALID_DATA); attributes["POSITION"] = _encode_accessor_as_vec3(p_state, a, true); vertex_num = a.size(); } @@ -2566,8 +2553,10 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { String mesh_name = "mesh"; if (d.has("name") && !String(d["name"]).is_empty()) { mesh_name = d["name"]; + mesh->set_original_name(mesh_name); } import_mesh->set_name(_gen_unique_name(p_state, vformat("%s_%s", p_state->scene_name, mesh_name))); + mesh->set_name(import_mesh->get_name()); for (int j = 0; j < primitives.size(); j++) { uint64_t flags = RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES; @@ -2782,7 +2771,7 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { } else if (primitive == Mesh::PRIMITIVE_TRIANGLES) { //generate indices because they need to be swapped for CW/CCW const Vector<Vector3> &vertices = array[Mesh::ARRAY_VERTEX]; - ERR_FAIL_COND_V(vertices.size() == 0, ERR_PARSE_ERROR); + ERR_FAIL_COND_V(vertices.is_empty(), ERR_PARSE_ERROR); Vector<int> indices; const int vs = vertices.size(); indices.resize(vs); @@ -2797,9 +2786,26 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { array[Mesh::ARRAY_INDEX] = indices; } - bool generate_tangents = p_state->force_generate_tangents && (primitive == Mesh::PRIMITIVE_TRIANGLES && !a.has("TANGENT") && a.has("TEXCOORD_0") && a.has("NORMAL")); + bool generate_tangents = p_state->force_generate_tangents && (primitive == Mesh::PRIMITIVE_TRIANGLES && !a.has("TANGENT") && a.has("NORMAL")); - if (p_state->force_disable_compression || !a.has("POSITION") || !a.has("NORMAL") || !(a.has("TANGENT") || generate_tangents) || p.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) { + if (generate_tangents && !a.has("TEXCOORD_0")) { + // If we don't have UVs we provide a dummy tangent array. + Vector<float> tangents; + tangents.resize(vertex_num * 4); + float *tangentsw = tangents.ptrw(); + + Vector<Vector3> normals = array[Mesh::ARRAY_NORMAL]; + for (int k = 0; k < vertex_num; k++) { + Vector3 tan = Vector3(normals[k].z, -normals[k].x, normals[k].y).cross(normals[k].normalized()).normalized(); + tangentsw[k * 4 + 0] = tan.x; + tangentsw[k * 4 + 1] = tan.y; + tangentsw[k * 4 + 2] = tan.z; + tangentsw[k * 4 + 3] = 1.0; + } + array[Mesh::ARRAY_TANGENT] = tangents; + } + + if (p_state->force_disable_compression || !a.has("POSITION") || !a.has("NORMAL") || p.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) { flags &= ~RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES; } @@ -2810,12 +2816,25 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { mesh_surface_tool->set_skin_weight_count(SurfaceTool::SKIN_8_WEIGHTS); } mesh_surface_tool->index(); - if (generate_tangents) { + if (generate_tangents && a.has("TEXCOORD_0")) { //must generate mikktspace tangents.. ergh.. mesh_surface_tool->generate_tangents(); } array = mesh_surface_tool->commit_to_arrays(); + if ((flags & RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES) && a.has("NORMAL") && (a.has("TANGENT") || generate_tangents)) { + // Compression is enabled, so let's validate that the normals and tangents are correct. + Vector<Vector3> normals = array[Mesh::ARRAY_NORMAL]; + Vector<float> tangents = array[Mesh::ARRAY_TANGENT]; + for (int vert = 0; vert < normals.size(); vert++) { + Vector3 tan = Vector3(tangents[vert * 4 + 0], tangents[vert * 4 + 1], tangents[vert * 4 + 2]); + if (abs(tan.dot(normals[vert])) > 0.0001) { + // Tangent is not perpendicular to the normal, so we can't use compression. + flags &= ~RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES; + } + } + } + Array morphs; //blend shapes if (p.has("targets")) { @@ -2896,7 +2915,7 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { if (t.has("TANGENT")) { const Vector<Vector3> tangents_v3 = _decode_accessor_as_vec3(p_state, t["TANGENT"], true); const Vector<float> src_tangents = array[Mesh::ARRAY_TANGENT]; - ERR_FAIL_COND_V(src_tangents.size() == 0, ERR_PARSE_ERROR); + ERR_FAIL_COND_V(src_tangents.is_empty(), ERR_PARSE_ERROR); Vector<float> tangents_v4; @@ -3006,6 +3025,14 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { return OK; } +void GLTFDocument::set_naming_version(int p_version) { + _naming_version = p_version; +} + +int GLTFDocument::get_naming_version() const { + return _naming_version; +} + void GLTFDocument::set_image_format(const String &p_image_format) { _image_format = p_image_format; } @@ -3190,7 +3217,7 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector< bool must_import = true; Vector<uint8_t> img_data = p_image->get_data(); Dictionary generator_parameters; - String file_path = p_state->get_base_path() + "/" + p_state->filename.get_basename() + "_" + p_image->get_name(); + String file_path = p_state->get_base_path().path_join(p_state->filename.get_basename() + "_" + p_image->get_name()); file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; if (FileAccess::exists(file_path + ".import")) { Ref<ConfigFile> config; @@ -3202,6 +3229,8 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector< if (!generator_parameters.has("md5")) { must_import = false; // Didn't come from a gltf document; don't overwrite. } + } + if (must_import) { String existing_md5 = generator_parameters["md5"]; unsigned char md5_hash[16]; CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); @@ -3630,141 +3659,143 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> p_state) { mr["metallicFactor"] = base_material->get_metallic(); mr["roughnessFactor"] = base_material->get_roughness(); - bool has_roughness = base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS).is_valid() && base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS)->get_image().is_valid(); - bool has_ao = base_material->get_feature(BaseMaterial3D::FEATURE_AMBIENT_OCCLUSION) && base_material->get_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION).is_valid(); - bool has_metalness = base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC).is_valid() && base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC)->get_image().is_valid(); - if (has_ao || has_roughness || has_metalness) { - Dictionary mrt; - Ref<Texture2D> roughness_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS); - BaseMaterial3D::TextureChannel roughness_channel = base_material->get_roughness_texture_channel(); - Ref<Texture2D> metallic_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC); - BaseMaterial3D::TextureChannel metalness_channel = base_material->get_metallic_texture_channel(); - Ref<Texture2D> ao_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION); - BaseMaterial3D::TextureChannel ao_channel = base_material->get_ao_texture_channel(); - Ref<ImageTexture> orm_texture; - orm_texture.instantiate(); - Ref<Image> orm_image; - orm_image.instantiate(); - int32_t height = 0; - int32_t width = 0; - Ref<Image> ao_image; - if (has_ao) { - height = ao_texture->get_height(); - width = ao_texture->get_width(); - ao_image = ao_texture->get_image(); - Ref<ImageTexture> img_tex = ao_image; - if (img_tex.is_valid()) { - ao_image = img_tex->get_image(); - } - if (ao_image->is_compressed()) { - ao_image->decompress(); - } - } - Ref<Image> roughness_image; - if (has_roughness) { - height = roughness_texture->get_height(); - width = roughness_texture->get_width(); - roughness_image = roughness_texture->get_image(); - Ref<ImageTexture> img_tex = roughness_image; - if (img_tex.is_valid()) { - roughness_image = img_tex->get_image(); - } - if (roughness_image->is_compressed()) { - roughness_image->decompress(); - } - } - Ref<Image> metallness_image; - if (has_metalness) { - height = metallic_texture->get_height(); - width = metallic_texture->get_width(); - metallness_image = metallic_texture->get_image(); - Ref<ImageTexture> img_tex = metallness_image; - if (img_tex.is_valid()) { - metallness_image = img_tex->get_image(); - } - if (metallness_image->is_compressed()) { - metallness_image->decompress(); + if (_image_format != "None") { + bool has_roughness = base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS).is_valid() && base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS)->get_image().is_valid(); + bool has_ao = base_material->get_feature(BaseMaterial3D::FEATURE_AMBIENT_OCCLUSION) && base_material->get_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION).is_valid(); + bool has_metalness = base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC).is_valid() && base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC)->get_image().is_valid(); + if (has_ao || has_roughness || has_metalness) { + Dictionary mrt; + Ref<Texture2D> roughness_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ROUGHNESS); + BaseMaterial3D::TextureChannel roughness_channel = base_material->get_roughness_texture_channel(); + Ref<Texture2D> metallic_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_METALLIC); + BaseMaterial3D::TextureChannel metalness_channel = base_material->get_metallic_texture_channel(); + Ref<Texture2D> ao_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_AMBIENT_OCCLUSION); + BaseMaterial3D::TextureChannel ao_channel = base_material->get_ao_texture_channel(); + Ref<ImageTexture> orm_texture; + orm_texture.instantiate(); + Ref<Image> orm_image; + orm_image.instantiate(); + int32_t height = 0; + int32_t width = 0; + Ref<Image> ao_image; + if (has_ao) { + height = ao_texture->get_height(); + width = ao_texture->get_width(); + ao_image = ao_texture->get_image(); + Ref<ImageTexture> img_tex = ao_image; + if (img_tex.is_valid()) { + ao_image = img_tex->get_image(); + } + if (ao_image->is_compressed()) { + ao_image->decompress(); + } } - } - Ref<Texture2D> albedo_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ALBEDO); - if (albedo_texture.is_valid() && albedo_texture->get_image().is_valid()) { - height = albedo_texture->get_height(); - width = albedo_texture->get_width(); - } - orm_image->initialize_data(width, height, false, Image::FORMAT_RGBA8); - if (ao_image.is_valid() && ao_image->get_size() != Vector2(width, height)) { - ao_image->resize(width, height, Image::INTERPOLATE_LANCZOS); - } - if (roughness_image.is_valid() && roughness_image->get_size() != Vector2(width, height)) { - roughness_image->resize(width, height, Image::INTERPOLATE_LANCZOS); - } - if (metallness_image.is_valid() && metallness_image->get_size() != Vector2(width, height)) { - metallness_image->resize(width, height, Image::INTERPOLATE_LANCZOS); - } - for (int32_t h = 0; h < height; h++) { - for (int32_t w = 0; w < width; w++) { - Color c = Color(1.0f, 1.0f, 1.0f); - if (has_ao) { - if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == ao_channel) { - c.r = ao_image->get_pixel(w, h).r; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == ao_channel) { - c.r = ao_image->get_pixel(w, h).g; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == ao_channel) { - c.r = ao_image->get_pixel(w, h).b; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == ao_channel) { - c.r = ao_image->get_pixel(w, h).a; - } + Ref<Image> roughness_image; + if (has_roughness) { + height = roughness_texture->get_height(); + width = roughness_texture->get_width(); + roughness_image = roughness_texture->get_image(); + Ref<ImageTexture> img_tex = roughness_image; + if (img_tex.is_valid()) { + roughness_image = img_tex->get_image(); } - if (has_roughness) { - if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == roughness_channel) { - c.g = roughness_image->get_pixel(w, h).r; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == roughness_channel) { - c.g = roughness_image->get_pixel(w, h).g; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == roughness_channel) { - c.g = roughness_image->get_pixel(w, h).b; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == roughness_channel) { - c.g = roughness_image->get_pixel(w, h).a; - } + if (roughness_image->is_compressed()) { + roughness_image->decompress(); } - if (has_metalness) { - if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == metalness_channel) { - c.b = metallness_image->get_pixel(w, h).r; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == metalness_channel) { - c.b = metallness_image->get_pixel(w, h).g; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == metalness_channel) { - c.b = metallness_image->get_pixel(w, h).b; - } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == metalness_channel) { - c.b = metallness_image->get_pixel(w, h).a; + } + Ref<Image> metallness_image; + if (has_metalness) { + height = metallic_texture->get_height(); + width = metallic_texture->get_width(); + metallness_image = metallic_texture->get_image(); + Ref<ImageTexture> img_tex = metallness_image; + if (img_tex.is_valid()) { + metallness_image = img_tex->get_image(); + } + if (metallness_image->is_compressed()) { + metallness_image->decompress(); + } + } + Ref<Texture2D> albedo_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ALBEDO); + if (albedo_texture.is_valid() && albedo_texture->get_image().is_valid()) { + height = albedo_texture->get_height(); + width = albedo_texture->get_width(); + } + orm_image->initialize_data(width, height, false, Image::FORMAT_RGBA8); + if (ao_image.is_valid() && ao_image->get_size() != Vector2(width, height)) { + ao_image->resize(width, height, Image::INTERPOLATE_LANCZOS); + } + if (roughness_image.is_valid() && roughness_image->get_size() != Vector2(width, height)) { + roughness_image->resize(width, height, Image::INTERPOLATE_LANCZOS); + } + if (metallness_image.is_valid() && metallness_image->get_size() != Vector2(width, height)) { + metallness_image->resize(width, height, Image::INTERPOLATE_LANCZOS); + } + for (int32_t h = 0; h < height; h++) { + for (int32_t w = 0; w < width; w++) { + Color c = Color(1.0f, 1.0f, 1.0f); + if (has_ao) { + if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == ao_channel) { + c.r = ao_image->get_pixel(w, h).r; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == ao_channel) { + c.r = ao_image->get_pixel(w, h).g; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == ao_channel) { + c.r = ao_image->get_pixel(w, h).b; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == ao_channel) { + c.r = ao_image->get_pixel(w, h).a; + } + } + if (has_roughness) { + if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == roughness_channel) { + c.g = roughness_image->get_pixel(w, h).r; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == roughness_channel) { + c.g = roughness_image->get_pixel(w, h).g; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == roughness_channel) { + c.g = roughness_image->get_pixel(w, h).b; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == roughness_channel) { + c.g = roughness_image->get_pixel(w, h).a; + } + } + if (has_metalness) { + if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_RED == metalness_channel) { + c.b = metallness_image->get_pixel(w, h).r; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_GREEN == metalness_channel) { + c.b = metallness_image->get_pixel(w, h).g; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_BLUE == metalness_channel) { + c.b = metallness_image->get_pixel(w, h).b; + } else if (BaseMaterial3D::TextureChannel::TEXTURE_CHANNEL_ALPHA == metalness_channel) { + c.b = metallness_image->get_pixel(w, h).a; + } } + orm_image->set_pixel(w, h, c); } - orm_image->set_pixel(w, h, c); } - } - orm_image->generate_mipmaps(); - orm_texture->set_image(orm_image); - GLTFTextureIndex orm_texture_index = -1; - if (has_ao || has_roughness || has_metalness) { - orm_texture->set_name(material->get_name() + "_orm"); - orm_texture_index = _set_texture(p_state, orm_texture, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); - } - if (has_ao) { - Dictionary occt; - occt["index"] = orm_texture_index; - d["occlusionTexture"] = occt; - } - if (has_roughness || has_metalness) { - mrt["index"] = orm_texture_index; - Dictionary extensions = _serialize_texture_transform_uv1(material); - if (!extensions.is_empty()) { - mrt["extensions"] = extensions; - p_state->use_khr_texture_transform = true; + orm_image->generate_mipmaps(); + orm_texture->set_image(orm_image); + GLTFTextureIndex orm_texture_index = -1; + if (has_ao || has_roughness || has_metalness) { + orm_texture->set_name(material->get_name() + "_orm"); + orm_texture_index = _set_texture(p_state, orm_texture, base_material->get_texture_filter(), base_material->get_flag(BaseMaterial3D::FLAG_USE_TEXTURE_REPEAT)); + } + if (has_ao) { + Dictionary occt; + occt["index"] = orm_texture_index; + d["occlusionTexture"] = occt; + } + if (has_roughness || has_metalness) { + mrt["index"] = orm_texture_index; + Dictionary extensions = _serialize_texture_transform_uv1(material); + if (!extensions.is_empty()) { + mrt["extensions"] = extensions; + p_state->use_khr_texture_transform = true; + } + mr["metallicRoughnessTexture"] = mrt; } - mr["metallicRoughnessTexture"] = mrt; } } d["pbrMetallicRoughness"] = mr; - if (base_material->get_feature(BaseMaterial3D::FEATURE_NORMAL_MAPPING)) { + if (base_material->get_feature(BaseMaterial3D::FEATURE_NORMAL_MAPPING) && _image_format != "None") { Dictionary nt; Ref<ImageTexture> tex; tex.instantiate(); @@ -3780,7 +3811,6 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> p_state) { } img->decompress(); img->convert(Image::FORMAT_RGBA8); - img->convert_ra_rgba8_to_rg(); for (int32_t y = 0; y < img->get_height(); y++) { for (int32_t x = 0; x < img->get_width(); x++) { Color c = img->get_pixel(x, y); @@ -3817,7 +3847,7 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> p_state) { d["emissiveFactor"] = arr; } - if (base_material->get_feature(BaseMaterial3D::FEATURE_EMISSION)) { + if (base_material->get_feature(BaseMaterial3D::FEATURE_EMISSION) && _image_format != "None") { Dictionary et; Ref<Texture2D> emission_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_EMISSION); GLTFTextureIndex gltf_texture_index = -1; @@ -4173,237 +4203,6 @@ void GLTFDocument::spec_gloss_to_metal_base_color(const Color &p_specular_factor r_base_color.a = p_diffuse.a; r_base_color = r_base_color.clamp(); } - -GLTFNodeIndex GLTFDocument::_find_highest_node(Ref<GLTFState> p_state, const Vector<GLTFNodeIndex> &p_subset) { - int highest = -1; - GLTFNodeIndex best_node = -1; - - for (int i = 0; i < p_subset.size(); ++i) { - const GLTFNodeIndex node_i = p_subset[i]; - const Ref<GLTFNode> node = p_state->nodes[node_i]; - - if (highest == -1 || node->height < highest) { - highest = node->height; - best_node = node_i; - } - } - - return best_node; -} - -bool GLTFDocument::_capture_nodes_in_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin, const GLTFNodeIndex p_node_index) { - bool found_joint = false; - - for (int i = 0; i < p_state->nodes[p_node_index]->children.size(); ++i) { - found_joint |= _capture_nodes_in_skin(p_state, p_skin, p_state->nodes[p_node_index]->children[i]); - } - - if (found_joint) { - // Mark it if we happen to find another skins joint... - if (p_state->nodes[p_node_index]->joint && p_skin->joints.find(p_node_index) < 0) { - p_skin->joints.push_back(p_node_index); - } else if (p_skin->non_joints.find(p_node_index) < 0) { - p_skin->non_joints.push_back(p_node_index); - } - } - - if (p_skin->joints.find(p_node_index) > 0) { - return true; - } - - return false; -} - -void GLTFDocument::_capture_nodes_for_multirooted_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin) { - DisjointSet<GLTFNodeIndex> disjoint_set; - - for (int i = 0; i < p_skin->joints.size(); ++i) { - const GLTFNodeIndex node_index = p_skin->joints[i]; - const GLTFNodeIndex parent = p_state->nodes[node_index]->parent; - disjoint_set.insert(node_index); - - if (p_skin->joints.find(parent) >= 0) { - disjoint_set.create_union(parent, node_index); - } - } - - Vector<GLTFNodeIndex> roots; - disjoint_set.get_representatives(roots); - - if (roots.size() <= 1) { - return; - } - - int maxHeight = -1; - - // Determine the max height rooted tree - for (int i = 0; i < roots.size(); ++i) { - const GLTFNodeIndex root = roots[i]; - - if (maxHeight == -1 || p_state->nodes[root]->height < maxHeight) { - maxHeight = p_state->nodes[root]->height; - } - } - - // Go up the tree till all of the multiple roots of the skin are at the same hierarchy level. - // This sucks, but 99% of all game engines (not just Godot) would have this same issue. - for (int i = 0; i < roots.size(); ++i) { - GLTFNodeIndex current_node = roots[i]; - while (p_state->nodes[current_node]->height > maxHeight) { - GLTFNodeIndex parent = p_state->nodes[current_node]->parent; - - if (p_state->nodes[parent]->joint && p_skin->joints.find(parent) < 0) { - p_skin->joints.push_back(parent); - } else if (p_skin->non_joints.find(parent) < 0) { - p_skin->non_joints.push_back(parent); - } - - current_node = parent; - } - - // replace the roots - roots.write[i] = current_node; - } - - // Climb up the tree until they all have the same parent - bool all_same; - - do { - all_same = true; - const GLTFNodeIndex first_parent = p_state->nodes[roots[0]]->parent; - - for (int i = 1; i < roots.size(); ++i) { - all_same &= (first_parent == p_state->nodes[roots[i]]->parent); - } - - if (!all_same) { - for (int i = 0; i < roots.size(); ++i) { - const GLTFNodeIndex current_node = roots[i]; - const GLTFNodeIndex parent = p_state->nodes[current_node]->parent; - - if (p_state->nodes[parent]->joint && p_skin->joints.find(parent) < 0) { - p_skin->joints.push_back(parent); - } else if (p_skin->non_joints.find(parent) < 0) { - p_skin->non_joints.push_back(parent); - } - - roots.write[i] = parent; - } - } - - } while (!all_same); -} - -Error GLTFDocument::_expand_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin) { - _capture_nodes_for_multirooted_skin(p_state, p_skin); - - // Grab all nodes that lay in between skin joints/nodes - DisjointSet<GLTFNodeIndex> disjoint_set; - - Vector<GLTFNodeIndex> all_skin_nodes; - all_skin_nodes.append_array(p_skin->joints); - all_skin_nodes.append_array(p_skin->non_joints); - - for (int i = 0; i < all_skin_nodes.size(); ++i) { - const GLTFNodeIndex node_index = all_skin_nodes[i]; - const GLTFNodeIndex parent = p_state->nodes[node_index]->parent; - disjoint_set.insert(node_index); - - if (all_skin_nodes.find(parent) >= 0) { - disjoint_set.create_union(parent, node_index); - } - } - - Vector<GLTFNodeIndex> out_owners; - disjoint_set.get_representatives(out_owners); - - Vector<GLTFNodeIndex> out_roots; - - for (int i = 0; i < out_owners.size(); ++i) { - Vector<GLTFNodeIndex> set; - disjoint_set.get_members(set, out_owners[i]); - - const GLTFNodeIndex root = _find_highest_node(p_state, set); - ERR_FAIL_COND_V(root < 0, FAILED); - out_roots.push_back(root); - } - - out_roots.sort(); - - for (int i = 0; i < out_roots.size(); ++i) { - _capture_nodes_in_skin(p_state, p_skin, out_roots[i]); - } - - p_skin->roots = out_roots; - - return OK; -} - -Error GLTFDocument::_verify_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin) { - // This may seem duplicated from expand_skins, but this is really a safety check! (so it kinda is) - // In case additional interpolating logic is added to the skins, this will help ensure that you - // do not cause it to self implode into a fiery blaze - - // We are going to re-calculate the root nodes and compare them to the ones saved in the skin, - // then ensure the multiple trees (if they exist) are on the same sublevel - - // Grab all nodes that lay in between skin joints/nodes - DisjointSet<GLTFNodeIndex> disjoint_set; - - Vector<GLTFNodeIndex> all_skin_nodes; - all_skin_nodes.append_array(p_skin->joints); - all_skin_nodes.append_array(p_skin->non_joints); - - for (int i = 0; i < all_skin_nodes.size(); ++i) { - const GLTFNodeIndex node_index = all_skin_nodes[i]; - const GLTFNodeIndex parent = p_state->nodes[node_index]->parent; - disjoint_set.insert(node_index); - - if (all_skin_nodes.find(parent) >= 0) { - disjoint_set.create_union(parent, node_index); - } - } - - Vector<GLTFNodeIndex> out_owners; - disjoint_set.get_representatives(out_owners); - - Vector<GLTFNodeIndex> out_roots; - - for (int i = 0; i < out_owners.size(); ++i) { - Vector<GLTFNodeIndex> set; - disjoint_set.get_members(set, out_owners[i]); - - const GLTFNodeIndex root = _find_highest_node(p_state, set); - ERR_FAIL_COND_V(root < 0, FAILED); - out_roots.push_back(root); - } - - out_roots.sort(); - - ERR_FAIL_COND_V(out_roots.size() == 0, FAILED); - - // Make sure the roots are the exact same (they better be) - ERR_FAIL_COND_V(out_roots.size() != p_skin->roots.size(), FAILED); - for (int i = 0; i < out_roots.size(); ++i) { - ERR_FAIL_COND_V(out_roots[i] != p_skin->roots[i], FAILED); - } - - // Single rooted skin? Perfectly ok! - if (out_roots.size() == 1) { - return OK; - } - - // Make sure all parents of a multi-rooted skin are the SAME - const GLTFNodeIndex parent = p_state->nodes[out_roots[0]]->parent; - for (int i = 1; i < out_roots.size(); ++i) { - if (p_state->nodes[out_roots[i]]->parent != parent) { - return FAILED; - } - } - - return OK; -} - Error GLTFDocument::_parse_skins(Ref<GLTFState> p_state) { if (!p_state->json.has("skins")) { return OK; @@ -4455,357 +4254,14 @@ Error GLTFDocument::_parse_skins(Ref<GLTFState> p_state) { // Expand the skin to capture all the extra non-joints that lie in between the actual joints, // and expand the hierarchy to ensure multi-rooted trees lie on the same height level - ERR_FAIL_COND_V(_expand_skin(p_state, skin), ERR_PARSE_ERROR); - ERR_FAIL_COND_V(_verify_skin(p_state, skin), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(SkinTool::_expand_skin(p_state->nodes, skin), ERR_PARSE_ERROR); + ERR_FAIL_COND_V(SkinTool::_verify_skin(p_state->nodes, skin), ERR_PARSE_ERROR); } print_verbose("glTF: Total skins: " + itos(p_state->skins.size())); return OK; } - -void GLTFDocument::_recurse_children(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, - RBSet<GLTFNodeIndex> &p_all_skin_nodes, HashSet<GLTFNodeIndex> &p_child_visited_set) { - if (p_child_visited_set.has(p_node_index)) { - return; - } - p_child_visited_set.insert(p_node_index); - for (int i = 0; i < p_state->nodes[p_node_index]->children.size(); ++i) { - _recurse_children(p_state, p_state->nodes[p_node_index]->children[i], p_all_skin_nodes, p_child_visited_set); - } - - if (p_state->nodes[p_node_index]->skin < 0 || p_state->nodes[p_node_index]->mesh < 0 || !p_state->nodes[p_node_index]->children.is_empty()) { - p_all_skin_nodes.insert(p_node_index); - } -} - -Error GLTFDocument::_determine_skeletons(Ref<GLTFState> p_state) { - // Using a disjoint set, we are going to potentially combine all skins that are actually branches - // of a main skeleton, or treat skins defining the same set of nodes as ONE skeleton. - // This is another unclear issue caused by the current glTF specification. - - DisjointSet<GLTFNodeIndex> skeleton_sets; - - for (GLTFSkinIndex skin_i = 0; skin_i < p_state->skins.size(); ++skin_i) { - const Ref<GLTFSkin> skin = p_state->skins[skin_i]; - - HashSet<GLTFNodeIndex> child_visited_set; - RBSet<GLTFNodeIndex> all_skin_nodes; - for (int i = 0; i < skin->joints.size(); ++i) { - all_skin_nodes.insert(skin->joints[i]); - _recurse_children(p_state, skin->joints[i], all_skin_nodes, child_visited_set); - } - for (int i = 0; i < skin->non_joints.size(); ++i) { - all_skin_nodes.insert(skin->non_joints[i]); - _recurse_children(p_state, skin->non_joints[i], all_skin_nodes, child_visited_set); - } - for (GLTFNodeIndex node_index : all_skin_nodes) { - const GLTFNodeIndex parent = p_state->nodes[node_index]->parent; - skeleton_sets.insert(node_index); - - if (all_skin_nodes.has(parent)) { - skeleton_sets.create_union(parent, node_index); - } - } - - // We are going to connect the separate skin subtrees in each skin together - // so that the final roots are entire sets of valid skin trees - for (int i = 1; i < skin->roots.size(); ++i) { - skeleton_sets.create_union(skin->roots[0], skin->roots[i]); - } - } - - { // attempt to joint all touching subsets (siblings/parent are part of another skin) - Vector<GLTFNodeIndex> groups_representatives; - skeleton_sets.get_representatives(groups_representatives); - - Vector<GLTFNodeIndex> highest_group_members; - Vector<Vector<GLTFNodeIndex>> groups; - for (int i = 0; i < groups_representatives.size(); ++i) { - Vector<GLTFNodeIndex> group; - skeleton_sets.get_members(group, groups_representatives[i]); - highest_group_members.push_back(_find_highest_node(p_state, group)); - groups.push_back(group); - } - - for (int i = 0; i < highest_group_members.size(); ++i) { - const GLTFNodeIndex node_i = highest_group_members[i]; - - // Attach any siblings together (this needs to be done n^2/2 times) - for (int j = i + 1; j < highest_group_members.size(); ++j) { - const GLTFNodeIndex node_j = highest_group_members[j]; - - // Even if they are siblings under the root! :) - if (p_state->nodes[node_i]->parent == p_state->nodes[node_j]->parent) { - skeleton_sets.create_union(node_i, node_j); - } - } - - // Attach any parenting going on together (we need to do this n^2 times) - const GLTFNodeIndex node_i_parent = p_state->nodes[node_i]->parent; - if (node_i_parent >= 0) { - for (int j = 0; j < groups.size() && i != j; ++j) { - const Vector<GLTFNodeIndex> &group = groups[j]; - - if (group.find(node_i_parent) >= 0) { - const GLTFNodeIndex node_j = highest_group_members[j]; - skeleton_sets.create_union(node_i, node_j); - } - } - } - } - } - - // At this point, the skeleton groups should be finalized - Vector<GLTFNodeIndex> skeleton_owners; - skeleton_sets.get_representatives(skeleton_owners); - - // Mark all the skins actual skeletons, after we have merged them - for (GLTFSkeletonIndex skel_i = 0; skel_i < skeleton_owners.size(); ++skel_i) { - const GLTFNodeIndex skeleton_owner = skeleton_owners[skel_i]; - Ref<GLTFSkeleton> skeleton; - skeleton.instantiate(); - - Vector<GLTFNodeIndex> skeleton_nodes; - skeleton_sets.get_members(skeleton_nodes, skeleton_owner); - - for (GLTFSkinIndex skin_i = 0; skin_i < p_state->skins.size(); ++skin_i) { - Ref<GLTFSkin> skin = p_state->skins.write[skin_i]; - - // If any of the the skeletons nodes exist in a skin, that skin now maps to the skeleton - for (int i = 0; i < skeleton_nodes.size(); ++i) { - GLTFNodeIndex skel_node_i = skeleton_nodes[i]; - if (skin->joints.find(skel_node_i) >= 0 || skin->non_joints.find(skel_node_i) >= 0) { - skin->skeleton = skel_i; - continue; - } - } - } - - Vector<GLTFNodeIndex> non_joints; - for (int i = 0; i < skeleton_nodes.size(); ++i) { - const GLTFNodeIndex node_i = skeleton_nodes[i]; - - if (p_state->nodes[node_i]->joint) { - skeleton->joints.push_back(node_i); - } else { - non_joints.push_back(node_i); - } - } - - p_state->skeletons.push_back(skeleton); - - _reparent_non_joint_skeleton_subtrees(p_state, p_state->skeletons.write[skel_i], non_joints); - } - - for (GLTFSkeletonIndex skel_i = 0; skel_i < p_state->skeletons.size(); ++skel_i) { - Ref<GLTFSkeleton> skeleton = p_state->skeletons.write[skel_i]; - - for (int i = 0; i < skeleton->joints.size(); ++i) { - const GLTFNodeIndex node_i = skeleton->joints[i]; - Ref<GLTFNode> node = p_state->nodes[node_i]; - - ERR_FAIL_COND_V(!node->joint, ERR_PARSE_ERROR); - ERR_FAIL_COND_V(node->skeleton >= 0, ERR_PARSE_ERROR); - node->skeleton = skel_i; - } - - ERR_FAIL_COND_V(_determine_skeleton_roots(p_state, skel_i), ERR_PARSE_ERROR); - } - - return OK; -} - -Error GLTFDocument::_reparent_non_joint_skeleton_subtrees(Ref<GLTFState> p_state, Ref<GLTFSkeleton> p_skeleton, const Vector<GLTFNodeIndex> &p_non_joints) { - DisjointSet<GLTFNodeIndex> subtree_set; - - // Populate the disjoint set with ONLY non joints that are in the skeleton hierarchy (non_joints vector) - // This way we can find any joints that lie in between joints, as the current glTF specification - // mentions nothing about non-joints being in between joints of the same skin. Hopefully one day we - // can remove this code. - - // skinD depicted here explains this issue: - // https://github.com/KhronosGroup/glTF-Asset-Generator/blob/master/Output/Positive/Animation_Skin - - for (int i = 0; i < p_non_joints.size(); ++i) { - const GLTFNodeIndex node_i = p_non_joints[i]; - - subtree_set.insert(node_i); - - const GLTFNodeIndex parent_i = p_state->nodes[node_i]->parent; - if (parent_i >= 0 && p_non_joints.find(parent_i) >= 0 && !p_state->nodes[parent_i]->joint) { - subtree_set.create_union(parent_i, node_i); - } - } - - // Find all the non joint subtrees and re-parent them to a new "fake" joint - - Vector<GLTFNodeIndex> non_joint_subtree_roots; - subtree_set.get_representatives(non_joint_subtree_roots); - - for (int root_i = 0; root_i < non_joint_subtree_roots.size(); ++root_i) { - const GLTFNodeIndex subtree_root = non_joint_subtree_roots[root_i]; - - Vector<GLTFNodeIndex> subtree_nodes; - subtree_set.get_members(subtree_nodes, subtree_root); - - for (int subtree_i = 0; subtree_i < subtree_nodes.size(); ++subtree_i) { - Ref<GLTFNode> node = p_state->nodes[subtree_nodes[subtree_i]]; - node->joint = true; - // Add the joint to the skeletons joints - p_skeleton->joints.push_back(subtree_nodes[subtree_i]); - } - } - - return OK; -} - -Error GLTFDocument::_determine_skeleton_roots(Ref<GLTFState> p_state, const GLTFSkeletonIndex p_skel_i) { - DisjointSet<GLTFNodeIndex> disjoint_set; - - for (GLTFNodeIndex i = 0; i < p_state->nodes.size(); ++i) { - const Ref<GLTFNode> node = p_state->nodes[i]; - - if (node->skeleton != p_skel_i) { - continue; - } - - disjoint_set.insert(i); - - if (node->parent >= 0 && p_state->nodes[node->parent]->skeleton == p_skel_i) { - disjoint_set.create_union(node->parent, i); - } - } - - Ref<GLTFSkeleton> skeleton = p_state->skeletons.write[p_skel_i]; - - Vector<GLTFNodeIndex> representatives; - disjoint_set.get_representatives(representatives); - - Vector<GLTFNodeIndex> roots; - - for (int i = 0; i < representatives.size(); ++i) { - Vector<GLTFNodeIndex> set; - disjoint_set.get_members(set, representatives[i]); - const GLTFNodeIndex root = _find_highest_node(p_state, set); - ERR_FAIL_COND_V(root < 0, FAILED); - roots.push_back(root); - } - - roots.sort(); - - skeleton->roots = roots; - - if (roots.size() == 0) { - return FAILED; - } else if (roots.size() == 1) { - return OK; - } - - // Check that the subtrees have the same parent root - const GLTFNodeIndex parent = p_state->nodes[roots[0]]->parent; - for (int i = 1; i < roots.size(); ++i) { - if (p_state->nodes[roots[i]]->parent != parent) { - return FAILED; - } - } - - return OK; -} - -Error GLTFDocument::_create_skeletons(Ref<GLTFState> p_state) { - for (GLTFSkeletonIndex skel_i = 0; skel_i < p_state->skeletons.size(); ++skel_i) { - Ref<GLTFSkeleton> gltf_skeleton = p_state->skeletons.write[skel_i]; - - Skeleton3D *skeleton = memnew(Skeleton3D); - gltf_skeleton->godot_skeleton = skeleton; - p_state->skeleton3d_to_gltf_skeleton[skeleton->get_instance_id()] = skel_i; - - // Make a unique name, no gltf node represents this skeleton - skeleton->set_name("Skeleton3D"); - - List<GLTFNodeIndex> bones; - - for (int i = 0; i < gltf_skeleton->roots.size(); ++i) { - bones.push_back(gltf_skeleton->roots[i]); - } - - // Make the skeleton creation deterministic by going through the roots in - // a sorted order, and DEPTH FIRST - bones.sort(); - - while (!bones.is_empty()) { - const GLTFNodeIndex node_i = bones.front()->get(); - bones.pop_front(); - - Ref<GLTFNode> node = p_state->nodes[node_i]; - ERR_FAIL_COND_V(node->skeleton != skel_i, FAILED); - - { // Add all child nodes to the stack (deterministically) - Vector<GLTFNodeIndex> child_nodes; - for (int i = 0; i < node->children.size(); ++i) { - const GLTFNodeIndex child_i = node->children[i]; - if (p_state->nodes[child_i]->skeleton == skel_i) { - child_nodes.push_back(child_i); - } - } - - // Depth first insertion - child_nodes.sort(); - for (int i = child_nodes.size() - 1; i >= 0; --i) { - bones.push_front(child_nodes[i]); - } - } - - const int bone_index = skeleton->get_bone_count(); - - if (node->get_name().is_empty()) { - node->set_name("bone"); - } - - node->set_name(_gen_unique_bone_name(p_state, skel_i, node->get_name())); - - skeleton->add_bone(node->get_name()); - skeleton->set_bone_rest(bone_index, node->xform); - skeleton->set_bone_pose_position(bone_index, node->position); - skeleton->set_bone_pose_rotation(bone_index, node->rotation.normalized()); - skeleton->set_bone_pose_scale(bone_index, node->scale); - - if (node->parent >= 0 && p_state->nodes[node->parent]->skeleton == skel_i) { - const int bone_parent = skeleton->find_bone(p_state->nodes[node->parent]->get_name()); - ERR_FAIL_COND_V(bone_parent < 0, FAILED); - skeleton->set_bone_parent(bone_index, skeleton->find_bone(p_state->nodes[node->parent]->get_name())); - } - - p_state->scene_nodes.insert(node_i, skeleton); - } - } - - ERR_FAIL_COND_V(_map_skin_joints_indices_to_skeleton_bone_indices(p_state), ERR_PARSE_ERROR); - - return OK; -} - -Error GLTFDocument::_map_skin_joints_indices_to_skeleton_bone_indices(Ref<GLTFState> p_state) { - for (GLTFSkinIndex skin_i = 0; skin_i < p_state->skins.size(); ++skin_i) { - Ref<GLTFSkin> skin = p_state->skins.write[skin_i]; - - Ref<GLTFSkeleton> skeleton = p_state->skeletons[skin->skeleton]; - - for (int joint_index = 0; joint_index < skin->joints_original.size(); ++joint_index) { - const GLTFNodeIndex node_i = skin->joints_original[joint_index]; - const Ref<GLTFNode> node = p_state->nodes[node_i]; - - const int bone_index = skeleton->godot_skeleton->find_bone(node->get_name()); - ERR_FAIL_COND_V(bone_index < 0, FAILED); - - skin->joint_i_to_bone_i.insert(joint_index, bone_index); - } - } - - return OK; -} - Error GLTFDocument::_serialize_skins(Ref<GLTFState> p_state) { _remove_duplicate_skins(p_state); Array json_skins; @@ -5017,7 +4473,7 @@ Error GLTFDocument::_serialize_animations(Ref<GLTFState> p_state) { AnimationPlayer *animation_player = p_state->animation_players[player_i]; List<StringName> animations; animation_player->get_animation_list(&animations); - for (StringName animation_name : animations) { + for (const StringName &animation_name : animations) { _convert_animation(p_state, animation_player, animation_name); } } @@ -5220,6 +4676,7 @@ Error GLTFDocument::_parse_animations(Ref<GLTFState> p_state) { if (anim_name_lower.begins_with("loop") || anim_name_lower.ends_with("loop") || anim_name_lower.begins_with("cycle") || anim_name_lower.ends_with("cycle")) { animation->set_loop(true); } + animation->set_original_name(anim_name); animation->set_name(_gen_unique_animation_name(p_state, anim_name)); } @@ -5341,12 +4798,22 @@ void GLTFDocument::_assign_node_names(Ref<GLTFState> p_state) { } String gltf_node_name = gltf_node->get_name(); if (gltf_node_name.is_empty()) { - if (gltf_node->mesh >= 0) { - gltf_node_name = "Mesh"; - } else if (gltf_node->camera >= 0) { - gltf_node_name = "Camera"; + if (_naming_version == 0) { + if (gltf_node->mesh >= 0) { + gltf_node_name = _gen_unique_name(p_state, "Mesh"); + } else if (gltf_node->camera >= 0) { + gltf_node_name = _gen_unique_name(p_state, "Camera3D"); + } else { + gltf_node_name = _gen_unique_name(p_state, "Node"); + } } else { - gltf_node_name = "Node"; + if (gltf_node->mesh >= 0) { + gltf_node_name = "Mesh"; + } else if (gltf_node->camera >= 0) { + gltf_node_name = "Camera"; + } else { + gltf_node_name = "Node"; + } } } gltf_node->set_name(_gen_unique_name(p_state, gltf_node_name)); @@ -5368,14 +4835,13 @@ BoneAttachment3D *GLTFDocument::_generate_bone_attachment(Ref<GLTFState> p_state GLTFMeshIndex GLTFDocument::_convert_mesh_to_gltf(Ref<GLTFState> p_state, MeshInstance3D *p_mesh_instance) { ERR_FAIL_NULL_V(p_mesh_instance, -1); - if (p_mesh_instance->get_mesh().is_null()) { - return -1; - } + ERR_FAIL_COND_V_MSG(p_mesh_instance->get_mesh().is_null(), -1, "glTF: Tried to export a MeshInstance3D node named " + p_mesh_instance->get_name() + ", but it has no mesh. This node will be exported without a mesh."); + Ref<Mesh> mesh_resource = p_mesh_instance->get_mesh(); + ERR_FAIL_COND_V_MSG(mesh_resource->get_surface_count() == 0, -1, "glTF: Tried to export a MeshInstance3D node named " + p_mesh_instance->get_name() + ", but its mesh has no surfaces. This node will be exported without a mesh."); - Ref<Mesh> import_mesh = p_mesh_instance->get_mesh(); - Ref<ImporterMesh> current_mesh = _mesh_to_importer_mesh(import_mesh); + Ref<ImporterMesh> current_mesh = _mesh_to_importer_mesh(mesh_resource); Vector<float> blend_weights; - int32_t blend_count = import_mesh->get_blend_shape_count(); + int32_t blend_count = mesh_resource->get_blend_shape_count(); blend_weights.resize(blend_count); for (int32_t blend_i = 0; blend_i < blend_count; blend_i++) { blend_weights.write[blend_i] = 0.0f; @@ -5465,10 +4931,7 @@ GLTFLightIndex GLTFDocument::_convert_light(Ref<GLTFState> p_state, Light3D *p_l } void GLTFDocument::_convert_spatial(Ref<GLTFState> p_state, Node3D *p_spatial, Ref<GLTFNode> p_node) { - Transform3D xform = p_spatial->get_transform(); - p_node->scale = xform.basis.get_scale(); - p_node->rotation = xform.basis.get_rotation_quaternion(); - p_node->position = xform.origin; + p_node->transform = p_spatial->get_transform(); } Node3D *GLTFDocument::_generate_spatial(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index) { @@ -5486,8 +4949,15 @@ void GLTFDocument::_convert_scene_node(Ref<GLTFState> p_state, Node *p_current, if (retflag) { return; } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint() && p_gltf_root != -1 && p_current->get_owner() == nullptr) { + WARN_VERBOSE("glTF export warning: Node '" + p_current->get_name() + "' has no owner. This is likely a temporary node generated by a @tool script. This would not be saved when saving the Godot scene, therefore it will not be exported to glTF."); + return; + } +#endif // TOOLS_ENABLED Ref<GLTFNode> gltf_node; gltf_node.instantiate(); + gltf_node->set_original_name(p_current->get_name()); gltf_node->set_name(_gen_unique_name(p_state, p_current->get_name())); if (cast_to<Node3D>(p_current)) { Node3D *spatial = cast_to<Node3D>(p_current); @@ -5579,10 +5049,12 @@ void GLTFDocument::_convert_csg_shape_to_gltf(CSGShape3D *p_current, GLTFNodeInd Ref<GLTFMesh> gltf_mesh; gltf_mesh.instantiate(); gltf_mesh->set_mesh(mesh); + gltf_mesh->set_original_name(csg->get_name()); GLTFMeshIndex mesh_i = p_state->meshes.size(); p_state->meshes.push_back(gltf_mesh); p_gltf_node->mesh = mesh_i; - p_gltf_node->xform = csg->get_meshes()[0]; + p_gltf_node->transform = csg->get_meshes()[0]; + p_gltf_node->set_original_name(csg->get_name()); p_gltf_node->set_name(_gen_unique_name(p_state, csg->get_name())); } #endif // MODULE_CSG_ENABLED @@ -5656,9 +5128,11 @@ void GLTFDocument::_convert_grid_map_to_gltf(GridMap *p_grid_map, GLTFNodeIndex Ref<GLTFMesh> gltf_mesh; gltf_mesh.instantiate(); gltf_mesh->set_mesh(_mesh_to_importer_mesh(p_grid_map->get_mesh_library()->get_item_mesh(cell))); + gltf_mesh->set_original_name(p_grid_map->get_mesh_library()->get_item_name(cell)); new_gltf_node->mesh = p_state->meshes.size(); p_state->meshes.push_back(gltf_mesh); - new_gltf_node->xform = cell_xform * p_grid_map->get_transform(); + new_gltf_node->transform = cell_xform * p_grid_map->get_transform(); + new_gltf_node->set_original_name(p_grid_map->get_mesh_library()->get_item_name(cell)); new_gltf_node->set_name(_gen_unique_name(p_state, p_grid_map->get_mesh_library()->get_item_name(cell))); } } @@ -5680,6 +5154,7 @@ void GLTFDocument::_convert_multi_mesh_instance_to_gltf( if (mesh.is_null()) { return; } + gltf_mesh->set_original_name(multi_mesh->get_name()); gltf_mesh->set_name(multi_mesh->get_name()); Ref<ImporterMesh> importer_mesh; importer_mesh.instantiate(); @@ -5726,7 +5201,8 @@ void GLTFDocument::_convert_multi_mesh_instance_to_gltf( Ref<GLTFNode> new_gltf_node; new_gltf_node.instantiate(); new_gltf_node->mesh = mesh_index; - new_gltf_node->xform = transform; + new_gltf_node->transform = transform; + new_gltf_node->set_original_name(p_multi_mesh_instance->get_name()); new_gltf_node->set_name(_gen_unique_name(p_state, p_multi_mesh_instance->get_name())); p_gltf_node->children.push_back(p_state->nodes.size()); p_state->nodes.push_back(new_gltf_node); @@ -5750,11 +5226,9 @@ void GLTFDocument::_convert_skeleton_to_gltf(Skeleton3D *p_skeleton3d, Ref<GLTFS joint_node.instantiate(); // Note that we cannot use _gen_unique_bone_name here, because glTF spec requires all node // names to be unique regardless of whether or not they are used as joints. + joint_node->set_original_name(skeleton->get_bone_name(bone_i)); joint_node->set_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i))); - Transform3D xform = skeleton->get_bone_pose(bone_i); - joint_node->scale = xform.basis.get_scale(); - joint_node->rotation = xform.basis.get_rotation_quaternion(); - joint_node->position = xform.origin; + joint_node->transform = skeleton->get_bone_pose(bone_i); joint_node->joint = true; GLTFNodeIndex current_node_i = p_state->nodes.size(); p_state->scene_nodes.insert(current_node_i, skeleton); @@ -5846,6 +5320,10 @@ void GLTFDocument::_generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIn BoneAttachment3D *bone_attachment = _generate_bone_attachment(p_state, active_skeleton, p_node_index, gltf_node->parent); p_scene_parent->add_child(bone_attachment, true); + + // Find the correct bone_idx so we can properly serialize it. + bone_attachment->set_bone_idx(active_skeleton->find_bone(gltf_node->get_name())); + bone_attachment->set_owner(p_scene_root); // There is no gltf_node that represent this, so just directly create a unique name @@ -5899,7 +5377,7 @@ void GLTFDocument::_generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIn Array args; args.append(p_scene_root); current_node->propagate_call(StringName("set_owner"), args); - current_node->set_transform(gltf_node->xform); + current_node->set_transform(gltf_node->transform); } p_state->scene_nodes.insert(p_node_index, current_node); @@ -5933,8 +5411,13 @@ void GLTFDocument::_generate_skeleton_bone_node(Ref<GLTFState> p_state, const GL p_scene_parent = bone_attachment; } if (skeleton->get_parent() == nullptr) { - p_scene_parent->add_child(skeleton, true); - skeleton->set_owner(p_scene_root); + if (p_scene_root) { + p_scene_parent->add_child(skeleton, true); + skeleton->set_owner(p_scene_root); + } else { + p_scene_parent = skeleton; + p_scene_root = skeleton; + } } } @@ -5949,6 +5432,10 @@ void GLTFDocument::_generate_skeleton_bone_node(Ref<GLTFState> p_state, const GL BoneAttachment3D *bone_attachment = _generate_bone_attachment(p_state, active_skeleton, p_node_index, p_node_index); p_scene_parent->add_child(bone_attachment, true); + + // Find the correct bone_idx so we can properly serialize it. + bone_attachment->set_bone_idx(active_skeleton->find_bone(gltf_node->get_name())); + bone_attachment->set_owner(p_scene_root); // There is no gltf_node that represent this, so just directly create a unique name @@ -6025,22 +5512,22 @@ struct SceneFormatImporterGLTFInterpolate { template <> struct SceneFormatImporterGLTFInterpolate<Quaternion> { Quaternion lerp(const Quaternion &a, const Quaternion &b, const float c) const { - ERR_FAIL_COND_V_MSG(!a.is_normalized(), Quaternion(), "The quaternion \"a\" must be normalized."); - ERR_FAIL_COND_V_MSG(!b.is_normalized(), Quaternion(), "The quaternion \"b\" must be normalized."); + ERR_FAIL_COND_V_MSG(!a.is_normalized(), Quaternion(), vformat("The quaternion \"a\" %s must be normalized.", a)); + ERR_FAIL_COND_V_MSG(!b.is_normalized(), Quaternion(), vformat("The quaternion \"b\" %s must be normalized.", b)); return a.slerp(b, c).normalized(); } Quaternion catmull_rom(const Quaternion &p0, const Quaternion &p1, const Quaternion &p2, const Quaternion &p3, const float c) { - ERR_FAIL_COND_V_MSG(!p1.is_normalized(), Quaternion(), "The quaternion \"p1\" must be normalized."); - ERR_FAIL_COND_V_MSG(!p2.is_normalized(), Quaternion(), "The quaternion \"p2\" must be normalized."); + ERR_FAIL_COND_V_MSG(!p1.is_normalized(), Quaternion(), vformat("The quaternion \"p1\" (%s) must be normalized.", p1)); + ERR_FAIL_COND_V_MSG(!p2.is_normalized(), Quaternion(), vformat("The quaternion \"p2\" (%s) must be normalized.", p2)); return p1.slerp(p2, c).normalized(); } Quaternion bezier(const Quaternion start, const Quaternion control_1, const Quaternion control_2, const Quaternion end, const float t) { - ERR_FAIL_COND_V_MSG(!start.is_normalized(), Quaternion(), "The start quaternion must be normalized."); - ERR_FAIL_COND_V_MSG(!end.is_normalized(), Quaternion(), "The end quaternion must be normalized."); + ERR_FAIL_COND_V_MSG(!start.is_normalized(), Quaternion(), vformat("The start quaternion %s must be normalized.", start)); + ERR_FAIL_COND_V_MSG(!end.is_normalized(), Quaternion(), vformat("The end quaternion %s must be normalized.", end)); return start.slerp(end, t).normalized(); } @@ -6048,7 +5535,7 @@ struct SceneFormatImporterGLTFInterpolate<Quaternion> { template <class T> T GLTFDocument::_interpolate_track(const Vector<real_t> &p_times, const Vector<T> &p_values, const float p_time, const GLTFAnimation::Interpolation p_interp) { - ERR_FAIL_COND_V(!p_values.size(), T()); + ERR_FAIL_COND_V(p_values.is_empty(), T()); if (p_times.size() != (p_values.size() / (p_interp == GLTFAnimation::INTERP_CUBIC_SPLINE ? 3 : 1))) { ERR_PRINT_ONCE("The interpolated values are not corresponding to its times."); return p_values[0]; @@ -6105,9 +5592,9 @@ T GLTFDocument::_interpolate_track(const Vector<real_t> &p_times, const Vector<T const float c = (p_time - p_times[idx]) / (p_times[idx + 1] - p_times[idx]); - const T from = p_values[idx * 3 + 1]; + const T &from = p_values[idx * 3 + 1]; const T c1 = from + p_values[idx * 3 + 2]; - const T to = p_values[idx * 3 + 4]; + const T &to = p_values[idx * 3 + 4]; const T c2 = to + p_values[idx * 3 + 3]; return interp.bezier(from, c1, c2, to, c); @@ -6222,9 +5709,11 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ if (track.position_track.values.size()) { bool is_default = true; //discard the track if all it contains is default values if (p_remove_immutable_tracks) { - Vector3 base_pos = p_state->nodes[track_i.key]->position; + Vector3 base_pos = gltf_node->get_position(); for (int i = 0; i < track.position_track.times.size(); i++) { - Vector3 value = track.position_track.values[track.position_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i]; + int value_index = track.position_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i; + ERR_FAIL_COND_MSG(value_index >= track.position_track.values.size(), "Animation sampler output accessor with 'CUBICSPLINE' interpolation doesn't have enough elements."); + Vector3 value = track.position_track.values[value_index]; if (!value.is_equal_approx(base_pos)) { is_default = false; break; @@ -6236,15 +5725,20 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ animation->add_track(Animation::TYPE_POSITION_3D); animation->track_set_path(position_idx, transform_node_path); animation->track_set_imported(position_idx, true); //helps merging later + if (track.position_track.interpolation == GLTFAnimation::INTERP_STEP) { + animation->track_set_interpolation_type(position_idx, Animation::InterpolationType::INTERPOLATION_NEAREST); + } base_idx++; } } if (track.rotation_track.values.size()) { bool is_default = true; //discard the track if all it contains is default values if (p_remove_immutable_tracks) { - Quaternion base_rot = p_state->nodes[track_i.key]->rotation.normalized(); + Quaternion base_rot = gltf_node->get_rotation(); for (int i = 0; i < track.rotation_track.times.size(); i++) { - Quaternion value = track.rotation_track.values[track.rotation_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i].normalized(); + int value_index = track.rotation_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i; + ERR_FAIL_COND_MSG(value_index >= track.rotation_track.values.size(), "Animation sampler output accessor with 'CUBICSPLINE' interpolation doesn't have enough elements."); + Quaternion value = track.rotation_track.values[value_index].normalized(); if (!value.is_equal_approx(base_rot)) { is_default = false; break; @@ -6256,15 +5750,20 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ animation->add_track(Animation::TYPE_ROTATION_3D); animation->track_set_path(rotation_idx, transform_node_path); animation->track_set_imported(rotation_idx, true); //helps merging later + if (track.rotation_track.interpolation == GLTFAnimation::INTERP_STEP) { + animation->track_set_interpolation_type(rotation_idx, Animation::InterpolationType::INTERPOLATION_NEAREST); + } base_idx++; } } if (track.scale_track.values.size()) { bool is_default = true; //discard the track if all it contains is default values if (p_remove_immutable_tracks) { - Vector3 base_scale = p_state->nodes[track_i.key]->scale; + Vector3 base_scale = gltf_node->get_scale(); for (int i = 0; i < track.scale_track.times.size(); i++) { - Vector3 value = track.scale_track.values[track.scale_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i]; + int value_index = track.scale_track.interpolation == GLTFAnimation::INTERP_CUBIC_SPLINE ? (1 + i * 3) : i; + ERR_FAIL_COND_MSG(value_index >= track.scale_track.values.size(), "Animation sampler output accessor with 'CUBICSPLINE' interpolation doesn't have enough elements."); + Vector3 value = track.scale_track.values[value_index]; if (!value.is_equal_approx(base_scale)) { is_default = false; break; @@ -6276,6 +5775,9 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ animation->add_track(Animation::TYPE_SCALE_3D); animation->track_set_path(scale_idx, transform_node_path); animation->track_set_imported(scale_idx, true); //helps merging later + if (track.scale_track.interpolation == GLTFAnimation::INTERP_STEP) { + animation->track_set_interpolation_type(scale_idx, Animation::InterpolationType::INTERPOLATION_NEAREST); + } base_idx++; } } @@ -6288,15 +5790,15 @@ void GLTFDocument::_import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_ Vector3 base_scale = Vector3(1, 1, 1); if (rotation_idx == -1) { - base_rot = p_state->nodes[track_i.key]->rotation.normalized(); + base_rot = gltf_node->get_rotation(); } if (position_idx == -1) { - base_pos = p_state->nodes[track_i.key]->position; + base_pos = gltf_node->get_position(); } if (scale_idx == -1) { - base_scale = p_state->nodes[track_i.key]->scale; + base_scale = gltf_node->get_scale(); } bool last = false; @@ -6403,10 +5905,7 @@ void GLTFDocument::_convert_mesh_instances(Ref<GLTFState> p_state) { if (!mi) { continue; } - Transform3D mi_xform = mi->get_transform(); - node->scale = mi_xform.basis.get_scale(); - node->rotation = mi_xform.basis.get_rotation_quaternion(); - node->position = mi_xform.origin; + node->transform = mi->get_transform(); Node *skel_node = mi->get_node_or_null(mi->get_skeleton_path()); Skeleton3D *godot_skeleton = Object::cast_to<Skeleton3D>(skel_node); @@ -6514,7 +6013,7 @@ float GLTFDocument::get_max_component(const Color &p_color) { return MAX(MAX(r, g), b); } -void GLTFDocument::_process_mesh_instances(Ref<GLTFState> p_state) { +void GLTFDocument::_process_mesh_instances(Ref<GLTFState> p_state, Node *p_scene_root) { for (GLTFNodeIndex node_i = 0; node_i < p_state->nodes.size(); ++node_i) { Ref<GLTFNode> node = p_state->nodes[node_i]; @@ -6539,7 +6038,7 @@ void GLTFDocument::_process_mesh_instances(Ref<GLTFState> p_state) { mi->get_parent()->remove_child(mi); skeleton->add_child(mi, true); - mi->set_owner(skeleton->get_owner()); + mi->set_owner(p_scene_root); mi->set_skin(p_state->skins.write[skin_i]->godot_skin); mi->set_skeleton_path(mi->get_path_to(skeleton)); @@ -6866,6 +6365,7 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p Ref<Animation> animation = p_animation_player->get_animation(p_animation_track_name); Ref<GLTFAnimation> gltf_animation; gltf_animation.instantiate(); + gltf_animation->set_original_name(p_animation_track_name); gltf_animation->set_name(_gen_unique_name(p_state, p_animation_track_name)); for (int32_t track_i = 0; track_i < animation->get_track_count(); track_i++) { if (!animation->track_is_enabled(track_i)) { @@ -7000,9 +6500,9 @@ void GLTFDocument::_convert_animation(Ref<GLTFState> p_state, AnimationPlayer *p } else if (String(final_track_path).contains(":")) { //Process skeleton const Vector<String> node_suffix = String(final_track_path).split(":"); - const String node = node_suffix[0]; + const String &node = node_suffix[0]; const NodePath node_path = node; - const String suffix = node_suffix[1]; + const String &suffix = node_suffix[1]; Node *godot_node = animation_base_node->get_node_or_null(node_path); if (!godot_node) { continue; @@ -7216,6 +6716,16 @@ Error GLTFDocument::_serialize_file(Ref<GLTFState> p_state, const String p_path) } void GLTFDocument::_bind_methods() { + BIND_ENUM_CONSTANT(ROOT_NODE_MODE_SINGLE_ROOT); + BIND_ENUM_CONSTANT(ROOT_NODE_MODE_KEEP_ROOT); + BIND_ENUM_CONSTANT(ROOT_NODE_MODE_MULTI_ROOT); + + ClassDB::bind_method(D_METHOD("set_image_format", "image_format"), &GLTFDocument::set_image_format); + ClassDB::bind_method(D_METHOD("get_image_format"), &GLTFDocument::get_image_format); + ClassDB::bind_method(D_METHOD("set_lossy_quality", "lossy_quality"), &GLTFDocument::set_lossy_quality); + ClassDB::bind_method(D_METHOD("get_lossy_quality"), &GLTFDocument::get_lossy_quality); + ClassDB::bind_method(D_METHOD("set_root_node_mode", "root_node_mode"), &GLTFDocument::set_root_node_mode); + ClassDB::bind_method(D_METHOD("get_root_node_mode"), &GLTFDocument::get_root_node_mode); ClassDB::bind_method(D_METHOD("append_from_file", "path", "state", "flags", "base_path"), &GLTFDocument::append_from_file, DEFVAL(0), DEFVAL(String())); ClassDB::bind_method(D_METHOD("append_from_buffer", "bytes", "base_path", "state", "flags"), @@ -7229,17 +6739,6 @@ void GLTFDocument::_bind_methods() { ClassDB::bind_method(D_METHOD("write_to_filesystem", "state", "path"), &GLTFDocument::write_to_filesystem); - BIND_ENUM_CONSTANT(ROOT_NODE_MODE_SINGLE_ROOT); - BIND_ENUM_CONSTANT(ROOT_NODE_MODE_KEEP_ROOT); - BIND_ENUM_CONSTANT(ROOT_NODE_MODE_MULTI_ROOT); - - ClassDB::bind_method(D_METHOD("set_image_format", "image_format"), &GLTFDocument::set_image_format); - ClassDB::bind_method(D_METHOD("get_image_format"), &GLTFDocument::get_image_format); - ClassDB::bind_method(D_METHOD("set_lossy_quality", "lossy_quality"), &GLTFDocument::set_lossy_quality); - ClassDB::bind_method(D_METHOD("get_lossy_quality"), &GLTFDocument::get_lossy_quality); - ClassDB::bind_method(D_METHOD("set_root_node_mode", "root_node_mode"), &GLTFDocument::set_root_node_mode); - ClassDB::bind_method(D_METHOD("get_root_node_mode"), &GLTFDocument::get_root_node_mode); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "image_format"), "set_image_format", "get_image_format"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lossy_quality"), "set_lossy_quality", "get_lossy_quality"); ADD_PROPERTY(PropertyInfo(Variant::INT, "root_node_mode"), "set_root_node_mode", "get_root_node_mode"); @@ -7284,6 +6783,10 @@ void GLTFDocument::unregister_all_gltf_document_extensions() { all_document_extensions.clear(); } +Vector<Ref<GLTFDocumentExtension>> GLTFDocument::get_all_gltf_document_extensions() { + return all_document_extensions; +} + PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref<GLTFState> p_state, Error *r_err) { Error err = _encode_buffer_glb(p_state, ""); if (r_err) { @@ -7326,35 +6829,11 @@ PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref<GLTFState> p_state, Erro return buffer->get_data_array(); } -PackedByteArray GLTFDocument::generate_buffer(Ref<GLTFState> p_state) { - ERR_FAIL_NULL_V(p_state, PackedByteArray()); - // For buffers, set the state filename to an empty string, but - // don't touch the base path, in case the user set it manually. - p_state->filename = ""; - Error err = _serialize(p_state); - ERR_FAIL_COND_V(err != OK, PackedByteArray()); - PackedByteArray bytes = _serialize_glb_buffer(p_state, &err); - return bytes; -} - -Error GLTFDocument::write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) { - ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER); - p_state->base_path = p_path.get_base_dir(); - p_state->filename = p_path.get_file(); - Error err = _serialize(p_state); - if (err != OK) { - return err; - } - err = _serialize_file(p_state, p_path); - if (err != OK) { - return Error::FAILED; - } - return OK; -} - Node *GLTFDocument::_generate_scene_node_tree(Ref<GLTFState> p_state) { // Generate the skeletons and skins (if any). - Error err = _create_skeletons(p_state); + HashMap<ObjectID, SkinSkeletonIndex> skeleton_map; + Error err = SkinTool::_create_skeletons(p_state->unique_names, p_state->skins, p_state->nodes, + skeleton_map, p_state->skeletons, p_state->scene_nodes); ERR_FAIL_COND_V_MSG(err != OK, nullptr, "GLTF: Failed to create skeletons."); err = _create_skins(p_state); ERR_FAIL_COND_V_MSG(err != OK, nullptr, "GLTF: Failed to create skins."); @@ -7374,107 +6853,13 @@ Node *GLTFDocument::_generate_scene_node_tree(Ref<GLTFState> p_state) { if (unlikely(p_state->scene_name.is_empty())) { p_state->scene_name = single_root->get_name(); } else if (single_root->get_name() == StringName()) { - single_root->set_name(_gen_unique_name(p_state, p_state->scene_name)); - } - return single_root; -} - -Node *GLTFDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, bool p_trimming, bool p_remove_immutable_tracks) { - ERR_FAIL_NULL_V(p_state, nullptr); - ERR_FAIL_INDEX_V(0, p_state->root_nodes.size(), nullptr); - Error err = OK; - Node *root = _generate_scene_node_tree(p_state); - ERR_FAIL_NULL_V(root, nullptr); - _process_mesh_instances(p_state); - if (p_state->get_create_animations() && p_state->animations.size()) { - AnimationPlayer *ap = memnew(AnimationPlayer); - root->add_child(ap, true); - ap->set_owner(root); - for (int i = 0; i < p_state->animations.size(); i++) { - _import_animation(p_state, ap, i, p_bake_fps, p_trimming, p_remove_immutable_tracks); - } - } - for (KeyValue<GLTFNodeIndex, Node *> E : p_state->scene_nodes) { - ERR_CONTINUE(!E.value); - for (Ref<GLTFDocumentExtension> ext : document_extensions) { - ERR_CONTINUE(ext.is_null()); - ERR_CONTINUE(!p_state->json.has("nodes")); - Array nodes = p_state->json["nodes"]; - ERR_CONTINUE(E.key >= nodes.size()); - ERR_CONTINUE(E.key < 0); - Dictionary node_json = nodes[E.key]; - Ref<GLTFNode> gltf_node = p_state->nodes[E.key]; - err = ext->import_node(p_state, gltf_node, node_json, E.value); - ERR_CONTINUE(err != OK); - } - } - for (Ref<GLTFDocumentExtension> ext : document_extensions) { - ERR_CONTINUE(ext.is_null()); - err = ext->import_post(p_state, root); - ERR_CONTINUE(err != OK); - } - ERR_FAIL_NULL_V(root, nullptr); - return root; -} - -Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags) { - ERR_FAIL_COND_V(p_state.is_null(), FAILED); - p_state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; - p_state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; - p_state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; - p_state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; - if (!p_state->buffers.size()) { - p_state->buffers.push_back(Vector<uint8_t>()); - } - // Perform export preflight for document extensions. Only extensions that - // return OK will be used for the rest of the export steps. - document_extensions.clear(); - for (Ref<GLTFDocumentExtension> ext : all_document_extensions) { - ERR_CONTINUE(ext.is_null()); - Error err = ext->export_preflight(p_state, p_node); - if (err == OK) { - document_extensions.push_back(ext); - } - } - // Add the root node(s) and their descendants to the state. - if (_root_node_mode == RootNodeMode::ROOT_NODE_MODE_MULTI_ROOT) { - const int child_count = p_node->get_child_count(); - if (child_count > 0) { - for (int i = 0; i < child_count; i++) { - _convert_scene_node(p_state, p_node->get_child(i), -1, -1); - } - p_state->scene_name = p_node->get_name(); - return OK; + if (_naming_version == 0) { + single_root->set_name(p_state->scene_name); + } else { + single_root->set_name(_gen_unique_name(p_state, p_state->scene_name)); } } - if (_root_node_mode == RootNodeMode::ROOT_NODE_MODE_SINGLE_ROOT) { - p_state->extensions_used.append("GODOT_single_root"); - } - _convert_scene_node(p_state, p_node, -1, -1); - return OK; -} - -Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags) { - ERR_FAIL_COND_V(p_state.is_null(), FAILED); - // TODO Add missing texture and missing .bin file paths to r_missing_deps 2021-09-10 fire - Error err = FAILED; - p_state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; - p_state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; - p_state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; - p_state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; - - Ref<FileAccessMemory> file_access; - file_access.instantiate(); - file_access->open_custom(p_bytes.ptr(), p_bytes.size()); - p_state->base_path = p_base_path.get_base_dir(); - err = _parse(p_state, p_state->base_path, file_access); - ERR_FAIL_COND_V(err != OK, err); - for (Ref<GLTFDocumentExtension> ext : document_extensions) { - ERR_CONTINUE(ext.is_null()); - err = ext->import_post_parse(p_state); - ERR_FAIL_COND_V(err != OK, err); - } - return OK; + return single_root; } Error GLTFDocument::_parse_asset_header(Ref<GLTFState> p_state) { @@ -7552,7 +6937,7 @@ Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> p_state, const String &p_se ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); /* DETERMINE SKELETONS */ - err = _determine_skeletons(p_state); + err = SkinTool::_determine_skeletons(p_state->skins, p_state->nodes, p_state->skeletons, p_state->get_import_as_skeleton_bones() ? p_state->root_nodes : Vector<GLTFNodeIndex>()); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); /* PARSE MESHES (we have enough info now) */ @@ -7577,16 +6962,148 @@ Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> p_state, const String &p_se return OK; } +PackedByteArray GLTFDocument::generate_buffer(Ref<GLTFState> p_state) { + Ref<GLTFState> state = p_state; + ERR_FAIL_NULL_V(state, PackedByteArray()); + // For buffers, set the state filename to an empty string, but + // don't touch the base path, in case the user set it manually. + state->filename = ""; + Error err = _serialize(state); + ERR_FAIL_COND_V(err != OK, PackedByteArray()); + PackedByteArray bytes = _serialize_glb_buffer(state, &err); + return bytes; +} + +Error GLTFDocument::write_to_filesystem(Ref<GLTFState> p_state, const String &p_path) { + Ref<GLTFState> state = p_state; + ERR_FAIL_NULL_V(state, ERR_INVALID_PARAMETER); + state->base_path = p_path.get_base_dir(); + state->filename = p_path.get_file(); + Error err = _serialize(state); + if (err != OK) { + return err; + } + err = _serialize_file(state, p_path); + if (err != OK) { + return Error::FAILED; + } + return OK; +} + +Node *GLTFDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, bool p_trimming, bool p_remove_immutable_tracks) { + Ref<GLTFState> state = p_state; + ERR_FAIL_NULL_V(state, nullptr); + ERR_FAIL_INDEX_V(0, state->root_nodes.size(), nullptr); + Error err = OK; + Node *root = _generate_scene_node_tree(state); + ERR_FAIL_NULL_V(root, nullptr); + _process_mesh_instances(state, root); + if (state->get_create_animations() && state->animations.size()) { + AnimationPlayer *ap = memnew(AnimationPlayer); + root->add_child(ap, true); + ap->set_owner(root); + for (int i = 0; i < state->animations.size(); i++) { + _import_animation(state, ap, i, p_bake_fps, p_trimming, p_remove_immutable_tracks); + } + } + for (KeyValue<GLTFNodeIndex, Node *> E : state->scene_nodes) { + ERR_CONTINUE(!E.value); + for (Ref<GLTFDocumentExtension> ext : document_extensions) { + ERR_CONTINUE(ext.is_null()); + Dictionary node_json; + if (state->json.has("nodes")) { + Array nodes = state->json["nodes"]; + if (0 <= E.key && E.key < nodes.size()) { + node_json = nodes[E.key]; + } + } + Ref<GLTFNode> gltf_node = state->nodes[E.key]; + err = ext->import_node(p_state, gltf_node, node_json, E.value); + ERR_CONTINUE(err != OK); + } + } + for (Ref<GLTFDocumentExtension> ext : document_extensions) { + ERR_CONTINUE(ext.is_null()); + err = ext->import_post(p_state, root); + ERR_CONTINUE(err != OK); + } + ERR_FAIL_NULL_V(root, nullptr); + return root; +} + +Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags) { + Ref<GLTFState> state = p_state; + ERR_FAIL_COND_V(state.is_null(), FAILED); + state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; + state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; + state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; + state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; + if (!state->buffers.size()) { + state->buffers.push_back(Vector<uint8_t>()); + } + // Perform export preflight for document extensions. Only extensions that + // return OK will be used for the rest of the export steps. + document_extensions.clear(); + for (Ref<GLTFDocumentExtension> ext : all_document_extensions) { + ERR_CONTINUE(ext.is_null()); + Error err = ext->export_preflight(state, p_node); + if (err == OK) { + document_extensions.push_back(ext); + } + } + // Add the root node(s) and their descendants to the state. + if (_root_node_mode == RootNodeMode::ROOT_NODE_MODE_MULTI_ROOT) { + const int child_count = p_node->get_child_count(); + if (child_count > 0) { + for (int i = 0; i < child_count; i++) { + _convert_scene_node(state, p_node->get_child(i), -1, -1); + } + state->scene_name = p_node->get_name(); + return OK; + } + } + if (_root_node_mode == RootNodeMode::ROOT_NODE_MODE_SINGLE_ROOT) { + state->extensions_used.append("GODOT_single_root"); + } + _convert_scene_node(state, p_node, -1, -1); + return OK; +} + +Error GLTFDocument::append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags) { + Ref<GLTFState> state = p_state; + ERR_FAIL_COND_V(state.is_null(), FAILED); + // TODO Add missing texture and missing .bin file paths to r_missing_deps 2021-09-10 fire + Error err = FAILED; + state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; + state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; + state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; + state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; + + Ref<FileAccessMemory> file_access; + file_access.instantiate(); + file_access->open_custom(p_bytes.ptr(), p_bytes.size()); + state->base_path = p_base_path.get_base_dir(); + err = _parse(p_state, state->base_path, file_access); + ERR_FAIL_COND_V(err != OK, err); + for (Ref<GLTFDocumentExtension> ext : document_extensions) { + ERR_CONTINUE(ext.is_null()); + err = ext->import_post_parse(state); + ERR_FAIL_COND_V(err != OK, err); + } + return OK; +} + Error GLTFDocument::append_from_file(String p_path, Ref<GLTFState> p_state, uint32_t p_flags, String p_base_path) { + Ref<GLTFState> state = p_state; // TODO Add missing texture and missing .bin file paths to r_missing_deps 2021-09-10 fire - if (p_state == Ref<GLTFState>()) { - p_state.instantiate(); + if (state == Ref<GLTFState>()) { + state.instantiate(); } - p_state->filename = p_path.get_file().get_basename(); - p_state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; - p_state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; - p_state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; - p_state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; + state->filename = p_path.get_file().get_basename(); + state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS; + state->discard_meshes_and_materials = p_flags & GLTF_IMPORT_DISCARD_MESHES_AND_MATERIALS; + state->force_generate_tangents = p_flags & GLTF_IMPORT_GENERATE_TANGENT_ARRAYS; + state->force_disable_compression = p_flags & GLTF_IMPORT_FORCE_DISABLE_MESH_COMPRESSION; Error err; Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ, &err); @@ -7596,7 +7113,7 @@ Error GLTFDocument::append_from_file(String p_path, Ref<GLTFState> p_state, uint if (base_path.is_empty()) { base_path = p_path.get_base_dir(); } - p_state->base_path = base_path; + state->base_path = base_path; err = _parse(p_state, base_path, file); ERR_FAIL_COND_V(err != OK, err); for (Ref<GLTFDocumentExtension> ext : document_extensions) { @@ -7647,3 +7164,25 @@ void GLTFDocument::set_root_node_mode(GLTFDocument::RootNodeMode p_root_node_mod GLTFDocument::RootNodeMode GLTFDocument::get_root_node_mode() const { return _root_node_mode; } + +String GLTFDocument::_gen_unique_name_static(HashSet<String> &r_unique_names, const String &p_name) { + const String s_name = p_name.validate_node_name(); + + String u_name; + int index = 1; + while (true) { + u_name = s_name; + + if (index > 1) { + u_name += itos(index); + } + if (!r_unique_names.has(u_name)) { + break; + } + index++; + } + + r_unique_names.insert(u_name); + + return u_name; +} diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 828d650cff..04de2ac5f8 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -32,13 +32,23 @@ #define GLTF_DOCUMENT_H #include "extensions/gltf_document_extension.h" +#include "extensions/gltf_spec_gloss.h" +#include "gltf_defines.h" +#include "gltf_state.h" + +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/multimesh_instance_3d.h" #include "modules/modules_enabled.gen.h" // For csg, gridmap. +#ifdef MODULE_CSG_ENABLED +#include "modules/csg/csg_shape.h" +#endif // MODULE_CSG_ENABLED +#ifdef MODULE_GRIDMAP_ENABLED +#include "modules/gridmap/grid_map.h" +#endif // MODULE_GRIDMAP_ENABLED class GLTFDocument : public Resource { GDCLASS(GLTFDocument, Resource); - static Vector<Ref<GLTFDocumentExtension>> all_document_extensions; - Vector<Ref<GLTFDocumentExtension>> document_extensions; public: const int32_t JOINT_GROUP_SIZE = 4; @@ -73,6 +83,7 @@ public: private: const float BAKE_FPS = 30.0f; + int _naming_version = 1; String _image_format = "PNG"; float _lossy_quality = 0.75f; Ref<GLTFDocumentExtension> _image_save_extension; @@ -80,18 +91,25 @@ private: protected: static void _bind_methods(); + String _gen_unique_name(Ref<GLTFState> p_state, const String &p_name); + static Vector<Ref<GLTFDocumentExtension>> all_document_extensions; + Vector<Ref<GLTFDocumentExtension>> document_extensions; public: static void register_gltf_document_extension(Ref<GLTFDocumentExtension> p_extension, bool p_first_priority = false); static void unregister_gltf_document_extension(Ref<GLTFDocumentExtension> p_extension); static void unregister_all_gltf_document_extensions(); + static Vector<Ref<GLTFDocumentExtension>> get_all_gltf_document_extensions(); + void set_naming_version(int p_version); + int get_naming_version() const; void set_image_format(const String &p_image_format); String get_image_format() const; void set_lossy_quality(float p_lossy_quality); float get_lossy_quality() const; void set_root_node_mode(RootNodeMode p_root_node_mode); RootNodeMode get_root_node_mode() const; + static String _gen_unique_name_static(HashSet<String> &r_unique_names, const String &p_name); private: void _build_parent_hierachy(Ref<GLTFState> p_state); @@ -102,7 +120,6 @@ private: Error _parse_nodes(Ref<GLTFState> p_state); String _get_type_name(const GLTFType p_component); String _get_accessor_type_name(const GLTFType p_type); - String _gen_unique_name(Ref<GLTFState> p_state, const String &p_name); String _sanitize_animation_name(const String &p_name); String _gen_unique_animation_name(Ref<GLTFState> p_state, const String &p_name); String _sanitize_bone_name(const String &p_name); @@ -180,24 +197,7 @@ private: const Color &p_diffuse, Color &r_base_color, float &r_metallic); - GLTFNodeIndex _find_highest_node(Ref<GLTFState> p_state, - const Vector<GLTFNodeIndex> &p_subset); - void _recurse_children(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, - RBSet<GLTFNodeIndex> &p_all_skin_nodes, HashSet<GLTFNodeIndex> &p_child_visited_set); - bool _capture_nodes_in_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin, - const GLTFNodeIndex p_node_index); - void _capture_nodes_for_multirooted_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin); - Error _expand_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin); - Error _verify_skin(Ref<GLTFState> p_state, Ref<GLTFSkin> p_skin); Error _parse_skins(Ref<GLTFState> p_state); - Error _determine_skeletons(Ref<GLTFState> p_state); - Error _reparent_non_joint_skeleton_subtrees( - Ref<GLTFState> p_state, Ref<GLTFSkeleton> p_skeleton, - const Vector<GLTFNodeIndex> &p_non_joints); - Error _determine_skeleton_roots(Ref<GLTFState> p_state, - const GLTFSkeletonIndex p_skel_i); - Error _create_skeletons(Ref<GLTFState> p_state); - Error _map_skin_joints_indices_to_skeleton_bone_indices(Ref<GLTFState> p_state); Error _serialize_skins(Ref<GLTFState> p_state); Error _create_skins(Ref<GLTFState> p_state); bool _skins_are_same(const Ref<Skin> p_skin_a, const Ref<Skin> p_skin_b); @@ -313,7 +313,6 @@ public: Error append_from_buffer(PackedByteArray p_bytes, String p_base_path, Ref<GLTFState> p_state, uint32_t p_flags = 0); Error append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags = 0); -public: Node *generate_scene(Ref<GLTFState> p_state, float p_bake_fps = 30.0f, bool p_trimming = false, bool p_remove_immutable_tracks = true); PackedByteArray generate_buffer(Ref<GLTFState> p_state); Error write_to_filesystem(Ref<GLTFState> p_state, const String &p_path); @@ -322,7 +321,7 @@ public: Error _parse_gltf_state(Ref<GLTFState> p_state, const String &p_search_path); Error _parse_asset_header(Ref<GLTFState> p_state); Error _parse_gltf_extensions(Ref<GLTFState> p_state); - void _process_mesh_instances(Ref<GLTFState> p_state); + void _process_mesh_instances(Ref<GLTFState> p_state, Node *p_scene_root); Node *_generate_scene_node_tree(Ref<GLTFState> p_state); void _generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); void _generate_skeleton_bone_node(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); diff --git a/modules/gltf/gltf_state.cpp b/modules/gltf/gltf_state.cpp index c0ec004fd6..ed31aadc01 100644 --- a/modules/gltf/gltf_state.cpp +++ b/modules/gltf/gltf_state.cpp @@ -34,6 +34,8 @@ void GLTFState::_bind_methods() { ClassDB::bind_method(D_METHOD("add_used_extension", "extension_name", "required"), &GLTFState::add_used_extension); + ClassDB::bind_method(D_METHOD("append_data_to_buffers", "data", "deduplication"), &GLTFState::append_data_to_buffers); + ClassDB::bind_method(D_METHOD("get_json"), &GLTFState::get_json); ClassDB::bind_method(D_METHOD("set_json", "json"), &GLTFState::set_json); ClassDB::bind_method(D_METHOD("get_major_version"), &GLTFState::get_major_version); @@ -88,6 +90,8 @@ void GLTFState::_bind_methods() { ClassDB::bind_method(D_METHOD("set_skeletons", "skeletons"), &GLTFState::set_skeletons); ClassDB::bind_method(D_METHOD("get_create_animations"), &GLTFState::get_create_animations); ClassDB::bind_method(D_METHOD("set_create_animations", "create_animations"), &GLTFState::set_create_animations); + ClassDB::bind_method(D_METHOD("get_import_as_skeleton_bones"), &GLTFState::get_import_as_skeleton_bones); + ClassDB::bind_method(D_METHOD("set_import_as_skeleton_bones", "import_as_skeleton_bones"), &GLTFState::set_import_as_skeleton_bones); ClassDB::bind_method(D_METHOD("get_animations"), &GLTFState::get_animations); ClassDB::bind_method(D_METHOD("set_animations", "animations"), &GLTFState::set_animations); ClassDB::bind_method(D_METHOD("get_scene_node", "idx"), &GLTFState::get_scene_node); @@ -123,8 +127,9 @@ void GLTFState::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "unique_animation_names", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_unique_animation_names", "get_unique_animation_names"); // Set<String> ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "skeletons", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_skeletons", "get_skeletons"); // Vector<Ref<GLTFSkeleton>> ADD_PROPERTY(PropertyInfo(Variant::BOOL, "create_animations"), "set_create_animations", "get_create_animations"); // bool + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "import_as_skeleton_bones"), "set_import_as_skeleton_bones", "get_import_as_skeleton_bones"); // bool ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "animations", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_animations", "get_animations"); // Vector<Ref<GLTFAnimation>> - ADD_PROPERTY(PropertyInfo(Variant::INT, "handle_binary_image", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed As Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_handle_binary_image", "get_handle_binary_image"); // enum + ADD_PROPERTY(PropertyInfo(Variant::INT, "handle_binary_image", PROPERTY_HINT_ENUM, "Discard All Textures,Extract Textures,Embed as Basis Universal,Embed as Uncompressed", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_handle_binary_image", "get_handle_binary_image"); // enum BIND_CONSTANT(HANDLE_BINARY_DISCARD_TEXTURES); BIND_CONSTANT(HANDLE_BINARY_EXTRACT_TEXTURES); @@ -335,6 +340,14 @@ void GLTFState::set_create_animations(bool p_create_animations) { create_animations = p_create_animations; } +bool GLTFState::get_import_as_skeleton_bones() { + return import_as_skeleton_bones; +} + +void GLTFState::set_import_as_skeleton_bones(bool p_import_as_skeleton_bones) { + import_as_skeleton_bones = p_import_as_skeleton_bones; +} + TypedArray<GLTFAnimation> GLTFState::get_animations() { return GLTFTemplateConvert::to_array(animations); } @@ -399,3 +412,29 @@ Variant GLTFState::get_additional_data(const StringName &p_extension_name) { void GLTFState::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { additional_data[p_extension_name] = p_additional_data; } + +GLTFBufferViewIndex GLTFState::append_data_to_buffers(const Vector<uint8_t> &p_data, const bool p_deduplication = false) { + if (p_deduplication) { + for (int i = 0; i < buffer_views.size(); i++) { + Ref<GLTFBufferView> buffer_view = buffer_views[i]; + Vector<uint8_t> buffer_view_data = buffer_view->load_buffer_view_data(this); + if (buffer_view_data == p_data) { + return i; + } + } + } + // Append the given data to a buffer and create a buffer view for it. + if (unlikely(buffers.is_empty())) { + buffers.push_back(Vector<uint8_t>()); + } + Vector<uint8_t> &destination_buffer = buffers.write[0]; + Ref<GLTFBufferView> buffer_view; + buffer_view.instantiate(); + buffer_view->set_buffer(0); + buffer_view->set_byte_offset(destination_buffer.size()); + buffer_view->set_byte_length(p_data.size()); + destination_buffer.append_array(p_data); + const int new_index = buffer_views.size(); + buffer_views.push_back(buffer_view); + return new_index; +} diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index 1ed8ce3629..c9efffa3ae 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -43,10 +43,13 @@ #include "structures/gltf_texture.h" #include "structures/gltf_texture_sampler.h" +#include "scene/3d/importer_mesh_instance_3d.h" + class GLTFState : public Resource { GDCLASS(GLTFState, Resource); friend class GLTFDocument; +protected: String base_path; String filename; Dictionary json; @@ -61,6 +64,7 @@ class GLTFState : public Resource { bool force_generate_tangents = false; bool create_animations = true; bool force_disable_compression = false; + bool import_as_skeleton_bones = false; int handle_binary_image = HANDLE_BINARY_EXTRACT_TEXTURES; @@ -69,7 +73,7 @@ class GLTFState : public Resource { Vector<Ref<GLTFBufferView>> buffer_views; Vector<Ref<GLTFAccessor>> accessors; - Vector<Ref<GLTFMesh>> meshes; // meshes are loaded directly, no reason not to. + Vector<Ref<GLTFMesh>> meshes; // Meshes are loaded directly, no reason not to. Vector<AnimationPlayer *> animation_players; HashMap<Ref<Material>, GLTFMaterialIndex> material_cache; @@ -105,12 +109,13 @@ protected: public: void add_used_extension(const String &p_extension, bool p_required = false); + GLTFBufferViewIndex append_data_to_buffers(const Vector<uint8_t> &p_data, const bool p_deduplication); enum GLTFHandleBinary { HANDLE_BINARY_DISCARD_TEXTURES = 0, HANDLE_BINARY_EXTRACT_TEXTURES, HANDLE_BINARY_EMBED_AS_BASISU, - HANDLE_BINARY_EMBED_AS_UNCOMPRESSED, // if this value changes from 3, ResourceImporterScene::pre_import must be changed as well. + HANDLE_BINARY_EMBED_AS_UNCOMPRESSED, // If this value changes from 3, ResourceImporterScene::pre_import must be changed as well. }; int32_t get_handle_binary_image() { return handle_binary_image; @@ -209,6 +214,9 @@ public: bool get_create_animations(); void set_create_animations(bool p_create_animations); + bool get_import_as_skeleton_bones(); + void set_import_as_skeleton_bones(bool p_import_as_skeleton_bones); + TypedArray<GLTFAnimation> get_animations(); void set_animations(TypedArray<GLTFAnimation> p_animations); diff --git a/modules/gltf/gltf_template_convert.h b/modules/gltf/gltf_template_convert.h index 8616665f8b..2743cd8a9b 100644 --- a/modules/gltf/gltf_template_convert.h +++ b/modules/gltf/gltf_template_convert.h @@ -34,6 +34,7 @@ #include "core/templates/hash_set.h" #include "core/variant/array.h" #include "core/variant/dictionary.h" +#include "core/variant/typed_array.h" namespace GLTFTemplateConvert { template <class T> @@ -73,7 +74,7 @@ static void set_from_array(HashSet<T> &r_out, const TypedArray<T> &p_inp) { } template <class K, class V> -static Dictionary to_dict(const HashMap<K, V> &p_inp) { +static Dictionary to_dictionary(const HashMap<K, V> &p_inp) { Dictionary ret; for (const KeyValue<K, V> &E : p_inp) { ret[E.key] = E.value; @@ -82,7 +83,7 @@ static Dictionary to_dict(const HashMap<K, V> &p_inp) { } template <class K, class V> -static void set_from_dict(HashMap<K, V> &r_out, const Dictionary &p_inp) { +static void set_from_dictionary(HashMap<K, V> &r_out, const Dictionary &p_inp) { r_out.clear(); Array keys = p_inp.keys(); for (int i = 0; i < keys.size(); i++) { diff --git a/modules/gltf/register_types.cpp b/modules/gltf/register_types.cpp index fecea45fc9..53e9f2b84c 100644 --- a/modules/gltf/register_types.cpp +++ b/modules/gltf/register_types.cpp @@ -36,12 +36,12 @@ #include "extensions/gltf_spec_gloss.h" #include "extensions/physics/gltf_document_extension_physics.h" #include "gltf_document.h" +#include "gltf_state.h" #ifdef TOOLS_ENABLED #include "editor/editor_import_blend_runner.h" #include "editor/editor_scene_exporter_gltf_plugin.h" #include "editor/editor_scene_importer_blend.h" -#include "editor/editor_scene_importer_fbx.h" #include "editor/editor_scene_importer_gltf.h" #include "core/config/project_settings.h" @@ -51,43 +51,46 @@ static void _editor_init() { Ref<EditorSceneFormatImporterGLTF> import_gltf; import_gltf.instantiate(); - ResourceImporterScene::add_importer(import_gltf); + ResourceImporterScene::add_scene_importer(import_gltf); // Blend to glTF importer. - bool blend_enabled = GLOBAL_GET("filesystem/import/blender/enabled"); - String blender3_path = EDITOR_GET("filesystem/import/blender/blender3_path"); - if (blend_enabled) { - Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (blender3_path.is_empty()) { - WARN_PRINT(TTR("Blend file import is enabled in the project settings, but no Blender path is configured in the editor settings. Blend files will not be imported.")); - } else if (!da->dir_exists(blender3_path)) { - WARN_PRINT(TTR("Blend file import is enabled, but the Blender path doesn't point to an accessible directory. Blend files will not be imported.")); - } else { - Ref<EditorSceneFormatImporterBlend> importer; - importer.instantiate(); - ResourceImporterScene::add_importer(importer); - - Ref<EditorFileSystemImportFormatSupportQueryBlend> blend_import_query; - blend_import_query.instantiate(); - EditorFileSystem::get_singleton()->add_import_format_support_query(blend_import_query); + String blender_path = EDITOR_GET("filesystem/import/blender/blender_path"); + if (blender_path.is_empty() && EditorSettings::get_singleton()->has_setting("filesystem/import/blender/blender3_path")) { + blender_path = EditorSettings::get_singleton()->get("filesystem/import/blender/blender3_path"); + + if (!blender_path.is_empty()) { +#if defined(MACOS_ENABLED) + if (blender_path.contains(".app")) { + blender_path += "/Contents/MacOS/Blender"; + } else { + blender_path += "/blender"; + } +#elif defined(WINDOWS_ENABLED) + blender_path += "\\blender.exe"; +#elif defined(UNIX_ENABLED) + blender_path += "/blender"; +#endif + + EditorSettings::get_singleton()->set("filesystem/import/blender/blender_path", blender_path); } - } - memnew(EditorImportBlendRunner); - EditorNode::get_singleton()->add_child(EditorImportBlendRunner::get_singleton()); - // FBX to glTF importer. + EditorSettings::get_singleton()->erase("filesystem/import/blender/blender3_path"); + EditorSettings::get_singleton()->save(); + } - bool fbx_enabled = GLOBAL_GET("filesystem/import/fbx/enabled"); - if (fbx_enabled) { - Ref<EditorSceneFormatImporterFBX> importer; + bool blend_enabled = GLOBAL_GET("filesystem/import/blender/enabled"); + if (blend_enabled) { + Ref<EditorSceneFormatImporterBlend> importer; importer.instantiate(); - ResourceImporterScene::get_scene_singleton()->add_importer(importer); + ResourceImporterScene::add_scene_importer(importer); - Ref<EditorFileSystemImportFormatSupportQueryFBX> fbx_import_query; - fbx_import_query.instantiate(); - EditorFileSystem::get_singleton()->add_import_format_support_query(fbx_import_query); + Ref<EditorFileSystemImportFormatSupportQueryBlend> blend_import_query; + blend_import_query.instantiate(); + EditorFileSystem::get_singleton()->add_import_format_support_query(blend_import_query); } + memnew(EditorImportBlendRunner); + EditorNode::get_singleton()->add_child(EditorImportBlendRunner::get_singleton()); } #endif // TOOLS_ENABLED @@ -118,10 +121,11 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(GLTFTexture); GDREGISTER_CLASS(GLTFTextureSampler); // Register GLTFDocumentExtension classes with GLTFDocument. + // Ensure physics is first in this list so that physics nodes are created before other nodes. GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionPhysics); GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionTextureKTX); GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionTextureWebP); - bool is_editor = ::Engine::get_singleton()->is_editor_hint(); + bool is_editor = Engine::get_singleton()->is_editor_hint(); if (!is_editor) { GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionConvertImporterMesh); } @@ -138,14 +142,10 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) { // Project settings defined here so doctool finds them. GLOBAL_DEF_RST_BASIC("filesystem/import/blender/enabled", true); - GLOBAL_DEF_RST_BASIC("filesystem/import/fbx/enabled", true); GDREGISTER_CLASS(EditorSceneFormatImporterBlend); - GDREGISTER_CLASS(EditorSceneFormatImporterFBX); // Can't (a priori) run external app on these platforms. GLOBAL_DEF_RST("filesystem/import/blender/enabled.android", false); GLOBAL_DEF_RST("filesystem/import/blender/enabled.web", false); - GLOBAL_DEF_RST("filesystem/import/fbx/enabled.android", false); - GLOBAL_DEF_RST("filesystem/import/fbx/enabled.web", false); ClassDB::set_current_api(prev_api); EditorNode::add_init_callback(_editor_init); diff --git a/modules/gltf/skin_tool.cpp b/modules/gltf/skin_tool.cpp new file mode 100644 index 0000000000..2fb55a5f9e --- /dev/null +++ b/modules/gltf/skin_tool.cpp @@ -0,0 +1,811 @@ +/**************************************************************************/ +/* skin_tool.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "skin_tool.h" + +SkinNodeIndex SkinTool::_find_highest_node(Vector<Ref<GLTFNode>> &r_nodes, const Vector<GLTFNodeIndex> &p_subset) { + int highest = -1; + SkinNodeIndex best_node = -1; + + for (int i = 0; i < p_subset.size(); ++i) { + const SkinNodeIndex node_i = p_subset[i]; + const Ref<GLTFNode> node = r_nodes[node_i]; + + if (highest == -1 || node->height < highest) { + highest = node->height; + best_node = node_i; + } + } + + return best_node; +} + +bool SkinTool::_capture_nodes_in_skin(const Vector<Ref<GLTFNode>> &nodes, Ref<GLTFSkin> p_skin, const SkinNodeIndex p_node_index) { + bool found_joint = false; + Ref<GLTFNode> current_node = nodes[p_node_index]; + + for (int i = 0; i < current_node->children.size(); ++i) { + found_joint |= _capture_nodes_in_skin(nodes, p_skin, current_node->children[i]); + } + + if (found_joint) { + // Mark it if we happen to find another skins joint... + if (current_node->joint && p_skin->joints.find(p_node_index) < 0) { + p_skin->joints.push_back(p_node_index); + } else if (p_skin->non_joints.find(p_node_index) < 0) { + p_skin->non_joints.push_back(p_node_index); + } + } + + if (p_skin->joints.find(p_node_index) > 0) { + return true; + } + + return false; +} + +void SkinTool::_capture_nodes_for_multirooted_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin) { + DisjointSet<SkinNodeIndex> disjoint_set; + + for (int i = 0; i < p_skin->joints.size(); ++i) { + const SkinNodeIndex node_index = p_skin->joints[i]; + const SkinNodeIndex parent = r_nodes[node_index]->parent; + disjoint_set.insert(node_index); + + if (p_skin->joints.find(parent) >= 0) { + disjoint_set.create_union(parent, node_index); + } + } + + Vector<SkinNodeIndex> roots; + disjoint_set.get_representatives(roots); + + if (roots.size() <= 1) { + return; + } + + int maxHeight = -1; + + // Determine the max height rooted tree + for (int i = 0; i < roots.size(); ++i) { + const SkinNodeIndex root = roots[i]; + + if (maxHeight == -1 || r_nodes[root]->height < maxHeight) { + maxHeight = r_nodes[root]->height; + } + } + + // Go up the tree till all of the multiple roots of the skin are at the same hierarchy level. + // This sucks, but 99% of all game engines (not just Godot) would have this same issue. + for (int i = 0; i < roots.size(); ++i) { + SkinNodeIndex current_node = roots[i]; + while (r_nodes[current_node]->height > maxHeight) { + SkinNodeIndex parent = r_nodes[current_node]->parent; + + if (r_nodes[parent]->joint && p_skin->joints.find(parent) < 0) { + p_skin->joints.push_back(parent); + } else if (p_skin->non_joints.find(parent) < 0) { + p_skin->non_joints.push_back(parent); + } + + current_node = parent; + } + + // replace the roots + roots.write[i] = current_node; + } + + // Climb up the tree until they all have the same parent + bool all_same; + + do { + all_same = true; + const SkinNodeIndex first_parent = r_nodes[roots[0]]->parent; + + for (int i = 1; i < roots.size(); ++i) { + all_same &= (first_parent == r_nodes[roots[i]]->parent); + } + + if (!all_same) { + for (int i = 0; i < roots.size(); ++i) { + const SkinNodeIndex current_node = roots[i]; + const SkinNodeIndex parent = r_nodes[current_node]->parent; + + if (r_nodes[parent]->joint && p_skin->joints.find(parent) < 0) { + p_skin->joints.push_back(parent); + } else if (p_skin->non_joints.find(parent) < 0) { + p_skin->non_joints.push_back(parent); + } + + roots.write[i] = parent; + } + } + + } while (!all_same); +} + +Error SkinTool::_expand_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin) { + _capture_nodes_for_multirooted_skin(r_nodes, p_skin); + + // Grab all nodes that lay in between skin joints/nodes + DisjointSet<GLTFNodeIndex> disjoint_set; + + Vector<SkinNodeIndex> all_skin_nodes; + all_skin_nodes.append_array(p_skin->joints); + all_skin_nodes.append_array(p_skin->non_joints); + + for (int i = 0; i < all_skin_nodes.size(); ++i) { + const SkinNodeIndex node_index = all_skin_nodes[i]; + const SkinNodeIndex parent = r_nodes[node_index]->parent; + disjoint_set.insert(node_index); + + if (all_skin_nodes.find(parent) >= 0) { + disjoint_set.create_union(parent, node_index); + } + } + + Vector<SkinNodeIndex> out_owners; + disjoint_set.get_representatives(out_owners); + + Vector<SkinNodeIndex> out_roots; + + for (int i = 0; i < out_owners.size(); ++i) { + Vector<SkinNodeIndex> set; + disjoint_set.get_members(set, out_owners[i]); + + const SkinNodeIndex root = _find_highest_node(r_nodes, set); + ERR_FAIL_COND_V(root < 0, FAILED); + out_roots.push_back(root); + } + + out_roots.sort(); + + for (int i = 0; i < out_roots.size(); ++i) { + _capture_nodes_in_skin(r_nodes, p_skin, out_roots[i]); + } + + p_skin->roots = out_roots; + + return OK; +} + +Error SkinTool::_verify_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin) { + // This may seem duplicated from expand_skins, but this is really a sanity check! (so it kinda is) + // In case additional interpolating logic is added to the skins, this will help ensure that you + // do not cause it to self implode into a fiery blaze + + // We are going to re-calculate the root nodes and compare them to the ones saved in the skin, + // then ensure the multiple trees (if they exist) are on the same sublevel + + // Grab all nodes that lay in between skin joints/nodes + DisjointSet<GLTFNodeIndex> disjoint_set; + + Vector<SkinNodeIndex> all_skin_nodes; + all_skin_nodes.append_array(p_skin->joints); + all_skin_nodes.append_array(p_skin->non_joints); + + for (int i = 0; i < all_skin_nodes.size(); ++i) { + const SkinNodeIndex node_index = all_skin_nodes[i]; + const SkinNodeIndex parent = r_nodes[node_index]->parent; + disjoint_set.insert(node_index); + + if (all_skin_nodes.find(parent) >= 0) { + disjoint_set.create_union(parent, node_index); + } + } + + Vector<SkinNodeIndex> out_owners; + disjoint_set.get_representatives(out_owners); + + Vector<SkinNodeIndex> out_roots; + + for (int i = 0; i < out_owners.size(); ++i) { + Vector<SkinNodeIndex> set; + disjoint_set.get_members(set, out_owners[i]); + + const SkinNodeIndex root = _find_highest_node(r_nodes, set); + ERR_FAIL_COND_V(root < 0, FAILED); + out_roots.push_back(root); + } + + out_roots.sort(); + + ERR_FAIL_COND_V(out_roots.is_empty(), FAILED); + + // Make sure the roots are the exact same (they better be) + ERR_FAIL_COND_V(out_roots.size() != p_skin->roots.size(), FAILED); + for (int i = 0; i < out_roots.size(); ++i) { + ERR_FAIL_COND_V(out_roots[i] != p_skin->roots[i], FAILED); + } + + // Single rooted skin? Perfectly ok! + if (out_roots.size() == 1) { + return OK; + } + + // Make sure all parents of a multi-rooted skin are the SAME + const SkinNodeIndex parent = r_nodes[out_roots[0]]->parent; + for (int i = 1; i < out_roots.size(); ++i) { + if (r_nodes[out_roots[i]]->parent != parent) { + return FAILED; + } + } + + return OK; +} + +void SkinTool::_recurse_children( + Vector<Ref<GLTFNode>> &nodes, + const SkinNodeIndex p_node_index, + RBSet<GLTFNodeIndex> &p_all_skin_nodes, + HashSet<GLTFNodeIndex> &p_child_visited_set) { + if (p_child_visited_set.has(p_node_index)) { + return; + } + p_child_visited_set.insert(p_node_index); + + Ref<GLTFNode> current_node = nodes[p_node_index]; + for (int i = 0; i < current_node->children.size(); ++i) { + _recurse_children(nodes, current_node->children[i], p_all_skin_nodes, p_child_visited_set); + } + + // Continue to use 'current_node' for clarity and direct access. + if (current_node->skin < 0 || current_node->mesh < 0 || !current_node->children.is_empty()) { + p_all_skin_nodes.insert(p_node_index); + } +} + +Error SkinTool::_determine_skeletons( + Vector<Ref<GLTFSkin>> &skins, + Vector<Ref<GLTFNode>> &nodes, + Vector<Ref<GLTFSkeleton>> &skeletons, + const Vector<GLTFNodeIndex> &p_single_skeleton_roots) { + if (!p_single_skeleton_roots.is_empty()) { + Ref<GLTFSkin> skin; + skin.instantiate(); + skin->set_name("godot_single_skeleton_root"); + for (GLTFNodeIndex i = 0; i < p_single_skeleton_roots.size(); i++) { + skin->joints.push_back(p_single_skeleton_roots[i]); + } + skins.push_back(skin); + } + + // Using a disjoint set, we are going to potentially combine all skins that are actually branches + // of a main skeleton, or treat skins defining the same set of nodes as ONE skeleton. + // This is another unclear issue caused by the current glTF specification. + + DisjointSet<GLTFNodeIndex> skeleton_sets; + + for (GLTFSkinIndex skin_i = 0; skin_i < skins.size(); ++skin_i) { + const Ref<GLTFSkin> skin = skins[skin_i]; + ERR_CONTINUE(skin.is_null()); + + HashSet<GLTFNodeIndex> child_visited_set; + RBSet<GLTFNodeIndex> all_skin_nodes; + for (int i = 0; i < skin->joints.size(); ++i) { + all_skin_nodes.insert(skin->joints[i]); + SkinTool::_recurse_children(nodes, skin->joints[i], all_skin_nodes, child_visited_set); + } + for (int i = 0; i < skin->non_joints.size(); ++i) { + all_skin_nodes.insert(skin->non_joints[i]); + SkinTool::_recurse_children(nodes, skin->non_joints[i], all_skin_nodes, child_visited_set); + } + for (GLTFNodeIndex node_index : all_skin_nodes) { + const GLTFNodeIndex parent = nodes[node_index]->parent; + skeleton_sets.insert(node_index); + + if (all_skin_nodes.has(parent)) { + skeleton_sets.create_union(parent, node_index); + } + } + + // We are going to connect the separate skin subtrees in each skin together + // so that the final roots are entire sets of valid skin trees + for (int i = 1; i < skin->roots.size(); ++i) { + skeleton_sets.create_union(skin->roots[0], skin->roots[i]); + } + } + + { // attempt to joint all touching subsets (siblings/parent are part of another skin) + Vector<SkinNodeIndex> groups_representatives; + skeleton_sets.get_representatives(groups_representatives); + + Vector<SkinNodeIndex> highest_group_members; + Vector<Vector<SkinNodeIndex>> groups; + for (int i = 0; i < groups_representatives.size(); ++i) { + Vector<SkinNodeIndex> group; + skeleton_sets.get_members(group, groups_representatives[i]); + highest_group_members.push_back(SkinTool::_find_highest_node(nodes, group)); + groups.push_back(group); + } + + for (int i = 0; i < highest_group_members.size(); ++i) { + const SkinNodeIndex node_i = highest_group_members[i]; + + // Attach any siblings together (this needs to be done n^2/2 times) + for (int j = i + 1; j < highest_group_members.size(); ++j) { + const SkinNodeIndex node_j = highest_group_members[j]; + + // Even if they are siblings under the root! :) + if (nodes[node_i]->parent == nodes[node_j]->parent) { + skeleton_sets.create_union(node_i, node_j); + } + } + + // Attach any parenting going on together (we need to do this n^2 times) + const SkinNodeIndex node_i_parent = nodes[node_i]->parent; + if (node_i_parent >= 0) { + for (int j = 0; j < groups.size() && i != j; ++j) { + const Vector<SkinNodeIndex> &group = groups[j]; + + if (group.find(node_i_parent) >= 0) { + const SkinNodeIndex node_j = highest_group_members[j]; + skeleton_sets.create_union(node_i, node_j); + } + } + } + } + } + + // At this point, the skeleton groups should be finalized + Vector<SkinNodeIndex> skeleton_owners; + skeleton_sets.get_representatives(skeleton_owners); + + // Mark all the skins actual skeletons, after we have merged them + for (SkinSkeletonIndex skel_i = 0; skel_i < skeleton_owners.size(); ++skel_i) { + const SkinNodeIndex skeleton_owner = skeleton_owners[skel_i]; + Ref<GLTFSkeleton> skeleton; + skeleton.instantiate(); + + Vector<SkinNodeIndex> skeleton_nodes; + skeleton_sets.get_members(skeleton_nodes, skeleton_owner); + + for (GLTFSkinIndex skin_i = 0; skin_i < skins.size(); ++skin_i) { + Ref<GLTFSkin> skin = skins.write[skin_i]; + + // If any of the the skeletons nodes exist in a skin, that skin now maps to the skeleton + for (int i = 0; i < skeleton_nodes.size(); ++i) { + SkinNodeIndex skel_node_i = skeleton_nodes[i]; + if (skin->joints.find(skel_node_i) >= 0 || skin->non_joints.find(skel_node_i) >= 0) { + skin->skeleton = skel_i; + continue; + } + } + } + + Vector<SkinNodeIndex> non_joints; + for (int i = 0; i < skeleton_nodes.size(); ++i) { + const SkinNodeIndex node_i = skeleton_nodes[i]; + + if (nodes[node_i]->joint) { + skeleton->joints.push_back(node_i); + } else { + non_joints.push_back(node_i); + } + } + + skeletons.push_back(skeleton); + + SkinTool::_reparent_non_joint_skeleton_subtrees(nodes, skeletons.write[skel_i], non_joints); + } + + for (SkinSkeletonIndex skel_i = 0; skel_i < skeletons.size(); ++skel_i) { + Ref<GLTFSkeleton> skeleton = skeletons.write[skel_i]; + + for (int i = 0; i < skeleton->joints.size(); ++i) { + const SkinNodeIndex node_i = skeleton->joints[i]; + Ref<GLTFNode> node = nodes[node_i]; + + ERR_FAIL_COND_V(!node->joint, ERR_PARSE_ERROR); + ERR_FAIL_COND_V(node->skeleton >= 0, ERR_PARSE_ERROR); + node->skeleton = skel_i; + } + + ERR_FAIL_COND_V(SkinTool::_determine_skeleton_roots(nodes, skeletons, skel_i), ERR_PARSE_ERROR); + } + + return OK; +} + +Error SkinTool::_reparent_non_joint_skeleton_subtrees( + Vector<Ref<GLTFNode>> &nodes, + Ref<GLTFSkeleton> p_skeleton, + const Vector<SkinNodeIndex> &p_non_joints) { + DisjointSet<GLTFNodeIndex> subtree_set; + + // Populate the disjoint set with ONLY non joints that are in the skeleton hierarchy (non_joints vector) + // This way we can find any joints that lie in between joints, as the current glTF specification + // mentions nothing about non-joints being in between joints of the same skin. Hopefully one day we + // can remove this code. + + // skinD depicted here explains this issue: + // https://github.com/KhronosGroup/glTF-Asset-Generator/blob/master/Output/Positive/Animation_Skin + + for (int i = 0; i < p_non_joints.size(); ++i) { + const SkinNodeIndex node_i = p_non_joints[i]; + + subtree_set.insert(node_i); + + const SkinNodeIndex parent_i = nodes[node_i]->parent; + if (parent_i >= 0 && p_non_joints.find(parent_i) >= 0 && !nodes[parent_i]->joint) { + subtree_set.create_union(parent_i, node_i); + } + } + + // Find all the non joint subtrees and re-parent them to a new "fake" joint + + Vector<SkinNodeIndex> non_joint_subtree_roots; + subtree_set.get_representatives(non_joint_subtree_roots); + + for (int root_i = 0; root_i < non_joint_subtree_roots.size(); ++root_i) { + const SkinNodeIndex subtree_root = non_joint_subtree_roots[root_i]; + + Vector<SkinNodeIndex> subtree_nodes; + subtree_set.get_members(subtree_nodes, subtree_root); + + for (int subtree_i = 0; subtree_i < subtree_nodes.size(); ++subtree_i) { + Ref<GLTFNode> node = nodes[subtree_nodes[subtree_i]]; + node->joint = true; + // Add the joint to the skeletons joints + p_skeleton->joints.push_back(subtree_nodes[subtree_i]); + } + } + + return OK; +} + +Error SkinTool::_determine_skeleton_roots( + Vector<Ref<GLTFNode>> &nodes, + Vector<Ref<GLTFSkeleton>> &skeletons, + const SkinSkeletonIndex p_skel_i) { + DisjointSet<GLTFNodeIndex> disjoint_set; + + for (SkinNodeIndex i = 0; i < nodes.size(); ++i) { + const Ref<GLTFNode> node = nodes[i]; + + if (node->skeleton != p_skel_i) { + continue; + } + + disjoint_set.insert(i); + + if (node->parent >= 0 && nodes[node->parent]->skeleton == p_skel_i) { + disjoint_set.create_union(node->parent, i); + } + } + + Ref<GLTFSkeleton> skeleton = skeletons.write[p_skel_i]; + + Vector<SkinNodeIndex> representatives; + disjoint_set.get_representatives(representatives); + + Vector<SkinNodeIndex> roots; + + for (int i = 0; i < representatives.size(); ++i) { + Vector<SkinNodeIndex> set; + disjoint_set.get_members(set, representatives[i]); + const SkinNodeIndex root = _find_highest_node(nodes, set); + ERR_FAIL_COND_V(root < 0, FAILED); + roots.push_back(root); + } + + roots.sort(); + + skeleton->roots = roots; + + if (roots.size() == 0) { + return FAILED; + } else if (roots.size() == 1) { + return OK; + } + + // Check that the subtrees have the same parent root + const SkinNodeIndex parent = nodes[roots[0]]->parent; + for (int i = 1; i < roots.size(); ++i) { + if (nodes[roots[i]]->parent != parent) { + return FAILED; + } + } + + return OK; +} + +Error SkinTool::_create_skeletons( + HashSet<String> &unique_names, + Vector<Ref<GLTFSkin>> &skins, + Vector<Ref<GLTFNode>> &nodes, + HashMap<ObjectID, GLTFSkeletonIndex> &skeleton3d_to_gltf_skeleton, + Vector<Ref<GLTFSkeleton>> &skeletons, + HashMap<GLTFNodeIndex, Node *> &scene_nodes) { + for (SkinSkeletonIndex skel_i = 0; skel_i < skeletons.size(); ++skel_i) { + Ref<GLTFSkeleton> gltf_skeleton = skeletons.write[skel_i]; + + Skeleton3D *skeleton = memnew(Skeleton3D); + gltf_skeleton->godot_skeleton = skeleton; + skeleton3d_to_gltf_skeleton[skeleton->get_instance_id()] = skel_i; + + // Make a unique name, no gltf node represents this skeleton + skeleton->set_name("Skeleton3D"); + + List<GLTFNodeIndex> bones; + + for (int i = 0; i < gltf_skeleton->roots.size(); ++i) { + bones.push_back(gltf_skeleton->roots[i]); + } + + // Make the skeleton creation deterministic by going through the roots in + // a sorted order, and DEPTH FIRST + bones.sort(); + + while (!bones.is_empty()) { + const SkinNodeIndex node_i = bones.front()->get(); + bones.pop_front(); + + Ref<GLTFNode> node = nodes[node_i]; + ERR_FAIL_COND_V(node->skeleton != skel_i, FAILED); + + { // Add all child nodes to the stack (deterministically) + Vector<SkinNodeIndex> child_nodes; + for (int i = 0; i < node->children.size(); ++i) { + const SkinNodeIndex child_i = node->children[i]; + if (nodes[child_i]->skeleton == skel_i) { + child_nodes.push_back(child_i); + } + } + + // Depth first insertion + child_nodes.sort(); + for (int i = child_nodes.size() - 1; i >= 0; --i) { + bones.push_front(child_nodes[i]); + } + } + + const int bone_index = skeleton->get_bone_count(); + + if (node->get_name().is_empty()) { + node->set_name("bone"); + } + + node->set_name(_gen_unique_bone_name(unique_names, node->get_name())); + + skeleton->add_bone(node->get_name()); + Transform3D rest_transform = node->get_additional_data("GODOT_rest_transform"); + skeleton->set_bone_rest(bone_index, rest_transform); + skeleton->set_bone_pose_position(bone_index, node->transform.origin); + skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion()); + skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale()); + + if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) { + const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name()); + ERR_FAIL_COND_V(bone_parent < 0, FAILED); + skeleton->set_bone_parent(bone_index, skeleton->find_bone(nodes[node->parent]->get_name())); + } + + scene_nodes.insert(node_i, skeleton); + } + } + + ERR_FAIL_COND_V(_map_skin_joints_indices_to_skeleton_bone_indices(skins, skeletons, nodes), ERR_PARSE_ERROR); + + return OK; +} + +Error SkinTool::_map_skin_joints_indices_to_skeleton_bone_indices( + Vector<Ref<GLTFSkin>> &skins, + Vector<Ref<GLTFSkeleton>> &skeletons, + Vector<Ref<GLTFNode>> &nodes) { + for (GLTFSkinIndex skin_i = 0; skin_i < skins.size(); ++skin_i) { + Ref<GLTFSkin> skin = skins.write[skin_i]; + ERR_CONTINUE(skin.is_null()); + + Ref<GLTFSkeleton> skeleton = skeletons[skin->skeleton]; + + for (int joint_index = 0; joint_index < skin->joints_original.size(); ++joint_index) { + const SkinNodeIndex node_i = skin->joints_original[joint_index]; + const Ref<GLTFNode> node = nodes[node_i]; + + const int bone_index = skeleton->godot_skeleton->find_bone(node->get_name()); + ERR_FAIL_COND_V(bone_index < 0, FAILED); + + skin->joint_i_to_bone_i.insert(joint_index, bone_index); + } + } + + return OK; +} + +Error SkinTool::_create_skins(Vector<Ref<GLTFSkin>> &skins, Vector<Ref<GLTFNode>> &nodes, bool use_named_skin_binds, HashSet<String> &unique_names) { + for (GLTFSkinIndex skin_i = 0; skin_i < skins.size(); ++skin_i) { + Ref<GLTFSkin> gltf_skin = skins.write[skin_i]; + ERR_CONTINUE(gltf_skin.is_null()); + + Ref<Skin> skin; + skin.instantiate(); + + // Some skins don't have IBM's! What absolute monsters! + const bool has_ibms = !gltf_skin->inverse_binds.is_empty(); + + for (int joint_i = 0; joint_i < gltf_skin->joints_original.size(); ++joint_i) { + SkinNodeIndex node = gltf_skin->joints_original[joint_i]; + String bone_name = nodes[node]->get_name(); + + Transform3D xform; + if (has_ibms) { + xform = gltf_skin->inverse_binds[joint_i]; + } + + if (use_named_skin_binds) { + skin->add_named_bind(bone_name, xform); + } else { + int32_t bone_i = gltf_skin->joint_i_to_bone_i[joint_i]; + skin->add_bind(bone_i, xform); + } + } + + gltf_skin->godot_skin = skin; + } + + // Purge the duplicates! + _remove_duplicate_skins(skins); + + // Create unique names now, after removing duplicates + for (GLTFSkinIndex skin_i = 0; skin_i < skins.size(); ++skin_i) { + ERR_CONTINUE(skins.get(skin_i).is_null()); + Ref<Skin> skin = skins.write[skin_i]->godot_skin; + ERR_CONTINUE(skin.is_null()); + if (skin->get_name().is_empty()) { + // Make a unique name, no node represents this skin + skin->set_name(_gen_unique_name(unique_names, "Skin")); + } + } + + return OK; +} + +// FIXME: Duplicated from FBXDocument, very similar code in GLTFDocument too, +// and even below in this class for bone names. +String SkinTool::_gen_unique_name(HashSet<String> &unique_names, const String &p_name) { + const String s_name = p_name.validate_node_name(); + + String u_name; + int index = 1; + while (true) { + u_name = s_name; + + if (index > 1) { + u_name += itos(index); + } + if (!unique_names.has(u_name)) { + break; + } + index++; + } + + unique_names.insert(u_name); + + return u_name; +} + +bool SkinTool::_skins_are_same(const Ref<Skin> p_skin_a, const Ref<Skin> p_skin_b) { + if (p_skin_a->get_bind_count() != p_skin_b->get_bind_count()) { + return false; + } + + for (int i = 0; i < p_skin_a->get_bind_count(); ++i) { + if (p_skin_a->get_bind_bone(i) != p_skin_b->get_bind_bone(i)) { + return false; + } + if (p_skin_a->get_bind_name(i) != p_skin_b->get_bind_name(i)) { + return false; + } + + Transform3D a_xform = p_skin_a->get_bind_pose(i); + Transform3D b_xform = p_skin_b->get_bind_pose(i); + + if (a_xform != b_xform) { + return false; + } + } + + return true; +} + +void SkinTool::_remove_duplicate_skins(Vector<Ref<GLTFSkin>> &r_skins) { + for (int i = 0; i < r_skins.size(); ++i) { + for (int j = i + 1; j < r_skins.size(); ++j) { + const Ref<Skin> skin_i = r_skins[i]->godot_skin; + const Ref<Skin> skin_j = r_skins[j]->godot_skin; + + if (_skins_are_same(skin_i, skin_j)) { + // replace it and delete the old + r_skins.write[j]->godot_skin = skin_i; + } + } + } +} + +String SkinTool::_gen_unique_bone_name(HashSet<String> &r_unique_names, const String &p_name) { + String s_name = _sanitize_bone_name(p_name); + if (s_name.is_empty()) { + s_name = "bone"; + } + String u_name; + int index = 1; + while (true) { + u_name = s_name; + + if (index > 1) { + u_name += "_" + itos(index); + } + if (!r_unique_names.has(u_name)) { + break; + } + index++; + } + + r_unique_names.insert(u_name); + + return u_name; +} + +Error SkinTool::_asset_parse_skins( + const Vector<SkinNodeIndex> &input_skin_indices, + const Vector<Ref<GLTFSkin>> &input_skins, + const Vector<Ref<GLTFNode>> &input_nodes, + Vector<SkinNodeIndex> &output_skin_indices, + Vector<Ref<GLTFSkin>> &output_skins, + HashMap<GLTFNodeIndex, bool> &joint_mapping) { + output_skin_indices.clear(); + output_skins.clear(); + joint_mapping.clear(); + + for (int i = 0; i < input_skin_indices.size(); ++i) { + SkinNodeIndex skin_index = input_skin_indices[i]; + if (skin_index >= 0 && skin_index < input_skins.size()) { + output_skin_indices.push_back(skin_index); + output_skins.push_back(input_skins[skin_index]); + Ref<GLTFSkin> skin = input_skins[skin_index]; + Vector<SkinNodeIndex> skin_joints = skin->get_joints(); + for (int j = 0; j < skin_joints.size(); ++j) { + SkinNodeIndex joint_index = skin_joints[j]; + joint_mapping[joint_index] = true; + } + } + } + + return OK; +} + +String SkinTool::_sanitize_bone_name(const String &p_name) { + String bone_name = p_name; + bone_name = bone_name.replace(":", "_"); + bone_name = bone_name.replace("/", "_"); + return bone_name; +} diff --git a/modules/gltf/skin_tool.h b/modules/gltf/skin_tool.h new file mode 100644 index 0000000000..1ba95853f3 --- /dev/null +++ b/modules/gltf/skin_tool.h @@ -0,0 +1,99 @@ +/**************************************************************************/ +/* skin_tool.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef SKIN_TOOL_H +#define SKIN_TOOL_H + +#include "gltf_defines.h" + +#include "structures/gltf_node.h" +#include "structures/gltf_skeleton.h" +#include "structures/gltf_skin.h" + +#include "core/math/disjoint_set.h" +#include "core/templates/rb_set.h" + +using SkinNodeIndex = int; +using SkinSkeletonIndex = int; + +class SkinTool { +public: + static String _sanitize_bone_name(const String &p_name); + static String _gen_unique_bone_name(HashSet<String> &r_unique_names, const String &p_name); + static SkinNodeIndex _find_highest_node(Vector<Ref<GLTFNode>> &r_nodes, const Vector<SkinNodeIndex> &p_subset); + static bool _capture_nodes_in_skin(const Vector<Ref<GLTFNode>> &p_nodes, Ref<GLTFSkin> p_skin, const SkinNodeIndex p_node_index); + static void _capture_nodes_for_multirooted_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin); + static void _recurse_children( + Vector<Ref<GLTFNode>> &r_nodes, + const SkinNodeIndex p_node_index, + RBSet<SkinNodeIndex> &r_all_skin_nodes, + HashSet<SkinNodeIndex> &r_child_visited_set); + static Error _reparent_non_joint_skeleton_subtrees( + Vector<Ref<GLTFNode>> &r_nodes, + Ref<GLTFSkeleton> p_skeleton, + const Vector<SkinNodeIndex> &p_non_joints); + static Error _determine_skeleton_roots( + Vector<Ref<GLTFNode>> &r_nodes, + Vector<Ref<GLTFSkeleton>> &r_skeletons, + const SkinSkeletonIndex p_skel_i); + static Error _map_skin_joints_indices_to_skeleton_bone_indices( + Vector<Ref<GLTFSkin>> &r_skins, + Vector<Ref<GLTFSkeleton>> &r_skeletons, + Vector<Ref<GLTFNode>> &r_nodes); + static String _gen_unique_name(HashSet<String> &unique_names, const String &p_name); + static bool _skins_are_same(const Ref<Skin> p_skin_a, const Ref<Skin> p_skin_b); + static void _remove_duplicate_skins(Vector<Ref<GLTFSkin>> &r_skins); + +public: + static Error _expand_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin); + static Error _verify_skin(Vector<Ref<GLTFNode>> &r_nodes, Ref<GLTFSkin> p_skin); + static Error _asset_parse_skins( + const Vector<SkinNodeIndex> &p_input_skin_indices, + const Vector<Ref<GLTFSkin>> &p_input_skins, + const Vector<Ref<GLTFNode>> &p_input_nodes, + Vector<SkinNodeIndex> &r_output_skin_indices, + Vector<Ref<GLTFSkin>> &r_output_skins, + HashMap<GLTFNodeIndex, bool> &r_joint_mapping); + static Error _determine_skeletons( + Vector<Ref<GLTFSkin>> &r_skins, + Vector<Ref<GLTFNode>> &r_nodes, + Vector<Ref<GLTFSkeleton>> &r_skeletons, + const Vector<GLTFNodeIndex> &p_single_skeleton_roots); + static Error _create_skeletons( + HashSet<String> &r_unique_names, + Vector<Ref<GLTFSkin>> &r_skins, + Vector<Ref<GLTFNode>> &r_nodes, + HashMap<ObjectID, GLTFSkeletonIndex> &r_skeleton3d_to_fbx_skeleton, + Vector<Ref<GLTFSkeleton>> &r_skeletons, + HashMap<GLTFNodeIndex, Node *> &r_scene_nodes); + static Error _create_skins(Vector<Ref<GLTFSkin>> &skins, Vector<Ref<GLTFNode>> &nodes, bool use_named_skin_binds, HashSet<String> &unique_names); +}; + +#endif // SKIN_TOOL_H diff --git a/modules/gltf/structures/gltf_animation.cpp b/modules/gltf/structures/gltf_animation.cpp index fc2e8ee148..94fda8e2f5 100644 --- a/modules/gltf/structures/gltf_animation.cpp +++ b/modules/gltf/structures/gltf_animation.cpp @@ -31,12 +31,25 @@ #include "gltf_animation.h" void GLTFAnimation::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_original_name"), &GLTFAnimation::get_original_name); + ClassDB::bind_method(D_METHOD("set_original_name", "original_name"), &GLTFAnimation::set_original_name); ClassDB::bind_method(D_METHOD("get_loop"), &GLTFAnimation::get_loop); ClassDB::bind_method(D_METHOD("set_loop", "loop"), &GLTFAnimation::set_loop); + ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFAnimation::get_additional_data); + ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFAnimation::set_additional_data); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "original_name"), "set_original_name", "get_original_name"); // String ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "get_loop"); // bool } +String GLTFAnimation::get_original_name() { + return original_name; +} + +void GLTFAnimation::set_original_name(String p_name) { + original_name = p_name; +} + bool GLTFAnimation::get_loop() const { return loop; } @@ -51,3 +64,11 @@ HashMap<int, GLTFAnimation::Track> &GLTFAnimation::get_tracks() { GLTFAnimation::GLTFAnimation() { } + +Variant GLTFAnimation::get_additional_data(const StringName &p_extension_name) { + return additional_data[p_extension_name]; +} + +void GLTFAnimation::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { + additional_data[p_extension_name] = p_additional_data; +} diff --git a/modules/gltf/structures/gltf_animation.h b/modules/gltf/structures/gltf_animation.h index 33dc840eb3..7f769752c2 100644 --- a/modules/gltf/structures/gltf_animation.h +++ b/modules/gltf/structures/gltf_animation.h @@ -49,7 +49,7 @@ public: template <class T> struct Channel { - Interpolation interpolation; + Interpolation interpolation = INTERP_LINEAR; Vector<real_t> times; Vector<T> values; }; @@ -62,14 +62,21 @@ public: }; public: + String get_original_name(); + void set_original_name(String p_name); + bool get_loop() const; void set_loop(bool p_val); HashMap<int, GLTFAnimation::Track> &get_tracks(); + Variant get_additional_data(const StringName &p_extension_name); + void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); GLTFAnimation(); private: + String original_name; bool loop = false; HashMap<int, Track> tracks; + Dictionary additional_data; }; #endif // GLTF_ANIMATION_H diff --git a/modules/gltf/structures/gltf_buffer_view.compat.inc b/modules/gltf/structures/gltf_buffer_view.compat.inc new file mode 100644 index 0000000000..db2600a071 --- /dev/null +++ b/modules/gltf/structures/gltf_buffer_view.compat.inc @@ -0,0 +1,61 @@ +/**************************************************************************/ +/* gltf_buffer_view.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +GLTFBufferIndex GLTFBufferView::_get_buffer_bind_compat_86907() { + return get_buffer(); +} + +int GLTFBufferView::_get_byte_offset_bind_compat_86907() { + return get_byte_offset(); +} + +int GLTFBufferView::_get_byte_length_bind_compat_86907() { + return get_byte_length(); +} + +int GLTFBufferView::_get_byte_stride_bind_compat_86907() { + return get_byte_stride(); +} + +bool GLTFBufferView::_get_indices_bind_compat_86907() { + return get_indices(); +} + +void GLTFBufferView::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("get_buffer"), &GLTFBufferView::_get_buffer_bind_compat_86907); + ClassDB::bind_compatibility_method(D_METHOD("get_byte_offset"), &GLTFBufferView::_get_byte_offset_bind_compat_86907); + ClassDB::bind_compatibility_method(D_METHOD("get_byte_length"), &GLTFBufferView::_get_byte_length_bind_compat_86907); + ClassDB::bind_compatibility_method(D_METHOD("get_byte_stride"), &GLTFBufferView::_get_byte_stride_bind_compat_86907); + ClassDB::bind_compatibility_method(D_METHOD("get_indices"), &GLTFBufferView::_get_indices_bind_compat_86907); +} + +#endif // DISABLE_DEPRECATED diff --git a/modules/gltf/structures/gltf_buffer_view.cpp b/modules/gltf/structures/gltf_buffer_view.cpp index 7678f23f57..997c219bf0 100644 --- a/modules/gltf/structures/gltf_buffer_view.cpp +++ b/modules/gltf/structures/gltf_buffer_view.cpp @@ -29,8 +29,13 @@ /**************************************************************************/ #include "gltf_buffer_view.h" +#include "gltf_buffer_view.compat.inc" + +#include "../gltf_state.h" void GLTFBufferView::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_buffer_view_data", "state"), &GLTFBufferView::load_buffer_view_data); + ClassDB::bind_method(D_METHOD("get_buffer"), &GLTFBufferView::get_buffer); ClassDB::bind_method(D_METHOD("set_buffer", "buffer"), &GLTFBufferView::set_buffer); ClassDB::bind_method(D_METHOD("get_byte_offset"), &GLTFBufferView::get_byte_offset); @@ -49,7 +54,7 @@ void GLTFBufferView::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indices"), "set_indices", "get_indices"); // bool } -GLTFBufferIndex GLTFBufferView::get_buffer() { +GLTFBufferIndex GLTFBufferView::get_buffer() const { return buffer; } @@ -57,7 +62,7 @@ void GLTFBufferView::set_buffer(GLTFBufferIndex p_buffer) { buffer = p_buffer; } -int GLTFBufferView::get_byte_offset() { +int GLTFBufferView::get_byte_offset() const { return byte_offset; } @@ -65,7 +70,7 @@ void GLTFBufferView::set_byte_offset(int p_byte_offset) { byte_offset = p_byte_offset; } -int GLTFBufferView::get_byte_length() { +int GLTFBufferView::get_byte_length() const { return byte_length; } @@ -73,7 +78,7 @@ void GLTFBufferView::set_byte_length(int p_byte_length) { byte_length = p_byte_length; } -int GLTFBufferView::get_byte_stride() { +int GLTFBufferView::get_byte_stride() const { return byte_stride; } @@ -81,10 +86,20 @@ void GLTFBufferView::set_byte_stride(int p_byte_stride) { byte_stride = p_byte_stride; } -bool GLTFBufferView::get_indices() { +bool GLTFBufferView::get_indices() const { return indices; } void GLTFBufferView::set_indices(bool p_indices) { indices = p_indices; } + +Vector<uint8_t> GLTFBufferView::load_buffer_view_data(const Ref<GLTFState> p_state) const { + ERR_FAIL_COND_V(p_state.is_null(), Vector<uint8_t>()); + ERR_FAIL_COND_V_MSG(byte_stride > 0, Vector<uint8_t>(), "Buffer views with byte stride are not yet supported by this method."); + const TypedArray<Vector<uint8_t>> &buffers = p_state->get_buffers(); + ERR_FAIL_INDEX_V(buffer, buffers.size(), Vector<uint8_t>()); + const PackedByteArray &buffer_data = buffers[buffer]; + const int64_t byte_end = byte_offset + byte_length; + return buffer_data.slice(byte_offset, byte_end); +} diff --git a/modules/gltf/structures/gltf_buffer_view.h b/modules/gltf/structures/gltf_buffer_view.h index 6d138dbf11..1c7bd5c5c7 100644 --- a/modules/gltf/structures/gltf_buffer_view.h +++ b/modules/gltf/structures/gltf_buffer_view.h @@ -49,22 +49,32 @@ private: protected: static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + GLTFBufferIndex _get_buffer_bind_compat_86907(); + int _get_byte_offset_bind_compat_86907(); + int _get_byte_length_bind_compat_86907(); + int _get_byte_stride_bind_compat_86907(); + bool _get_indices_bind_compat_86907(); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + public: - GLTFBufferIndex get_buffer(); + GLTFBufferIndex get_buffer() const; void set_buffer(GLTFBufferIndex p_buffer); - int get_byte_offset(); + int get_byte_offset() const; void set_byte_offset(int p_byte_offset); - int get_byte_length(); + int get_byte_length() const; void set_byte_length(int p_byte_length); - int get_byte_stride(); + int get_byte_stride() const; void set_byte_stride(int p_byte_stride); - bool get_indices(); + bool get_indices() const; void set_indices(bool p_indices); - // matrices need to be transformed to this + + Vector<uint8_t> load_buffer_view_data(const Ref<GLTFState> p_state) const; }; #endif // GLTF_BUFFER_VIEW_H diff --git a/modules/gltf/structures/gltf_camera.h b/modules/gltf/structures/gltf_camera.h index 2a077a6da0..ef55b06a76 100644 --- a/modules/gltf/structures/gltf_camera.h +++ b/modules/gltf/structures/gltf_camera.h @@ -69,7 +69,7 @@ public: Camera3D *to_node() const; static Ref<GLTFCamera> from_dictionary(const Dictionary p_dictionary); - Dictionary to_dictionary() const; + virtual Dictionary to_dictionary() const; }; #endif // GLTF_CAMERA_H diff --git a/modules/gltf/structures/gltf_mesh.cpp b/modules/gltf/structures/gltf_mesh.cpp index 2f8f208d57..9566cc2379 100644 --- a/modules/gltf/structures/gltf_mesh.cpp +++ b/modules/gltf/structures/gltf_mesh.cpp @@ -30,21 +30,34 @@ #include "gltf_mesh.h" -#include "scene/resources/importer_mesh.h" +#include "scene/resources/3d/importer_mesh.h" void GLTFMesh::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_original_name"), &GLTFMesh::get_original_name); + ClassDB::bind_method(D_METHOD("set_original_name", "original_name"), &GLTFMesh::set_original_name); ClassDB::bind_method(D_METHOD("get_mesh"), &GLTFMesh::get_mesh); ClassDB::bind_method(D_METHOD("set_mesh", "mesh"), &GLTFMesh::set_mesh); ClassDB::bind_method(D_METHOD("get_blend_weights"), &GLTFMesh::get_blend_weights); ClassDB::bind_method(D_METHOD("set_blend_weights", "blend_weights"), &GLTFMesh::set_blend_weights); ClassDB::bind_method(D_METHOD("get_instance_materials"), &GLTFMesh::get_instance_materials); ClassDB::bind_method(D_METHOD("set_instance_materials", "instance_materials"), &GLTFMesh::set_instance_materials); + ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFMesh::get_additional_data); + ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFMesh::set_additional_data); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "original_name"), "set_original_name", "get_original_name"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "mesh"), "set_mesh", "get_mesh"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "blend_weights"), "set_blend_weights", "get_blend_weights"); // Vector<float> ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "instance_materials"), "set_instance_materials", "get_instance_materials"); } +String GLTFMesh::get_original_name() { + return original_name; +} + +void GLTFMesh::set_original_name(String p_name) { + original_name = p_name; +} + Ref<ImporterMesh> GLTFMesh::get_mesh() { return mesh; } @@ -68,3 +81,11 @@ Vector<float> GLTFMesh::get_blend_weights() { void GLTFMesh::set_blend_weights(Vector<float> p_blend_weights) { blend_weights = p_blend_weights; } + +Variant GLTFMesh::get_additional_data(const StringName &p_extension_name) { + return additional_data[p_extension_name]; +} + +void GLTFMesh::set_additional_data(const StringName &p_extension_name, Variant p_additional_data) { + additional_data[p_extension_name] = p_additional_data; +} diff --git a/modules/gltf/structures/gltf_mesh.h b/modules/gltf/structures/gltf_mesh.h index 639f28980e..6983efeb2a 100644 --- a/modules/gltf/structures/gltf_mesh.h +++ b/modules/gltf/structures/gltf_mesh.h @@ -33,26 +33,32 @@ #include "../gltf_defines.h" -#include "scene/resources/importer_mesh.h" +#include "scene/resources/3d/importer_mesh.h" class GLTFMesh : public Resource { GDCLASS(GLTFMesh, Resource); private: + String original_name; Ref<ImporterMesh> mesh; Vector<float> blend_weights; TypedArray<Material> instance_materials; + Dictionary additional_data; protected: static void _bind_methods(); public: + String get_original_name(); + void set_original_name(String p_name); Ref<ImporterMesh> get_mesh(); void set_mesh(Ref<ImporterMesh> p_mesh); Vector<float> get_blend_weights(); void set_blend_weights(Vector<float> p_blend_weights); TypedArray<Material> get_instance_materials(); void set_instance_materials(TypedArray<Material> p_instance_materials); + Variant get_additional_data(const StringName &p_extension_name); + void set_additional_data(const StringName &p_extension_name, Variant p_additional_data); }; #endif // GLTF_MESH_H diff --git a/modules/gltf/structures/gltf_node.cpp b/modules/gltf/structures/gltf_node.cpp index 30895034a9..2934e4b5ee 100644 --- a/modules/gltf/structures/gltf_node.cpp +++ b/modules/gltf/structures/gltf_node.cpp @@ -31,6 +31,8 @@ #include "gltf_node.h" void GLTFNode::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_original_name"), &GLTFNode::get_original_name); + ClassDB::bind_method(D_METHOD("set_original_name", "original_name"), &GLTFNode::set_original_name); ClassDB::bind_method(D_METHOD("get_parent"), &GLTFNode::get_parent); ClassDB::bind_method(D_METHOD("set_parent", "parent"), &GLTFNode::set_parent); ClassDB::bind_method(D_METHOD("get_height"), &GLTFNode::get_height); @@ -58,6 +60,7 @@ void GLTFNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_additional_data", "extension_name"), &GLTFNode::get_additional_data); ClassDB::bind_method(D_METHOD("set_additional_data", "extension_name", "additional_data"), &GLTFNode::set_additional_data); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "original_name"), "set_original_name", "get_original_name"); // String ADD_PROPERTY(PropertyInfo(Variant::INT, "parent"), "set_parent", "get_parent"); // GLTFNodeIndex ADD_PROPERTY(PropertyInfo(Variant::INT, "height"), "set_height", "get_height"); // int ADD_PROPERTY(PropertyInfo(Variant::TRANSFORM3D, "xform"), "set_xform", "get_xform"); // Transform3D @@ -72,6 +75,13 @@ void GLTFNode::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "light"), "set_light", "get_light"); // GLTFLightIndex } +String GLTFNode::get_original_name() { + return original_name; +} +void GLTFNode::set_original_name(String p_name) { + original_name = p_name; +} + GLTFNodeIndex GLTFNode::get_parent() { return parent; } @@ -89,11 +99,11 @@ void GLTFNode::set_height(int p_height) { } Transform3D GLTFNode::get_xform() { - return xform; + return transform; } void GLTFNode::set_xform(Transform3D p_xform) { - xform = p_xform; + transform = p_xform; } GLTFMeshIndex GLTFNode::get_mesh() { @@ -129,27 +139,27 @@ void GLTFNode::set_skeleton(GLTFSkeletonIndex p_skeleton) { } Vector3 GLTFNode::get_position() { - return position; + return transform.origin; } void GLTFNode::set_position(Vector3 p_position) { - position = p_position; + transform.origin = p_position; } Quaternion GLTFNode::get_rotation() { - return rotation; + return transform.basis.get_rotation_quaternion(); } void GLTFNode::set_rotation(Quaternion p_rotation) { - rotation = p_rotation; + transform.basis.set_quaternion_scale(p_rotation, transform.basis.get_scale()); } Vector3 GLTFNode::get_scale() { - return scale; + return transform.basis.get_scale(); } void GLTFNode::set_scale(Vector3 p_scale) { - scale = p_scale; + transform.basis = transform.basis.orthonormalized() * Basis::from_scale(p_scale); } Vector<int> GLTFNode::get_children() { diff --git a/modules/gltf/structures/gltf_node.h b/modules/gltf/structures/gltf_node.h index c2d2f64495..63399fb32b 100644 --- a/modules/gltf/structures/gltf_node.h +++ b/modules/gltf/structures/gltf_node.h @@ -38,20 +38,19 @@ class GLTFNode : public Resource { GDCLASS(GLTFNode, Resource); friend class GLTFDocument; + friend class SkinTool; + friend class FBXDocument; private: - // matrices need to be transformed to this + String original_name; GLTFNodeIndex parent = -1; int height = -1; - Transform3D xform; + Transform3D transform; GLTFMeshIndex mesh = -1; GLTFCameraIndex camera = -1; GLTFSkinIndex skin = -1; GLTFSkeletonIndex skeleton = -1; bool joint = false; - Vector3 position; - Quaternion rotation; - Vector3 scale = Vector3(1, 1, 1); Vector<int> children; GLTFLightIndex light = -1; Dictionary additional_data; @@ -60,6 +59,9 @@ protected: static void _bind_methods(); public: + String get_original_name(); + void set_original_name(String p_name); + GLTFNodeIndex get_parent(); void set_parent(GLTFNodeIndex p_parent); @@ -69,6 +71,9 @@ public: Transform3D get_xform(); void set_xform(Transform3D p_xform); + Transform3D get_rest_xform(); + void set_rest_xform(Transform3D p_rest_xform); + GLTFMeshIndex get_mesh(); void set_mesh(GLTFMeshIndex p_mesh); diff --git a/modules/gltf/structures/gltf_skeleton.cpp b/modules/gltf/structures/gltf_skeleton.cpp index b18aa759a5..492af3f3d2 100644 --- a/modules/gltf/structures/gltf_skeleton.cpp +++ b/modules/gltf/structures/gltf_skeleton.cpp @@ -82,11 +82,11 @@ void GLTFSkeleton::set_unique_names(TypedArray<String> p_unique_names) { } Dictionary GLTFSkeleton::get_godot_bone_node() { - return GLTFTemplateConvert::to_dict(godot_bone_node); + return GLTFTemplateConvert::to_dictionary(godot_bone_node); } void GLTFSkeleton::set_godot_bone_node(Dictionary p_indict) { - GLTFTemplateConvert::set_from_dict(godot_bone_node, p_indict); + GLTFTemplateConvert::set_from_dictionary(godot_bone_node, p_indict); } BoneAttachment3D *GLTFSkeleton::get_bone_attachment(int idx) { diff --git a/modules/gltf/structures/gltf_skeleton.h b/modules/gltf/structures/gltf_skeleton.h index 72a4a06e5c..2ed85b0cae 100644 --- a/modules/gltf/structures/gltf_skeleton.h +++ b/modules/gltf/structures/gltf_skeleton.h @@ -34,10 +34,14 @@ #include "../gltf_defines.h" #include "core/io/resource.h" +#include "scene/3d/bone_attachment_3d.h" +#include "scene/3d/skeleton_3d.h" class GLTFSkeleton : public Resource { GDCLASS(GLTFSkeleton, Resource); friend class GLTFDocument; + friend class SkinTool; + friend class FBXDocument; private: // The *synthesized* skeletons joints @@ -70,29 +74,29 @@ public: Skeleton3D *get_godot_skeleton(); // Skeleton *get_godot_skeleton() { - // return this->godot_skeleton; + // return godot_skeleton; // } // void set_godot_skeleton(Skeleton p_*godot_skeleton) { - // this->godot_skeleton = p_godot_skeleton; + // godot_skeleton = p_godot_skeleton; // } TypedArray<String> get_unique_names(); void set_unique_names(TypedArray<String> p_unique_names); //RBMap<int32_t, GLTFNodeIndex> get_godot_bone_node() { - // return this->godot_bone_node; + // return godot_bone_node; //} - //void set_godot_bone_node(RBMap<int32_t, GLTFNodeIndex> p_godot_bone_node) { - // this->godot_bone_node = p_godot_bone_node; + //void set_godot_bone_node(const RBMap<int32_t, GLTFNodeIndex> &p_godot_bone_node) { + // godot_bone_node = p_godot_bone_node; //} Dictionary get_godot_bone_node(); void set_godot_bone_node(Dictionary p_indict); //Dictionary get_godot_bone_node() { - // return VariantConversion::to_dict(this->godot_bone_node); + // return VariantConversion::to_dict(godot_bone_node); //} //void set_godot_bone_node(Dictionary p_indict) { - // VariantConversion::set_from_dict(this->godot_bone_node, p_indict); + // VariantConversion::set_from_dict(godot_bone_node, p_indict); //} BoneAttachment3D *get_bone_attachment(int idx); diff --git a/modules/gltf/structures/gltf_skin.cpp b/modules/gltf/structures/gltf_skin.cpp index 2827bc3e78..18aa90a628 100644 --- a/modules/gltf/structures/gltf_skin.cpp +++ b/modules/gltf/structures/gltf_skin.cpp @@ -33,7 +33,7 @@ #include "../gltf_template_convert.h" #include "core/variant/typed_array.h" -#include "scene/resources/skin.h" +#include "scene/resources/3d/skin.h" void GLTFSkin::_bind_methods() { ClassDB::bind_method(D_METHOD("get_skin_root"), &GLTFSkin::get_skin_root); @@ -126,11 +126,11 @@ void GLTFSkin::set_skeleton(int p_skeleton) { } Dictionary GLTFSkin::get_joint_i_to_bone_i() { - return GLTFTemplateConvert::to_dict(joint_i_to_bone_i); + return GLTFTemplateConvert::to_dictionary(joint_i_to_bone_i); } void GLTFSkin::set_joint_i_to_bone_i(Dictionary p_joint_i_to_bone_i) { - GLTFTemplateConvert::set_from_dict(joint_i_to_bone_i, p_joint_i_to_bone_i); + GLTFTemplateConvert::set_from_dictionary(joint_i_to_bone_i, p_joint_i_to_bone_i); } Dictionary GLTFSkin::get_joint_i_to_name() { @@ -158,3 +158,121 @@ Ref<Skin> GLTFSkin::get_godot_skin() { void GLTFSkin::set_godot_skin(Ref<Skin> p_godot_skin) { godot_skin = p_godot_skin; } + +Error GLTFSkin::from_dictionary(const Dictionary &dict) { + ERR_FAIL_COND_V(!dict.has("skin_root"), ERR_INVALID_DATA); + skin_root = dict["skin_root"]; + + ERR_FAIL_COND_V(!dict.has("joints_original"), ERR_INVALID_DATA); + Array joints_original_array = dict["joints_original"]; + joints_original.clear(); + for (int i = 0; i < joints_original_array.size(); ++i) { + joints_original.push_back(joints_original_array[i]); + } + + ERR_FAIL_COND_V(!dict.has("inverse_binds"), ERR_INVALID_DATA); + Array inverse_binds_array = dict["inverse_binds"]; + inverse_binds.clear(); + for (int i = 0; i < inverse_binds_array.size(); ++i) { + ERR_FAIL_COND_V(inverse_binds_array[i].get_type() != Variant::TRANSFORM3D, ERR_INVALID_DATA); + inverse_binds.push_back(inverse_binds_array[i]); + } + + ERR_FAIL_COND_V(!dict.has("joints"), ERR_INVALID_DATA); + Array joints_array = dict["joints"]; + joints.clear(); + for (int i = 0; i < joints_array.size(); ++i) { + joints.push_back(joints_array[i]); + } + + ERR_FAIL_COND_V(!dict.has("non_joints"), ERR_INVALID_DATA); + Array non_joints_array = dict["non_joints"]; + non_joints.clear(); + for (int i = 0; i < non_joints_array.size(); ++i) { + non_joints.push_back(non_joints_array[i]); + } + + ERR_FAIL_COND_V(!dict.has("roots"), ERR_INVALID_DATA); + Array roots_array = dict["roots"]; + roots.clear(); + for (int i = 0; i < roots_array.size(); ++i) { + roots.push_back(roots_array[i]); + } + + ERR_FAIL_COND_V(!dict.has("skeleton"), ERR_INVALID_DATA); + skeleton = dict["skeleton"]; + + ERR_FAIL_COND_V(!dict.has("joint_i_to_bone_i"), ERR_INVALID_DATA); + Dictionary joint_i_to_bone_i_dict = dict["joint_i_to_bone_i"]; + joint_i_to_bone_i.clear(); + for (int i = 0; i < joint_i_to_bone_i_dict.keys().size(); ++i) { + int key = joint_i_to_bone_i_dict.keys()[i]; + int value = joint_i_to_bone_i_dict[key]; + joint_i_to_bone_i[key] = value; + } + + ERR_FAIL_COND_V(!dict.has("joint_i_to_name"), ERR_INVALID_DATA); + Dictionary joint_i_to_name_dict = dict["joint_i_to_name"]; + joint_i_to_name.clear(); + for (int i = 0; i < joint_i_to_name_dict.keys().size(); ++i) { + int key = joint_i_to_name_dict.keys()[i]; + StringName value = joint_i_to_name_dict[key]; + joint_i_to_name[key] = value; + } + if (dict.has("godot_skin")) { + godot_skin = dict["godot_skin"]; + } + return OK; +} + +Dictionary GLTFSkin::to_dictionary() { + Dictionary dict; + dict["skin_root"] = skin_root; + + Array joints_original_array; + for (int i = 0; i < joints_original.size(); ++i) { + joints_original_array.push_back(joints_original[i]); + } + dict["joints_original"] = joints_original_array; + + Array inverse_binds_array; + for (int i = 0; i < inverse_binds.size(); ++i) { + inverse_binds_array.push_back(inverse_binds[i]); + } + dict["inverse_binds"] = inverse_binds_array; + + Array joints_array; + for (int i = 0; i < joints.size(); ++i) { + joints_array.push_back(joints[i]); + } + dict["joints"] = joints_array; + + Array non_joints_array; + for (int i = 0; i < non_joints.size(); ++i) { + non_joints_array.push_back(non_joints[i]); + } + dict["non_joints"] = non_joints_array; + + Array roots_array; + for (int i = 0; i < roots.size(); ++i) { + roots_array.push_back(roots[i]); + } + dict["roots"] = roots_array; + + dict["skeleton"] = skeleton; + + Dictionary joint_i_to_bone_i_dict; + for (HashMap<int, int>::Iterator E = joint_i_to_bone_i.begin(); E; ++E) { + joint_i_to_bone_i_dict[E->key] = E->value; + } + dict["joint_i_to_bone_i"] = joint_i_to_bone_i_dict; + + Dictionary joint_i_to_name_dict; + for (HashMap<int, StringName>::Iterator E = joint_i_to_name.begin(); E; ++E) { + joint_i_to_name_dict[E->key] = E->value; + } + dict["joint_i_to_name"] = joint_i_to_name_dict; + + dict["godot_skin"] = godot_skin; + return dict; +} diff --git a/modules/gltf/structures/gltf_skin.h b/modules/gltf/structures/gltf_skin.h index 164cabfe12..4649a918e3 100644 --- a/modules/gltf/structures/gltf_skin.h +++ b/modules/gltf/structures/gltf_skin.h @@ -34,6 +34,7 @@ #include "../gltf_defines.h" #include "core/io/resource.h" +#include "scene/resources/3d/skin.h" template <typename T> class TypedArray; @@ -41,6 +42,8 @@ class TypedArray; class GLTFSkin : public Resource { GDCLASS(GLTFSkin, Resource); friend class GLTFDocument; + friend class SkinTool; + friend class FBXDocument; private: // The "skeleton" property defined in the gltf spec. -1 = Scene Root @@ -109,6 +112,9 @@ public: Ref<Skin> get_godot_skin(); void set_godot_skin(Ref<Skin> p_godot_skin); + + Dictionary to_dictionary(); + Error from_dictionary(const Dictionary &dict); }; #endif // GLTF_SKIN_H diff --git a/modules/gridmap/doc_classes/GridMap.xml b/modules/gridmap/doc_classes/GridMap.xml index 3094a7bf80..7aeecef980 100644 --- a/modules/gridmap/doc_classes/GridMap.xml +++ b/modules/gridmap/doc_classes/GridMap.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<class name="GridMap" inherits="Node3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> +<class name="GridMap" inherits="Node3D" keywords="tilemap" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> Node for 3D tile-based maps. </brief_description> @@ -138,11 +138,11 @@ Returns the position of a grid cell in the GridMap's local coordinate space. To convert the returned value into global coordinates, use [method Node3D.to_global]. See also [method map_to_local]. </description> </method> - <method name="resource_changed" is_deprecated="true"> + <method name="resource_changed" deprecated="Use [signal Resource.changed] instead."> <return type="void" /> <param index="0" name="resource" type="Resource" /> <description> - [i]Obsoleted.[/i] Use [signal Resource.changed] instead. + This method does nothing. </description> </method> <method name="set_cell_item"> diff --git a/modules/gridmap/editor/grid_map_editor_plugin.cpp b/modules/gridmap/editor/grid_map_editor_plugin.cpp index f7c01ff840..d53bf7f7ec 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.cpp +++ b/modules/gridmap/editor/grid_map_editor_plugin.cpp @@ -36,11 +36,11 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "editor/editor_node.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/plugins/node_3d_editor_plugin.h" +#include "editor/themes/editor_scale.h" #include "scene/3d/camera_3d.h" #include "scene/gui/dialogs.h" #include "scene/gui/label.h" @@ -1316,6 +1316,7 @@ GridMapEditor::GridMapEditor() { EDITOR_DEF("editors/grid_map/preview_size", 64); mesh_library_palette = memnew(ItemList); + mesh_library_palette->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); add_child(mesh_library_palette); mesh_library_palette->set_v_size_flags(SIZE_EXPAND_FILL); mesh_library_palette->connect("gui_input", callable_mp(this, &GridMapEditor::_mesh_library_palette_input)); @@ -1500,6 +1501,9 @@ GridMapEditor::~GridMapEditor() { void GridMapEditorPlugin::_notification(int p_what) { switch (p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/grid_map")) { + break; + } switch ((int)EDITOR_GET("editors/grid_map/editor_side")) { case 0: { // Left. Node3DEditor::get_singleton()->move_control_to_left_panel(grid_map_editor); diff --git a/modules/gridmap/grid_map.cpp b/modules/gridmap/grid_map.cpp index 6f493f48e3..fb449a67f8 100644 --- a/modules/gridmap/grid_map.cpp +++ b/modules/gridmap/grid_map.cpp @@ -32,11 +32,10 @@ #include "core/core_string_names.h" #include "core/io/marshalls.h" -#include "core/object/message_queue.h" #include "scene/3d/light_3d.h" -#include "scene/resources/mesh_library.h" +#include "scene/resources/3d/mesh_library.h" +#include "scene/resources/3d/primitive_meshes.h" #include "scene/resources/physics_material.h" -#include "scene/resources/primitive_meshes.h" #include "scene/resources/surface_tool.h" #include "scene/scene_string_names.h" #include "servers/navigation_server_3d.h" @@ -975,7 +974,7 @@ void GridMap::_queue_octants_dirty() { return; } - MessageQueue::get_singleton()->push_call(this, "_update_octants_callback"); + callable_mp(this, &GridMap::_update_octants_callback).call_deferred(); awaiting_update = true; } @@ -1082,7 +1081,6 @@ void GridMap::_bind_methods() { ClassDB::bind_method(D_METHOD("local_to_map", "local_position"), &GridMap::local_to_map); ClassDB::bind_method(D_METHOD("map_to_local", "map_position"), &GridMap::map_to_local); - ClassDB::bind_method(D_METHOD("_update_octants_callback"), &GridMap::_update_octants_callback); #ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("resource_changed", "resource"), &GridMap::resource_changed); #endif diff --git a/modules/gridmap/grid_map.h b/modules/gridmap/grid_map.h index e05979efbc..7398a540de 100644 --- a/modules/gridmap/grid_map.h +++ b/modules/gridmap/grid_map.h @@ -32,7 +32,7 @@ #define GRID_MAP_H #include "scene/3d/node_3d.h" -#include "scene/resources/mesh_library.h" +#include "scene/resources/3d/mesh_library.h" #include "scene/resources/multimesh.h" //heh heh, godotsphir!! this shares no code and the design is completely different with previous projects i've done.. @@ -155,7 +155,6 @@ class GridMap : public Node3D { Ref<PhysicsMaterial> physics_material; bool bake_navigation = false; RID map_override; - uint32_t navigation_layers = 1; Transform3D last_transform; diff --git a/modules/jpg/image_loader_jpegd.cpp b/modules/jpg/image_loader_jpegd.cpp index 0b9fcf4455..ada0cd01fa 100644 --- a/modules/jpg/image_loader_jpegd.cpp +++ b/modules/jpg/image_loader_jpegd.cpp @@ -156,8 +156,13 @@ public: static Error _jpgd_save_to_output_stream(jpge::output_stream *p_output_stream, const Ref<Image> &p_img, float p_quality) { ERR_FAIL_COND_V(p_img.is_null() || p_img->is_empty(), ERR_INVALID_PARAMETER); - Ref<Image> image = p_img; + Ref<Image> image = p_img->duplicate(); + if (image->is_compressed()) { + Error error = image->decompress(); + ERR_FAIL_COND_V_MSG(error != OK, error, "Couldn't decompress image."); + } if (image->get_format() != Image::FORMAT_RGB8) { + image = p_img->duplicate(); image->convert(Image::FORMAT_RGB8); } @@ -169,12 +174,16 @@ static Error _jpgd_save_to_output_stream(jpge::output_stream *p_output_stream, c const uint8_t *src_data = image->get_data().ptr(); for (int i = 0; i < image->get_height(); i++) { - enc.process_scanline(&src_data[i * image->get_width() * 3]); + if (!enc.process_scanline(&src_data[i * image->get_width() * 3])) { + return FAILED; + } } - enc.process_scanline(nullptr); - - return OK; + if (enc.process_scanline(nullptr)) { + return OK; + } else { + return FAILED; + } } static Vector<uint8_t> _jpgd_buffer_save_func(const Ref<Image> &p_img, float p_quality) { diff --git a/modules/ktx/SCsub b/modules/ktx/SCsub index b160f7a287..acdb829979 100644 --- a/modules/ktx/SCsub +++ b/modules/ktx/SCsub @@ -16,10 +16,12 @@ thirdparty_sources = [ "lib/filestream.c", "lib/hashlist.c", "lib/memstream.c", + "lib/miniz_wrapper.cpp", "lib/swap.c", "lib/texture.c", "lib/texture1.c", "lib/texture2.c", + "lib/vkformat_check.c", "lib/dfdutils/createdfd.c", "lib/dfdutils/colourspaces.c", "lib/dfdutils/interpretdfd.c", @@ -33,7 +35,11 @@ env_ktx.Prepend(CPPPATH=[thirdparty_dir + "include"]) env_ktx.Prepend(CPPPATH=[thirdparty_dir + "utils"]) env_ktx.Prepend(CPPPATH=[thirdparty_dir + "lib"]) env_ktx.Prepend(CPPPATH=[thirdparty_dir + "other_include"]) + env_ktx.Prepend(CPPPATH=["#thirdparty/basis_universal"]) +if env.editor_build: + # We already build miniz in the basis_universal module (editor only). + env_ktx.Append(CPPDEFINES=["MINIZ_HEADER_FILE_ONLY"]) if env["vulkan"]: env_ktx.Prepend(CPPPATH=["#thirdparty/vulkan/include"]) diff --git a/modules/ktx/texture_loader_ktx.cpp b/modules/ktx/texture_loader_ktx.cpp index 155ed56bd0..026c0ce510 100644 --- a/modules/ktx/texture_loader_ktx.cpp +++ b/modules/ktx/texture_loader_ktx.cpp @@ -287,7 +287,7 @@ static Ref<Image> load_from_file_access(Ref<FileAccess> f, Error *r_error) { ktxfmt = KTX_TTF_BC7_RGBA; } else if (RS::get_singleton()->has_os_feature("s3tc")) { ktxfmt = KTX_TTF_BC1_RGB; - } else if (RS::get_singleton()->has_os_feature("etc")) { + } else if (RS::get_singleton()->has_os_feature("etc2")) { ktxfmt = KTX_TTF_ETC1_RGB; } else { ktxfmt = KTX_TTF_RGBA32; diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index feb9a2274e..5d22cb1301 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -41,6 +41,10 @@ #include "editor/editor_settings.h" #include "servers/rendering/rendering_device_binds.h" +#if defined(VULKAN_ENABLED) +#include "drivers/vulkan/rendering_context_driver_vulkan.h" +#endif + //uncomment this if you want to see textures from all the process saved //#define DEBUG_TEXTURES @@ -49,7 +53,7 @@ void LightmapperRD::add_mesh(const MeshData &p_mesh) { ERR_FAIL_COND(p_mesh.emission_on_uv2.is_null() || p_mesh.emission_on_uv2->is_empty()); ERR_FAIL_COND(p_mesh.albedo_on_uv2->get_width() != p_mesh.emission_on_uv2->get_width()); ERR_FAIL_COND(p_mesh.albedo_on_uv2->get_height() != p_mesh.emission_on_uv2->get_height()); - ERR_FAIL_COND(p_mesh.points.size() == 0); + ERR_FAIL_COND(p_mesh.points.is_empty()); MeshInstance mi; mi.data = p_mesh; mesh_instances.push_back(mi); @@ -124,7 +128,7 @@ void LightmapperRD::add_probe(const Vector3 &p_position) { probe_positions.push_back(probe); } -void LightmapperRD::_plot_triangle_into_triangle_index_list(int p_size, const Vector3i &p_ofs, const AABB &p_bounds, const Vector3 p_points[3], uint32_t p_triangle_index, LocalVector<TriangleSort> &triangles, uint32_t p_grid_size) { +void LightmapperRD::_plot_triangle_into_triangle_index_list(int p_size, const Vector3i &p_ofs, const AABB &p_bounds, const Vector3 p_points[3], uint32_t p_triangle_index, LocalVector<TriangleSort> &p_triangles_sort, uint32_t p_grid_size) { int half_size = p_size / 2; for (int i = 0; i < 8; i++) { @@ -159,10 +163,66 @@ void LightmapperRD::_plot_triangle_into_triangle_index_list(int p_size, const Ve TriangleSort ts; ts.cell_index = n.x + (n.y * p_grid_size) + (n.z * p_grid_size * p_grid_size); ts.triangle_index = p_triangle_index; - triangles.push_back(ts); + ts.triangle_aabb.position = p_points[0]; + ts.triangle_aabb.size = Vector3(); + ts.triangle_aabb.expand_to(p_points[1]); + ts.triangle_aabb.expand_to(p_points[2]); + p_triangles_sort.push_back(ts); } else { - _plot_triangle_into_triangle_index_list(half_size, n, aabb, p_points, p_triangle_index, triangles, p_grid_size); + _plot_triangle_into_triangle_index_list(half_size, n, aabb, p_points, p_triangle_index, p_triangles_sort, p_grid_size); + } + } +} + +void LightmapperRD::_sort_triangle_clusters(uint32_t p_cluster_size, uint32_t p_cluster_index, uint32_t p_index_start, uint32_t p_count, LocalVector<TriangleSort> &p_triangle_sort, LocalVector<ClusterAABB> &p_cluster_aabb) { + if (p_count == 0) { + return; + } + + // Compute AABB for all triangles in the range. + SortArray<TriangleSort, TriangleSortAxis<0>> triangle_sorter_x; + SortArray<TriangleSort, TriangleSortAxis<1>> triangle_sorter_y; + SortArray<TriangleSort, TriangleSortAxis<2>> triangle_sorter_z; + AABB cluster_aabb = p_triangle_sort[p_index_start].triangle_aabb; + for (uint32_t i = 1; i < p_count; i++) { + cluster_aabb.merge_with(p_triangle_sort[p_index_start + i].triangle_aabb); + } + + if (p_count > p_cluster_size) { + int longest_axis_index = cluster_aabb.get_longest_axis_index(); + switch (longest_axis_index) { + case 0: + triangle_sorter_x.sort(&p_triangle_sort[p_index_start], p_count); + break; + case 1: + triangle_sorter_y.sort(&p_triangle_sort[p_index_start], p_count); + break; + case 2: + triangle_sorter_z.sort(&p_triangle_sort[p_index_start], p_count); + break; + default: + DEV_ASSERT(false && "Invalid axis returned by AABB."); + break; + } + + uint32_t left_cluster_count = next_power_of_2(p_count / 2); + left_cluster_count = MAX(left_cluster_count, p_cluster_size); + left_cluster_count = MIN(left_cluster_count, p_count); + _sort_triangle_clusters(p_cluster_size, p_cluster_index, p_index_start, left_cluster_count, p_triangle_sort, p_cluster_aabb); + + if (left_cluster_count < p_count) { + uint32_t cluster_index_right = p_cluster_index + (left_cluster_count / p_cluster_size); + _sort_triangle_clusters(p_cluster_size, cluster_index_right, p_index_start + left_cluster_count, p_count - left_cluster_count, p_triangle_sort, p_cluster_aabb); } + } else { + ClusterAABB &aabb = p_cluster_aabb[p_cluster_index]; + Vector3 aabb_end = cluster_aabb.get_end(); + aabb.min_bounds[0] = cluster_aabb.position.x; + aabb.min_bounds[1] = cluster_aabb.position.y; + aabb.min_bounds[2] = cluster_aabb.position.z; + aabb.max_bounds[0] = aabb_end.x; + aabb.max_bounds[1] = aabb_end.y; + aabb.max_bounds[2] = aabb_end.z; } } @@ -281,7 +341,7 @@ Lightmapper::BakeError LightmapperRD::_blit_meshes_into_atlas(int p_max_texture_ return BAKE_OK; } -void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, AABB &bounds, int grid_size, Vector<Probe> &p_probe_positions, GenerateProbes p_generate_probes, Vector<int> &slice_triangle_count, Vector<int> &slice_seam_count, RID &vertex_buffer, RID &triangle_buffer, RID &lights_buffer, RID &triangle_cell_indices_buffer, RID &probe_positions_buffer, RID &grid_texture, RID &seams_buffer, BakeStepFunc p_step_function, void *p_bake_userdata) { +void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, AABB &bounds, int grid_size, uint32_t p_cluster_size, Vector<Probe> &p_probe_positions, GenerateProbes p_generate_probes, Vector<int> &slice_triangle_count, Vector<int> &slice_seam_count, RID &vertex_buffer, RID &triangle_buffer, RID &lights_buffer, RID &r_triangle_indices_buffer, RID &r_cluster_indices_buffer, RID &r_cluster_aabbs_buffer, RID &probe_positions_buffer, RID &grid_texture, RID &seams_buffer, BakeStepFunc p_step_function, void *p_bake_userdata) { HashMap<Vertex, uint32_t, VertexHash> vertex_map; //fill triangles array and vertex array @@ -301,7 +361,7 @@ void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i for (int m_i = 0; m_i < mesh_instances.size(); m_i++) { if (p_step_function) { - float p = float(m_i + 1) / mesh_instances.size() * 0.1; + float p = float(m_i + 1) / MAX(1, mesh_instances.size()) * 0.1; p_step_function(0.3 + p, vformat(RTR("Plotting mesh into acceleration structure %d/%d"), m_i + 1, mesh_instances.size()), p_bake_userdata, false); } @@ -433,31 +493,70 @@ void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i //sort it triangle_sort.sort(); + LocalVector<uint32_t> cluster_indices; + LocalVector<ClusterAABB> cluster_aabbs; Vector<uint32_t> triangle_indices; triangle_indices.resize(triangle_sort.size()); Vector<uint32_t> grid_indices; grid_indices.resize(grid_size * grid_size * grid_size * 2); memset(grid_indices.ptrw(), 0, grid_indices.size() * sizeof(uint32_t)); - Vector<bool> solid; - solid.resize(grid_size * grid_size * grid_size); - memset(solid.ptrw(), 0, solid.size() * sizeof(bool)); { - uint32_t *tiw = triangle_indices.ptrw(); + // Fill grid with cell indices. uint32_t last_cell = 0xFFFFFFFF; uint32_t *giw = grid_indices.ptrw(); - bool *solidw = solid.ptrw(); + uint32_t cluster_count = 0; + uint32_t solid_cell_count = 0; for (uint32_t i = 0; i < triangle_sort.size(); i++) { uint32_t cell = triangle_sort[i].cell_index; if (cell != last_cell) { - //cell changed, update pointer to indices - giw[cell * 2 + 1] = i; - solidw[cell] = true; + giw[cell * 2 + 1] = solid_cell_count; + solid_cell_count++; } - tiw[i] = triangle_sort[i].triangle_index; - giw[cell * 2]++; //update counter + + if ((giw[cell * 2] % p_cluster_size) == 0) { + // Add an extra cluster every time the triangle counter reaches a multiple of the cluster size. + cluster_count++; + } + + giw[cell * 2]++; last_cell = cell; } + + // Build fixed-size triangle clusters for all the cells to speed up the traversal. A cell can hold multiple clusters that each contain a fixed + // amount of triangles and an AABB. The tracer will check against the AABBs first to know whether it needs to visit the cell's triangles. + // + // The building algorithm will divide the triangles recursively contained inside each cell, sorting by the longest axis of the AABB on each step. + // + // - If the amount of triangles is less or equal to the cluster size, the AABB will be stored and the algorithm stops. + // + // - The division by two is increased to the next power of two of half the amount of triangles (with cluster size as the minimum value) to + // ensure the first half always fills the cluster. + + cluster_indices.resize(solid_cell_count * 2); + cluster_aabbs.resize(cluster_count); + + uint32_t i = 0; + uint32_t cluster_index = 0; + uint32_t solid_cell_index = 0; + uint32_t *tiw = triangle_indices.ptrw(); + while (i < triangle_sort.size()) { + cluster_indices[solid_cell_index * 2] = cluster_index; + cluster_indices[solid_cell_index * 2 + 1] = i; + + uint32_t cell = triangle_sort[i].cell_index; + uint32_t triangle_count = giw[cell * 2]; + uint32_t cell_cluster_count = (triangle_count + p_cluster_size - 1) / p_cluster_size; + _sort_triangle_clusters(p_cluster_size, cluster_index, i, triangle_count, triangle_sort, cluster_aabbs); + + for (uint32_t j = 0; j < triangle_count; j++) { + tiw[i + j] = triangle_sort[i + j].triangle_index; + } + + i += triangle_count; + cluster_index += cell_cluster_count; + solid_cell_index++; + } } #if 0 for (int i = 0; i < grid_size; i++) { @@ -507,7 +606,13 @@ void LightmapperRD::_create_acceleration_structures(RenderingDevice *rd, Size2i triangle_buffer = rd->storage_buffer_create(tb.size(), tb); Vector<uint8_t> tib = triangle_indices.to_byte_array(); - triangle_cell_indices_buffer = rd->storage_buffer_create(tib.size(), tib); + r_triangle_indices_buffer = rd->storage_buffer_create(tib.size(), tib); + + Vector<uint8_t> cib = cluster_indices.to_byte_array(); + r_cluster_indices_buffer = rd->storage_buffer_create(cib.size(), cib); + + Vector<uint8_t> cab = cluster_aabbs.to_byte_array(); + r_cluster_aabbs_buffer = rd->storage_buffer_create(cab.size(), cab); Vector<uint8_t> lb = lights.to_byte_array(); if (lb.size() == 0) { @@ -602,7 +707,7 @@ void LightmapperRD::_raster_geometry(RenderingDevice *rd, Size2i atlas_size, int raster_push_constant.uv_offset[0] = -0.5f / float(atlas_size.x); raster_push_constant.uv_offset[1] = -0.5f / float(atlas_size.y); - RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i], RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); + RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i], RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); //draw opaque rd->draw_list_bind_render_pipeline(draw_list, raster_pipeline); rd->draw_list_bind_uniform_set(draw_list, raster_base_uniform, 0); @@ -655,7 +760,7 @@ LightmapperRD::BakeError LightmapperRD::_dilate(RenderingDevice *rd, Ref<RDShade rd->compute_list_bind_uniform_set(compute_list, dilate_uniform_set, 1); push_constant.region_ofs[0] = 0; push_constant.region_ofs[1] = 0; - Vector3i group_size((atlas_size.x - 1) / 8 + 1, (atlas_size.y - 1) / 8 + 1, 1); //restore group size + Vector3i group_size(Math::division_round_up(atlas_size.x, 8), Math::division_round_up(atlas_size.y, 8), 1); //restore group size for (int i = 0; i < atlas_slices; i++) { push_constant.atlas_slice = i; @@ -668,7 +773,7 @@ LightmapperRD::BakeError LightmapperRD::_dilate(RenderingDevice *rd, Ref<RDShade #ifdef DEBUG_TEXTURES for (int i = 0; i < atlas_slices; i++) { - Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i); + Vector<uint8_t> s = rd->texture_get_data(source_light_tex, i); Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s); img->convert(Image::FORMAT_RGBA8); img->save_png("res://5_dilated_" + itos(i) + ".png"); @@ -776,7 +881,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID if (err != OK || exitcode != 0) { da->remove(fname_out); print_verbose(str); - ERR_FAIL_V_MSG(BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, vformat(TTR("OIDN denoiser failed, return code: %d"), exitcode)); + ERR_FAIL_V_MSG(BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, vformat("OIDN denoiser failed, return code: %d", exitcode)); } Ref<Image> img = _read_pfm(fname_out); @@ -838,8 +943,8 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDSh // We use a region with 1/4 the amount of pixels if we're denoising SH lightmaps, as // all four of them are denoised in the shader in one dispatch. const int max_region_size = p_bake_sh ? 512 : 1024; - int x_regions = (p_atlas_size.width - 1) / max_region_size + 1; - int y_regions = (p_atlas_size.height - 1) / max_region_size + 1; + int x_regions = Math::division_round_up(p_atlas_size.width, max_region_size); + int y_regions = Math::division_round_up(p_atlas_size.height, max_region_size); for (int s = 0; s < p_atlas_slices; s++) { p_push_constant.atlas_slice = s; @@ -857,7 +962,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDSh p_rd->compute_list_bind_uniform_set(compute_list, p_compute_base_uniform_set, 0); p_rd->compute_list_bind_uniform_set(compute_list, denoise_uniform_set, 1); p_rd->compute_list_set_push_constant(compute_list, &p_push_constant, sizeof(PushConstant)); - p_rd->compute_list_dispatch(compute_list, (w - 1) / 8 + 1, (h - 1) / 8 + 1, 1); + p_rd->compute_list_dispatch(compute_list, Math::division_round_up(w, 8), Math::division_round_up(h, 8), 1); p_rd->compute_list_end(); p_rd->submit(); @@ -887,7 +992,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d oidn_path = oidn_path.path_join("oidnDenoise"); } } - ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, TTR("OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings.")); + ERR_FAIL_COND_V_MSG(oidn_path.is_empty() || !da->file_exists(oidn_path), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES, "OIDN denoiser is selected in the project settings, but no or invalid OIDN executable path is configured in the editor settings."); } if (p_step_function) { @@ -916,7 +1021,35 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d } #endif - RenderingDevice *rd = RenderingDevice::get_singleton()->create_local_device(); + // Attempt to create a local device by requesting it from rendering server first. + // If that fails because the current renderer is not implemented on top of RD, we fall back to creating + // a local rendering device manually depending on the current platform. + Error err; + RenderingContextDriver *rcd = nullptr; + RenderingDevice *rd = RenderingServer::get_singleton()->create_local_rendering_device(); + if (rd == nullptr) { +#if defined(RD_ENABLED) +#if defined(VULKAN_ENABLED) + rcd = memnew(RenderingContextDriverVulkan); + rd = memnew(RenderingDevice); +#endif +#endif + if (rcd != nullptr && rd != nullptr) { + err = rcd->initialize(); + if (err == OK) { + err = rd->initialize(rcd); + } + + if (err != OK) { + memdelete(rd); + memdelete(rcd); + rd = nullptr; + rcd = nullptr; + } + } + } + + ERR_FAIL_NULL_V(rd, BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES); RID albedo_array_tex; RID emission_array_tex; @@ -1020,24 +1153,29 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d RID vertex_buffer; RID triangle_buffer; RID lights_buffer; - RID triangle_cell_indices_buffer; + RID triangle_indices_buffer; + RID cluster_indices_buffer; + RID cluster_aabbs_buffer; RID grid_texture; RID seams_buffer; RID probe_positions_buffer; Vector<int> slice_seam_count; -#define FREE_BUFFERS \ - rd->free(bake_parameters_buffer); \ - rd->free(vertex_buffer); \ - rd->free(triangle_buffer); \ - rd->free(lights_buffer); \ - rd->free(triangle_cell_indices_buffer); \ - rd->free(grid_texture); \ - rd->free(seams_buffer); \ +#define FREE_BUFFERS \ + rd->free(bake_parameters_buffer); \ + rd->free(vertex_buffer); \ + rd->free(triangle_buffer); \ + rd->free(lights_buffer); \ + rd->free(triangle_indices_buffer); \ + rd->free(cluster_indices_buffer); \ + rd->free(cluster_aabbs_buffer); \ + rd->free(grid_texture); \ + rd->free(seams_buffer); \ rd->free(probe_positions_buffer); - _create_acceleration_structures(rd, atlas_size, atlas_slices, bounds, grid_size, probe_positions, p_generate_probes, slice_triangle_count, slice_seam_count, vertex_buffer, triangle_buffer, lights_buffer, triangle_cell_indices_buffer, probe_positions_buffer, grid_texture, seams_buffer, p_step_function, p_bake_userdata); + const uint32_t cluster_size = 16; + _create_acceleration_structures(rd, atlas_size, atlas_slices, bounds, grid_size, cluster_size, probe_positions, p_generate_probes, slice_triangle_count, slice_seam_count, vertex_buffer, triangle_buffer, lights_buffer, triangle_indices_buffer, cluster_indices_buffer, cluster_aabbs_buffer, probe_positions_buffer, grid_texture, seams_buffer, p_step_function, p_bake_userdata); // Create global bake parameters buffer. BakeParameters bake_parameters; @@ -1081,7 +1219,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d //shaders Ref<RDShaderFile> raster_shader; raster_shader.instantiate(); - Error err = raster_shader->parse_versions_from_text(lm_raster_shader_glsl); + err = raster_shader->parse_versions_from_text(lm_raster_shader_glsl); if (err != OK) { raster_shader->print_errors("raster_shader"); @@ -1089,6 +1227,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d FREE_BUFFERS memdelete(rd); + + if (rcd != nullptr) { + memdelete(rcd); + } } ERR_FAIL_COND_V(err != OK, BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES); @@ -1133,7 +1275,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d RD::Uniform u; u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; u.binding = 3; - u.append_id(triangle_cell_indices_buffer); + u.append_id(triangle_indices_buffer); base_uniforms.push_back(u); } { @@ -1185,6 +1327,20 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d u.append_id(sampler); base_uniforms.push_back(u); } + { + RD::Uniform u; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + u.binding = 11; + u.append_id(cluster_indices_buffer); + base_uniforms.push_back(u); + } + { + RD::Uniform u; + u.uniform_type = RD::UNIFORM_TYPE_STORAGE_BUFFER; + u.binding = 12; + u.append_id(cluster_aabbs_buffer); + base_uniforms.push_back(u); + } } RID raster_base_uniform = rd->uniform_set_create(base_uniforms, rasterize_shader, 0); @@ -1230,6 +1386,8 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d Ref<RDShaderFile> compute_shader; String defines = ""; + defines += "\n#define CLUSTER_SIZE " + uitos(cluster_size) + "\n"; + if (p_bake_sh) { defines += "\n#define USE_SH_LIGHTMAPS\n"; } @@ -1245,6 +1403,11 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d FREE_BUFFERS FREE_RASTER_RESOURCES memdelete(rd); + + if (rcd != nullptr) { + memdelete(rcd); + } + compute_shader->print_errors("compute_shader"); } ERR_FAIL_COND_V(err != OK, BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES); @@ -1277,7 +1440,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d rd->free(compute_shader_secondary); \ rd->free(compute_shader_light_probes); - Vector3i group_size((atlas_size.x - 1) / 8 + 1, (atlas_size.y - 1) / 8 + 1, 1); + Vector3i group_size(Math::division_round_up(atlas_size.x, 8), Math::division_round_up(atlas_size.y, 8), 1); rd->submit(); rd->sync(); @@ -1470,10 +1633,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d int max_region_size = nearest_power_of_2_templated(int(GLOBAL_GET("rendering/lightmapping/bake_performance/region_size"))); int max_rays = GLOBAL_GET("rendering/lightmapping/bake_performance/max_rays_per_pass"); - int x_regions = (atlas_size.width - 1) / max_region_size + 1; - int y_regions = (atlas_size.height - 1) / max_region_size + 1; + int x_regions = Math::division_round_up(atlas_size.width, max_region_size); + int y_regions = Math::division_round_up(atlas_size.height, max_region_size); - int ray_iterations = (push_constant.ray_count - 1) / max_rays + 1; + int ray_iterations = Math::division_round_up((int32_t)push_constant.ray_count, max_rays); rd->submit(); rd->sync(); @@ -1492,7 +1655,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d push_constant.region_ofs[0] = x; push_constant.region_ofs[1] = y; - group_size = Vector3i((w - 1) / 8 + 1, (h - 1) / 8 + 1, 1); + group_size = Vector3i(Math::division_round_up(w, 8), Math::division_round_up(h, 8), 1); for (int k = 0; k < ray_iterations; k++) { RD::ComputeListID compute_list = rd->compute_list_begin(); @@ -1578,7 +1741,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d push_constant.probe_count = probe_positions.size(); int max_rays = GLOBAL_GET("rendering/lightmapping/bake_performance/max_rays_per_probe_pass"); - int ray_iterations = (push_constant.ray_count - 1) / max_rays + 1; + int ray_iterations = Math::division_round_up((int32_t)push_constant.ray_count, max_rays); for (int i = 0; i < ray_iterations; i++) { RD::ComputeListID compute_list = rd->compute_list_begin(); @@ -1589,7 +1752,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d push_constant.ray_from = i * max_rays; push_constant.ray_to = MIN((i + 1) * max_rays, int32_t(push_constant.ray_count)); rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(PushConstant)); - rd->compute_list_dispatch(compute_list, (probe_positions.size() - 1) / 64 + 1, 1, 1); + rd->compute_list_dispatch(compute_list, Math::division_round_up((int)probe_positions.size(), 64), 1, 1); rd->compute_list_end(); //done rd->submit(); @@ -1667,6 +1830,11 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d FREE_RASTER_RESOURCES FREE_COMPUTE_RESOURCES memdelete(rd); + + if (rcd != nullptr) { + memdelete(rcd); + } + blendseams_shader->print_errors("blendseams_shader"); } ERR_FAIL_COND_V(err != OK, BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES); @@ -1741,7 +1909,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d seams_push_constant.slice = uint32_t(i * subslices + k); seams_push_constant.debug = debug; - RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i], RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); + RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i], RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); rd->draw_list_bind_uniform_set(draw_list, raster_base_uniform, 0); rd->draw_list_bind_uniform_set(draw_list, blendseams_raster_uniform, 1); @@ -1842,6 +2010,10 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d memdelete(rd); + if (rcd != nullptr) { + memdelete(rcd); + } + return BAKE_OK; } @@ -1864,7 +2036,7 @@ Variant LightmapperRD::get_bake_mesh_userdata(int p_index) const { } Rect2 LightmapperRD::get_bake_mesh_uv_scale(int p_index) const { - ERR_FAIL_COND_V(bake_textures.size() == 0, Rect2()); + ERR_FAIL_COND_V(bake_textures.is_empty(), Rect2()); Rect2 uv_ofs; Vector2 atlas_size = Vector2(bake_textures[0]->get_width(), bake_textures[0]->get_height()); uv_ofs.position = Vector2(mesh_instances[p_index].offset) / atlas_size; diff --git a/modules/lightmapper_rd/lightmapper_rd.h b/modules/lightmapper_rd/lightmapper_rd.h index 8c1c4deba6..5414048ddc 100644 --- a/modules/lightmapper_rd/lightmapper_rd.h +++ b/modules/lightmapper_rd/lightmapper_rd.h @@ -192,6 +192,13 @@ class LightmapperRD : public Lightmapper { } }; + struct ClusterAABB { + float min_bounds[3]; + float pad0 = 0.0f; + float max_bounds[3]; + float pad1 = 0.0f; + }; + Vector<MeshInstance> mesh_instances; Vector<Light> lights; @@ -199,12 +206,22 @@ class LightmapperRD : public Lightmapper { struct TriangleSort { uint32_t cell_index = 0; uint32_t triangle_index = 0; + AABB triangle_aabb; + bool operator<(const TriangleSort &p_triangle_sort) const { return cell_index < p_triangle_sort.cell_index; //sorting by triangle index in this case makes no sense } }; + template <int T> + struct TriangleSortAxis { + bool operator()(const TriangleSort &p_a, const TriangleSort &p_b) const { + return p_a.triangle_aabb.get_center()[T] < p_b.triangle_aabb.get_center()[T]; + } + }; + void _plot_triangle_into_triangle_index_list(int p_size, const Vector3i &p_ofs, const AABB &p_bounds, const Vector3 p_points[3], uint32_t p_triangle_index, LocalVector<TriangleSort> &triangles, uint32_t p_grid_size); + void _sort_triangle_clusters(uint32_t p_cluster_size, uint32_t p_cluster_index, uint32_t p_index_start, uint32_t p_count, LocalVector<TriangleSort> &p_triangle_sort, LocalVector<ClusterAABB> &p_cluster_aabb); struct RasterPushConstant { float atlas_size[2] = {}; @@ -250,7 +267,7 @@ class LightmapperRD : public Lightmapper { }; BakeError _blit_meshes_into_atlas(int p_max_texture_size, Vector<Ref<Image>> &albedo_images, Vector<Ref<Image>> &emission_images, AABB &bounds, Size2i &atlas_size, int &atlas_slices, BakeStepFunc p_step_function, void *p_bake_userdata); - void _create_acceleration_structures(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, AABB &bounds, int grid_size, Vector<Probe> &probe_positions, GenerateProbes p_generate_probes, Vector<int> &slice_triangle_count, Vector<int> &slice_seam_count, RID &vertex_buffer, RID &triangle_buffer, RID &lights_buffer, RID &triangle_cell_indices_buffer, RID &probe_positions_buffer, RID &grid_texture, RID &seams_buffer, BakeStepFunc p_step_function, void *p_bake_userdata); + void _create_acceleration_structures(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, AABB &bounds, int grid_size, uint32_t p_cluster_size, Vector<Probe> &probe_positions, GenerateProbes p_generate_probes, Vector<int> &slice_triangle_count, Vector<int> &slice_seam_count, RID &vertex_buffer, RID &triangle_buffer, RID &lights_buffer, RID &r_triangle_indices_buffer, RID &r_cluster_indices_buffer, RID &r_cluster_aabbs_buffer, RID &probe_positions_buffer, RID &grid_texture, RID &seams_buffer, BakeStepFunc p_step_function, void *p_bake_userdata); void _raster_geometry(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, int grid_size, AABB bounds, float p_bias, Vector<int> slice_triangle_count, RID position_tex, RID unocclude_tex, RID normal_tex, RID raster_depth_buffer, RID rasterize_shader, RID raster_base_uniform); BakeError _dilate(RenderingDevice *rd, Ref<RDShaderFile> &compute_shader, RID &compute_base_uniform_set, PushConstant &push_constant, RID &source_light_tex, RID &dest_light_tex, const Size2i &atlas_size, int atlas_slices); diff --git a/modules/lightmapper_rd/lm_common_inc.glsl b/modules/lightmapper_rd/lm_common_inc.glsl index c91f06d0f3..98d11b9e69 100644 --- a/modules/lightmapper_rd/lm_common_inc.glsl +++ b/modules/lightmapper_rd/lm_common_inc.glsl @@ -42,15 +42,22 @@ struct Triangle { uint pad1; }; +struct ClusterAABB { + vec3 min_bounds; + uint pad0; + vec3 max_bounds; + uint pad1; +}; + layout(set = 0, binding = 2, std430) restrict readonly buffer Triangles { Triangle data[]; } triangles; -layout(set = 0, binding = 3, std430) restrict readonly buffer GridIndices { +layout(set = 0, binding = 3, std430) restrict readonly buffer TriangleIndices { uint data[]; } -grid_indices; +triangle_indices; #define LIGHT_TYPE_DIRECTIONAL 0 #define LIGHT_TYPE_OMNI 1 @@ -104,6 +111,16 @@ layout(set = 0, binding = 9) uniform texture2DArray emission_tex; layout(set = 0, binding = 10) uniform sampler linear_sampler; +layout(set = 0, binding = 11, std430) restrict readonly buffer ClusterIndices { + uint data[]; +} +cluster_indices; + +layout(set = 0, binding = 12, std430) restrict readonly buffer ClusterAABBs { + ClusterAABB data[]; +} +cluster_aabbs; + // Fragment action constants const uint FA_NONE = 0; const uint FA_SMOOTHEN_POSITION = 1; diff --git a/modules/lightmapper_rd/lm_compute.glsl b/modules/lightmapper_rd/lm_compute.glsl index 572e6d55d8..1d088450e9 100644 --- a/modules/lightmapper_rd/lm_compute.glsl +++ b/modules/lightmapper_rd/lm_compute.glsl @@ -13,6 +13,8 @@ denoise = "#define MODE_DENOISE"; #VERSION_DEFINES +#extension GL_EXT_samplerless_texture_functions : enable + // One 2D local group focusing in one layer at a time, though all // in parallel (no barriers) makes more sense than a 3D local group // as this can take more advantage of the cache for each group. @@ -119,6 +121,17 @@ const uint RAY_FRONT = 1; const uint RAY_BACK = 2; const uint RAY_ANY = 3; +bool ray_box_test(vec3 p_from, vec3 p_inv_dir, vec3 p_box_min, vec3 p_box_max) { + vec3 t0 = (p_box_min - p_from) * p_inv_dir; + vec3 t1 = (p_box_max - p_from) * p_inv_dir; + vec3 tmin = min(t0, t1), tmax = max(t0, t1); + return max(tmin.x, max(tmin.y, tmin.z)) <= min(tmax.x, min(tmax.y, tmax.z)); +} + +#if CLUSTER_SIZE > 32 +#define CLUSTER_TRIANGLE_ITERATION +#endif + uint trace_ray(vec3 p_from, vec3 p_to, bool p_any_hit, out float r_distance, out vec3 r_normal, out uint r_triangle, out vec3 r_barycentric) { // World coordinates. vec3 rel = p_to - p_from; @@ -141,61 +154,107 @@ uint trace_ray(vec3 p_from, vec3 p_to, bool p_any_hit, out float r_distance, out uint iters = 0; while (all(greaterThanEqual(icell, ivec3(0))) && all(lessThan(icell, ivec3(bake_params.grid_size))) && (iters < 1000)) { - uvec2 cell_data = texelFetch(usampler3D(grid, linear_sampler), icell, 0).xy; - if (cell_data.x > 0) { //triangles here + uvec2 cell_data = texelFetch(grid, icell, 0).xy; + uint triangle_count = cell_data.x; + if (triangle_count > 0) { uint hit = RAY_MISS; float best_distance = 1e20; - - for (uint i = 0; i < cell_data.x; i++) { - uint tidx = grid_indices.data[cell_data.y + i]; - - // Ray-Box test. - Triangle triangle = triangles.data[tidx]; - vec3 t0 = (triangle.min_bounds - p_from) * inv_dir; - vec3 t1 = (triangle.max_bounds - p_from) * inv_dir; - vec3 tmin = min(t0, t1), tmax = max(t0, t1); - - if (max(tmin.x, max(tmin.y, tmin.z)) > min(tmax.x, min(tmax.y, tmax.z))) { - continue; // Ray-Box test failed. - } - - // Prepare triangle vertices. - vec3 vtx0 = vertices.data[triangle.indices.x].position; - vec3 vtx1 = vertices.data[triangle.indices.y].position; - vec3 vtx2 = vertices.data[triangle.indices.z].position; - vec3 normal = -normalize(cross((vtx0 - vtx1), (vtx0 - vtx2))); - bool backface = dot(normal, dir) >= 0.0; - float distance; - vec3 barycentric; - if (ray_hits_triangle(p_from, dir, rel_len, vtx0, vtx1, vtx2, distance, barycentric)) { - if (p_any_hit) { - // Return early if any hit was requested. - return RAY_ANY; + uint cluster_start = cluster_indices.data[cell_data.y * 2]; + uint cell_triangle_start = cluster_indices.data[cell_data.y * 2 + 1]; + uint cluster_count = (triangle_count + CLUSTER_SIZE - 1) / CLUSTER_SIZE; + uint cluster_base_index = 0; + while (cluster_base_index < cluster_count) { + // To minimize divergence, all Ray-AABB tests on the clusters contained in the cell are performed + // before checking against the triangles. We do this 32 clusters at a time and store the intersected + // clusters on each bit of the 32-bit integer. + uint cluster_test_count = min(32, cluster_count - cluster_base_index); + uint cluster_hits = 0; + for (uint i = 0; i < cluster_test_count; i++) { + uint cluster_index = cluster_start + cluster_base_index + i; + ClusterAABB cluster_aabb = cluster_aabbs.data[cluster_index]; + if (ray_box_test(p_from, inv_dir, cluster_aabb.min_bounds, cluster_aabb.max_bounds)) { + cluster_hits |= (1 << i); } + } - vec3 position = p_from + dir * distance; - vec3 hit_cell = (position - bake_params.to_cell_offset) * bake_params.to_cell_size; - if (icell != ivec3(hit_cell)) { - // It's possible for the ray to hit a triangle in a position outside the bounds of the cell - // if it's large enough to cover multiple ones. The hit must be ignored if this is the case. - continue; - } + // Check the triangles in any of the clusters that were intersected by toggling off the bits in the + // 32-bit integer counter until no bits are left. + while (cluster_hits > 0) { + uint cluster_index = findLSB(cluster_hits); + cluster_hits &= ~(1 << cluster_index); + cluster_index += cluster_base_index; + + // Do the same divergence execution trick with triangles as well. + uint triangle_base_index = 0; +#ifdef CLUSTER_TRIANGLE_ITERATION + while (triangle_base_index < triangle_count) +#endif + { + uint triangle_start_index = cell_triangle_start + cluster_index * CLUSTER_SIZE + triangle_base_index; + uint triangle_test_count = min(CLUSTER_SIZE, triangle_count - triangle_base_index); + uint triangle_hits = 0; + for (uint i = 0; i < triangle_test_count; i++) { + uint triangle_index = triangle_indices.data[triangle_start_index + i]; + if (ray_box_test(p_from, inv_dir, triangles.data[triangle_index].min_bounds, triangles.data[triangle_index].max_bounds)) { + triangle_hits |= (1 << i); + } + } - if (!backface) { - // The case of meshes having both a front and back face in the same plane is more common than expected. - // If this is a front-face, bias it closer to the ray origin, so it always wins over the back-face. - distance = max(bake_params.bias, distance - bake_params.bias); - } + while (triangle_hits > 0) { + uint cluster_triangle_index = findLSB(triangle_hits); + triangle_hits &= ~(1 << cluster_triangle_index); + cluster_triangle_index += triangle_start_index; + + uint triangle_index = triangle_indices.data[cluster_triangle_index]; + Triangle triangle = triangles.data[triangle_index]; + + // Gather the triangle vertex positions. + vec3 vtx0 = vertices.data[triangle.indices.x].position; + vec3 vtx1 = vertices.data[triangle.indices.y].position; + vec3 vtx2 = vertices.data[triangle.indices.z].position; + vec3 normal = -normalize(cross((vtx0 - vtx1), (vtx0 - vtx2))); + bool backface = dot(normal, dir) >= 0.0; + float distance; + vec3 barycentric; + if (ray_hits_triangle(p_from, dir, rel_len, vtx0, vtx1, vtx2, distance, barycentric)) { + if (p_any_hit) { + // Return early if any hit was requested. + return RAY_ANY; + } + + vec3 position = p_from + dir * distance; + vec3 hit_cell = (position - bake_params.to_cell_offset) * bake_params.to_cell_size; + if (icell != ivec3(hit_cell)) { + // It's possible for the ray to hit a triangle in a position outside the bounds of the cell + // if it's large enough to cover multiple ones. The hit must be ignored if this is the case. + continue; + } + + if (!backface) { + // The case of meshes having both a front and back face in the same plane is more common than + // expected, so if this is a front-face, bias it closer to the ray origin, so it always wins + // over the back-face. + distance = max(bake_params.bias, distance - bake_params.bias); + } + + if (distance < best_distance) { + hit = backface ? RAY_BACK : RAY_FRONT; + best_distance = distance; + r_distance = distance; + r_normal = normal; + r_triangle = triangle_index; + r_barycentric = barycentric; + } + } + } - if (distance < best_distance) { - hit = backface ? RAY_BACK : RAY_FRONT; - best_distance = distance; - r_distance = distance; - r_normal = normal; - r_triangle = tidx; - r_barycentric = barycentric; +#ifdef CLUSTER_TRIANGLE_ITERATION + triangle_base_index += CLUSTER_SIZE; +#endif } } + + cluster_base_index += 32; } if (hit != RAY_MISS) { @@ -207,7 +266,16 @@ uint trace_ray(vec3 p_from, vec3 p_to, bool p_any_hit, out float r_distance, out break; } - bvec3 mask = lessThanEqual(side.xyz, min(side.yzx, side.zxy)); + // There should be only one axis updated at a time for DDA to work properly. + bvec3 mask = bvec3(true, false, false); + float m = side.x; + if (side.y < m) { + m = side.y; + mask = bvec3(false, true, false); + } + if (side.z < m) { + mask = bvec3(false, false, true); + } side += vec3(mask) * delta; icell += ivec3(vec3(mask)) * step; iters++; diff --git a/modules/lightmapper_rd/register_types.cpp b/modules/lightmapper_rd/register_types.cpp index 8103799974..4fe8f20723 100644 --- a/modules/lightmapper_rd/register_types.cpp +++ b/modules/lightmapper_rd/register_types.cpp @@ -46,18 +46,18 @@ void initialize_lightmapper_rd_module(ModuleInitializationLevel p_level) { return; } - GLOBAL_DEF("rendering/lightmapping/bake_quality/low_quality_ray_count", 32); - GLOBAL_DEF("rendering/lightmapping/bake_quality/medium_quality_ray_count", 128); - GLOBAL_DEF("rendering/lightmapping/bake_quality/high_quality_ray_count", 512); - GLOBAL_DEF("rendering/lightmapping/bake_quality/ultra_quality_ray_count", 2048); - GLOBAL_DEF("rendering/lightmapping/bake_performance/max_rays_per_pass", 32); - GLOBAL_DEF("rendering/lightmapping/bake_performance/region_size", 512); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/low_quality_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 32); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/medium_quality_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 128); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/high_quality_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 512); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/ultra_quality_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 2048); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_performance/max_rays_per_pass", PROPERTY_HINT_RANGE, "1,256,1,or_greater"), 32); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_performance/region_size", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 512); - GLOBAL_DEF("rendering/lightmapping/bake_quality/low_quality_probe_ray_count", 64); - GLOBAL_DEF("rendering/lightmapping/bake_quality/medium_quality_probe_ray_count", 256); - GLOBAL_DEF("rendering/lightmapping/bake_quality/high_quality_probe_ray_count", 512); - GLOBAL_DEF("rendering/lightmapping/bake_quality/ultra_quality_probe_ray_count", 2048); - GLOBAL_DEF("rendering/lightmapping/bake_performance/max_rays_per_probe_pass", 64); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/low_quality_probe_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 64); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/medium_quality_probe_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 256); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/high_quality_probe_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 512); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_quality/ultra_quality_probe_ray_count", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 2048); + GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/bake_performance/max_rays_per_probe_pass", PROPERTY_HINT_RANGE, "1,256,1,or_greater"), 64); GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/lightmapping/denoising/denoiser", PROPERTY_HINT_ENUM, "JNLM,OIDN"), 0); #ifndef _3D_DISABLED diff --git a/modules/mbedtls/SCsub b/modules/mbedtls/SCsub index 7c1204d2b7..4b8f65d8ff 100644 --- a/modules/mbedtls/SCsub +++ b/modules/mbedtls/SCsub @@ -121,6 +121,9 @@ if env["tests"]: env_mbed_tls.Append(CPPDEFINES=["TESTS_ENABLED"]) env_mbed_tls.add_source_files(module_obj, "./tests/*.cpp") + if env["disable_exceptions"]: + env_mbed_tls.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"]) + env.modules_sources += module_obj # Needed to force rebuilding the module files when the thirdparty library is updated. diff --git a/modules/mbedtls/crypto_mbedtls.cpp b/modules/mbedtls/crypto_mbedtls.cpp index 381ed42fe1..859278d65e 100644 --- a/modules/mbedtls/crypto_mbedtls.cpp +++ b/modules/mbedtls/crypto_mbedtls.cpp @@ -53,7 +53,7 @@ CryptoKey *CryptoKeyMbedTLS::create() { return memnew(CryptoKeyMbedTLS); } -Error CryptoKeyMbedTLS::load(String p_path, bool p_public_only) { +Error CryptoKeyMbedTLS::load(const String &p_path, bool p_public_only) { ERR_FAIL_COND_V_MSG(locks, ERR_ALREADY_IN_USE, "Key is in use"); PackedByteArray out; @@ -79,7 +79,7 @@ Error CryptoKeyMbedTLS::load(String p_path, bool p_public_only) { return OK; } -Error CryptoKeyMbedTLS::save(String p_path, bool p_public_only) { +Error CryptoKeyMbedTLS::save(const String &p_path, bool p_public_only) { Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); ERR_FAIL_COND_V_MSG(f.is_null(), ERR_INVALID_PARAMETER, "Cannot save CryptoKeyMbedTLS file '" + p_path + "'."); @@ -103,7 +103,7 @@ Error CryptoKeyMbedTLS::save(String p_path, bool p_public_only) { return OK; } -Error CryptoKeyMbedTLS::load_from_string(String p_string_key, bool p_public_only) { +Error CryptoKeyMbedTLS::load_from_string(const String &p_string_key, bool p_public_only) { int ret = 0; if (p_public_only) { ret = mbedtls_pk_parse_public_key(&pkey, (unsigned char *)p_string_key.utf8().get_data(), p_string_key.utf8().size()); @@ -138,7 +138,7 @@ X509Certificate *X509CertificateMbedTLS::create() { return memnew(X509CertificateMbedTLS); } -Error X509CertificateMbedTLS::load(String p_path) { +Error X509CertificateMbedTLS::load(const String &p_path) { ERR_FAIL_COND_V_MSG(locks, ERR_ALREADY_IN_USE, "Certificate is already in use."); PackedByteArray out; @@ -170,7 +170,7 @@ Error X509CertificateMbedTLS::load_from_memory(const uint8_t *p_buffer, int p_le return OK; } -Error X509CertificateMbedTLS::save(String p_path) { +Error X509CertificateMbedTLS::save(const String &p_path) { Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); ERR_FAIL_COND_V_MSG(f.is_null(), ERR_INVALID_PARAMETER, vformat("Cannot save X509CertificateMbedTLS file '%s'.", p_path)); @@ -235,7 +235,7 @@ HMACContext *HMACContextMbedTLS::create() { return memnew(HMACContextMbedTLS); } -Error HMACContextMbedTLS::start(HashingContext::HashType p_hash_type, PackedByteArray p_key) { +Error HMACContextMbedTLS::start(HashingContext::HashType p_hash_type, const PackedByteArray &p_key) { ERR_FAIL_COND_V_MSG(ctx != nullptr, ERR_FILE_ALREADY_IN_USE, "HMACContext already started."); // HMAC keys can be any size. @@ -255,7 +255,7 @@ Error HMACContextMbedTLS::start(HashingContext::HashType p_hash_type, PackedByte return ret ? FAILED : OK; } -Error HMACContextMbedTLS::update(PackedByteArray p_data) { +Error HMACContextMbedTLS::update(const PackedByteArray &p_data) { ERR_FAIL_NULL_V_MSG(ctx, ERR_INVALID_DATA, "Start must be called before update."); ERR_FAIL_COND_V_MSG(p_data.is_empty(), ERR_INVALID_PARAMETER, "Src must not be empty."); @@ -338,7 +338,7 @@ X509CertificateMbedTLS *CryptoMbedTLS::get_default_certificates() { return default_certs; } -void CryptoMbedTLS::load_default_certificates(String p_path) { +void CryptoMbedTLS::load_default_certificates(const String &p_path) { ERR_FAIL_COND(default_certs != nullptr); default_certs = memnew(X509CertificateMbedTLS); @@ -380,7 +380,7 @@ Ref<CryptoKey> CryptoMbedTLS::generate_rsa(int p_bytes) { return out; } -Ref<X509Certificate> CryptoMbedTLS::generate_self_signed_certificate(Ref<CryptoKey> p_key, String p_issuer_name, String p_not_before, String p_not_after) { +Ref<X509Certificate> CryptoMbedTLS::generate_self_signed_certificate(Ref<CryptoKey> p_key, const String &p_issuer_name, const String &p_not_before, const String &p_not_after) { Ref<CryptoKeyMbedTLS> key = static_cast<Ref<CryptoKeyMbedTLS>>(p_key); ERR_FAIL_COND_V_MSG(key.is_null(), nullptr, "Invalid private key argument."); mbedtls_x509write_cert crt; @@ -452,7 +452,7 @@ mbedtls_md_type_t CryptoMbedTLS::md_type_from_hashtype(HashingContext::HashType } } -Vector<uint8_t> CryptoMbedTLS::sign(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Ref<CryptoKey> p_key) { +Vector<uint8_t> CryptoMbedTLS::sign(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, Ref<CryptoKey> p_key) { int size; mbedtls_md_type_t type = CryptoMbedTLS::md_type_from_hashtype(p_hash_type, size); ERR_FAIL_COND_V_MSG(type == MBEDTLS_MD_NONE, Vector<uint8_t>(), "Invalid hash type."); @@ -470,7 +470,7 @@ Vector<uint8_t> CryptoMbedTLS::sign(HashingContext::HashType p_hash_type, Vector return out; } -bool CryptoMbedTLS::verify(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Vector<uint8_t> p_signature, Ref<CryptoKey> p_key) { +bool CryptoMbedTLS::verify(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, const Vector<uint8_t> &p_signature, Ref<CryptoKey> p_key) { int size; mbedtls_md_type_t type = CryptoMbedTLS::md_type_from_hashtype(p_hash_type, size); ERR_FAIL_COND_V_MSG(type == MBEDTLS_MD_NONE, false, "Invalid hash type."); @@ -480,7 +480,7 @@ bool CryptoMbedTLS::verify(HashingContext::HashType p_hash_type, Vector<uint8_t> return mbedtls_pk_verify(&(key->pkey), type, p_hash.ptr(), size, p_signature.ptr(), p_signature.size()) == 0; } -Vector<uint8_t> CryptoMbedTLS::encrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_plaintext) { +Vector<uint8_t> CryptoMbedTLS::encrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_plaintext) { Ref<CryptoKeyMbedTLS> key = static_cast<Ref<CryptoKeyMbedTLS>>(p_key); ERR_FAIL_COND_V_MSG(!key.is_valid(), Vector<uint8_t>(), "Invalid key provided."); uint8_t buf[1024]; @@ -493,7 +493,7 @@ Vector<uint8_t> CryptoMbedTLS::encrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_p return out; } -Vector<uint8_t> CryptoMbedTLS::decrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_ciphertext) { +Vector<uint8_t> CryptoMbedTLS::decrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_ciphertext) { Ref<CryptoKeyMbedTLS> key = static_cast<Ref<CryptoKeyMbedTLS>>(p_key); ERR_FAIL_COND_V_MSG(!key.is_valid(), Vector<uint8_t>(), "Invalid key provided."); ERR_FAIL_COND_V_MSG(key->is_public_only(), Vector<uint8_t>(), "Invalid key provided. Cannot decrypt using a public_only key."); diff --git a/modules/mbedtls/crypto_mbedtls.h b/modules/mbedtls/crypto_mbedtls.h index 0168e1f663..60a413ed7c 100644 --- a/modules/mbedtls/crypto_mbedtls.h +++ b/modules/mbedtls/crypto_mbedtls.h @@ -51,10 +51,10 @@ public: static void make_default() { CryptoKey::_create = create; } static void finalize() { CryptoKey::_create = nullptr; } - virtual Error load(String p_path, bool p_public_only); - virtual Error save(String p_path, bool p_public_only); + virtual Error load(const String &p_path, bool p_public_only); + virtual Error save(const String &p_path, bool p_public_only); virtual String save_to_string(bool p_public_only); - virtual Error load_from_string(String p_string_key, bool p_public_only); + virtual Error load_from_string(const String &p_string_key, bool p_public_only); virtual bool is_public_only() const { return public_only; }; CryptoKeyMbedTLS() { @@ -82,9 +82,9 @@ public: static void make_default() { X509Certificate::_create = create; } static void finalize() { X509Certificate::_create = nullptr; } - virtual Error load(String p_path); + virtual Error load(const String &p_path); virtual Error load_from_memory(const uint8_t *p_buffer, int p_len); - virtual Error save(String p_path); + virtual Error save(const String &p_path); virtual String save_to_string(); virtual Error load_from_string(const String &p_string_key); @@ -116,8 +116,8 @@ public: static bool is_md_type_allowed(mbedtls_md_type_t p_md_type); - virtual Error start(HashingContext::HashType p_hash_type, PackedByteArray p_key); - virtual Error update(PackedByteArray p_data); + virtual Error start(HashingContext::HashType p_hash_type, const PackedByteArray &p_key); + virtual Error update(const PackedByteArray &p_data); virtual PackedByteArray finish(); HMACContextMbedTLS() {} @@ -135,16 +135,16 @@ public: static void initialize_crypto(); static void finalize_crypto(); static X509CertificateMbedTLS *get_default_certificates(); - static void load_default_certificates(String p_path); + static void load_default_certificates(const String &p_path); static mbedtls_md_type_t md_type_from_hashtype(HashingContext::HashType p_hash_type, int &r_size); virtual PackedByteArray generate_random_bytes(int p_bytes); virtual Ref<CryptoKey> generate_rsa(int p_bytes); - virtual Ref<X509Certificate> generate_self_signed_certificate(Ref<CryptoKey> p_key, String p_issuer_name, String p_not_before, String p_not_after); - virtual Vector<uint8_t> sign(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Ref<CryptoKey> p_key); - virtual bool verify(HashingContext::HashType p_hash_type, Vector<uint8_t> p_hash, Vector<uint8_t> p_signature, Ref<CryptoKey> p_key); - virtual Vector<uint8_t> encrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_plaintext); - virtual Vector<uint8_t> decrypt(Ref<CryptoKey> p_key, Vector<uint8_t> p_ciphertext); + virtual Ref<X509Certificate> generate_self_signed_certificate(Ref<CryptoKey> p_key, const String &p_issuer_name, const String &p_not_before, const String &p_not_after); + virtual Vector<uint8_t> sign(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, Ref<CryptoKey> p_key); + virtual bool verify(HashingContext::HashType p_hash_type, const Vector<uint8_t> &p_hash, const Vector<uint8_t> &p_signature, Ref<CryptoKey> p_key); + virtual Vector<uint8_t> encrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_plaintext); + virtual Vector<uint8_t> decrypt(Ref<CryptoKey> p_key, const Vector<uint8_t> &p_ciphertext); CryptoMbedTLS(); ~CryptoMbedTLS(); diff --git a/modules/mbedtls/tests/test_crypto_mbedtls.cpp b/modules/mbedtls/tests/test_crypto_mbedtls.cpp index 22d79b79f9..b96a072146 100644 --- a/modules/mbedtls/tests/test_crypto_mbedtls.cpp +++ b/modules/mbedtls/tests/test_crypto_mbedtls.cpp @@ -33,6 +33,7 @@ #include "../crypto_mbedtls.h" #include "tests/test_macros.h" +#include "tests/test_utils.h" namespace TestCryptoMbedTLS { @@ -60,4 +61,42 @@ void hmac_context_digest_test(HashingContext::HashType ht, String expected_hex) String hex = String::hex_encode_buffer(digest.ptr(), digest.size()); CHECK(hex == expected_hex); } + +Ref<CryptoKey> create_crypto_key(const String &p_key_path, bool p_public_only) { + Ref<CryptoKey> crypto_key = Ref<CryptoKey>(CryptoKey::create()); + crypto_key->load(p_key_path, p_public_only); + return crypto_key; +} + +String read_file_s(const String &p_file_path) { + Ref<FileAccess> file_access = FileAccess::open(p_file_path, FileAccess::READ); + REQUIRE(file_access.is_valid()); + return file_access->get_as_utf8_string(); +} + +bool files_equal(const String &p_in_path, const String &p_out_path) { + const String s_in = read_file_s(p_in_path); + const String s_out = read_file_s(p_out_path); + return s_in == s_out; +} + +void crypto_key_public_only_test(const String &p_key_path, bool p_public_only) { + Ref<CryptoKey> crypto_key = create_crypto_key(p_key_path, p_public_only); + bool is_equal = crypto_key->is_public_only() == p_public_only; + CHECK(is_equal); +} + +void crypto_key_save_test(const String &p_in_path, const String &p_out_path, bool p_public_only) { + Ref<CryptoKey> crypto_key = create_crypto_key(p_in_path, p_public_only); + crypto_key->save(p_out_path, p_public_only); + bool is_equal = files_equal(p_in_path, p_out_path); + CHECK(is_equal); +} + +void crypto_key_save_public_only_test(const String &p_in_priv_path, const String &p_in_pub_path, const String &p_out_path) { + Ref<CryptoKey> crypto_key = create_crypto_key(p_in_priv_path, false); + crypto_key->save(p_out_path, true); + bool is_equal = files_equal(p_in_pub_path, p_out_path); + CHECK(is_equal); +} } // namespace TestCryptoMbedTLS diff --git a/modules/mbedtls/tests/test_crypto_mbedtls.h b/modules/mbedtls/tests/test_crypto_mbedtls.h index 0b24925d6b..5ec78d18a3 100644 --- a/modules/mbedtls/tests/test_crypto_mbedtls.h +++ b/modules/mbedtls/tests/test_crypto_mbedtls.h @@ -31,9 +31,11 @@ #ifndef TEST_CRYPTO_MBEDTLS_H #define TEST_CRYPTO_MBEDTLS_H +#include "core/crypto/crypto.h" #include "core/crypto/hashing_context.h" #include "tests/test_macros.h" +#include "tests/test_utils.h" namespace TestCryptoMbedTLS { @@ -56,6 +58,35 @@ TEST_CASE("[HMACContext] HMAC digest") { // SHA-1 hmac_context_digest_test(HashingContext::HashType::HASH_SHA1, "a0ac4cd68a2f4812c355983d94e8d025afe7dddf"); } + +void crypto_key_public_only_test(const String &p_key_path, bool public_only); + +TEST_CASE("[Crypto] CryptoKey is_public_only") { + crypto_key_public_only_test(TestUtils::get_data_path("crypto/in.key"), false); + crypto_key_public_only_test(TestUtils::get_data_path("crypto/in.pub"), true); +} + +void crypto_key_save_test(const String &p_in_path, const String &p_out_path, bool public_only); + +TEST_CASE("[Crypto] CryptoKey save") { + const String in_priv_path = TestUtils::get_data_path("crypto/in.key"); + const String out_priv_path = TestUtils::get_data_path("crypto/out.key"); + crypto_key_save_test(in_priv_path, out_priv_path, false); + + const String in_pub_path = TestUtils::get_data_path("crypto/in.pub"); + const String out_pub_path = TestUtils::get_data_path("crypto/out.pub"); + crypto_key_save_test(in_pub_path, out_pub_path, true); +} + +void crypto_key_save_public_only_test(const String &p_in_priv_path, const String &p_in_pub_path, const String &p_out_path); + +TEST_CASE("[Crypto] CryptoKey save public_only") { + const String in_priv_path = TestUtils::get_data_path("crypto/in.key"); + const String in_pub_path = TestUtils::get_data_path("crypto/in.pub"); + const String out_path = TestUtils::get_data_path("crypto/out_public_only.pub"); + crypto_key_save_public_only_test(in_priv_path, in_pub_path, out_path); +} + } // namespace TestCryptoMbedTLS #endif // TEST_CRYPTO_MBEDTLS_H diff --git a/modules/minimp3/audio_stream_mp3.cpp b/modules/minimp3/audio_stream_mp3.cpp index 4efa4d329e..a46a1c93b5 100644 --- a/modules/minimp3/audio_stream_mp3.cpp +++ b/modules/minimp3/audio_stream_mp3.cpp @@ -46,7 +46,9 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) { int frames_mixed_this_step = p_frames; int beat_length_frames = -1; - bool beat_loop = mp3_stream->has_loop() && mp3_stream->get_bpm() > 0 && mp3_stream->get_beat_count() > 0; + bool use_loop = looping_override ? looping : mp3_stream->loop; + + bool beat_loop = use_loop && mp3_stream->get_bpm() > 0 && mp3_stream->get_beat_count() > 0; if (beat_loop) { beat_length_frames = mp3_stream->get_beat_count() * mp3_stream->sample_rate * 60 / mp3_stream->get_bpm(); } @@ -82,7 +84,7 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) { else { //EOF - if (mp3_stream->loop) { + if (use_loop) { seek(mp3_stream->loop_offset); loops++; } else { @@ -143,6 +145,25 @@ void AudioStreamPlaybackMP3::tag_used_streams() { mp3_stream->tag_used(get_playback_position()); } +void AudioStreamPlaybackMP3::set_parameter(const StringName &p_name, const Variant &p_value) { + if (p_name == SNAME("looping")) { + if (p_value == Variant()) { + looping_override = false; + looping = false; + } else { + looping_override = true; + looping = p_value; + } + } +} + +Variant AudioStreamPlaybackMP3::get_parameter(const StringName &p_name) const { + if (looping_override && p_name == SNAME("looping")) { + return looping; + } + return Variant(); +} + AudioStreamPlaybackMP3::~AudioStreamPlaybackMP3() { if (mp3d) { mp3dec_ex_close(mp3d); @@ -232,6 +253,10 @@ bool AudioStreamMP3::is_monophonic() const { return false; } +void AudioStreamMP3::get_parameter_list(List<Parameter> *r_parameters) { + r_parameters->push_back(Parameter(PropertyInfo(Variant::BOOL, "looping", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE), Variant())); +} + void AudioStreamMP3::set_bpm(double p_bpm) { ERR_FAIL_COND(p_bpm < 0); bpm = p_bpm; diff --git a/modules/minimp3/audio_stream_mp3.h b/modules/minimp3/audio_stream_mp3.h index 30760703e3..7d85e0a321 100644 --- a/modules/minimp3/audio_stream_mp3.h +++ b/modules/minimp3/audio_stream_mp3.h @@ -47,6 +47,8 @@ class AudioStreamPlaybackMP3 : public AudioStreamPlaybackResampled { AudioFrame loop_fade[FADE_SIZE]; int loop_fade_remaining = FADE_SIZE; + bool looping_override = false; + bool looping = false; mp3dec_ex_t *mp3d = nullptr; uint32_t frames_mixed = 0; bool active = false; @@ -72,6 +74,9 @@ public: virtual void tag_used_streams() override; + virtual void set_parameter(const StringName &p_name, const Variant &p_value) override; + virtual Variant get_parameter(const StringName &p_name) const override; + AudioStreamPlaybackMP3() {} ~AudioStreamPlaybackMP3(); }; @@ -126,6 +131,8 @@ public: virtual bool is_monophonic() const override; + virtual void get_parameter_list(List<Parameter> *r_parameters) override; + AudioStreamMP3(); virtual ~AudioStreamMP3(); }; diff --git a/modules/minimp3/register_types.cpp b/modules/minimp3/register_types.cpp index 627d093bc1..c85f0b3389 100644 --- a/modules/minimp3/register_types.cpp +++ b/modules/minimp3/register_types.cpp @@ -52,8 +52,13 @@ void initialize_minimp3_module(ModuleInitializationLevel p_level) { ResourceFormatImporter::get_singleton()->add_importer(mp3_import); } + ClassDB::APIType prev_api = ClassDB::get_current_api(); + ClassDB::set_current_api(ClassDB::API_EDITOR); + // Required to document import options in the class reference. GDREGISTER_CLASS(ResourceImporterMP3); + + ClassDB::set_current_api(prev_api); #endif GDREGISTER_CLASS(AudioStreamMP3); diff --git a/modules/minimp3/resource_importer_mp3.cpp b/modules/minimp3/resource_importer_mp3.cpp index d60b979c3f..e4b54ef050 100644 --- a/modules/minimp3/resource_importer_mp3.cpp +++ b/modules/minimp3/resource_importer_mp3.cpp @@ -89,7 +89,7 @@ bool ResourceImporterMP3::has_advanced_options() const { void ResourceImporterMP3::show_advanced_options(const String &p_path) { Ref<AudioStreamMP3> mp3_stream = import_mp3(p_path); if (mp3_stream.is_valid()) { - AudioStreamImportSettings::get_singleton()->edit(p_path, "mp3", mp3_stream); + AudioStreamImportSettingsDialog::get_singleton()->edit(p_path, "mp3", mp3_stream); } } #endif @@ -110,7 +110,7 @@ Ref<AudioStreamMP3> ResourceImporterMP3::import_mp3(const String &p_path) { mp3_stream.instantiate(); mp3_stream->set_data(data); - ERR_FAIL_COND_V(!mp3_stream->get_data().size(), Ref<AudioStreamMP3>()); + ERR_FAIL_COND_V(mp3_stream->get_data().is_empty(), Ref<AudioStreamMP3>()); return mp3_stream; } diff --git a/modules/mono/.editorconfig b/modules/mono/.editorconfig index 9434d0693c..1e5ae28396 100644 --- a/modules/mono/.editorconfig +++ b/modules/mono/.editorconfig @@ -13,31 +13,59 @@ trim_trailing_whitespace = true max_line_length = 120 csharp_indent_case_contents_when_block = false -[*.cs] -# CA1707: Identifiers should not contain underscores -# TODO: -# Maybe we could disable this selectively only -# where it's not desired and for generated code. -dotnet_diagnostic.CA1707.severity = none -# CA1711: Identifiers should not have incorrect suffix -# Disable warning for suffixes like EventHandler, Flags, Enum, etc. -dotnet_diagnostic.CA1711.severity = none -# CA1716: Identifiers should not match keywords -# TODO: We should look into this. -dotnet_diagnostic.CA1716.severity = warning -# CA1720: Identifiers should not contain type names -dotnet_diagnostic.CA1720.severity = none -# CA1805: Do not initialize unnecessarily -# Don't tell me what to do. -dotnet_diagnostic.CA1805.severity = none -# CA1304: Specify CultureInfo -# TODO: We should look into this. -dotnet_diagnostic.CA1304.severity = warning -# CA1305: Specify IFormatProvider -# TODO: We should look into this. Disabled for now because it's annoying. -dotnet_diagnostic.CA1305.severity = none -# CA1310: Specify StringComparison for correctness -# TODO: We should look into this. Disabled for now because it's annoying. -dotnet_diagnostic.CA1310.severity = none +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +dotnet_style_require_accessibility_modifiers = always + # Diagnostics to prevent defensive copies of `in` struct parameters resharper_possibly_impure_method_call_on_readonly_variable_highlighting = error + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# IDE1006: Naming rule violation +dotnet_diagnostic.IDE1006.severity = suggestion + +# Severity levels for dotnet_naming_rule only affect IDE environments. +# To have them extra visible to people, we can set them as 'warning' here without affecting compilation. + +# Everything should be PascalCase by default +dotnet_naming_rule.all_should_be_camel_case.severity = warning +dotnet_naming_rule.all_should_be_camel_case.symbols = all +dotnet_naming_rule.all_should_be_camel_case.style = pascal_case_style +# Non-public fields should be _camelCase +dotnet_naming_rule.non_public_fields_should_be_underscore_camel_case.severity = warning +dotnet_naming_rule.non_public_fields_should_be_underscore_camel_case.symbols = non_public_fields +dotnet_naming_rule.non_public_fields_should_be_underscore_camel_case.style = underscore_camel_case_style +# Constant fields (and local vars) should be PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = warning +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case_style +# Locals variables should be camelCase +dotnet_naming_rule.local_vars_should_be_camel_case.severity = warning +dotnet_naming_rule.local_vars_should_be_camel_case.symbols = local_vars +dotnet_naming_rule.local_vars_should_be_camel_case.style = camel_case_style +# Parameters should be camelCase +dotnet_naming_rule.parameters_should_be_camel_case.severity = warning +dotnet_naming_rule.parameters_should_be_camel_case.symbols = parameters +dotnet_naming_rule.parameters_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.all.applicable_kinds = * +dotnet_naming_symbols.local_vars.applicable_kinds = local +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const +dotnet_naming_symbols.non_public_fields.applicable_kinds = field +dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private, protected, private_protected + +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_style.camel_case_style.required_prefix = +dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case +dotnet_naming_style.underscore_camel_case_style.required_prefix = _ +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_style.pascal_case_style.required_prefix = diff --git a/modules/mono/SCsub b/modules/mono/SCsub index 564c5929c7..d267df938a 100644 --- a/modules/mono/SCsub +++ b/modules/mono/SCsub @@ -18,12 +18,6 @@ env_mono.add_source_files(env.modules_sources, "glue/*.cpp") env_mono.add_source_files(env.modules_sources, "mono_gd/*.cpp") env_mono.add_source_files(env.modules_sources, "utils/*.cpp") -env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.cpp") - -if env["platform"] in ["macos", "ios"]: - env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.mm") - env_mono.add_source_files(env.modules_sources, "mono_gd/support/*.m") - if env.editor_build: env_mono.add_source_files(env.modules_sources, "editor/*.cpp") SConscript("editor/script_templates/SCsub") diff --git a/modules/mono/build_scripts/build_assemblies.py b/modules/mono/build_scripts/build_assemblies.py index 580f51c973..aa6f6ef05e 100755 --- a/modules/mono/build_scripts/build_assemblies.py +++ b/modules/mono/build_scripts/build_assemblies.py @@ -312,13 +312,41 @@ def generate_sdk_package_versions(): ) # We write in ../SdkPackageVersions.props. - with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w") as f: + with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w", encoding="utf-8") as f: f.write(props) f.close() + # Also write the versioned docs URL to a constant for the Source Generators. + + constants = """namespace Godot.SourceGenerators +{{ +// TODO: This is currently disabled because of https://github.com/dotnet/roslyn/issues/52904 +#pragma warning disable IDE0040 // Add accessibility modifiers. + partial class Common + {{ + public const string VersionDocsUrl = "https://docs.godotengine.org/en/{docs_branch}"; + }} +}} +""".format( + **version_info + ) + + generators_dir = os.path.join( + dirname(script_path), + "editor", + "Godot.NET.Sdk", + "Godot.SourceGenerators", + "Generated", + ) + os.makedirs(generators_dir, exist_ok=True) + + with open(os.path.join(generators_dir, "Common.Constants.cs"), "w", newline="\n", encoding="utf-8") as f: + f.write(constants) + f.close() + def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision): - # Generate SdkPackageVersions.props + # Generate SdkPackageVersions.props and VersionDocsUrl constant generate_sdk_package_versions() # Godot API diff --git a/modules/mono/config.py b/modules/mono/config.py index 859d77b262..3d087c9e27 100644 --- a/modules/mono/config.py +++ b/modules/mono/config.py @@ -1,8 +1,3 @@ -# Prior to .NET Core, we supported these: ["windows", "macos", "linuxbsd", "android", "web", "ios"] -# Eventually support for each them should be added back. -supported_platforms = ["windows", "macos", "linuxbsd", "android", "ios"] - - def can_build(env, platform): if env["arch"].startswith("rv"): return False @@ -14,9 +9,10 @@ def can_build(env, platform): def configure(env): - platform = env["platform"] + # Check if the platform has marked mono as supported. + supported = env.get("supported", []) - if platform not in supported_platforms: + if not "mono" in supported: raise RuntimeError("This module does not currently support building for this platform") env.add_module_version_string("mono") diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 44bcd4cfe4..9ccaa27e84 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -142,6 +142,10 @@ void CSharpLanguage::finalize() { return; } + if (gdmono && gdmono->is_runtime_initialized() && GDMonoCache::godot_api_cache_updated) { + GDMonoCache::managed_callbacks.DisposablesTracker_OnGodotShuttingDown(); + } + finalizing = true; // Make sure all script binding gchandles are released before finalizing GDMono @@ -303,7 +307,7 @@ void CSharpLanguage::get_reserved_words(List<String> *p_words) const { } } -bool CSharpLanguage::is_control_flow_keyword(String p_keyword) const { +bool CSharpLanguage::is_control_flow_keyword(const String &p_keyword) const { return p_keyword == "break" || p_keyword == "case" || p_keyword == "catch" || @@ -367,7 +371,7 @@ Ref<Script> CSharpLanguage::make_template(const String &p_template, const String return scr; } -Vector<ScriptLanguage::ScriptTemplate> CSharpLanguage::get_built_in_templates(StringName p_object) { +Vector<ScriptLanguage::ScriptTemplate> CSharpLanguage::get_built_in_templates(const StringName &p_object) { Vector<ScriptLanguage::ScriptTemplate> templates; #ifdef TOOLS_ENABLED for (int i = 0; i < TEMPLATES_ARRAY_SIZE; i++) { @@ -512,22 +516,11 @@ static String variant_type_to_managed_name(const String &p_var_type_name) { } String CSharpLanguage::make_function(const String &, const String &p_name, const PackedStringArray &p_args) const { - // FIXME - // - Due to Godot's API limitation this just appends the function to the end of the file - // - Use fully qualified name if there is ambiguity - String s = "private void " + p_name + "("; - for (int i = 0; i < p_args.size(); i++) { - const String &arg = p_args[i]; - - if (i > 0) { - s += ", "; - } - - s += variant_type_to_managed_name(arg.get_slice(":", 1)) + " " + escape_csharp_keyword(arg.get_slice(":", 0)); - } - s += ")\n{\n // Replace with function body.\n}\n"; - - return s; + // The make_function() API does not work for C# scripts. + // It will always append the generated function at the very end of the script. In C#, it will break compilation by + // appending code after the final closing bracket (either the class' or the namespace's). + // To prevent issues, we have can_make_function() returning false, and make_function() is never implemented. + return String(); } #else String CSharpLanguage::make_function(const String &, const String &, const PackedStringArray &) const { @@ -554,42 +547,9 @@ bool CSharpLanguage::handles_global_class_type(const String &p_type) const { } String CSharpLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const { - Ref<CSharpScript> scr = ResourceLoader::load(p_path, get_type()); - // Always assign r_base_type and r_icon_path, even if the script - // is not a global one. In the case that it is not a global script, - // return an empty string AFTER assigning the return parameters. - // See GDScriptLanguage::get_global_class_name() in modules/gdscript/gdscript.cpp - - if (!scr.is_valid() || !scr->valid) { - // Invalid script. - return String(); - } - - if (r_icon_path) { - if (scr->icon_path.is_empty() || scr->icon_path.is_absolute_path()) { - *r_icon_path = scr->icon_path.simplify_path(); - } else if (scr->icon_path.is_relative_path()) { - *r_icon_path = p_path.get_base_dir().path_join(scr->icon_path).simplify_path(); - } - } - if (r_base_type) { - bool found_global_base_script = false; - const CSharpScript *top = scr->base_script.ptr(); - while (top != nullptr) { - if (top->global_class) { - *r_base_type = top->class_name; - found_global_base_script = true; - break; - } - - top = top->base_script.ptr(); - } - if (!found_global_base_script) { - *r_base_type = scr->get_instance_base_type(); - } - } - - return scr->global_class ? scr->class_name : String(); + String class_name; + GDMonoCache::managed_callbacks.ScriptManagerBridge_GetGlobalClassName(&p_path, r_base_type, r_icon_path, &class_name); + return class_name; } String CSharpLanguage::debug_get_error() const { @@ -716,9 +676,15 @@ void CSharpLanguage::reload_all_scripts() { #endif } -void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) { - (void)p_script; // UNUSED +void CSharpLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload) { +#ifdef GD_MONO_HOT_RELOAD + if (is_assembly_reloading_needed()) { + reload_assemblies(p_soft_reload); + } +#endif +} +void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) { CRASH_COND(!Engine::get_singleton()->is_editor_hint()); #ifdef TOOLS_ENABLED @@ -921,7 +887,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { obj->set_script(Ref<RefCounted>()); // Remove script and existing script instances (placeholder are not removed before domain reload) } - scr->was_tool_before_reload = scr->tool; + scr->was_tool_before_reload = scr->type_info.is_tool; scr->_clear(); } @@ -981,7 +947,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { scr->exports_invalidated = true; #endif - if (!scr->get_path().is_empty()) { + if (!scr->get_path().is_empty() && !scr->get_path().begins_with("csharp://")) { scr->reload(p_soft_reload); if (!scr->valid) { @@ -1070,7 +1036,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) { } // The script instance could not be instantiated or wasn't in the list of placeholders to replace. obj->set_script(scr); -#if DEBUG_ENABLED +#ifdef DEBUG_ENABLED // If we reached here, the instantiated script must be a placeholder. CRASH_COND(!obj->get_script_instance()->is_placeholder()); #endif @@ -1823,6 +1789,7 @@ bool CSharpInstance::_internal_new_managed() { ERR_FAIL_NULL_V(owner, false); ERR_FAIL_COND_V(script.is_null(), false); + ERR_FAIL_COND_V(!script->can_instantiate(), false); bool ok = GDMonoCache::managed_callbacks.ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance( script.ptr(), owner, nullptr, 0); @@ -1985,24 +1952,31 @@ const Variant CSharpInstance::get_rpc_config() const { void CSharpInstance::notification(int p_notification, bool p_reversed) { if (p_notification == Object::NOTIFICATION_PREDELETE) { - // When NOTIFICATION_PREDELETE is sent, we also take the chance to call Dispose(). - // It's safe to call Dispose() multiple times and NOTIFICATION_PREDELETE is guaranteed + if (base_ref_counted) { + // At this point, Dispose() was already called (manually or from the finalizer). + // The RefCounted wouldn't have reached 0 otherwise, since the managed side + // references it and Dispose() needs to be called to release it. + // However, this means C# RefCounted scripts can't receive NOTIFICATION_PREDELETE, but + // this is likely the case with GDScript as well: https://github.com/godotengine/godot/issues/6784 + return; + } + } else if (p_notification == Object::NOTIFICATION_PREDELETE_CLEANUP) { + // When NOTIFICATION_PREDELETE_CLEANUP is sent, we also take the chance to call Dispose(). + // It's safe to call Dispose() multiple times and NOTIFICATION_PREDELETE_CLEANUP is guaranteed // to be sent at least once, which happens right before the call to the destructor. predelete_notified = true; if (base_ref_counted) { - // It's not safe to proceed if the owner derives RefCounted and the refcount reached 0. - // At this point, Dispose() was already called (manually or from the finalizer) so - // that's not a problem. The refcount wouldn't have reached 0 otherwise, since the - // managed side references it and Dispose() needs to be called to release it. - // However, this means C# RefCounted scripts can't receive NOTIFICATION_PREDELETE, but - // this is likely the case with GDScript as well: https://github.com/godotengine/godot/issues/6784 + // At this point, Dispose() was already called (manually or from the finalizer). + // The RefCounted wouldn't have reached 0 otherwise, since the managed side + // references it and Dispose() needs to be called to release it. return; } - _call_notification(p_notification, p_reversed); - + // NOTIFICATION_PREDELETE_CLEANUP is not sent to scripts. + // After calling Dispose() the C# instance can no longer be used, + // so it should be the last thing we do. GDMonoCache::managed_callbacks.CSharpInstanceBridge_CallDispose( gchandle.get_intptr(), /* okIfNull */ false); @@ -2138,7 +2112,7 @@ void GD_CLR_STDCALL CSharpScript::_add_property_info_list_callback(CSharpScript #ifdef TOOLS_ENABLED p_script->exported_members_cache.push_back(PropertyInfo( - Variant::NIL, *p_current_class_name, PROPERTY_HINT_NONE, + Variant::NIL, p_script->type_info.class_name, PROPERTY_HINT_NONE, p_script->get_path(), PROPERTY_USAGE_CATEGORY)); #endif @@ -2235,6 +2209,17 @@ bool CSharpScript::_update_exports(PlaceHolderScriptInstance *p_instance_to_upda } else { p_instance_to_update->update(propnames, values); } + } else if (placeholders.size()) { + uint64_t script_modified_time = FileAccess::get_modified_time(get_path()); + uint64_t last_valid_build_time = GDMono::get_singleton()->get_project_assembly_modified_time(); + if (script_modified_time > last_valid_build_time) { + for (PlaceHolderScriptInstance *instance : placeholders) { + Object *owner = instance->get_owner(); + if (owner->get_script_instance() == instance) { + owner->notify_property_list_changed(); + } + } + } } } #endif @@ -2288,7 +2273,7 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) { p_script->_update_exports(); -#if TOOLS_ENABLED +#ifdef TOOLS_ENABLED // If the EditorFileSystem singleton is available, update the file; // otherwise, the file will be updated when the singleton becomes available. EditorFileSystem *efs = EditorFileSystem::get_singleton(); @@ -2300,9 +2285,7 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) { // Extract information about the script using the mono class. void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { - bool tool = false; - bool global_class = false; - bool abstract_class = false; + TypeInfo type_info; // TODO: Use GDExtension godot_dictionary Array methods_array; @@ -2312,18 +2295,12 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { Dictionary signals_dict; signals_dict.~Dictionary(); - String class_name; - String icon_path; Ref<CSharpScript> base_script; GDMonoCache::managed_callbacks.ScriptManagerBridge_UpdateScriptClassInfo( - p_script.ptr(), &class_name, &tool, &global_class, &abstract_class, &icon_path, + p_script.ptr(), &type_info, &methods_array, &rpc_functions_dict, &signals_dict, &base_script); - p_script->class_name = class_name; - p_script->tool = tool; - p_script->global_class = global_class; - p_script->abstract_class = abstract_class; - p_script->icon_path = icon_path; + p_script->type_info = type_info; p_script->rpc_config.clear(); p_script->rpc_config = rpc_functions_dict; @@ -2343,6 +2320,8 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { MethodInfo mi; mi.name = name; + mi.return_val = PropertyInfo::from_dict(method_info_dict["return_val"]); + Array params = method_info_dict["params"]; for (int j = 0; j < params.size(); j++) { @@ -2400,7 +2379,7 @@ void CSharpScript::update_script_class_info(Ref<CSharpScript> p_script) { bool CSharpScript::can_instantiate() const { #ifdef TOOLS_ENABLED - bool extra_cond = tool || ScriptServer::is_scripting_enabled(); + bool extra_cond = type_info.is_tool || ScriptServer::is_scripting_enabled(); #else bool extra_cond = true; #endif @@ -2409,10 +2388,10 @@ bool CSharpScript::can_instantiate() const { // For tool scripts, this will never fire if the class is not found. That's because we // don't know if it's a tool script if we can't find the class to access the attributes. if (extra_cond && !valid) { - ERR_FAIL_V_MSG(false, "Cannot instance script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive)."); + ERR_FAIL_V_MSG(false, "Cannot instantiate C# script because the associated class could not be found. Script: '" + get_path() + "'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive)."); } - return valid && !abstract_class && extra_cond; + return valid && type_info.can_instantiate() && extra_cond; } StringName CSharpScript::get_instance_base_type() const { @@ -2422,6 +2401,8 @@ StringName CSharpScript::get_instance_base_type() const { } CSharpInstance *CSharpScript::_create_instance(const Variant **p_args, int p_argcount, Object *p_owner, bool p_is_ref_counted, Callable::CallError &r_error) { + ERR_FAIL_COND_V_MSG(!type_info.can_instantiate(), nullptr, "Cannot instantiate C# script. Script: '" + get_path() + "'."); + /* STEP 1, CREATE */ Ref<RefCounted> ref; @@ -2609,12 +2590,12 @@ MethodInfo CSharpScript::get_method_info(const StringName &p_method) const { } Variant CSharpScript::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { - ERR_FAIL_COND_V(!valid, Variant()); - - Variant ret; - bool ok = GDMonoCache::managed_callbacks.ScriptManagerBridge_CallStatic(this, &p_method, p_args, p_argcount, &r_error, &ret); - if (ok) { - return ret; + if (valid) { + Variant ret; + bool ok = GDMonoCache::managed_callbacks.ScriptManagerBridge_CallStatic(this, &p_method, p_args, p_argcount, &r_error, &ret); + if (ok) { + return ret; + } } return Script::callp(p_method, p_args, p_argcount, r_error); @@ -2642,7 +2623,7 @@ Error CSharpScript::reload(bool p_keep_state) { _update_exports(); -#if TOOLS_ENABLED +#ifdef TOOLS_ENABLED // If the EditorFileSystem singleton is available, update the file; // otherwise, the file will be updated when the singleton becomes available. EditorFileSystem *efs = EditorFileSystem::get_singleton(); @@ -2736,11 +2717,11 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const { } Ref<Script> CSharpScript::get_base_script() const { - return base_script.is_valid() && !base_script->get_path().is_empty() ? base_script : nullptr; + return base_script; } StringName CSharpScript::get_global_name() const { - return global_class ? StringName(class_name) : StringName(); + return type_info.is_global_class ? StringName(type_info.class_name) : StringName(); } void CSharpScript::get_script_property_list(List<PropertyInfo> *r_list) const { @@ -2797,7 +2778,7 @@ Error CSharpScript::load_source_code(const String &p_path) { } void CSharpScript::_clear() { - tool = false; + type_info = TypeInfo(); valid = false; reload_invalidated = true; } @@ -2808,15 +2789,17 @@ CSharpScript::CSharpScript() { #ifdef DEBUG_ENABLED { MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); - CSharpLanguage::get_singleton()->script_list.add(&this->script_list); + CSharpLanguage::get_singleton()->script_list.add(&script_list); } #endif } CSharpScript::~CSharpScript() { #ifdef DEBUG_ENABLED - MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); - CSharpLanguage::get_singleton()->script_list.remove(&this->script_list); + { + MutexLock lock(CSharpLanguage::get_singleton()->script_instances_mutex); + CSharpLanguage::get_singleton()->script_list.remove(&script_list); + } #endif if (GDMonoCache::godot_api_cache_updated) { @@ -2843,20 +2826,48 @@ Ref<Resource> ResourceFormatLoaderCSharpScript::load(const String &p_path, const // TODO ignore anything inside bin/ and obj/ in tools builds? + String real_path = p_path; + if (p_path.begins_with("csharp://")) { + // This is a virtual path used by generic types, extract the real path. + real_path = "res://" + p_path.trim_prefix("csharp://"); + real_path = real_path.substr(0, real_path.rfind(":")); + } + Ref<CSharpScript> scr; if (GDMonoCache::godot_api_cache_updated) { GDMonoCache::managed_callbacks.ScriptManagerBridge_GetOrCreateScriptBridgeForPath(&p_path, &scr); + ERR_FAIL_NULL_V_MSG(scr, Ref<Resource>(), "Could not create C# script '" + real_path + "'."); } else { scr = Ref<CSharpScript>(memnew(CSharpScript)); } #if defined(DEBUG_ENABLED) || defined(TOOLS_ENABLED) - Error err = scr->load_source_code(p_path); - ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + p_path + "'."); -#endif - - scr->set_path(p_original_path); + Error err = scr->load_source_code(real_path); + ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot load C# script file '" + real_path + "'."); +#endif + + // Only one instance of a C# script is allowed to exist. + ERR_FAIL_COND_V_MSG(!scr->get_path().is_empty() && scr->get_path() != p_original_path, Ref<Resource>(), + "The C# script path is different from the path it was registered in the C# dictionary."); + + Ref<Resource> existing = ResourceCache::get_ref(p_path); + switch (p_cache_mode) { + case ResourceFormatLoader::CACHE_MODE_IGNORE: + case ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP: + break; + case ResourceFormatLoader::CACHE_MODE_REUSE: + if (existing.is_null()) { + scr->set_path(p_original_path); + } else { + scr = existing; + } + break; + case ResourceFormatLoader::CACHE_MODE_REPLACE: + case ResourceFormatLoader::CACHE_MODE_REPLACE_DEEP: + scr->set_path(p_original_path, true); + break; + } scr->reload(); diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index e381f0e840..99e6ebf2e3 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -61,12 +61,87 @@ class CSharpScript : public Script { friend class CSharpInstance; friend class CSharpLanguage; - bool tool = false; - bool global_class = false; - bool abstract_class = false; +public: + struct TypeInfo { + /** + * Name of the C# class. + */ + String class_name; + + /** + * Path to the icon that will be used for this class by the editor. + */ + String icon_path; + + /** + * Script is marked as tool and runs in the editor. + */ + bool is_tool = false; + + /** + * Script is marked as global class and will be registered in the editor. + * Registered classes can be created using certain editor dialogs and + * can be referenced by name from other languages that support the feature. + */ + bool is_global_class = false; + + /** + * Script is declared abstract. + */ + bool is_abstract = false; + + /** + * The C# type that corresponds to this script is a constructed generic type. + * E.g.: `Dictionary<int, string>` + */ + bool is_constructed_generic_type = false; + + /** + * The C# type that corresponds to this script is a generic type definition. + * E.g.: `Dictionary<,>` + */ + bool is_generic_type_definition = false; + + /** + * The C# type that corresponds to this script contains generic type parameters, + * regardless of whether the type parameters are bound or not. + */ + bool is_generic() const { + return is_constructed_generic_type || is_generic_type_definition; + } + + /** + * Check if the script can be instantiated. + * C# types can't be instantiated if they are abstract or contain generic + * type parameters, but a CSharpScript is still created for them. + */ + bool can_instantiate() const { + return !is_abstract && !is_generic_type_definition; + } + }; + +private: + /** + * Contains the C# type information for this script. + */ + TypeInfo type_info; + + /** + * Scripts are valid when the corresponding C# class is found and used + * to extract the script info using the [update_script_class_info] method. + */ bool valid = false; + /** + * Scripts extract info from the C# class in the reload methods but, + * if the reload is not invalidated, then the current extracted info + * is still valid and there's no need to reload again. + */ bool reload_invalidated = false; + /** + * Base script that this script derives from, or null if it derives from a + * native Godot class. + */ Ref<CSharpScript> base_script; HashSet<Object *> instances; @@ -87,9 +162,10 @@ class CSharpScript : public Script { HashSet<ObjectID> pending_replace_placeholders; #endif + /** + * Script source code. + */ String source; - String class_name; - String icon_path; SelfList<CSharpScript> script_list = this; @@ -166,7 +242,7 @@ public: return docs; } virtual String get_class_icon_path() const override { - return icon_path; + return type_info.icon_path; } #endif // TOOLS_ENABLED @@ -184,13 +260,13 @@ public: void get_members(HashSet<StringName> *p_members) override; bool is_tool() const override { - return tool; + return type_info.is_tool; } bool is_valid() const override { return valid; } bool is_abstract() const override { - return abstract_class; + return type_info.is_abstract; } bool inherits_script(const Ref<Script> &p_script) const override; @@ -417,13 +493,13 @@ public: /* EDITOR FUNCTIONS */ void get_reserved_words(List<String> *p_words) const override; - bool is_control_flow_keyword(String p_keyword) const override; + bool is_control_flow_keyword(const String &p_keyword) const override; void get_comment_delimiters(List<String> *p_delimiters) const override; void get_doc_comment_delimiters(List<String> *p_delimiters) const override; void get_string_delimiters(List<String> *p_delimiters) const override; bool is_using_templates() override; virtual Ref<Script> make_template(const String &p_template, const String &p_class_name, const String &p_base_class_name) const override; - virtual Vector<ScriptTemplate> get_built_in_templates(StringName p_object) override; + virtual Vector<ScriptTemplate> get_built_in_templates(const StringName &p_object) override; /* TODO */ bool validate(const String &p_script, const String &p_path, List<String> *r_functions, List<ScriptLanguage::ScriptError> *r_errors = nullptr, List<ScriptLanguage::Warning> *r_warnings = nullptr, HashSet<int> *r_safe_lines = nullptr) const override { return true; @@ -438,6 +514,7 @@ public: return -1; } String make_function(const String &p_class, const String &p_name, const PackedStringArray &p_args) const override; + virtual bool can_make_function() const override { return false; } virtual String _get_indentation() const; /* TODO? */ void auto_indent_code(String &p_code, int p_from_line, int p_to_line) const override {} /* TODO */ void add_global_constant(const StringName &p_variable, const Variant &p_value) override {} @@ -463,6 +540,7 @@ public: /* PROFILING FUNCTIONS */ /* TODO */ void profiling_start() override {} /* TODO */ void profiling_stop() override {} + /* TODO */ void profiling_set_save_native_calls(bool p_enable) override {} /* TODO */ int profiling_get_accumulated_data(ProfilingInfo *p_info_arr, int p_info_max) override { return 0; } @@ -477,6 +555,7 @@ public: /* TODO? */ void get_public_annotations(List<MethodInfo> *p_annotations) const override {} void reload_all_scripts() override; + void reload_scripts(const Array &p_scripts, bool p_soft_reload) override; void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override; /* LOADER FUNCTIONS */ diff --git a/modules/mono/editor/Godot.NET.Sdk/.gitignore b/modules/mono/editor/Godot.NET.Sdk/.gitignore new file mode 100644 index 0000000000..55ec4bcc64 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/.gitignore @@ -0,0 +1,2 @@ +# Generated sources directories +Godot.SourceGenerators/Generated diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln index 03a7dc453c..9674626183 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "G EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Sample", "Godot.SourceGenerators.Sample\Godot.SourceGenerators.Sample.csproj", "{7297A614-8DF5-43DE-9EAD-99671B26BD1F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Tests", "Godot.SourceGenerators.Tests\Godot.SourceGenerators.Tests.csproj", "{07E6D201-35C9-4463-9B29-D16621EA733D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}" EndProject Global @@ -26,6 +28,10 @@ Global {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.Build.0 = Release|Any CPU + {07E6D201-35C9-4463-9B29-D16621EA733D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E6D201-35C9-4463-9B29-D16621EA733D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E6D201-35C9-4463-9B29-D16621EA733D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E6D201-35C9-4463-9B29-D16621EA733D}.Release|Any CPU.Build.0 = Release|Any CPU {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index ad3a10ba49..ccef90c911 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -7,7 +7,7 @@ <Authors>Godot Engine contributors</Authors> <PackageId>Godot.NET.Sdk</PackageId> - <Version>4.2.0</Version> + <Version>4.3.0</Version> <PackageVersion>$(PackageVersion_Godot_NET_Sdk)</PackageVersion> <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets index d8129a6652..b51ce5cb8c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/iOSNativeAOT.targets @@ -33,9 +33,9 @@ <Message Importance="normal" Text="Found XCode at $(XcodeSelect)" Condition=" '$(FindXCode)' == 'true' "/> <ItemGroup> - <LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk%22" + <LinkerArg Include="-mios-simulator-version-min=12.0 -isysroot %22$(XCodePath)Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk%22" Condition=" $(RuntimeIdentifier.Contains('simulator')) "/> - <LinkerArg Include="-isysroot %22$(XCodePath)Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk%22" + <LinkerArg Include="-miphoneos-version-min=12.0 -isysroot %22$(XCodePath)Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk%22" Condition=" !$(RuntimeIdentifier.Contains('simulator')) "/> </ItemGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs index 2d797e2f46..bf37651787 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Bar.cs @@ -1,15 +1,15 @@ namespace Godot.SourceGenerators.Sample { - partial class Bar : GodotObject + public partial class Bar : GodotObject { } // Foo in another file - partial class Foo + public partial class Foo { } - partial class NotSameNameAsFile : GodotObject + public partial class NotSameNameAsFile : GodotObject { } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs index 31e66ac306..c9e504fec7 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedFields.cs @@ -13,106 +13,106 @@ namespace Godot.SourceGenerators.Sample [SuppressMessage("ReSharper", "InconsistentNaming")] public partial class ExportedFields : GodotObject { - [Export] private Boolean field_Boolean = true; - [Export] private Char field_Char = 'f'; - [Export] private SByte field_SByte = 10; - [Export] private Int16 field_Int16 = 10; - [Export] private Int32 field_Int32 = 10; - [Export] private Int64 field_Int64 = 10; - [Export] private Byte field_Byte = 10; - [Export] private UInt16 field_UInt16 = 10; - [Export] private UInt32 field_UInt32 = 10; - [Export] private UInt64 field_UInt64 = 10; - [Export] private Single field_Single = 10; - [Export] private Double field_Double = 10; - [Export] private String field_String = "foo"; + [Export] private Boolean _fieldBoolean = true; + [Export] private Char _fieldChar = 'f'; + [Export] private SByte _fieldSByte = 10; + [Export] private Int16 _fieldInt16 = 10; + [Export] private Int32 _fieldInt32 = 10; + [Export] private Int64 _fieldInt64 = 10; + [Export] private Byte _fieldByte = 10; + [Export] private UInt16 _fieldUInt16 = 10; + [Export] private UInt32 _fieldUInt32 = 10; + [Export] private UInt64 _fieldUInt64 = 10; + [Export] private Single _fieldSingle = 10; + [Export] private Double _fieldDouble = 10; + [Export] private String _fieldString = "foo"; // Godot structs - [Export] private Vector2 field_Vector2 = new(10f, 10f); - [Export] private Vector2I field_Vector2I = Vector2I.Up; - [Export] private Rect2 field_Rect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); - [Export] private Rect2I field_Rect2I = new(new Vector2I(10, 10), new Vector2I(10, 10)); - [Export] private Transform2D field_Transform2D = Transform2D.Identity; - [Export] private Vector3 field_Vector3 = new(10f, 10f, 10f); - [Export] private Vector3I field_Vector3I = Vector3I.Back; - [Export] private Basis field_Basis = new Basis(Quaternion.Identity); - [Export] private Quaternion field_Quaternion = new Quaternion(Basis.Identity); - [Export] private Transform3D field_Transform3D = Transform3D.Identity; - [Export] private Vector4 field_Vector4 = new(10f, 10f, 10f, 10f); - [Export] private Vector4I field_Vector4I = Vector4I.One; - [Export] private Projection field_Projection = Projection.Identity; - [Export] private Aabb field_Aabb = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); - [Export] private Color field_Color = Colors.Aquamarine; - [Export] private Plane field_Plane = Plane.PlaneXZ; - [Export] private Callable field_Callable = new Callable(Engine.GetMainLoop(), "_process"); - [Export] private Signal field_Signal = new Signal(Engine.GetMainLoop(), "property_list_changed"); + [Export] private Vector2 _fieldVector2 = new(10f, 10f); + [Export] private Vector2I _fieldVector2I = Vector2I.Up; + [Export] private Rect2 _fieldRect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2I _fieldRect2I = new(new Vector2I(10, 10), new Vector2I(10, 10)); + [Export] private Transform2D _fieldTransform2D = Transform2D.Identity; + [Export] private Vector3 _fieldVector3 = new(10f, 10f, 10f); + [Export] private Vector3I _fieldVector3I = Vector3I.Back; + [Export] private Basis _fieldBasis = new Basis(Quaternion.Identity); + [Export] private Quaternion _fieldQuaternion = new Quaternion(Basis.Identity); + [Export] private Transform3D _fieldTransform3D = Transform3D.Identity; + [Export] private Vector4 _fieldVector4 = new(10f, 10f, 10f, 10f); + [Export] private Vector4I _fieldVector4I = Vector4I.One; + [Export] private Projection _fieldProjection = Projection.Identity; + [Export] private Aabb _fieldAabb = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color _fieldColor = Colors.Aquamarine; + [Export] private Plane _fieldPlane = Plane.PlaneXZ; + [Export] private Callable _fieldCallable = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private Signal _fieldSignal = new Signal(Engine.GetMainLoop(), "property_list_changed"); // Enums [SuppressMessage("ReSharper", "UnusedMember.Local")] - enum MyEnum + public enum MyEnum { A, B, C } - [Export] private MyEnum field_Enum = MyEnum.C; + [Export] private MyEnum _fieldEnum = MyEnum.C; [Flags] [SuppressMessage("ReSharper", "UnusedMember.Local")] - enum MyFlagsEnum + public enum MyFlagsEnum { A, B, C } - [Export] private MyFlagsEnum field_FlagsEnum = MyFlagsEnum.C; + [Export] private MyFlagsEnum _fieldFlagsEnum = MyFlagsEnum.C; // Arrays - [Export] private Byte[] field_ByteArray = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Int32[] field_Int32Array = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Int64[] field_Int64Array = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Single[] field_SingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; - [Export] private Double[] field_DoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; - [Export] private String[] field_StringArray = { "foo", "bar" }; - [Export(PropertyHint.Enum, "A,B,C")] private String[] field_StringArrayEnum = { "foo", "bar" }; - [Export] private Vector2[] field_Vector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; - [Export] private Vector3[] field_Vector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; - [Export] private Color[] field_ColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; - [Export] private GodotObject[] field_GodotObjectOrDerivedArray = { null }; - [Export] private StringName[] field_StringNameArray = { "foo", "bar" }; - [Export] private NodePath[] field_NodePathArray = { "foo", "bar" }; - [Export] private Rid[] field_RidArray = { default, default, default }; + [Export] private Byte[] _fieldByteArray = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] _fieldInt32Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] _fieldInt64Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] _fieldSingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] _fieldDoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] _fieldStringArray = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] _fieldStringArrayEnum = { "foo", "bar" }; + [Export] private Vector2[] _fieldVector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] _fieldVector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] _fieldColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private GodotObject[] _fieldGodotObjectOrDerivedArray = { null }; + [Export] private StringName[] _fieldStringNameArray = { "foo", "bar" }; + [Export] private NodePath[] _fieldNodePathArray = { "foo", "bar" }; + [Export] private Rid[] _fieldRidArray = { default, default, default }; // Note we use Array and not System.Array. This tests the generated namespace qualification. - [Export] private Int32[] field_empty_Int32Array = Array.Empty<Int32>(); + [Export] private Int32[] _fieldEmptyInt32Array = Array.Empty<Int32>(); // Note we use List and not System.Collections.Generic. - [Export] private int[] field_array_from_list = new List<int>(Array.Empty<int>()).ToArray(); + [Export] private int[] _fieldArrayFromList = new List<int>(Array.Empty<int>()).ToArray(); // Variant - [Export] private Variant field_Variant = "foo"; + [Export] private Variant _fieldVariant = "foo"; // Classes - [Export] private GodotObject field_GodotObjectOrDerived; - [Export] private Godot.Texture field_GodotResourceTexture; - [Export] private StringName field_StringName = new StringName("foo"); - [Export] private NodePath field_NodePath = new NodePath("foo"); - [Export] private Rid field_Rid; + [Export] private GodotObject _fieldGodotObjectOrDerived; + [Export] private Godot.Texture _fieldGodotResourceTexture; + [Export] private StringName _fieldStringName = new StringName("foo"); + [Export] private NodePath _fieldNodePath = new NodePath("foo"); + [Export] private Rid _fieldRid; [Export] - private Godot.Collections.Dictionary field_GodotDictionary = + private Godot.Collections.Dictionary _fieldGodotDictionary = new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; [Export] - private Godot.Collections.Array field_GodotArray = + private Godot.Collections.Array _fieldGodotArray = new() { "foo", 10, Vector2.Up, Colors.Chocolate }; [Export] - private Godot.Collections.Dictionary<string, bool> field_GodotGenericDictionary = + private Godot.Collections.Dictionary<string, bool> _fieldGodotGenericDictionary = new() { { "foo", true }, { "bar", false } }; [Export] - private Godot.Collections.Array<int> field_GodotGenericArray = + private Godot.Collections.Array<int> _fieldGodotGenericArray = new() { 0, 1, 2, 3, 4, 5, 6 }; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs index aef2a8824e..91f33383ab 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ExportedProperties.cs @@ -13,41 +13,41 @@ namespace Godot.SourceGenerators.Sample public partial class ExportedProperties : GodotObject { // Do not generate default value - private String _notGenerate_Property_String = new string("not generate"); + private String _notGeneratePropertyString = new string("not generate"); [Export] - public String NotGenerate_Complex_Lamda_Property + public String NotGenerateComplexLambdaProperty { - get => _notGenerate_Property_String + Convert.ToInt32("1"); - set => _notGenerate_Property_String = value; + get => _notGeneratePropertyString + Convert.ToInt32("1"); + set => _notGeneratePropertyString = value; } [Export] - public String NotGenerate_Lamda_NoField_Property + public String NotGenerateLambdaNoFieldProperty { get => new string("not generate"); - set => _notGenerate_Property_String = value; + set => _notGeneratePropertyString = value; } [Export] - public String NotGenerate_Complex_Return_Property + public String NotGenerateComplexReturnProperty { get { - return _notGenerate_Property_String + Convert.ToInt32("1"); + return _notGeneratePropertyString + Convert.ToInt32("1"); } set { - _notGenerate_Property_String = value; + _notGeneratePropertyString = value; } } - private int _notGenerate_Property_Int = 1; + private int _notGeneratePropertyInt = 1; [Export] public string NotGenerate_Returns_Property { get { - if (_notGenerate_Property_Int == 1) + if (_notGeneratePropertyInt == 1) { return "a"; } @@ -58,145 +58,145 @@ namespace Godot.SourceGenerators.Sample } set { - _notGenerate_Property_Int = value == "a" ? 1 : 2; + _notGeneratePropertyInt = value == "a" ? 1 : 2; } } // Full Property - private String _fullProperty_String = "FullProperty_String"; + private String _fullPropertyString = "FullPropertyString"; [Export] - public String FullProperty_String + public String FullPropertyString { get { - return _fullProperty_String; + return _fullPropertyString; } set { - _fullProperty_String = value; + _fullPropertyString = value; } } - private String _fullProperty_String_Complex = new string("FullProperty_String_Complex") + Convert.ToInt32("1"); + private String _fullPropertyStringComplex = new string("FullPropertyString_Complex") + Convert.ToInt32("1"); [Export] - public String FullProperty_String_Complex + public String FullPropertyStringComplex { get { - return _fullProperty_String_Complex; + return _fullPropertyStringComplex; } set { - _fullProperty_String_Complex = value; + _fullPropertyStringComplex = value; } } // Lambda Property - private String _lamdaProperty_String = "LamdaProperty_String"; + private String _lamdaPropertyString = "LamdaPropertyString"; [Export] - public String LamdaProperty_String + public String LamdaPropertyString { - get => _lamdaProperty_String; - set => _lamdaProperty_String = value; + get => _lamdaPropertyString; + set => _lamdaPropertyString = value; } // Auto Property - [Export] private Boolean property_Boolean { get; set; } = true; - [Export] private Char property_Char { get; set; } = 'f'; - [Export] private SByte property_SByte { get; set; } = 10; - [Export] private Int16 property_Int16 { get; set; } = 10; - [Export] private Int32 property_Int32 { get; set; } = 10; - [Export] private Int64 property_Int64 { get; set; } = 10; - [Export] private Byte property_Byte { get; set; } = 10; - [Export] private UInt16 property_UInt16 { get; set; } = 10; - [Export] private UInt32 property_UInt32 { get; set; } = 10; - [Export] private UInt64 property_UInt64 { get; set; } = 10; - [Export] private Single property_Single { get; set; } = 10; - [Export] private Double property_Double { get; set; } = 10; - [Export] private String property_String { get; set; } = "foo"; + [Export] private Boolean PropertyBoolean { get; set; } = true; + [Export] private Char PropertyChar { get; set; } = 'f'; + [Export] private SByte PropertySByte { get; set; } = 10; + [Export] private Int16 PropertyInt16 { get; set; } = 10; + [Export] private Int32 PropertyInt32 { get; set; } = 10; + [Export] private Int64 PropertyInt64 { get; set; } = 10; + [Export] private Byte PropertyByte { get; set; } = 10; + [Export] private UInt16 PropertyUInt16 { get; set; } = 10; + [Export] private UInt32 PropertyUInt32 { get; set; } = 10; + [Export] private UInt64 PropertyUInt64 { get; set; } = 10; + [Export] private Single PropertySingle { get; set; } = 10; + [Export] private Double PropertyDouble { get; set; } = 10; + [Export] private String PropertyString { get; set; } = "foo"; // Godot structs - [Export] private Vector2 property_Vector2 { get; set; } = new(10f, 10f); - [Export] private Vector2I property_Vector2I { get; set; } = Vector2I.Up; - [Export] private Rect2 property_Rect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); - [Export] private Rect2I property_Rect2I { get; set; } = new(new Vector2I(10, 10), new Vector2I(10, 10)); - [Export] private Transform2D property_Transform2D { get; set; } = Transform2D.Identity; - [Export] private Vector3 property_Vector3 { get; set; } = new(10f, 10f, 10f); - [Export] private Vector3I property_Vector3I { get; set; } = Vector3I.Back; - [Export] private Basis property_Basis { get; set; } = new Basis(Quaternion.Identity); - [Export] private Quaternion property_Quaternion { get; set; } = new Quaternion(Basis.Identity); - [Export] private Transform3D property_Transform3D { get; set; } = Transform3D.Identity; - [Export] private Vector4 property_Vector4 { get; set; } = new(10f, 10f, 10f, 10f); - [Export] private Vector4I property_Vector4I { get; set; } = Vector4I.One; - [Export] private Projection property_Projection { get; set; } = Projection.Identity; - [Export] private Aabb property_Aabb { get; set; } = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); - [Export] private Color property_Color { get; set; } = Colors.Aquamarine; - [Export] private Plane property_Plane { get; set; } = Plane.PlaneXZ; - [Export] private Callable property_Callable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); - [Export] private Signal property_Signal { get; set; } = new Signal(Engine.GetMainLoop(), "property_list_changed"); + [Export] private Vector2 PropertyVector2 { get; set; } = new(10f, 10f); + [Export] private Vector2I PropertyVector2I { get; set; } = Vector2I.Up; + [Export] private Rect2 PropertyRect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2I PropertyRect2I { get; set; } = new(new Vector2I(10, 10), new Vector2I(10, 10)); + [Export] private Transform2D PropertyTransform2D { get; set; } = Transform2D.Identity; + [Export] private Vector3 PropertyVector3 { get; set; } = new(10f, 10f, 10f); + [Export] private Vector3I PropertyVector3I { get; set; } = Vector3I.Back; + [Export] private Basis PropertyBasis { get; set; } = new Basis(Quaternion.Identity); + [Export] private Quaternion PropertyQuaternion { get; set; } = new Quaternion(Basis.Identity); + [Export] private Transform3D PropertyTransform3D { get; set; } = Transform3D.Identity; + [Export] private Vector4 PropertyVector4 { get; set; } = new(10f, 10f, 10f, 10f); + [Export] private Vector4I PropertyVector4I { get; set; } = Vector4I.One; + [Export] private Projection PropertyProjection { get; set; } = Projection.Identity; + [Export] private Aabb PropertyAabb { get; set; } = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color PropertyColor { get; set; } = Colors.Aquamarine; + [Export] private Plane PropertyPlane { get; set; } = Plane.PlaneXZ; + [Export] private Callable PropertyCallable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private Signal PropertySignal { get; set; } = new Signal(Engine.GetMainLoop(), "Propertylist_changed"); // Enums [SuppressMessage("ReSharper", "UnusedMember.Local")] - enum MyEnum + public enum MyEnum { A, B, C } - [Export] private MyEnum property_Enum { get; set; } = MyEnum.C; + [Export] private MyEnum PropertyEnum { get; set; } = MyEnum.C; [Flags] [SuppressMessage("ReSharper", "UnusedMember.Local")] - enum MyFlagsEnum + public enum MyFlagsEnum { A, B, C } - [Export] private MyFlagsEnum property_FlagsEnum { get; set; } = MyFlagsEnum.C; + [Export] private MyFlagsEnum PropertyFlagsEnum { get; set; } = MyFlagsEnum.C; // Arrays - [Export] private Byte[] property_ByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Int32[] property_Int32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Int64[] property_Int64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; - [Export] private Single[] property_SingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; - [Export] private Double[] property_DoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; - [Export] private String[] property_StringArray { get; set; } = { "foo", "bar" }; - [Export(PropertyHint.Enum, "A,B,C")] private String[] property_StringArrayEnum { get; set; } = { "foo", "bar" }; - [Export] private Vector2[] property_Vector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; - [Export] private Vector3[] property_Vector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; - [Export] private Color[] property_ColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; - [Export] private GodotObject[] property_GodotObjectOrDerivedArray { get; set; } = { null }; - [Export] private StringName[] field_StringNameArray { get; set; } = { "foo", "bar" }; - [Export] private NodePath[] field_NodePathArray { get; set; } = { "foo", "bar" }; - [Export] private Rid[] field_RidArray { get; set; } = { default, default, default }; + [Export] private Byte[] PropertyByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] PropertyInt32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] PropertyInt64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] PropertySingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] PropertyDoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] PropertyStringArray { get; set; } = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] PropertyStringArrayEnum { get; set; } = { "foo", "bar" }; + [Export] private Vector2[] PropertyVector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] PropertyVector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] PropertyColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private GodotObject[] PropertyGodotObjectOrDerivedArray { get; set; } = { null }; + [Export] private StringName[] PropertyStringNameArray { get; set; } = { "foo", "bar" }; + [Export] private NodePath[] PropertyNodePathArray { get; set; } = { "foo", "bar" }; + [Export] private Rid[] PropertyRidArray { get; set; } = { default, default, default }; // Variant - [Export] private Variant property_Variant { get; set; } = "foo"; + [Export] private Variant PropertyVariant { get; set; } = "foo"; // Classes - [Export] private GodotObject property_GodotObjectOrDerived { get; set; } - [Export] private Godot.Texture property_GodotResourceTexture { get; set; } - [Export] private StringName property_StringName { get; set; } = new StringName("foo"); - [Export] private NodePath property_NodePath { get; set; } = new NodePath("foo"); - [Export] private Rid property_Rid { get; set; } + [Export] private GodotObject PropertyGodotObjectOrDerived { get; set; } + [Export] private Godot.Texture PropertyGodotResourceTexture { get; set; } + [Export] private StringName PropertyStringName { get; set; } = new StringName("foo"); + [Export] private NodePath PropertyNodePath { get; set; } = new NodePath("foo"); + [Export] private Rid PropertyRid { get; set; } [Export] - private Godot.Collections.Dictionary property_GodotDictionary { get; set; } = + private Godot.Collections.Dictionary PropertyGodotDictionary { get; set; } = new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; [Export] - private Godot.Collections.Array property_GodotArray { get; set; } = + private Godot.Collections.Array PropertyGodotArray { get; set; } = new() { "foo", 10, Vector2.Up, Colors.Chocolate }; [Export] - private Godot.Collections.Dictionary<string, bool> property_GodotGenericDictionary { get; set; } = + private Godot.Collections.Dictionary<string, bool> PropertyGodotGenericDictionary { get; set; } = new() { { "foo", true }, { "bar", false } }; [Export] - private Godot.Collections.Array<int> property_GodotGenericArray { get; set; } = + private Godot.Collections.Array<int> PropertyGodotGenericArray { get; set; } = new() { 0, 1, 2, 3, 4, 5, 6 }; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs index 9ef72d9e02..d2293e0731 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Foo.cs @@ -1,11 +1,11 @@ namespace Godot.SourceGenerators.Sample { - partial class Foo : GodotObject + public partial class Foo : GodotObject { } // Foo again in the same file - partial class Foo + public partial class Foo { } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs index 2cd1a08c0e..9d16a3170b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic.cs @@ -2,19 +2,8 @@ namespace Godot.SourceGenerators.Sample { - partial class Generic<T> : GodotObject - { - private int _field; - } - - // Generic again but different generic parameters - partial class Generic<T, R> : GodotObject - { - private int _field; - } - // Generic again but without generic parameters - partial class Generic : GodotObject + public partial class Generic : GodotObject { private int _field; } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic1T.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic1T.cs new file mode 100644 index 0000000000..6d92ff1b0b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic1T.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS0169 + +namespace Godot.SourceGenerators.Sample +{ + public partial class Generic1T<T> : GodotObject + { + private int _field; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic2T.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic2T.cs new file mode 100644 index 0000000000..eef80ff951 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Generic2T.cs @@ -0,0 +1,10 @@ +#pragma warning disable CS0169 + +namespace Godot.SourceGenerators.Sample +{ + // Generic again but different generic parameters + public partial class Generic2T<T, R> : GodotObject + { + private int _field; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs new file mode 100644 index 0000000000..b9f11908e1 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs @@ -0,0 +1,14 @@ +namespace Godot.SourceGenerators.Sample; + +[GlobalClass] +public partial class CustomGlobalClass : GodotObject +{ +} + +// This doesn't works because global classes can't have any generic type parameter. +/* +[GlobalClass] +public partial class CustomGlobalClass<T> : Node +{ +} +*/ diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj index 3f569ebac3..d0907c1cd4 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj @@ -2,6 +2,7 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> + <LangVersion>11</LangVersion> </PropertyGroup> <PropertyGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MoreExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MoreExportedFields.cs index 64088215e9..28685ac87a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MoreExportedFields.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MoreExportedFields.cs @@ -14,6 +14,6 @@ namespace Godot.SourceGenerators.Sample public partial class ExportedFields : GodotObject { // Note we use Array and not System.Array. This tests the generated namespace qualification. - [Export] private Int64[] field_empty_Int64Array = Array.Empty<Int64>(); + [Export] private Int64[] _fieldEmptyInt64Array = Array.Empty<Int64>(); } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs new file mode 100644 index 0000000000..f19da77be6 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs @@ -0,0 +1,654 @@ +using System; +using Godot.Collections; +using Array = Godot.Collections.Array; + +namespace Godot.SourceGenerators.Sample; + +public class MustBeVariantMethods +{ + public void MustBeVariantMethodCalls() + { + Method<bool>(); + Method<char>(); + Method<sbyte>(); + Method<byte>(); + Method<short>(); + Method<ushort>(); + Method<int>(); + Method<uint>(); + Method<long>(); + Method<ulong>(); + Method<float>(); + Method<double>(); + Method<string>(); + Method<Vector2>(); + Method<Vector2I>(); + Method<Rect2>(); + Method<Rect2I>(); + Method<Transform2D>(); + Method<Vector3>(); + Method<Vector3I>(); + Method<Vector4>(); + Method<Vector4I>(); + Method<Basis>(); + Method<Quaternion>(); + Method<Transform3D>(); + Method<Projection>(); + Method<Aabb>(); + Method<Color>(); + Method<Plane>(); + Method<Callable>(); + Method<Signal>(); + Method<GodotObject>(); + Method<StringName>(); + Method<NodePath>(); + Method<Rid>(); + Method<Dictionary>(); + Method<Array>(); + Method<byte[]>(); + Method<int[]>(); + Method<long[]>(); + Method<float[]>(); + Method<double[]>(); + Method<string[]>(); + Method<Vector2[]>(); + Method<Vector3[]>(); + Method<Color[]>(); + Method<GodotObject[]>(); + Method<StringName[]>(); + Method<NodePath[]>(); + Method<Rid[]>(); + + // This call fails because generic type is not Variant-compatible. + //Method<object>(); + } + + public void Method<[MustBeVariant] T>() + { + } + + public void MustBeVariantClasses() + { + new ClassWithGenericVariant<bool>(); + new ClassWithGenericVariant<char>(); + new ClassWithGenericVariant<sbyte>(); + new ClassWithGenericVariant<byte>(); + new ClassWithGenericVariant<short>(); + new ClassWithGenericVariant<ushort>(); + new ClassWithGenericVariant<int>(); + new ClassWithGenericVariant<uint>(); + new ClassWithGenericVariant<long>(); + new ClassWithGenericVariant<ulong>(); + new ClassWithGenericVariant<float>(); + new ClassWithGenericVariant<double>(); + new ClassWithGenericVariant<string>(); + new ClassWithGenericVariant<Vector2>(); + new ClassWithGenericVariant<Vector2I>(); + new ClassWithGenericVariant<Rect2>(); + new ClassWithGenericVariant<Rect2I>(); + new ClassWithGenericVariant<Transform2D>(); + new ClassWithGenericVariant<Vector3>(); + new ClassWithGenericVariant<Vector3I>(); + new ClassWithGenericVariant<Vector4>(); + new ClassWithGenericVariant<Vector4I>(); + new ClassWithGenericVariant<Basis>(); + new ClassWithGenericVariant<Quaternion>(); + new ClassWithGenericVariant<Transform3D>(); + new ClassWithGenericVariant<Projection>(); + new ClassWithGenericVariant<Aabb>(); + new ClassWithGenericVariant<Color>(); + new ClassWithGenericVariant<Plane>(); + new ClassWithGenericVariant<Callable>(); + new ClassWithGenericVariant<Signal>(); + new ClassWithGenericVariant<GodotObject>(); + new ClassWithGenericVariant<StringName>(); + new ClassWithGenericVariant<NodePath>(); + new ClassWithGenericVariant<Rid>(); + new ClassWithGenericVariant<Dictionary>(); + new ClassWithGenericVariant<Array>(); + new ClassWithGenericVariant<byte[]>(); + new ClassWithGenericVariant<int[]>(); + new ClassWithGenericVariant<long[]>(); + new ClassWithGenericVariant<float[]>(); + new ClassWithGenericVariant<double[]>(); + new ClassWithGenericVariant<string[]>(); + new ClassWithGenericVariant<Vector2[]>(); + new ClassWithGenericVariant<Vector3[]>(); + new ClassWithGenericVariant<Color[]>(); + new ClassWithGenericVariant<GodotObject[]>(); + new ClassWithGenericVariant<StringName[]>(); + new ClassWithGenericVariant<NodePath[]>(); + new ClassWithGenericVariant<Rid[]>(); + + // This class fails because generic type is not Variant-compatible. + //new ClassWithGenericVariant<object>(); + } +} + +public class ClassWithGenericVariant<[MustBeVariant] T> +{ +} + +public class MustBeVariantAnnotatedMethods +{ + [GenericTypeAttribute<bool>()] + public void MethodWithAttributeBool() + { + } + + [GenericTypeAttribute<char>()] + public void MethodWithAttributeChar() + { + } + + [GenericTypeAttribute<sbyte>()] + public void MethodWithAttributeSByte() + { + } + + [GenericTypeAttribute<byte>()] + public void MethodWithAttributeByte() + { + } + + [GenericTypeAttribute<short>()] + public void MethodWithAttributeInt16() + { + } + + [GenericTypeAttribute<ushort>()] + public void MethodWithAttributeUInt16() + { + } + + [GenericTypeAttribute<int>()] + public void MethodWithAttributeInt32() + { + } + + [GenericTypeAttribute<uint>()] + public void MethodWithAttributeUInt32() + { + } + + [GenericTypeAttribute<long>()] + public void MethodWithAttributeInt64() + { + } + + [GenericTypeAttribute<ulong>()] + public void MethodWithAttributeUInt64() + { + } + + [GenericTypeAttribute<float>()] + public void MethodWithAttributeSingle() + { + } + + [GenericTypeAttribute<double>()] + public void MethodWithAttributeDouble() + { + } + + [GenericTypeAttribute<string>()] + public void MethodWithAttributeString() + { + } + + [GenericTypeAttribute<Vector2>()] + public void MethodWithAttributeVector2() + { + } + + [GenericTypeAttribute<Vector2I>()] + public void MethodWithAttributeVector2I() + { + } + + [GenericTypeAttribute<Rect2>()] + public void MethodWithAttributeRect2() + { + } + + [GenericTypeAttribute<Rect2I>()] + public void MethodWithAttributeRect2I() + { + } + + [GenericTypeAttribute<Transform2D>()] + public void MethodWithAttributeTransform2D() + { + } + + [GenericTypeAttribute<Vector3>()] + public void MethodWithAttributeVector3() + { + } + + [GenericTypeAttribute<Vector3I>()] + public void MethodWithAttributeVector3I() + { + } + + [GenericTypeAttribute<Vector4>()] + public void MethodWithAttributeVector4() + { + } + + [GenericTypeAttribute<Vector4I>()] + public void MethodWithAttributeVector4I() + { + } + + [GenericTypeAttribute<Basis>()] + public void MethodWithAttributeBasis() + { + } + + [GenericTypeAttribute<Quaternion>()] + public void MethodWithAttributeQuaternion() + { + } + + [GenericTypeAttribute<Transform3D>()] + public void MethodWithAttributeTransform3D() + { + } + + [GenericTypeAttribute<Projection>()] + public void MethodWithAttributeProjection() + { + } + + [GenericTypeAttribute<Aabb>()] + public void MethodWithAttributeAabb() + { + } + + [GenericTypeAttribute<Color>()] + public void MethodWithAttributeColor() + { + } + + [GenericTypeAttribute<Plane>()] + public void MethodWithAttributePlane() + { + } + + [GenericTypeAttribute<Callable>()] + public void MethodWithAttributeCallable() + { + } + + [GenericTypeAttribute<Signal>()] + public void MethodWithAttributeSignal() + { + } + + [GenericTypeAttribute<GodotObject>()] + public void MethodWithAttributeGodotObject() + { + } + + [GenericTypeAttribute<StringName>()] + public void MethodWithAttributeStringName() + { + } + + [GenericTypeAttribute<NodePath>()] + public void MethodWithAttributeNodePath() + { + } + + [GenericTypeAttribute<Rid>()] + public void MethodWithAttributeRid() + { + } + + [GenericTypeAttribute<Dictionary>()] + public void MethodWithAttributeDictionary() + { + } + + [GenericTypeAttribute<Array>()] + public void MethodWithAttributeArray() + { + } + + [GenericTypeAttribute<byte[]>()] + public void MethodWithAttributeByteArray() + { + } + + [GenericTypeAttribute<int[]>()] + public void MethodWithAttributeInt32Array() + { + } + + [GenericTypeAttribute<long[]>()] + public void MethodWithAttributeInt64Array() + { + } + + [GenericTypeAttribute<float[]>()] + public void MethodWithAttributeSingleArray() + { + } + + [GenericTypeAttribute<double[]>()] + public void MethodWithAttributeDoubleArray() + { + } + + [GenericTypeAttribute<string[]>()] + public void MethodWithAttributeStringArray() + { + } + + [GenericTypeAttribute<Vector2[]>()] + public void MethodWithAttributeVector2Array() + { + } + + [GenericTypeAttribute<Vector3[]>()] + public void MethodWithAttributeVector3Array() + { + } + + [GenericTypeAttribute<Color[]>()] + public void MethodWithAttributeColorArray() + { + } + + [GenericTypeAttribute<GodotObject[]>()] + public void MethodWithAttributeGodotObjectArray() + { + } + + [GenericTypeAttribute<StringName[]>()] + public void MethodWithAttributeStringNameArray() + { + } + + [GenericTypeAttribute<NodePath[]>()] + public void MethodWithAttributeNodePathArray() + { + } + + [GenericTypeAttribute<Rid[]>()] + public void MethodWithAttributeRidArray() + { + } + + // This method definition fails because generic type is not Variant-compatible. + /* + [GenericTypeAttribute<object>()] + public void MethodWithWrongAttribute() + { + } + */ +} + +[GenericTypeAttribute<bool>()] +public class ClassVariantAnnotatedBool +{ +} + +[GenericTypeAttribute<char>()] +public class ClassVariantAnnotatedChar +{ +} + +[GenericTypeAttribute<sbyte>()] +public class ClassVariantAnnotatedSByte +{ +} + +[GenericTypeAttribute<byte>()] +public class ClassVariantAnnotatedByte +{ +} + +[GenericTypeAttribute<short>()] +public class ClassVariantAnnotatedInt16 +{ +} + +[GenericTypeAttribute<ushort>()] +public class ClassVariantAnnotatedUInt16 +{ +} + +[GenericTypeAttribute<int>()] +public class ClassVariantAnnotatedInt32 +{ +} + +[GenericTypeAttribute<uint>()] +public class ClassVariantAnnotatedUInt32 +{ +} + +[GenericTypeAttribute<long>()] +public class ClassVariantAnnotatedInt64 +{ +} + +[GenericTypeAttribute<ulong>()] +public class ClassVariantAnnotatedUInt64 +{ +} + +[GenericTypeAttribute<float>()] +public class ClassVariantAnnotatedSingle +{ +} + +[GenericTypeAttribute<double>()] +public class ClassVariantAnnotatedDouble +{ +} + +[GenericTypeAttribute<string>()] +public class ClassVariantAnnotatedString +{ +} + +[GenericTypeAttribute<Vector2>()] +public class ClassVariantAnnotatedVector2 +{ +} + +[GenericTypeAttribute<Vector2I>()] +public class ClassVariantAnnotatedVector2I +{ +} + +[GenericTypeAttribute<Rect2>()] +public class ClassVariantAnnotatedRect2 +{ +} + +[GenericTypeAttribute<Rect2I>()] +public class ClassVariantAnnotatedRect2I +{ +} + +[GenericTypeAttribute<Transform2D>()] +public class ClassVariantAnnotatedTransform2D +{ +} + +[GenericTypeAttribute<Vector3>()] +public class ClassVariantAnnotatedVector3 +{ +} + +[GenericTypeAttribute<Vector3I>()] +public class ClassVariantAnnotatedVector3I +{ +} + +[GenericTypeAttribute<Vector4>()] +public class ClassVariantAnnotatedVector4 +{ +} + +[GenericTypeAttribute<Vector4I>()] +public class ClassVariantAnnotatedVector4I +{ +} + +[GenericTypeAttribute<Basis>()] +public class ClassVariantAnnotatedBasis +{ +} + +[GenericTypeAttribute<Quaternion>()] +public class ClassVariantAnnotatedQuaternion +{ +} + +[GenericTypeAttribute<Transform3D>()] +public class ClassVariantAnnotatedTransform3D +{ +} + +[GenericTypeAttribute<Projection>()] +public class ClassVariantAnnotatedProjection +{ +} + +[GenericTypeAttribute<Aabb>()] +public class ClassVariantAnnotatedAabb +{ +} + +[GenericTypeAttribute<Color>()] +public class ClassVariantAnnotatedColor +{ +} + +[GenericTypeAttribute<Plane>()] +public class ClassVariantAnnotatedPlane +{ +} + +[GenericTypeAttribute<Callable>()] +public class ClassVariantAnnotatedCallable +{ +} + +[GenericTypeAttribute<Signal>()] +public class ClassVariantAnnotatedSignal +{ +} + +[GenericTypeAttribute<GodotObject>()] +public class ClassVariantAnnotatedGodotObject +{ +} + +[GenericTypeAttribute<StringName>()] +public class ClassVariantAnnotatedStringName +{ +} + +[GenericTypeAttribute<NodePath>()] +public class ClassVariantAnnotatedNodePath +{ +} + +[GenericTypeAttribute<Rid>()] +public class ClassVariantAnnotatedRid +{ +} + +[GenericTypeAttribute<Dictionary>()] +public class ClassVariantAnnotatedDictionary +{ +} + +[GenericTypeAttribute<Array>()] +public class ClassVariantAnnotatedArray +{ +} + +[GenericTypeAttribute<byte[]>()] +public class ClassVariantAnnotatedByteArray +{ +} + +[GenericTypeAttribute<int[]>()] +public class ClassVariantAnnotatedInt32Array +{ +} + +[GenericTypeAttribute<long[]>()] +public class ClassVariantAnnotatedInt64Array +{ +} + +[GenericTypeAttribute<float[]>()] +public class ClassVariantAnnotatedSingleArray +{ +} + +[GenericTypeAttribute<double[]>()] +public class ClassVariantAnnotatedDoubleArray +{ +} + +[GenericTypeAttribute<string[]>()] +public class ClassVariantAnnotatedStringArray +{ +} + +[GenericTypeAttribute<Vector2[]>()] +public class ClassVariantAnnotatedVector2Array +{ +} + +[GenericTypeAttribute<Vector3[]>()] +public class ClassVariantAnnotatedVector3Array +{ +} + +[GenericTypeAttribute<Color[]>()] +public class ClassVariantAnnotatedColorArray +{ +} + +[GenericTypeAttribute<GodotObject[]>()] +public class ClassVariantAnnotatedGodotObjectArray +{ +} + +[GenericTypeAttribute<StringName[]>()] +public class ClassVariantAnnotatedStringNameArray +{ +} + +[GenericTypeAttribute<NodePath[]>()] +public class ClassVariantAnnotatedNodePathArray +{ +} + +[GenericTypeAttribute<Rid[]>()] +public class ClassVariantAnnotatedRidArray +{ +} + +// This class definition fails because generic type is not Variant-compatible. +/* +[GenericTypeAttribute<object>()] +public class ClassNonVariantAnnotated +{ +} +*/ + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class GenericTypeAttribute<[MustBeVariant] T> : Attribute +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/NestedClass.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/NestedClass.cs new file mode 100644 index 0000000000..3c533b712d --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/NestedClass.cs @@ -0,0 +1,22 @@ +using System; + +namespace Godot.SourceGenerators.Sample; + +public partial class NestedClass : GodotObject +{ + public partial class NestedClass2 : GodotObject + { + public partial class NestedClass3 : GodotObject + { + [Signal] + public delegate void MySignalEventHandler(string str, int num); + + [Export] private String _fieldString = "foo"; + [Export] private String PropertyString { get; set; } = "foo"; + + private void Method() + { + } + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllReadOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllReadOnly.cs index 0c374169b9..c36aa55eef 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllReadOnly.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllReadOnly.cs @@ -2,9 +2,9 @@ namespace Godot.SourceGenerators.Sample { public partial class AllReadOnly : GodotObject { - public readonly string readonly_field = "foo"; - public string readonly_auto_property { get; } = "foo"; - public string readonly_property { get => "foo"; } - public string initonly_auto_property { get; init; } + public readonly string ReadonlyField = "foo"; + public string ReadonlyAutoProperty { get; } = "foo"; + public string ReadonlyProperty { get => "foo"; } + public string InitonlyAutoProperty { get; init; } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllWriteOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllWriteOnly.cs index 14a1802330..48acfa6c95 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllWriteOnly.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/AllWriteOnly.cs @@ -4,7 +4,7 @@ namespace Godot.SourceGenerators.Sample { public partial class AllWriteOnly : GodotObject { - bool writeonly_backing_field = false; - public bool writeonly_property { set => writeonly_backing_field = value; } + private bool _writeOnlyBackingField = false; + public bool WriteOnlyProperty { set => _writeOnlyBackingField = value; } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/MixedReadOnlyWriteOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/MixedReadOnlyWriteOnly.cs index f556bdc7e4..c6ce6efe78 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/MixedReadOnlyWriteOnly.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/OneWayProperties/MixedReadOnlyWriteOnly.cs @@ -2,12 +2,12 @@ namespace Godot.SourceGenerators.Sample { public partial class MixedReadonlyWriteOnly : GodotObject { - public readonly string readonly_field = "foo"; - public string readonly_auto_property { get; } = "foo"; - public string readonly_property { get => "foo"; } - public string initonly_auto_property { get; init; } + public readonly string ReadOnlyField = "foo"; + public string ReadOnlyAutoProperty { get; } = "foo"; + public string ReadOnlyProperty { get => "foo"; } + public string InitOnlyAutoProperty { get; init; } - bool writeonly_backing_field = false; - public bool writeonly_property { set => writeonly_backing_field = value; } + private bool _writeOnlyBackingField = false; + public bool WriteOnlyProperty { set => _writeOnlyBackingField = value; } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs index e43a3469ae..1266f6d853 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/ScriptBoilerplate.cs @@ -26,7 +26,7 @@ namespace Godot.SourceGenerators.Sample } } - partial struct OuterClass + public partial struct OuterClass { public partial class NesterClass : RefCounted { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs new file mode 100644 index 0000000000..253889296b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators.Tests; + +public static class CSharpAnalyzerVerifier<TAnalyzer> + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public const LanguageVersion LangVersion = LanguageVersion.CSharp11; + + public class Test : CSharpAnalyzerTest<TAnalyzer, XUnitVerifier> + { + public Test() + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net60; + + SolutionTransforms.Add((Solution solution, ProjectId projectId) => + { + Project project = + solution.GetProject(projectId)!.AddMetadataReference(Constants.GodotSharpAssembly + .CreateMetadataReference()).WithParseOptions(new CSharpParseOptions(LangVersion)); + + return project.Solution; + }); + } + } + + public static Task Verify(string sources, params DiagnosticResult[] expected) + { + return MakeVerifier(new string[] { sources }, expected).RunAsync(); + } + + public static Test MakeVerifier(ICollection<string> sources, params DiagnosticResult[] expected) + { + var verifier = new Test(); + + verifier.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", $""" + is_global = true + build_property.GodotProjectDir = {Constants.ExecutingAssemblyPath} + """)); + + verifier.TestState.Sources.AddRange(sources.Select(source => + { + return (source, SourceText.From(File.ReadAllText(Path.Combine(Constants.SourceFolderPath, source)))); + })); + + verifier.ExpectedDiagnostics.AddRange(expected); + return verifier; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpCodeFixVerifier.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpCodeFixVerifier.cs new file mode 100644 index 0000000000..51e215a17b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpCodeFixVerifier.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace Godot.SourceGenerators.Tests; + +public static class CSharpCodeFixVerifier<TCodeFix, TAnalyzer> + where TCodeFix : CodeFixProvider, new() + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public class Test : CSharpCodeFixTest<TAnalyzer, TCodeFix, XUnitVerifier> + { + public Test() + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net60; + SolutionTransforms.Add((Solution solution, ProjectId projectId) => + { + Project project = solution.GetProject(projectId)! + .AddMetadataReference(Constants.GodotSharpAssembly.CreateMetadataReference()); + return project.Solution; + }); + } + } + + public static Task Verify(string sources, string fixedSources) + { + return MakeVerifier(sources, fixedSources).RunAsync(); + } + + public static Test MakeVerifier(string source, string results) + { + var verifier = new Test(); + + verifier.TestCode = File.ReadAllText(Path.Combine(Constants.SourceFolderPath, source)); + verifier.FixedCode = File.ReadAllText(Path.Combine(Constants.GeneratedSourceFolderPath, results)); + + verifier.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", $""" + is_global = true + build_property.GodotProjectDir = {Constants.ExecutingAssemblyPath} + """)); + + return verifier; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 0000000000..84e319352d --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.SourceGenerators.Tests; + +public static class CSharpSourceGeneratorVerifier<TSourceGenerator> +where TSourceGenerator : ISourceGenerator, new() +{ + public class Test : CSharpSourceGeneratorTest<TSourceGenerator, XUnitVerifier> + { + public Test() + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net60; + + SolutionTransforms.Add((Solution solution, ProjectId projectId) => + { + Project project = solution.GetProject(projectId)! + .AddMetadataReference(Constants.GodotSharpAssembly.CreateMetadataReference()); + + return project.Solution; + }); + } + } + + public static Task Verify(string source, params string[] generatedSources) + { + return Verify(new string[] { source }, generatedSources); + } + + public static Task VerifyNoCompilerDiagnostics(string source, params string[] generatedSources) + { + return VerifyNoCompilerDiagnostics(new string[] { source }, generatedSources); + } + + public static Task Verify(ICollection<string> sources, params string[] generatedSources) + { + return MakeVerifier(sources, generatedSources).RunAsync(); + } + + public static Task VerifyNoCompilerDiagnostics(ICollection<string> sources, params string[] generatedSources) + { + var verifier = MakeVerifier(sources, generatedSources); + verifier.CompilerDiagnostics = CompilerDiagnostics.None; + return verifier.RunAsync(); + } + + public static Test MakeVerifier(ICollection<string> sources, ICollection<string> generatedSources) + { + var verifier = new Test(); + + verifier.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", $""" + is_global = true + build_property.GodotProjectDir = {Constants.ExecutingAssemblyPath} + """)); + + verifier.TestState.Sources.AddRange(sources.Select(source => ( + source, + SourceText.From(File.ReadAllText(Path.Combine(Constants.SourceFolderPath, source))) + ))); + + verifier.TestState.GeneratedSources.AddRange(generatedSources.Select(generatedSource => ( + FullGeneratedSourceName(generatedSource), + SourceText.From(File.ReadAllText(Path.Combine(Constants.GeneratedSourceFolderPath, generatedSource)), Encoding.UTF8) + ))); + + return verifier; + } + + private static string FullGeneratedSourceName(string name) + { + var generatorType = typeof(TSourceGenerator); + return Path.Combine(generatorType.Namespace!, generatorType.FullName!, name); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ClassPartialModifierAnalyzerTest.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ClassPartialModifierAnalyzerTest.cs new file mode 100644 index 0000000000..19f05a2f02 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ClassPartialModifierAnalyzerTest.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ClassPartialModifierTest +{ + [Fact] + public async Task ClassPartialModifierCodeFixTest() + { + await CSharpCodeFixVerifier<ClassPartialModifierCodeFixProvider, ClassPartialModifierAnalyzer> + .Verify("ClassPartialModifier.GD0001.cs", "ClassPartialModifier.GD0001.fixed.cs"); + } + + [Fact] + public async void OuterClassPartialModifierAnalyzerTest() + { + await CSharpAnalyzerVerifier<ClassPartialModifierAnalyzer>.Verify("OuterClassPartialModifierAnalyzer.GD0002.cs"); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Constants.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Constants.cs new file mode 100644 index 0000000000..783b1e4298 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Constants.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Reflection; + +namespace Godot.SourceGenerators.Tests; + +public static class Constants +{ + public static Assembly GodotSharpAssembly => typeof(GodotObject).Assembly; + + public static string ExecutingAssemblyPath { get; } + public static string SourceFolderPath { get; } + public static string GeneratedSourceFolderPath { get; } + + static Constants() + { + ExecutingAssemblyPath = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!); + + var testDataPath = Path.Combine(ExecutingAssemblyPath, "TestData"); + + SourceFolderPath = Path.Combine(testDataPath, "Sources"); + GeneratedSourceFolderPath = Path.Combine(testDataPath, "GeneratedSources"); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ExportDiagnosticsTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ExportDiagnosticsTests.cs new file mode 100644 index 0000000000..6425c723dd --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ExportDiagnosticsTests.cs @@ -0,0 +1,77 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ExportDiagnosticsTests +{ + [Fact] + public async void StaticMembers() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportDiagnostics_GD0101.cs", + "ExportDiagnostics_GD0101_ScriptPropertyDefVal.generated.cs" + ); + } + + [Fact] + public async void TypeIsNotSupported() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportDiagnostics_GD0102.cs", + "ExportDiagnostics_GD0102_ScriptPropertyDefVal.generated.cs" + ); + } + + [Fact] + public async void ReadOnly() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportDiagnostics_GD0103.cs", + "ExportDiagnostics_GD0103_ScriptPropertyDefVal.generated.cs" + ); + } + + [Fact] + public async void WriteOnly() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportDiagnostics_GD0104.cs", + "ExportDiagnostics_GD0104_ScriptPropertyDefVal.generated.cs" + ); + } + + [Fact] + public async void Indexer() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportDiagnostics_GD0105.cs", + "ExportDiagnostics_GD0105_ScriptPropertyDefVal.generated.cs" + ); + } + + [Fact] + public async void ExplicitInterfaceImplementation() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + new string[] { "ExportDiagnostics_GD0106.cs" }, + new string[] + { + "ExportDiagnostics_GD0106_OK_ScriptPropertyDefVal.generated.cs", + "ExportDiagnostics_GD0106_KO_ScriptPropertyDefVal.generated.cs", + } + ); + } + + [Fact] + public async void NodeExports() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + new string[] { "ExportDiagnostics_GD0107.cs" }, + new string[] + { + "ExportDiagnostics_GD0107_OK_ScriptPropertyDefVal.generated.cs", + "ExportDiagnostics_GD0107_KO_ScriptPropertyDefVal.generated.cs", + } + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Extensions.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Extensions.cs new file mode 100644 index 0000000000..6ff890e5d8 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Extensions.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace Godot.SourceGenerators.Tests; + +public static class Extensions +{ + public static MetadataReference CreateMetadataReference(this Assembly assembly) + { + return MetadataReference.CreateFromFile(assembly.Location); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs new file mode 100644 index 0000000000..74d6afceb3 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class GlobalClassAnalyzerTests +{ + [Fact] + public async void GlobalClassMustDeriveFromGodotObjectTest() + { + const string GlobalClassGD0401 = "GlobalClass.GD0401.cs"; + await CSharpAnalyzerVerifier<GlobalClassAnalyzer>.Verify(GlobalClassGD0401); + } + + [Fact] + public async void GlobalClassMustNotBeGenericTest() + { + const string GlobalClassGD0402 = "GlobalClass.GD0402.cs"; + await CSharpAnalyzerVerifier<GlobalClassAnalyzer>.Verify(GlobalClassGD0402); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj new file mode 100644 index 0000000000..e5a81c0e1c --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj @@ -0,0 +1,45 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + + <LangVersion>11</LangVersion> + + <Nullable>enable</Nullable> + <IsPackable>false</IsPackable> + <IsTestProject>true</IsTestProject> + </PropertyGroup> + + <PropertyGroup> + <DefaultItemExcludesInProjectFolder>$(DefaultItemExcludesInProjectFolder);TestData\**</DefaultItemExcludesInProjectFolder> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.1" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" /> + <PackageReference Include="Microsoft.CodeAnalysis.Testing.Verifiers.XUnit" Version="1.1.1" /> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.1" /> + <PackageReference Include="xunit" Version="2.4.2" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="3.2.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj" /> + <ProjectReference Include="..\Godot.SourceGenerators\Godot.SourceGenerators.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Include="TestData\**\*.cs" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> + +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs new file mode 100644 index 0000000000..62c602efbb --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class MustBeVariantAnalyzerTests +{ + [Fact] + public async void GenericTypeArgumentMustBeVariantTest() + { + const string MustBeVariantGD0301 = "MustBeVariant.GD0301.cs"; + await CSharpAnalyzerVerifier<MustBeVariantAnalyzer>.Verify(MustBeVariantGD0301); + } + + [Fact] + public async void GenericTypeParameterMustBeVariantAnnotatedTest() + { + const string MustBeVariantGD0302 = "MustBeVariant.GD0302.cs"; + await CSharpAnalyzerVerifier<MustBeVariantAnalyzer>.Verify(MustBeVariantGD0302); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs new file mode 100644 index 0000000000..294932eb9a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptMethodsGeneratorTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptMethodsGeneratorTests +{ + [Fact] + public async void Methods() + { + await CSharpSourceGeneratorVerifier<ScriptMethodsGenerator>.Verify( + "Methods.cs", + "Methods_ScriptMethods.generated.cs" + ); + } + + [Fact] + public async void ScriptBoilerplate() + { + await CSharpSourceGeneratorVerifier<ScriptMethodsGenerator>.Verify( + "ScriptBoilerplate.cs", + "ScriptBoilerplate_ScriptMethods.generated.cs", "OuterClass.NestedClass_ScriptMethods.generated.cs" + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs new file mode 100644 index 0000000000..4f6b50cf02 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPathAttributeGeneratorTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptPathAttributeGeneratorTests +{ + private static (string, SourceText) MakeAssemblyScriptTypesGeneratedSource(ICollection<string> types) + { + return ( + Path.Combine("Godot.SourceGenerators", "Godot.SourceGenerators.ScriptPathAttributeGenerator", "AssemblyScriptTypes.generated.cs"), + SourceText.From($$""" + [assembly:Godot.AssemblyHasScriptsAttribute(new System.Type[] {{{string.Join(", ", types.Select(type => $"typeof({type})"))}}})] + + """, Encoding.UTF8) + ); + } + + [Fact] + public async void ScriptBoilerplate() + { + var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier( + new string[] { "ScriptBoilerplate.cs" }, + new string[] { "ScriptBoilerplate_ScriptPath.generated.cs" } + ); + verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::ScriptBoilerplate" })); + await verifier.RunAsync(); + } + + [Fact] + public async void FooBar() + { + var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier( + new string[] { "Foo.cs", "Bar.cs" }, + new string[] { "Foo_ScriptPath.generated.cs", "Bar_ScriptPath.generated.cs" } + ); + verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Foo", "global::Bar" })); + await verifier.RunAsync(); + } + + [Fact] + public async void Generic() + { + var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier( + new string[] { "Generic.cs" }, + new string[] { "Generic(Of T)_ScriptPath.generated.cs" } + ); + verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>" })); + await verifier.RunAsync(); + } + + [Fact] + public async void GenericMultipleClassesSameName() + { + var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier( + Array.Empty<string>(), + new string[] { "Generic(Of T)_ScriptPath.generated.cs" } + ); + verifier.TestState.Sources.Add(("Generic.cs", File.ReadAllText(Path.Combine(Constants.SourceFolderPath, "Generic.GD0003.cs")))); + verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::Generic<>", "global::Generic<,>", "global::Generic" })); + await verifier.RunAsync(); + } + + [Fact] + public async void NamespaceMultipleClassesSameName() + { + var verifier = CSharpSourceGeneratorVerifier<ScriptPathAttributeGenerator>.MakeVerifier( + Array.Empty<string>(), + new string[] { "NamespaceA.SameName_ScriptPath.generated.cs" } + ); + verifier.TestState.Sources.Add(("SameName.cs", File.ReadAllText(Path.Combine(Constants.SourceFolderPath, "SameName.GD0003.cs")))); + verifier.TestState.GeneratedSources.Add(MakeAssemblyScriptTypesGeneratedSource(new string[] { "global::NamespaceA.SameName", "global::NamespaceB.SameName" })); + await verifier.RunAsync(); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs new file mode 100644 index 0000000000..3cc5841097 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertiesGeneratorTests.cs @@ -0,0 +1,60 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptPropertiesGeneratorTests +{ + [Fact] + public async void ExportedFields() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + new string[] { "ExportedFields.cs", "MoreExportedFields.cs" }, + new string[] { "ExportedFields_ScriptProperties.generated.cs" } + ); + } + + [Fact] + public async void ExportedProperties() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + "ExportedProperties.cs", + "ExportedProperties_ScriptProperties.generated.cs" + ); + } + + [Fact] + public async void OneWayPropertiesAllReadOnly() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + "AllReadOnly.cs", + "AllReadOnly_ScriptProperties.generated.cs" + ); + } + + [Fact] + public async void OneWayPropertiesAllWriteOnly() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + "AllWriteOnly.cs", + "AllWriteOnly_ScriptProperties.generated.cs" + ); + } + + [Fact] + public async void OneWayPropertiesMixedReadOnlyWriteOnly() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + "MixedReadOnlyWriteOnly.cs", + "MixedReadOnlyWriteOnly_ScriptProperties.generated.cs" + ); + } + + [Fact] + public async void ScriptBoilerplate() + { + await CSharpSourceGeneratorVerifier<ScriptPropertiesGenerator>.Verify( + "ScriptBoilerplate.cs", + "ScriptBoilerplate_ScriptProperties.generated.cs", "OuterClass.NestedClass_ScriptProperties.generated.cs" + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertyDefValGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertyDefValGeneratorTests.cs new file mode 100644 index 0000000000..ae5fb86d77 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptPropertyDefValGeneratorTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptPropertyDefValGeneratorTests +{ + [Fact] + public async void ExportedFields() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + new string[] { "ExportedFields.cs", "MoreExportedFields.cs" }, + new string[] { "ExportedFields_ScriptPropertyDefVal.generated.cs" } + ); + } + + [Fact] + public async void ExportedProperties() + { + await CSharpSourceGeneratorVerifier<ScriptPropertyDefValGenerator>.Verify( + "ExportedProperties.cs", + "ExportedProperties_ScriptPropertyDefVal.generated.cs" + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSerializationGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSerializationGeneratorTests.cs new file mode 100644 index 0000000000..24cd446087 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSerializationGeneratorTests.cs @@ -0,0 +1,15 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptSerializationGeneratorTests +{ + [Fact] + public async void ScriptBoilerplate() + { + await CSharpSourceGeneratorVerifier<ScriptSerializationGenerator>.VerifyNoCompilerDiagnostics( + "ScriptBoilerplate.cs", + "ScriptBoilerplate_ScriptSerialization.generated.cs", "OuterClass.NestedClass_ScriptSerialization.generated.cs" + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs new file mode 100644 index 0000000000..2a783cedce --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/ScriptSignalsGeneratorTests.cs @@ -0,0 +1,15 @@ +using Xunit; + +namespace Godot.SourceGenerators.Tests; + +public class ScriptSignalsGeneratorTests +{ + [Fact] + public async void EventSignals() + { + await CSharpSourceGeneratorVerifier<ScriptSignalsGenerator>.Verify( + "EventSignals.cs", + "EventSignals_ScriptSignals.generated.cs" + ); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/.editorconfig b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/.editorconfig new file mode 100644 index 0000000000..9e2014e16c --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.cs] +exclude = true +generated_code = true diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllReadOnly_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllReadOnly_ScriptProperties.generated.cs new file mode 100644 index 0000000000..96ff0f75e9 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllReadOnly_ScriptProperties.generated.cs @@ -0,0 +1,66 @@ +using Godot; +using Godot.NativeInterop; + +partial class AllReadOnly +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// <summary> + /// Cached name for the 'ReadOnlyAutoProperty' property. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyAutoProperty = "ReadOnlyAutoProperty"; + /// <summary> + /// Cached name for the 'ReadOnlyProperty' property. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyProperty = "ReadOnlyProperty"; + /// <summary> + /// Cached name for the 'InitOnlyAutoProperty' property. + /// </summary> + public new static readonly global::Godot.StringName InitOnlyAutoProperty = "InitOnlyAutoProperty"; + /// <summary> + /// Cached name for the 'ReadOnlyField' field. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyField = "ReadOnlyField"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName.ReadOnlyAutoProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyAutoProperty); + return true; + } + else if (name == PropertyName.ReadOnlyProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyProperty); + return true; + } + else if (name == PropertyName.InitOnlyAutoProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.InitOnlyAutoProperty); + return true; + } + else if (name == PropertyName.ReadOnlyField) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyField); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyField, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyAutoProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.InitOnlyAutoProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllWriteOnly_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllWriteOnly_ScriptProperties.generated.cs new file mode 100644 index 0000000000..91dd282b99 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/AllWriteOnly_ScriptProperties.generated.cs @@ -0,0 +1,58 @@ +using Godot; +using Godot.NativeInterop; + +partial class AllWriteOnly +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// <summary> + /// Cached name for the 'WriteOnlyProperty' property. + /// </summary> + public new static readonly global::Godot.StringName WriteOnlyProperty = "WriteOnlyProperty"; + /// <summary> + /// Cached name for the '_writeOnlyBackingField' field. + /// </summary> + public new static readonly global::Godot.StringName _writeOnlyBackingField = "_writeOnlyBackingField"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName.WriteOnlyProperty) { + this.WriteOnlyProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + else if (name == PropertyName._writeOnlyBackingField) { + this._writeOnlyBackingField = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName._writeOnlyBackingField) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<bool>(this._writeOnlyBackingField); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName._writeOnlyBackingField, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName.WriteOnlyProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Bar_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Bar_ScriptPath.generated.cs new file mode 100644 index 0000000000..9096da1b42 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Bar_ScriptPath.generated.cs @@ -0,0 +1,5 @@ +using Godot; +[ScriptPathAttribute("res://Bar.cs")] +partial class Bar +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ClassPartialModifier.GD0001.fixed.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ClassPartialModifier.GD0001.fixed.cs new file mode 100644 index 0000000000..6ba6439d70 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ClassPartialModifier.GD0001.fixed.cs @@ -0,0 +1,6 @@ +using Godot; + +public partial class ClassPartialModifier : Node +{ + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/EventSignals_ScriptSignals.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/EventSignals_ScriptSignals.generated.cs new file mode 100644 index 0000000000..b1c57e6b26 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/EventSignals_ScriptSignals.generated.cs @@ -0,0 +1,54 @@ +using Godot; +using Godot.NativeInterop; + +partial class EventSignals +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the signals contained in this class, for fast lookup. + /// </summary> + public new class SignalName : global::Godot.GodotObject.SignalName { + /// <summary> + /// Cached name for the 'MySignal' signal. + /// </summary> + public new static readonly global::Godot.StringName MySignal = "MySignal"; + } + /// <summary> + /// Get the signal information for all the signals declared in this class. + /// This method is used by Godot to register the available signals in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo> GetGodotSignalList() + { + var signals = new global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>(1); + signals.Add(new(name: SignalName.MySignal, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)4, name: "str", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), new(type: (global::Godot.Variant.Type)2, name: "num", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return signals; + } +#pragma warning restore CS0109 + private global::EventSignals.MySignalEventHandler backing_MySignal; + /// <inheritdoc cref="global::EventSignals.MySignalEventHandler"/> + public event global::EventSignals.MySignalEventHandler MySignal { + add => backing_MySignal += value; + remove => backing_MySignal -= value; +} + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, NativeVariantPtrArgs args) + { + if (signal == SignalName.MySignal && args.Count == 2) { + backing_MySignal?.Invoke(global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(args[0]), global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(args[1])); + return; + } + base.RaiseGodotClassSignalCallbacks(signal, args); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassSignal(in godot_string_name signal) + { + if (signal == SignalName.MySignal) { + return true; + } + return base.HasGodotClassSignal(signal); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0101_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0101_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..47dba644ae --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0101_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0101 +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0102_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0102_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..636f5174d7 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0102_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0102 +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0103_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0103_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..ef969235f4 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0103_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0103 +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0104_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0104_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..8f01aaff6f --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0104_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0104 +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0105_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0105_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..52c4f1f732 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0105_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0105 +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_KO_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_KO_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..8f807cef10 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_KO_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0106_KO +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_OK_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_OK_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..f9dc4003e7 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0106_OK_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,21 @@ +partial class ExportDiagnostics_GD0106_OK +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword +#if TOOLS + /// <summary> + /// Get the default values for all properties declared in this class. + /// This method is used by Godot to determine the value that will be + /// used by the inspector when resetting properties. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant> GetGodotPropertyDefaultValues() + { + var values = new global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>(1); + int __MyProperty_default_value = default; + values.Add(PropertyName.MyProperty, global::Godot.Variant.From<int>(__MyProperty_default_value)); + return values; + } +#endif // TOOLS +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_KO_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_KO_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..21259107df --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_KO_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,3 @@ +partial class ExportDiagnostics_GD0107_KO +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_OK_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_OK_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..8b823d52c1 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportDiagnostics_GD0107_OK_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,23 @@ +partial class ExportDiagnostics_GD0107_OK +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword +#if TOOLS + /// <summary> + /// Get the default values for all properties declared in this class. + /// This method is used by Godot to determine the value that will be + /// used by the inspector when resetting properties. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant> GetGodotPropertyDefaultValues() + { + var values = new global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>(2); + global::Godot.Node __NodeProperty_default_value = default; + values.Add(PropertyName.NodeProperty, global::Godot.Variant.From<global::Godot.Node>(__NodeProperty_default_value)); + global::Godot.Node __NodeField_default_value = default; + values.Add(PropertyName.NodeField, global::Godot.Variant.From<global::Godot.Node>(__NodeField_default_value)); + return values; + } +#endif // TOOLS +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs new file mode 100644 index 0000000000..334adc1243 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs @@ -0,0 +1,816 @@ +using Godot; +using Godot.NativeInterop; + +partial class ExportedFields +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// <summary> + /// Cached name for the '_fieldBoolean' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldBoolean = "_fieldBoolean"; + /// <summary> + /// Cached name for the '_fieldChar' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldChar = "_fieldChar"; + /// <summary> + /// Cached name for the '_fieldSByte' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldSByte = "_fieldSByte"; + /// <summary> + /// Cached name for the '_fieldInt16' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldInt16 = "_fieldInt16"; + /// <summary> + /// Cached name for the '_fieldInt32' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldInt32 = "_fieldInt32"; + /// <summary> + /// Cached name for the '_fieldInt64' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldInt64 = "_fieldInt64"; + /// <summary> + /// Cached name for the '_fieldByte' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldByte = "_fieldByte"; + /// <summary> + /// Cached name for the '_fieldUInt16' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldUInt16 = "_fieldUInt16"; + /// <summary> + /// Cached name for the '_fieldUInt32' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldUInt32 = "_fieldUInt32"; + /// <summary> + /// Cached name for the '_fieldUInt64' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldUInt64 = "_fieldUInt64"; + /// <summary> + /// Cached name for the '_fieldSingle' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldSingle = "_fieldSingle"; + /// <summary> + /// Cached name for the '_fieldDouble' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldDouble = "_fieldDouble"; + /// <summary> + /// Cached name for the '_fieldString' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldString = "_fieldString"; + /// <summary> + /// Cached name for the '_fieldVector2' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector2 = "_fieldVector2"; + /// <summary> + /// Cached name for the '_fieldVector2I' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector2I = "_fieldVector2I"; + /// <summary> + /// Cached name for the '_fieldRect2' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldRect2 = "_fieldRect2"; + /// <summary> + /// Cached name for the '_fieldRect2I' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldRect2I = "_fieldRect2I"; + /// <summary> + /// Cached name for the '_fieldTransform2D' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldTransform2D = "_fieldTransform2D"; + /// <summary> + /// Cached name for the '_fieldVector3' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector3 = "_fieldVector3"; + /// <summary> + /// Cached name for the '_fieldVector3I' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector3I = "_fieldVector3I"; + /// <summary> + /// Cached name for the '_fieldBasis' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldBasis = "_fieldBasis"; + /// <summary> + /// Cached name for the '_fieldQuaternion' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldQuaternion = "_fieldQuaternion"; + /// <summary> + /// Cached name for the '_fieldTransform3D' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldTransform3D = "_fieldTransform3D"; + /// <summary> + /// Cached name for the '_fieldVector4' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector4 = "_fieldVector4"; + /// <summary> + /// Cached name for the '_fieldVector4I' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector4I = "_fieldVector4I"; + /// <summary> + /// Cached name for the '_fieldProjection' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldProjection = "_fieldProjection"; + /// <summary> + /// Cached name for the '_fieldAabb' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldAabb = "_fieldAabb"; + /// <summary> + /// Cached name for the '_fieldColor' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldColor = "_fieldColor"; + /// <summary> + /// Cached name for the '_fieldPlane' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldPlane = "_fieldPlane"; + /// <summary> + /// Cached name for the '_fieldCallable' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldCallable = "_fieldCallable"; + /// <summary> + /// Cached name for the '_fieldSignal' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldSignal = "_fieldSignal"; + /// <summary> + /// Cached name for the '_fieldEnum' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldEnum = "_fieldEnum"; + /// <summary> + /// Cached name for the '_fieldFlagsEnum' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldFlagsEnum = "_fieldFlagsEnum"; + /// <summary> + /// Cached name for the '_fieldByteArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldByteArray = "_fieldByteArray"; + /// <summary> + /// Cached name for the '_fieldInt32Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldInt32Array = "_fieldInt32Array"; + /// <summary> + /// Cached name for the '_fieldInt64Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldInt64Array = "_fieldInt64Array"; + /// <summary> + /// Cached name for the '_fieldSingleArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldSingleArray = "_fieldSingleArray"; + /// <summary> + /// Cached name for the '_fieldDoubleArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldDoubleArray = "_fieldDoubleArray"; + /// <summary> + /// Cached name for the '_fieldStringArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldStringArray = "_fieldStringArray"; + /// <summary> + /// Cached name for the '_fieldStringArrayEnum' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldStringArrayEnum = "_fieldStringArrayEnum"; + /// <summary> + /// Cached name for the '_fieldVector2Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector2Array = "_fieldVector2Array"; + /// <summary> + /// Cached name for the '_fieldVector3Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVector3Array = "_fieldVector3Array"; + /// <summary> + /// Cached name for the '_fieldColorArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldColorArray = "_fieldColorArray"; + /// <summary> + /// Cached name for the '_fieldGodotObjectOrDerivedArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotObjectOrDerivedArray = "_fieldGodotObjectOrDerivedArray"; + /// <summary> + /// Cached name for the '_fieldStringNameArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldStringNameArray = "_fieldStringNameArray"; + /// <summary> + /// Cached name for the '_fieldNodePathArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldNodePathArray = "_fieldNodePathArray"; + /// <summary> + /// Cached name for the '_fieldRidArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldRidArray = "_fieldRidArray"; + /// <summary> + /// Cached name for the '_fieldEmptyInt32Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldEmptyInt32Array = "_fieldEmptyInt32Array"; + /// <summary> + /// Cached name for the '_fieldArrayFromList' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldArrayFromList = "_fieldArrayFromList"; + /// <summary> + /// Cached name for the '_fieldVariant' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldVariant = "_fieldVariant"; + /// <summary> + /// Cached name for the '_fieldGodotObjectOrDerived' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotObjectOrDerived = "_fieldGodotObjectOrDerived"; + /// <summary> + /// Cached name for the '_fieldGodotResourceTexture' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotResourceTexture = "_fieldGodotResourceTexture"; + /// <summary> + /// Cached name for the '_fieldStringName' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldStringName = "_fieldStringName"; + /// <summary> + /// Cached name for the '_fieldNodePath' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldNodePath = "_fieldNodePath"; + /// <summary> + /// Cached name for the '_fieldRid' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldRid = "_fieldRid"; + /// <summary> + /// Cached name for the '_fieldGodotDictionary' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotDictionary = "_fieldGodotDictionary"; + /// <summary> + /// Cached name for the '_fieldGodotArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotArray = "_fieldGodotArray"; + /// <summary> + /// Cached name for the '_fieldGodotGenericDictionary' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotGenericDictionary = "_fieldGodotGenericDictionary"; + /// <summary> + /// Cached name for the '_fieldGodotGenericArray' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldGodotGenericArray = "_fieldGodotGenericArray"; + /// <summary> + /// Cached name for the '_fieldEmptyInt64Array' field. + /// </summary> + public new static readonly global::Godot.StringName _fieldEmptyInt64Array = "_fieldEmptyInt64Array"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName._fieldBoolean) { + this._fieldBoolean = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + else if (name == PropertyName._fieldChar) { + this._fieldChar = global::Godot.NativeInterop.VariantUtils.ConvertTo<char>(value); + return true; + } + else if (name == PropertyName._fieldSByte) { + this._fieldSByte = global::Godot.NativeInterop.VariantUtils.ConvertTo<sbyte>(value); + return true; + } + else if (name == PropertyName._fieldInt16) { + this._fieldInt16 = global::Godot.NativeInterop.VariantUtils.ConvertTo<short>(value); + return true; + } + else if (name == PropertyName._fieldInt32) { + this._fieldInt32 = global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(value); + return true; + } + else if (name == PropertyName._fieldInt64) { + this._fieldInt64 = global::Godot.NativeInterop.VariantUtils.ConvertTo<long>(value); + return true; + } + else if (name == PropertyName._fieldByte) { + this._fieldByte = global::Godot.NativeInterop.VariantUtils.ConvertTo<byte>(value); + return true; + } + else if (name == PropertyName._fieldUInt16) { + this._fieldUInt16 = global::Godot.NativeInterop.VariantUtils.ConvertTo<ushort>(value); + return true; + } + else if (name == PropertyName._fieldUInt32) { + this._fieldUInt32 = global::Godot.NativeInterop.VariantUtils.ConvertTo<uint>(value); + return true; + } + else if (name == PropertyName._fieldUInt64) { + this._fieldUInt64 = global::Godot.NativeInterop.VariantUtils.ConvertTo<ulong>(value); + return true; + } + else if (name == PropertyName._fieldSingle) { + this._fieldSingle = global::Godot.NativeInterop.VariantUtils.ConvertTo<float>(value); + return true; + } + else if (name == PropertyName._fieldDouble) { + this._fieldDouble = global::Godot.NativeInterop.VariantUtils.ConvertTo<double>(value); + return true; + } + else if (name == PropertyName._fieldString) { + this._fieldString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName._fieldVector2) { + this._fieldVector2 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2>(value); + return true; + } + else if (name == PropertyName._fieldVector2I) { + this._fieldVector2I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2I>(value); + return true; + } + else if (name == PropertyName._fieldRect2) { + this._fieldRect2 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rect2>(value); + return true; + } + else if (name == PropertyName._fieldRect2I) { + this._fieldRect2I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rect2I>(value); + return true; + } + else if (name == PropertyName._fieldTransform2D) { + this._fieldTransform2D = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Transform2D>(value); + return true; + } + else if (name == PropertyName._fieldVector3) { + this._fieldVector3 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3>(value); + return true; + } + else if (name == PropertyName._fieldVector3I) { + this._fieldVector3I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3I>(value); + return true; + } + else if (name == PropertyName._fieldBasis) { + this._fieldBasis = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Basis>(value); + return true; + } + else if (name == PropertyName._fieldQuaternion) { + this._fieldQuaternion = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Quaternion>(value); + return true; + } + else if (name == PropertyName._fieldTransform3D) { + this._fieldTransform3D = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Transform3D>(value); + return true; + } + else if (name == PropertyName._fieldVector4) { + this._fieldVector4 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector4>(value); + return true; + } + else if (name == PropertyName._fieldVector4I) { + this._fieldVector4I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector4I>(value); + return true; + } + else if (name == PropertyName._fieldProjection) { + this._fieldProjection = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Projection>(value); + return true; + } + else if (name == PropertyName._fieldAabb) { + this._fieldAabb = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Aabb>(value); + return true; + } + else if (name == PropertyName._fieldColor) { + this._fieldColor = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Color>(value); + return true; + } + else if (name == PropertyName._fieldPlane) { + this._fieldPlane = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Plane>(value); + return true; + } + else if (name == PropertyName._fieldCallable) { + this._fieldCallable = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Callable>(value); + return true; + } + else if (name == PropertyName._fieldSignal) { + this._fieldSignal = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Signal>(value); + return true; + } + else if (name == PropertyName._fieldEnum) { + this._fieldEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::ExportedFields.MyEnum>(value); + return true; + } + else if (name == PropertyName._fieldFlagsEnum) { + this._fieldFlagsEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::ExportedFields.MyFlagsEnum>(value); + return true; + } + else if (name == PropertyName._fieldByteArray) { + this._fieldByteArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<byte[]>(value); + return true; + } + else if (name == PropertyName._fieldInt32Array) { + this._fieldInt32Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<int[]>(value); + return true; + } + else if (name == PropertyName._fieldInt64Array) { + this._fieldInt64Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<long[]>(value); + return true; + } + else if (name == PropertyName._fieldSingleArray) { + this._fieldSingleArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<float[]>(value); + return true; + } + else if (name == PropertyName._fieldDoubleArray) { + this._fieldDoubleArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<double[]>(value); + return true; + } + else if (name == PropertyName._fieldStringArray) { + this._fieldStringArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<string[]>(value); + return true; + } + else if (name == PropertyName._fieldStringArrayEnum) { + this._fieldStringArrayEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<string[]>(value); + return true; + } + else if (name == PropertyName._fieldVector2Array) { + this._fieldVector2Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2[]>(value); + return true; + } + else if (name == PropertyName._fieldVector3Array) { + this._fieldVector3Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3[]>(value); + return true; + } + else if (name == PropertyName._fieldColorArray) { + this._fieldColorArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Color[]>(value); + return true; + } + else if (name == PropertyName._fieldGodotObjectOrDerivedArray) { + this._fieldGodotObjectOrDerivedArray = global::Godot.NativeInterop.VariantUtils.ConvertToSystemArrayOfGodotObject<global::Godot.GodotObject>(value); + return true; + } + else if (name == PropertyName._fieldStringNameArray) { + this._fieldStringNameArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName[]>(value); + return true; + } + else if (name == PropertyName._fieldNodePathArray) { + this._fieldNodePathArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.NodePath[]>(value); + return true; + } + else if (name == PropertyName._fieldRidArray) { + this._fieldRidArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rid[]>(value); + return true; + } + else if (name == PropertyName._fieldEmptyInt32Array) { + this._fieldEmptyInt32Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<int[]>(value); + return true; + } + else if (name == PropertyName._fieldArrayFromList) { + this._fieldArrayFromList = global::Godot.NativeInterop.VariantUtils.ConvertTo<int[]>(value); + return true; + } + else if (name == PropertyName._fieldVariant) { + this._fieldVariant = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Variant>(value); + return true; + } + else if (name == PropertyName._fieldGodotObjectOrDerived) { + this._fieldGodotObjectOrDerived = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.GodotObject>(value); + return true; + } + else if (name == PropertyName._fieldGodotResourceTexture) { + this._fieldGodotResourceTexture = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Texture>(value); + return true; + } + else if (name == PropertyName._fieldStringName) { + this._fieldStringName = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName>(value); + return true; + } + else if (name == PropertyName._fieldNodePath) { + this._fieldNodePath = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.NodePath>(value); + return true; + } + else if (name == PropertyName._fieldRid) { + this._fieldRid = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rid>(value); + return true; + } + else if (name == PropertyName._fieldGodotDictionary) { + this._fieldGodotDictionary = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Collections.Dictionary>(value); + return true; + } + else if (name == PropertyName._fieldGodotArray) { + this._fieldGodotArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Collections.Array>(value); + return true; + } + else if (name == PropertyName._fieldGodotGenericDictionary) { + this._fieldGodotGenericDictionary = global::Godot.NativeInterop.VariantUtils.ConvertToDictionary<string, bool>(value); + return true; + } + else if (name == PropertyName._fieldGodotGenericArray) { + this._fieldGodotGenericArray = global::Godot.NativeInterop.VariantUtils.ConvertToArray<int>(value); + return true; + } + else if (name == PropertyName._fieldEmptyInt64Array) { + this._fieldEmptyInt64Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<long[]>(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName._fieldBoolean) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<bool>(this._fieldBoolean); + return true; + } + else if (name == PropertyName._fieldChar) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<char>(this._fieldChar); + return true; + } + else if (name == PropertyName._fieldSByte) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<sbyte>(this._fieldSByte); + return true; + } + else if (name == PropertyName._fieldInt16) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<short>(this._fieldInt16); + return true; + } + else if (name == PropertyName._fieldInt32) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int>(this._fieldInt32); + return true; + } + else if (name == PropertyName._fieldInt64) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<long>(this._fieldInt64); + return true; + } + else if (name == PropertyName._fieldByte) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<byte>(this._fieldByte); + return true; + } + else if (name == PropertyName._fieldUInt16) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<ushort>(this._fieldUInt16); + return true; + } + else if (name == PropertyName._fieldUInt32) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<uint>(this._fieldUInt32); + return true; + } + else if (name == PropertyName._fieldUInt64) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<ulong>(this._fieldUInt64); + return true; + } + else if (name == PropertyName._fieldSingle) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<float>(this._fieldSingle); + return true; + } + else if (name == PropertyName._fieldDouble) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<double>(this._fieldDouble); + return true; + } + else if (name == PropertyName._fieldString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this._fieldString); + return true; + } + else if (name == PropertyName._fieldVector2) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2>(this._fieldVector2); + return true; + } + else if (name == PropertyName._fieldVector2I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2I>(this._fieldVector2I); + return true; + } + else if (name == PropertyName._fieldRect2) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rect2>(this._fieldRect2); + return true; + } + else if (name == PropertyName._fieldRect2I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rect2I>(this._fieldRect2I); + return true; + } + else if (name == PropertyName._fieldTransform2D) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Transform2D>(this._fieldTransform2D); + return true; + } + else if (name == PropertyName._fieldVector3) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3>(this._fieldVector3); + return true; + } + else if (name == PropertyName._fieldVector3I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3I>(this._fieldVector3I); + return true; + } + else if (name == PropertyName._fieldBasis) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Basis>(this._fieldBasis); + return true; + } + else if (name == PropertyName._fieldQuaternion) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Quaternion>(this._fieldQuaternion); + return true; + } + else if (name == PropertyName._fieldTransform3D) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Transform3D>(this._fieldTransform3D); + return true; + } + else if (name == PropertyName._fieldVector4) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector4>(this._fieldVector4); + return true; + } + else if (name == PropertyName._fieldVector4I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector4I>(this._fieldVector4I); + return true; + } + else if (name == PropertyName._fieldProjection) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Projection>(this._fieldProjection); + return true; + } + else if (name == PropertyName._fieldAabb) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Aabb>(this._fieldAabb); + return true; + } + else if (name == PropertyName._fieldColor) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Color>(this._fieldColor); + return true; + } + else if (name == PropertyName._fieldPlane) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Plane>(this._fieldPlane); + return true; + } + else if (name == PropertyName._fieldCallable) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this._fieldCallable); + return true; + } + else if (name == PropertyName._fieldSignal) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Signal>(this._fieldSignal); + return true; + } + else if (name == PropertyName._fieldEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::ExportedFields.MyEnum>(this._fieldEnum); + return true; + } + else if (name == PropertyName._fieldFlagsEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::ExportedFields.MyFlagsEnum>(this._fieldFlagsEnum); + return true; + } + else if (name == PropertyName._fieldByteArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<byte[]>(this._fieldByteArray); + return true; + } + else if (name == PropertyName._fieldInt32Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int[]>(this._fieldInt32Array); + return true; + } + else if (name == PropertyName._fieldInt64Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<long[]>(this._fieldInt64Array); + return true; + } + else if (name == PropertyName._fieldSingleArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<float[]>(this._fieldSingleArray); + return true; + } + else if (name == PropertyName._fieldDoubleArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<double[]>(this._fieldDoubleArray); + return true; + } + else if (name == PropertyName._fieldStringArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string[]>(this._fieldStringArray); + return true; + } + else if (name == PropertyName._fieldStringArrayEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string[]>(this._fieldStringArrayEnum); + return true; + } + else if (name == PropertyName._fieldVector2Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2[]>(this._fieldVector2Array); + return true; + } + else if (name == PropertyName._fieldVector3Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3[]>(this._fieldVector3Array); + return true; + } + else if (name == PropertyName._fieldColorArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Color[]>(this._fieldColorArray); + return true; + } + else if (name == PropertyName._fieldGodotObjectOrDerivedArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromSystemArrayOfGodotObject(this._fieldGodotObjectOrDerivedArray); + return true; + } + else if (name == PropertyName._fieldStringNameArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.StringName[]>(this._fieldStringNameArray); + return true; + } + else if (name == PropertyName._fieldNodePathArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.NodePath[]>(this._fieldNodePathArray); + return true; + } + else if (name == PropertyName._fieldRidArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rid[]>(this._fieldRidArray); + return true; + } + else if (name == PropertyName._fieldEmptyInt32Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int[]>(this._fieldEmptyInt32Array); + return true; + } + else if (name == PropertyName._fieldArrayFromList) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int[]>(this._fieldArrayFromList); + return true; + } + else if (name == PropertyName._fieldVariant) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Variant>(this._fieldVariant); + return true; + } + else if (name == PropertyName._fieldGodotObjectOrDerived) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.GodotObject>(this._fieldGodotObjectOrDerived); + return true; + } + else if (name == PropertyName._fieldGodotResourceTexture) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Texture>(this._fieldGodotResourceTexture); + return true; + } + else if (name == PropertyName._fieldStringName) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.StringName>(this._fieldStringName); + return true; + } + else if (name == PropertyName._fieldNodePath) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.NodePath>(this._fieldNodePath); + return true; + } + else if (name == PropertyName._fieldRid) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rid>(this._fieldRid); + return true; + } + else if (name == PropertyName._fieldGodotDictionary) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Collections.Dictionary>(this._fieldGodotDictionary); + return true; + } + else if (name == PropertyName._fieldGodotArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Collections.Array>(this._fieldGodotArray); + return true; + } + else if (name == PropertyName._fieldGodotGenericDictionary) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromDictionary(this._fieldGodotGenericDictionary); + return true; + } + else if (name == PropertyName._fieldGodotGenericArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromArray(this._fieldGodotGenericArray); + return true; + } + else if (name == PropertyName._fieldEmptyInt64Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<long[]>(this._fieldEmptyInt64Array); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName._fieldBoolean, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldChar, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldSByte, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldInt16, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldInt32, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldInt64, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldByte, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldUInt16, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldUInt32, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldUInt64, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)3, name: PropertyName._fieldSingle, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)3, name: PropertyName._fieldDouble, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName._fieldString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)5, name: PropertyName._fieldVector2, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)6, name: PropertyName._fieldVector2I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)7, name: PropertyName._fieldRect2, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)8, name: PropertyName._fieldRect2I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)11, name: PropertyName._fieldTransform2D, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)9, name: PropertyName._fieldVector3, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)10, name: PropertyName._fieldVector3I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)17, name: PropertyName._fieldBasis, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)15, name: PropertyName._fieldQuaternion, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)18, name: PropertyName._fieldTransform3D, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)12, name: PropertyName._fieldVector4, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)13, name: PropertyName._fieldVector4I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)19, name: PropertyName._fieldProjection, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)16, name: PropertyName._fieldAabb, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)20, name: PropertyName._fieldColor, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)14, name: PropertyName._fieldPlane, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)25, name: PropertyName._fieldCallable, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)26, name: PropertyName._fieldSignal, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldEnum, hint: (global::Godot.PropertyHint)2, hintString: "A,B,C", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._fieldFlagsEnum, hint: (global::Godot.PropertyHint)6, hintString: "A:0,B:1,C:2", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)29, name: PropertyName._fieldByteArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)30, name: PropertyName._fieldInt32Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)31, name: PropertyName._fieldInt64Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)32, name: PropertyName._fieldSingleArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)33, name: PropertyName._fieldDoubleArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)34, name: PropertyName._fieldStringArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)34, name: PropertyName._fieldStringArrayEnum, hint: (global::Godot.PropertyHint)23, hintString: "4/2:A,B,C", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)35, name: PropertyName._fieldVector2Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)36, name: PropertyName._fieldVector3Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)37, name: PropertyName._fieldColorArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldGodotObjectOrDerivedArray, hint: (global::Godot.PropertyHint)23, hintString: "24/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldStringNameArray, hint: (global::Godot.PropertyHint)23, hintString: "21/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldNodePathArray, hint: (global::Godot.PropertyHint)23, hintString: "22/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldRidArray, hint: (global::Godot.PropertyHint)23, hintString: "23/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)30, name: PropertyName._fieldEmptyInt32Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)30, name: PropertyName._fieldArrayFromList, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)0, name: PropertyName._fieldVariant, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)135174, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)24, name: PropertyName._fieldGodotObjectOrDerived, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)24, name: PropertyName._fieldGodotResourceTexture, hint: (global::Godot.PropertyHint)17, hintString: "Texture", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)21, name: PropertyName._fieldStringName, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)22, name: PropertyName._fieldNodePath, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)23, name: PropertyName._fieldRid, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName._fieldGodotDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldGodotArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName._fieldGodotGenericDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName._fieldGodotGenericArray, hint: (global::Godot.PropertyHint)23, hintString: "2/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)31, name: PropertyName._fieldEmptyInt64Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..367cb073c0 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,139 @@ +partial class ExportedFields +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword +#if TOOLS + /// <summary> + /// Get the default values for all properties declared in this class. + /// This method is used by Godot to determine the value that will be + /// used by the inspector when resetting properties. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant> GetGodotPropertyDefaultValues() + { + var values = new global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>(60); + bool ___fieldBoolean_default_value = true; + values.Add(PropertyName._fieldBoolean, global::Godot.Variant.From<bool>(___fieldBoolean_default_value)); + char ___fieldChar_default_value = 'f'; + values.Add(PropertyName._fieldChar, global::Godot.Variant.From<char>(___fieldChar_default_value)); + sbyte ___fieldSByte_default_value = 10; + values.Add(PropertyName._fieldSByte, global::Godot.Variant.From<sbyte>(___fieldSByte_default_value)); + short ___fieldInt16_default_value = 10; + values.Add(PropertyName._fieldInt16, global::Godot.Variant.From<short>(___fieldInt16_default_value)); + int ___fieldInt32_default_value = 10; + values.Add(PropertyName._fieldInt32, global::Godot.Variant.From<int>(___fieldInt32_default_value)); + long ___fieldInt64_default_value = 10; + values.Add(PropertyName._fieldInt64, global::Godot.Variant.From<long>(___fieldInt64_default_value)); + byte ___fieldByte_default_value = 10; + values.Add(PropertyName._fieldByte, global::Godot.Variant.From<byte>(___fieldByte_default_value)); + ushort ___fieldUInt16_default_value = 10; + values.Add(PropertyName._fieldUInt16, global::Godot.Variant.From<ushort>(___fieldUInt16_default_value)); + uint ___fieldUInt32_default_value = 10; + values.Add(PropertyName._fieldUInt32, global::Godot.Variant.From<uint>(___fieldUInt32_default_value)); + ulong ___fieldUInt64_default_value = 10; + values.Add(PropertyName._fieldUInt64, global::Godot.Variant.From<ulong>(___fieldUInt64_default_value)); + float ___fieldSingle_default_value = 10; + values.Add(PropertyName._fieldSingle, global::Godot.Variant.From<float>(___fieldSingle_default_value)); + double ___fieldDouble_default_value = 10; + values.Add(PropertyName._fieldDouble, global::Godot.Variant.From<double>(___fieldDouble_default_value)); + string ___fieldString_default_value = "foo"; + values.Add(PropertyName._fieldString, global::Godot.Variant.From<string>(___fieldString_default_value)); + global::Godot.Vector2 ___fieldVector2_default_value = new(10f, 10f); + values.Add(PropertyName._fieldVector2, global::Godot.Variant.From<global::Godot.Vector2>(___fieldVector2_default_value)); + global::Godot.Vector2I ___fieldVector2I_default_value = global::Godot.Vector2I.Up; + values.Add(PropertyName._fieldVector2I, global::Godot.Variant.From<global::Godot.Vector2I>(___fieldVector2I_default_value)); + global::Godot.Rect2 ___fieldRect2_default_value = new(new global::Godot.Vector2(10f, 10f), new global::Godot.Vector2(10f, 10f)); + values.Add(PropertyName._fieldRect2, global::Godot.Variant.From<global::Godot.Rect2>(___fieldRect2_default_value)); + global::Godot.Rect2I ___fieldRect2I_default_value = new(new global::Godot.Vector2I(10, 10), new global::Godot.Vector2I(10, 10)); + values.Add(PropertyName._fieldRect2I, global::Godot.Variant.From<global::Godot.Rect2I>(___fieldRect2I_default_value)); + global::Godot.Transform2D ___fieldTransform2D_default_value = global::Godot.Transform2D.Identity; + values.Add(PropertyName._fieldTransform2D, global::Godot.Variant.From<global::Godot.Transform2D>(___fieldTransform2D_default_value)); + global::Godot.Vector3 ___fieldVector3_default_value = new(10f, 10f, 10f); + values.Add(PropertyName._fieldVector3, global::Godot.Variant.From<global::Godot.Vector3>(___fieldVector3_default_value)); + global::Godot.Vector3I ___fieldVector3I_default_value = global::Godot.Vector3I.Back; + values.Add(PropertyName._fieldVector3I, global::Godot.Variant.From<global::Godot.Vector3I>(___fieldVector3I_default_value)); + global::Godot.Basis ___fieldBasis_default_value = new global::Godot.Basis(global::Godot.Quaternion.Identity); + values.Add(PropertyName._fieldBasis, global::Godot.Variant.From<global::Godot.Basis>(___fieldBasis_default_value)); + global::Godot.Quaternion ___fieldQuaternion_default_value = new global::Godot.Quaternion(global::Godot.Basis.Identity); + values.Add(PropertyName._fieldQuaternion, global::Godot.Variant.From<global::Godot.Quaternion>(___fieldQuaternion_default_value)); + global::Godot.Transform3D ___fieldTransform3D_default_value = global::Godot.Transform3D.Identity; + values.Add(PropertyName._fieldTransform3D, global::Godot.Variant.From<global::Godot.Transform3D>(___fieldTransform3D_default_value)); + global::Godot.Vector4 ___fieldVector4_default_value = new(10f, 10f, 10f, 10f); + values.Add(PropertyName._fieldVector4, global::Godot.Variant.From<global::Godot.Vector4>(___fieldVector4_default_value)); + global::Godot.Vector4I ___fieldVector4I_default_value = global::Godot.Vector4I.One; + values.Add(PropertyName._fieldVector4I, global::Godot.Variant.From<global::Godot.Vector4I>(___fieldVector4I_default_value)); + global::Godot.Projection ___fieldProjection_default_value = global::Godot.Projection.Identity; + values.Add(PropertyName._fieldProjection, global::Godot.Variant.From<global::Godot.Projection>(___fieldProjection_default_value)); + global::Godot.Aabb ___fieldAabb_default_value = new global::Godot.Aabb(10f, 10f, 10f, new global::Godot.Vector3(1f, 1f, 1f)); + values.Add(PropertyName._fieldAabb, global::Godot.Variant.From<global::Godot.Aabb>(___fieldAabb_default_value)); + global::Godot.Color ___fieldColor_default_value = global::Godot.Colors.Aquamarine; + values.Add(PropertyName._fieldColor, global::Godot.Variant.From<global::Godot.Color>(___fieldColor_default_value)); + global::Godot.Plane ___fieldPlane_default_value = global::Godot.Plane.PlaneXZ; + values.Add(PropertyName._fieldPlane, global::Godot.Variant.From<global::Godot.Plane>(___fieldPlane_default_value)); + global::Godot.Callable ___fieldCallable_default_value = new global::Godot.Callable(global::Godot.Engine.GetMainLoop(), "_process"); + values.Add(PropertyName._fieldCallable, global::Godot.Variant.From<global::Godot.Callable>(___fieldCallable_default_value)); + global::Godot.Signal ___fieldSignal_default_value = new global::Godot.Signal(global::Godot.Engine.GetMainLoop(), "property_list_changed"); + values.Add(PropertyName._fieldSignal, global::Godot.Variant.From<global::Godot.Signal>(___fieldSignal_default_value)); + global::ExportedFields.MyEnum ___fieldEnum_default_value = global::ExportedFields.MyEnum.C; + values.Add(PropertyName._fieldEnum, global::Godot.Variant.From<global::ExportedFields.MyEnum>(___fieldEnum_default_value)); + global::ExportedFields.MyFlagsEnum ___fieldFlagsEnum_default_value = global::ExportedFields.MyFlagsEnum.C; + values.Add(PropertyName._fieldFlagsEnum, global::Godot.Variant.From<global::ExportedFields.MyFlagsEnum>(___fieldFlagsEnum_default_value)); + byte[] ___fieldByteArray_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName._fieldByteArray, global::Godot.Variant.From<byte[]>(___fieldByteArray_default_value)); + int[] ___fieldInt32Array_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName._fieldInt32Array, global::Godot.Variant.From<int[]>(___fieldInt32Array_default_value)); + long[] ___fieldInt64Array_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName._fieldInt64Array, global::Godot.Variant.From<long[]>(___fieldInt64Array_default_value)); + float[] ___fieldSingleArray_default_value = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + values.Add(PropertyName._fieldSingleArray, global::Godot.Variant.From<float[]>(___fieldSingleArray_default_value)); + double[] ___fieldDoubleArray_default_value = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + values.Add(PropertyName._fieldDoubleArray, global::Godot.Variant.From<double[]>(___fieldDoubleArray_default_value)); + string[] ___fieldStringArray_default_value = { "foo", "bar" }; + values.Add(PropertyName._fieldStringArray, global::Godot.Variant.From<string[]>(___fieldStringArray_default_value)); + string[] ___fieldStringArrayEnum_default_value = { "foo", "bar" }; + values.Add(PropertyName._fieldStringArrayEnum, global::Godot.Variant.From<string[]>(___fieldStringArrayEnum_default_value)); + global::Godot.Vector2[] ___fieldVector2Array_default_value = { global::Godot.Vector2.Up, global::Godot.Vector2.Down, global::Godot.Vector2.Left, global::Godot.Vector2.Right }; + values.Add(PropertyName._fieldVector2Array, global::Godot.Variant.From<global::Godot.Vector2[]>(___fieldVector2Array_default_value)); + global::Godot.Vector3[] ___fieldVector3Array_default_value = { global::Godot.Vector3.Up, global::Godot.Vector3.Down, global::Godot.Vector3.Left, global::Godot.Vector3.Right }; + values.Add(PropertyName._fieldVector3Array, global::Godot.Variant.From<global::Godot.Vector3[]>(___fieldVector3Array_default_value)); + global::Godot.Color[] ___fieldColorArray_default_value = { global::Godot.Colors.Aqua, global::Godot.Colors.Aquamarine, global::Godot.Colors.Azure, global::Godot.Colors.Beige }; + values.Add(PropertyName._fieldColorArray, global::Godot.Variant.From<global::Godot.Color[]>(___fieldColorArray_default_value)); + global::Godot.GodotObject[] ___fieldGodotObjectOrDerivedArray_default_value = { null }; + values.Add(PropertyName._fieldGodotObjectOrDerivedArray, global::Godot.Variant.CreateFrom(___fieldGodotObjectOrDerivedArray_default_value)); + global::Godot.StringName[] ___fieldStringNameArray_default_value = { "foo", "bar" }; + values.Add(PropertyName._fieldStringNameArray, global::Godot.Variant.From<global::Godot.StringName[]>(___fieldStringNameArray_default_value)); + global::Godot.NodePath[] ___fieldNodePathArray_default_value = { "foo", "bar" }; + values.Add(PropertyName._fieldNodePathArray, global::Godot.Variant.From<global::Godot.NodePath[]>(___fieldNodePathArray_default_value)); + global::Godot.Rid[] ___fieldRidArray_default_value = { default, default, default }; + values.Add(PropertyName._fieldRidArray, global::Godot.Variant.From<global::Godot.Rid[]>(___fieldRidArray_default_value)); + int[] ___fieldEmptyInt32Array_default_value = global::System.Array.Empty<int>(); + values.Add(PropertyName._fieldEmptyInt32Array, global::Godot.Variant.From<int[]>(___fieldEmptyInt32Array_default_value)); + int[] ___fieldArrayFromList_default_value = new global::System.Collections.Generic.List<int>(global::System.Array.Empty<int>()).ToArray(); + values.Add(PropertyName._fieldArrayFromList, global::Godot.Variant.From<int[]>(___fieldArrayFromList_default_value)); + global::Godot.Variant ___fieldVariant_default_value = "foo"; + values.Add(PropertyName._fieldVariant, global::Godot.Variant.From<global::Godot.Variant>(___fieldVariant_default_value)); + global::Godot.GodotObject ___fieldGodotObjectOrDerived_default_value = default; + values.Add(PropertyName._fieldGodotObjectOrDerived, global::Godot.Variant.From<global::Godot.GodotObject>(___fieldGodotObjectOrDerived_default_value)); + global::Godot.Texture ___fieldGodotResourceTexture_default_value = default; + values.Add(PropertyName._fieldGodotResourceTexture, global::Godot.Variant.From<global::Godot.Texture>(___fieldGodotResourceTexture_default_value)); + global::Godot.StringName ___fieldStringName_default_value = new global::Godot.StringName("foo"); + values.Add(PropertyName._fieldStringName, global::Godot.Variant.From<global::Godot.StringName>(___fieldStringName_default_value)); + global::Godot.NodePath ___fieldNodePath_default_value = new global::Godot.NodePath("foo"); + values.Add(PropertyName._fieldNodePath, global::Godot.Variant.From<global::Godot.NodePath>(___fieldNodePath_default_value)); + global::Godot.Rid ___fieldRid_default_value = default; + values.Add(PropertyName._fieldRid, global::Godot.Variant.From<global::Godot.Rid>(___fieldRid_default_value)); + global::Godot.Collections.Dictionary ___fieldGodotDictionary_default_value = new() { { "foo", 10 }, { global::Godot.Vector2.Up, global::Godot.Colors.Chocolate } }; + values.Add(PropertyName._fieldGodotDictionary, global::Godot.Variant.From<global::Godot.Collections.Dictionary>(___fieldGodotDictionary_default_value)); + global::Godot.Collections.Array ___fieldGodotArray_default_value = new() { "foo", 10, global::Godot.Vector2.Up, global::Godot.Colors.Chocolate }; + values.Add(PropertyName._fieldGodotArray, global::Godot.Variant.From<global::Godot.Collections.Array>(___fieldGodotArray_default_value)); + global::Godot.Collections.Dictionary<string, bool> ___fieldGodotGenericDictionary_default_value = new() { { "foo", true }, { "bar", false } }; + values.Add(PropertyName._fieldGodotGenericDictionary, global::Godot.Variant.CreateFrom(___fieldGodotGenericDictionary_default_value)); + global::Godot.Collections.Array<int> ___fieldGodotGenericArray_default_value = new() { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName._fieldGodotGenericArray, global::Godot.Variant.CreateFrom(___fieldGodotGenericArray_default_value)); + long[] ___fieldEmptyInt64Array_default_value = global::System.Array.Empty<long>(); + values.Add(PropertyName._fieldEmptyInt64Array, global::Godot.Variant.From<long[]>(___fieldEmptyInt64Array_default_value)); + return values; + } +#endif // TOOLS +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs new file mode 100644 index 0000000000..6e0e9fffbe --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs @@ -0,0 +1,933 @@ +using Godot; +using Godot.NativeInterop; + +partial class ExportedProperties +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// <summary> + /// Cached name for the 'NotGenerateComplexLamdaProperty' property. + /// </summary> + public new static readonly global::Godot.StringName NotGenerateComplexLamdaProperty = "NotGenerateComplexLamdaProperty"; + /// <summary> + /// Cached name for the 'NotGenerateLamdaNoFieldProperty' property. + /// </summary> + public new static readonly global::Godot.StringName NotGenerateLamdaNoFieldProperty = "NotGenerateLamdaNoFieldProperty"; + /// <summary> + /// Cached name for the 'NotGenerateComplexReturnProperty' property. + /// </summary> + public new static readonly global::Godot.StringName NotGenerateComplexReturnProperty = "NotGenerateComplexReturnProperty"; + /// <summary> + /// Cached name for the 'NotGenerateReturnsProperty' property. + /// </summary> + public new static readonly global::Godot.StringName NotGenerateReturnsProperty = "NotGenerateReturnsProperty"; + /// <summary> + /// Cached name for the 'FullPropertyString' property. + /// </summary> + public new static readonly global::Godot.StringName FullPropertyString = "FullPropertyString"; + /// <summary> + /// Cached name for the 'FullPropertyString_Complex' property. + /// </summary> + public new static readonly global::Godot.StringName FullPropertyString_Complex = "FullPropertyString_Complex"; + /// <summary> + /// Cached name for the 'LamdaPropertyString' property. + /// </summary> + public new static readonly global::Godot.StringName LamdaPropertyString = "LamdaPropertyString"; + /// <summary> + /// Cached name for the 'PropertyBoolean' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyBoolean = "PropertyBoolean"; + /// <summary> + /// Cached name for the 'PropertyChar' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyChar = "PropertyChar"; + /// <summary> + /// Cached name for the 'PropertySByte' property. + /// </summary> + public new static readonly global::Godot.StringName PropertySByte = "PropertySByte"; + /// <summary> + /// Cached name for the 'PropertyInt16' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyInt16 = "PropertyInt16"; + /// <summary> + /// Cached name for the 'PropertyInt32' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyInt32 = "PropertyInt32"; + /// <summary> + /// Cached name for the 'PropertyInt64' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyInt64 = "PropertyInt64"; + /// <summary> + /// Cached name for the 'PropertyByte' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyByte = "PropertyByte"; + /// <summary> + /// Cached name for the 'PropertyUInt16' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyUInt16 = "PropertyUInt16"; + /// <summary> + /// Cached name for the 'PropertyUInt32' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyUInt32 = "PropertyUInt32"; + /// <summary> + /// Cached name for the 'PropertyUInt64' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyUInt64 = "PropertyUInt64"; + /// <summary> + /// Cached name for the 'PropertySingle' property. + /// </summary> + public new static readonly global::Godot.StringName PropertySingle = "PropertySingle"; + /// <summary> + /// Cached name for the 'PropertyDouble' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyDouble = "PropertyDouble"; + /// <summary> + /// Cached name for the 'PropertyString' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyString = "PropertyString"; + /// <summary> + /// Cached name for the 'PropertyVector2' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector2 = "PropertyVector2"; + /// <summary> + /// Cached name for the 'PropertyVector2I' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector2I = "PropertyVector2I"; + /// <summary> + /// Cached name for the 'PropertyRect2' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyRect2 = "PropertyRect2"; + /// <summary> + /// Cached name for the 'PropertyRect2I' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyRect2I = "PropertyRect2I"; + /// <summary> + /// Cached name for the 'PropertyTransform2D' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyTransform2D = "PropertyTransform2D"; + /// <summary> + /// Cached name for the 'PropertyVector3' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector3 = "PropertyVector3"; + /// <summary> + /// Cached name for the 'PropertyVector3I' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector3I = "PropertyVector3I"; + /// <summary> + /// Cached name for the 'PropertyBasis' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyBasis = "PropertyBasis"; + /// <summary> + /// Cached name for the 'PropertyQuaternion' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyQuaternion = "PropertyQuaternion"; + /// <summary> + /// Cached name for the 'PropertyTransform3D' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyTransform3D = "PropertyTransform3D"; + /// <summary> + /// Cached name for the 'PropertyVector4' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector4 = "PropertyVector4"; + /// <summary> + /// Cached name for the 'PropertyVector4I' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector4I = "PropertyVector4I"; + /// <summary> + /// Cached name for the 'PropertyProjection' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyProjection = "PropertyProjection"; + /// <summary> + /// Cached name for the 'PropertyAabb' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyAabb = "PropertyAabb"; + /// <summary> + /// Cached name for the 'PropertyColor' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyColor = "PropertyColor"; + /// <summary> + /// Cached name for the 'PropertyPlane' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyPlane = "PropertyPlane"; + /// <summary> + /// Cached name for the 'PropertyCallable' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyCallable = "PropertyCallable"; + /// <summary> + /// Cached name for the 'PropertySignal' property. + /// </summary> + public new static readonly global::Godot.StringName PropertySignal = "PropertySignal"; + /// <summary> + /// Cached name for the 'PropertyEnum' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyEnum = "PropertyEnum"; + /// <summary> + /// Cached name for the 'PropertyFlagsEnum' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyFlagsEnum = "PropertyFlagsEnum"; + /// <summary> + /// Cached name for the 'PropertyByteArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyByteArray = "PropertyByteArray"; + /// <summary> + /// Cached name for the 'PropertyInt32Array' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyInt32Array = "PropertyInt32Array"; + /// <summary> + /// Cached name for the 'PropertyInt64Array' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyInt64Array = "PropertyInt64Array"; + /// <summary> + /// Cached name for the 'PropertySingleArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertySingleArray = "PropertySingleArray"; + /// <summary> + /// Cached name for the 'PropertyDoubleArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyDoubleArray = "PropertyDoubleArray"; + /// <summary> + /// Cached name for the 'PropertyStringArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyStringArray = "PropertyStringArray"; + /// <summary> + /// Cached name for the 'PropertyStringArrayEnum' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyStringArrayEnum = "PropertyStringArrayEnum"; + /// <summary> + /// Cached name for the 'PropertyVector2Array' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector2Array = "PropertyVector2Array"; + /// <summary> + /// Cached name for the 'PropertyVector3Array' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVector3Array = "PropertyVector3Array"; + /// <summary> + /// Cached name for the 'PropertyColorArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyColorArray = "PropertyColorArray"; + /// <summary> + /// Cached name for the 'PropertyGodotObjectOrDerivedArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotObjectOrDerivedArray = "PropertyGodotObjectOrDerivedArray"; + /// <summary> + /// Cached name for the 'field_StringNameArray' property. + /// </summary> + public new static readonly global::Godot.StringName field_StringNameArray = "field_StringNameArray"; + /// <summary> + /// Cached name for the 'field_NodePathArray' property. + /// </summary> + public new static readonly global::Godot.StringName field_NodePathArray = "field_NodePathArray"; + /// <summary> + /// Cached name for the 'field_RidArray' property. + /// </summary> + public new static readonly global::Godot.StringName field_RidArray = "field_RidArray"; + /// <summary> + /// Cached name for the 'PropertyVariant' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyVariant = "PropertyVariant"; + /// <summary> + /// Cached name for the 'PropertyGodotObjectOrDerived' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotObjectOrDerived = "PropertyGodotObjectOrDerived"; + /// <summary> + /// Cached name for the 'PropertyGodotResourceTexture' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotResourceTexture = "PropertyGodotResourceTexture"; + /// <summary> + /// Cached name for the 'PropertyStringName' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyStringName = "PropertyStringName"; + /// <summary> + /// Cached name for the 'PropertyNodePath' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyNodePath = "PropertyNodePath"; + /// <summary> + /// Cached name for the 'PropertyRid' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyRid = "PropertyRid"; + /// <summary> + /// Cached name for the 'PropertyGodotDictionary' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotDictionary = "PropertyGodotDictionary"; + /// <summary> + /// Cached name for the 'PropertyGodotArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotArray = "PropertyGodotArray"; + /// <summary> + /// Cached name for the 'PropertyGodotGenericDictionary' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotGenericDictionary = "PropertyGodotGenericDictionary"; + /// <summary> + /// Cached name for the 'PropertyGodotGenericArray' property. + /// </summary> + public new static readonly global::Godot.StringName PropertyGodotGenericArray = "PropertyGodotGenericArray"; + /// <summary> + /// Cached name for the '_notGeneratePropertyString' field. + /// </summary> + public new static readonly global::Godot.StringName _notGeneratePropertyString = "_notGeneratePropertyString"; + /// <summary> + /// Cached name for the '_notGeneratePropertyInt' field. + /// </summary> + public new static readonly global::Godot.StringName _notGeneratePropertyInt = "_notGeneratePropertyInt"; + /// <summary> + /// Cached name for the '_fullPropertyString' field. + /// </summary> + public new static readonly global::Godot.StringName _fullPropertyString = "_fullPropertyString"; + /// <summary> + /// Cached name for the '_fullPropertyStringComplex' field. + /// </summary> + public new static readonly global::Godot.StringName _fullPropertyStringComplex = "_fullPropertyStringComplex"; + /// <summary> + /// Cached name for the '_lamdaPropertyString' field. + /// </summary> + public new static readonly global::Godot.StringName _lamdaPropertyString = "_lamdaPropertyString"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName.NotGenerateComplexLamdaProperty) { + this.NotGenerateComplexLamdaProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.NotGenerateLamdaNoFieldProperty) { + this.NotGenerateLamdaNoFieldProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.NotGenerateComplexReturnProperty) { + this.NotGenerateComplexReturnProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.NotGenerateReturnsProperty) { + this.NotGenerateReturnsProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.FullPropertyString) { + this.FullPropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.FullPropertyString_Complex) { + this.FullPropertyString_Complex = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.LamdaPropertyString) { + this.LamdaPropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.PropertyBoolean) { + this.PropertyBoolean = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + else if (name == PropertyName.PropertyChar) { + this.PropertyChar = global::Godot.NativeInterop.VariantUtils.ConvertTo<char>(value); + return true; + } + else if (name == PropertyName.PropertySByte) { + this.PropertySByte = global::Godot.NativeInterop.VariantUtils.ConvertTo<sbyte>(value); + return true; + } + else if (name == PropertyName.PropertyInt16) { + this.PropertyInt16 = global::Godot.NativeInterop.VariantUtils.ConvertTo<short>(value); + return true; + } + else if (name == PropertyName.PropertyInt32) { + this.PropertyInt32 = global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(value); + return true; + } + else if (name == PropertyName.PropertyInt64) { + this.PropertyInt64 = global::Godot.NativeInterop.VariantUtils.ConvertTo<long>(value); + return true; + } + else if (name == PropertyName.PropertyByte) { + this.PropertyByte = global::Godot.NativeInterop.VariantUtils.ConvertTo<byte>(value); + return true; + } + else if (name == PropertyName.PropertyUInt16) { + this.PropertyUInt16 = global::Godot.NativeInterop.VariantUtils.ConvertTo<ushort>(value); + return true; + } + else if (name == PropertyName.PropertyUInt32) { + this.PropertyUInt32 = global::Godot.NativeInterop.VariantUtils.ConvertTo<uint>(value); + return true; + } + else if (name == PropertyName.PropertyUInt64) { + this.PropertyUInt64 = global::Godot.NativeInterop.VariantUtils.ConvertTo<ulong>(value); + return true; + } + else if (name == PropertyName.PropertySingle) { + this.PropertySingle = global::Godot.NativeInterop.VariantUtils.ConvertTo<float>(value); + return true; + } + else if (name == PropertyName.PropertyDouble) { + this.PropertyDouble = global::Godot.NativeInterop.VariantUtils.ConvertTo<double>(value); + return true; + } + else if (name == PropertyName.PropertyString) { + this.PropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName.PropertyVector2) { + this.PropertyVector2 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2>(value); + return true; + } + else if (name == PropertyName.PropertyVector2I) { + this.PropertyVector2I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2I>(value); + return true; + } + else if (name == PropertyName.PropertyRect2) { + this.PropertyRect2 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rect2>(value); + return true; + } + else if (name == PropertyName.PropertyRect2I) { + this.PropertyRect2I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rect2I>(value); + return true; + } + else if (name == PropertyName.PropertyTransform2D) { + this.PropertyTransform2D = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Transform2D>(value); + return true; + } + else if (name == PropertyName.PropertyVector3) { + this.PropertyVector3 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3>(value); + return true; + } + else if (name == PropertyName.PropertyVector3I) { + this.PropertyVector3I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3I>(value); + return true; + } + else if (name == PropertyName.PropertyBasis) { + this.PropertyBasis = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Basis>(value); + return true; + } + else if (name == PropertyName.PropertyQuaternion) { + this.PropertyQuaternion = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Quaternion>(value); + return true; + } + else if (name == PropertyName.PropertyTransform3D) { + this.PropertyTransform3D = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Transform3D>(value); + return true; + } + else if (name == PropertyName.PropertyVector4) { + this.PropertyVector4 = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector4>(value); + return true; + } + else if (name == PropertyName.PropertyVector4I) { + this.PropertyVector4I = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector4I>(value); + return true; + } + else if (name == PropertyName.PropertyProjection) { + this.PropertyProjection = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Projection>(value); + return true; + } + else if (name == PropertyName.PropertyAabb) { + this.PropertyAabb = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Aabb>(value); + return true; + } + else if (name == PropertyName.PropertyColor) { + this.PropertyColor = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Color>(value); + return true; + } + else if (name == PropertyName.PropertyPlane) { + this.PropertyPlane = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Plane>(value); + return true; + } + else if (name == PropertyName.PropertyCallable) { + this.PropertyCallable = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Callable>(value); + return true; + } + else if (name == PropertyName.PropertySignal) { + this.PropertySignal = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Signal>(value); + return true; + } + else if (name == PropertyName.PropertyEnum) { + this.PropertyEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::ExportedProperties.MyEnum>(value); + return true; + } + else if (name == PropertyName.PropertyFlagsEnum) { + this.PropertyFlagsEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::ExportedProperties.MyFlagsEnum>(value); + return true; + } + else if (name == PropertyName.PropertyByteArray) { + this.PropertyByteArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<byte[]>(value); + return true; + } + else if (name == PropertyName.PropertyInt32Array) { + this.PropertyInt32Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<int[]>(value); + return true; + } + else if (name == PropertyName.PropertyInt64Array) { + this.PropertyInt64Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<long[]>(value); + return true; + } + else if (name == PropertyName.PropertySingleArray) { + this.PropertySingleArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<float[]>(value); + return true; + } + else if (name == PropertyName.PropertyDoubleArray) { + this.PropertyDoubleArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<double[]>(value); + return true; + } + else if (name == PropertyName.PropertyStringArray) { + this.PropertyStringArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<string[]>(value); + return true; + } + else if (name == PropertyName.PropertyStringArrayEnum) { + this.PropertyStringArrayEnum = global::Godot.NativeInterop.VariantUtils.ConvertTo<string[]>(value); + return true; + } + else if (name == PropertyName.PropertyVector2Array) { + this.PropertyVector2Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector2[]>(value); + return true; + } + else if (name == PropertyName.PropertyVector3Array) { + this.PropertyVector3Array = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Vector3[]>(value); + return true; + } + else if (name == PropertyName.PropertyColorArray) { + this.PropertyColorArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Color[]>(value); + return true; + } + else if (name == PropertyName.PropertyGodotObjectOrDerivedArray) { + this.PropertyGodotObjectOrDerivedArray = global::Godot.NativeInterop.VariantUtils.ConvertToSystemArrayOfGodotObject<global::Godot.GodotObject>(value); + return true; + } + else if (name == PropertyName.field_StringNameArray) { + this.field_StringNameArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName[]>(value); + return true; + } + else if (name == PropertyName.field_NodePathArray) { + this.field_NodePathArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.NodePath[]>(value); + return true; + } + else if (name == PropertyName.field_RidArray) { + this.field_RidArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rid[]>(value); + return true; + } + else if (name == PropertyName.PropertyVariant) { + this.PropertyVariant = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Variant>(value); + return true; + } + else if (name == PropertyName.PropertyGodotObjectOrDerived) { + this.PropertyGodotObjectOrDerived = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.GodotObject>(value); + return true; + } + else if (name == PropertyName.PropertyGodotResourceTexture) { + this.PropertyGodotResourceTexture = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Texture>(value); + return true; + } + else if (name == PropertyName.PropertyStringName) { + this.PropertyStringName = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName>(value); + return true; + } + else if (name == PropertyName.PropertyNodePath) { + this.PropertyNodePath = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.NodePath>(value); + return true; + } + else if (name == PropertyName.PropertyRid) { + this.PropertyRid = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Rid>(value); + return true; + } + else if (name == PropertyName.PropertyGodotDictionary) { + this.PropertyGodotDictionary = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Collections.Dictionary>(value); + return true; + } + else if (name == PropertyName.PropertyGodotArray) { + this.PropertyGodotArray = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.Collections.Array>(value); + return true; + } + else if (name == PropertyName.PropertyGodotGenericDictionary) { + this.PropertyGodotGenericDictionary = global::Godot.NativeInterop.VariantUtils.ConvertToDictionary<string, bool>(value); + return true; + } + else if (name == PropertyName.PropertyGodotGenericArray) { + this.PropertyGodotGenericArray = global::Godot.NativeInterop.VariantUtils.ConvertToArray<int>(value); + return true; + } + else if (name == PropertyName._notGeneratePropertyString) { + this._notGeneratePropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName._notGeneratePropertyInt) { + this._notGeneratePropertyInt = global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(value); + return true; + } + else if (name == PropertyName._fullPropertyString) { + this._fullPropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName._fullPropertyStringComplex) { + this._fullPropertyStringComplex = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + else if (name == PropertyName._lamdaPropertyString) { + this._lamdaPropertyString = global::Godot.NativeInterop.VariantUtils.ConvertTo<string>(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName.NotGenerateComplexLamdaProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.NotGenerateComplexLamdaProperty); + return true; + } + else if (name == PropertyName.NotGenerateLamdaNoFieldProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.NotGenerateLamdaNoFieldProperty); + return true; + } + else if (name == PropertyName.NotGenerateComplexReturnProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.NotGenerateComplexReturnProperty); + return true; + } + else if (name == PropertyName.NotGenerateReturnsProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.NotGenerateReturnsProperty); + return true; + } + else if (name == PropertyName.FullPropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.FullPropertyString); + return true; + } + else if (name == PropertyName.FullPropertyString_Complex) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.FullPropertyString_Complex); + return true; + } + else if (name == PropertyName.LamdaPropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.LamdaPropertyString); + return true; + } + else if (name == PropertyName.PropertyBoolean) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<bool>(this.PropertyBoolean); + return true; + } + else if (name == PropertyName.PropertyChar) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<char>(this.PropertyChar); + return true; + } + else if (name == PropertyName.PropertySByte) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<sbyte>(this.PropertySByte); + return true; + } + else if (name == PropertyName.PropertyInt16) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<short>(this.PropertyInt16); + return true; + } + else if (name == PropertyName.PropertyInt32) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int>(this.PropertyInt32); + return true; + } + else if (name == PropertyName.PropertyInt64) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<long>(this.PropertyInt64); + return true; + } + else if (name == PropertyName.PropertyByte) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<byte>(this.PropertyByte); + return true; + } + else if (name == PropertyName.PropertyUInt16) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<ushort>(this.PropertyUInt16); + return true; + } + else if (name == PropertyName.PropertyUInt32) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<uint>(this.PropertyUInt32); + return true; + } + else if (name == PropertyName.PropertyUInt64) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<ulong>(this.PropertyUInt64); + return true; + } + else if (name == PropertyName.PropertySingle) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<float>(this.PropertySingle); + return true; + } + else if (name == PropertyName.PropertyDouble) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<double>(this.PropertyDouble); + return true; + } + else if (name == PropertyName.PropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.PropertyString); + return true; + } + else if (name == PropertyName.PropertyVector2) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2>(this.PropertyVector2); + return true; + } + else if (name == PropertyName.PropertyVector2I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2I>(this.PropertyVector2I); + return true; + } + else if (name == PropertyName.PropertyRect2) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rect2>(this.PropertyRect2); + return true; + } + else if (name == PropertyName.PropertyRect2I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rect2I>(this.PropertyRect2I); + return true; + } + else if (name == PropertyName.PropertyTransform2D) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Transform2D>(this.PropertyTransform2D); + return true; + } + else if (name == PropertyName.PropertyVector3) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3>(this.PropertyVector3); + return true; + } + else if (name == PropertyName.PropertyVector3I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3I>(this.PropertyVector3I); + return true; + } + else if (name == PropertyName.PropertyBasis) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Basis>(this.PropertyBasis); + return true; + } + else if (name == PropertyName.PropertyQuaternion) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Quaternion>(this.PropertyQuaternion); + return true; + } + else if (name == PropertyName.PropertyTransform3D) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Transform3D>(this.PropertyTransform3D); + return true; + } + else if (name == PropertyName.PropertyVector4) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector4>(this.PropertyVector4); + return true; + } + else if (name == PropertyName.PropertyVector4I) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector4I>(this.PropertyVector4I); + return true; + } + else if (name == PropertyName.PropertyProjection) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Projection>(this.PropertyProjection); + return true; + } + else if (name == PropertyName.PropertyAabb) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Aabb>(this.PropertyAabb); + return true; + } + else if (name == PropertyName.PropertyColor) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Color>(this.PropertyColor); + return true; + } + else if (name == PropertyName.PropertyPlane) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Plane>(this.PropertyPlane); + return true; + } + else if (name == PropertyName.PropertyCallable) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Callable>(this.PropertyCallable); + return true; + } + else if (name == PropertyName.PropertySignal) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Signal>(this.PropertySignal); + return true; + } + else if (name == PropertyName.PropertyEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::ExportedProperties.MyEnum>(this.PropertyEnum); + return true; + } + else if (name == PropertyName.PropertyFlagsEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::ExportedProperties.MyFlagsEnum>(this.PropertyFlagsEnum); + return true; + } + else if (name == PropertyName.PropertyByteArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<byte[]>(this.PropertyByteArray); + return true; + } + else if (name == PropertyName.PropertyInt32Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int[]>(this.PropertyInt32Array); + return true; + } + else if (name == PropertyName.PropertyInt64Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<long[]>(this.PropertyInt64Array); + return true; + } + else if (name == PropertyName.PropertySingleArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<float[]>(this.PropertySingleArray); + return true; + } + else if (name == PropertyName.PropertyDoubleArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<double[]>(this.PropertyDoubleArray); + return true; + } + else if (name == PropertyName.PropertyStringArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string[]>(this.PropertyStringArray); + return true; + } + else if (name == PropertyName.PropertyStringArrayEnum) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string[]>(this.PropertyStringArrayEnum); + return true; + } + else if (name == PropertyName.PropertyVector2Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector2[]>(this.PropertyVector2Array); + return true; + } + else if (name == PropertyName.PropertyVector3Array) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Vector3[]>(this.PropertyVector3Array); + return true; + } + else if (name == PropertyName.PropertyColorArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Color[]>(this.PropertyColorArray); + return true; + } + else if (name == PropertyName.PropertyGodotObjectOrDerivedArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromSystemArrayOfGodotObject(this.PropertyGodotObjectOrDerivedArray); + return true; + } + else if (name == PropertyName.field_StringNameArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.StringName[]>(this.field_StringNameArray); + return true; + } + else if (name == PropertyName.field_NodePathArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.NodePath[]>(this.field_NodePathArray); + return true; + } + else if (name == PropertyName.field_RidArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rid[]>(this.field_RidArray); + return true; + } + else if (name == PropertyName.PropertyVariant) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Variant>(this.PropertyVariant); + return true; + } + else if (name == PropertyName.PropertyGodotObjectOrDerived) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.GodotObject>(this.PropertyGodotObjectOrDerived); + return true; + } + else if (name == PropertyName.PropertyGodotResourceTexture) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Texture>(this.PropertyGodotResourceTexture); + return true; + } + else if (name == PropertyName.PropertyStringName) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.StringName>(this.PropertyStringName); + return true; + } + else if (name == PropertyName.PropertyNodePath) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.NodePath>(this.PropertyNodePath); + return true; + } + else if (name == PropertyName.PropertyRid) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Rid>(this.PropertyRid); + return true; + } + else if (name == PropertyName.PropertyGodotDictionary) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Collections.Dictionary>(this.PropertyGodotDictionary); + return true; + } + else if (name == PropertyName.PropertyGodotArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Collections.Array>(this.PropertyGodotArray); + return true; + } + else if (name == PropertyName.PropertyGodotGenericDictionary) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromDictionary(this.PropertyGodotGenericDictionary); + return true; + } + else if (name == PropertyName.PropertyGodotGenericArray) { + value = global::Godot.NativeInterop.VariantUtils.CreateFromArray(this.PropertyGodotGenericArray); + return true; + } + else if (name == PropertyName._notGeneratePropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this._notGeneratePropertyString); + return true; + } + else if (name == PropertyName._notGeneratePropertyInt) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int>(this._notGeneratePropertyInt); + return true; + } + else if (name == PropertyName._fullPropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this._fullPropertyString); + return true; + } + else if (name == PropertyName._fullPropertyStringComplex) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this._fullPropertyStringComplex); + return true; + } + else if (name == PropertyName._lamdaPropertyString) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this._lamdaPropertyString); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName._notGeneratePropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.NotGenerateComplexLamdaProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.NotGenerateLamdaNoFieldProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.NotGenerateComplexReturnProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._notGeneratePropertyInt, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.NotGenerateReturnsProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName._fullPropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.FullPropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName._fullPropertyStringComplex, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.FullPropertyString_Complex, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName._lamdaPropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.LamdaPropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName.PropertyBoolean, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyChar, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertySByte, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyInt16, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyInt32, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyInt64, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyByte, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyUInt16, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyUInt32, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyUInt64, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)3, name: PropertyName.PropertySingle, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)3, name: PropertyName.PropertyDouble, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.PropertyString, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)5, name: PropertyName.PropertyVector2, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)6, name: PropertyName.PropertyVector2I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)7, name: PropertyName.PropertyRect2, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)8, name: PropertyName.PropertyRect2I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)11, name: PropertyName.PropertyTransform2D, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)9, name: PropertyName.PropertyVector3, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)10, name: PropertyName.PropertyVector3I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)17, name: PropertyName.PropertyBasis, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)15, name: PropertyName.PropertyQuaternion, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)18, name: PropertyName.PropertyTransform3D, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)12, name: PropertyName.PropertyVector4, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)13, name: PropertyName.PropertyVector4I, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)19, name: PropertyName.PropertyProjection, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)16, name: PropertyName.PropertyAabb, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)20, name: PropertyName.PropertyColor, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)14, name: PropertyName.PropertyPlane, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)25, name: PropertyName.PropertyCallable, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)26, name: PropertyName.PropertySignal, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyEnum, hint: (global::Godot.PropertyHint)2, hintString: "A,B,C", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName.PropertyFlagsEnum, hint: (global::Godot.PropertyHint)6, hintString: "A:0,B:1,C:2", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)29, name: PropertyName.PropertyByteArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)30, name: PropertyName.PropertyInt32Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)31, name: PropertyName.PropertyInt64Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)32, name: PropertyName.PropertySingleArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)33, name: PropertyName.PropertyDoubleArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)34, name: PropertyName.PropertyStringArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)34, name: PropertyName.PropertyStringArrayEnum, hint: (global::Godot.PropertyHint)23, hintString: "4/2:A,B,C", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)35, name: PropertyName.PropertyVector2Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)36, name: PropertyName.PropertyVector3Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)37, name: PropertyName.PropertyColorArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.PropertyGodotObjectOrDerivedArray, hint: (global::Godot.PropertyHint)23, hintString: "24/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.field_StringNameArray, hint: (global::Godot.PropertyHint)23, hintString: "21/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.field_NodePathArray, hint: (global::Godot.PropertyHint)23, hintString: "22/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.field_RidArray, hint: (global::Godot.PropertyHint)23, hintString: "23/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)0, name: PropertyName.PropertyVariant, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)135174, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)24, name: PropertyName.PropertyGodotObjectOrDerived, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)24, name: PropertyName.PropertyGodotResourceTexture, hint: (global::Godot.PropertyHint)17, hintString: "Texture", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)21, name: PropertyName.PropertyStringName, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)22, name: PropertyName.PropertyNodePath, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)23, name: PropertyName.PropertyRid, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.PropertyGodotDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.PropertyGodotArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.PropertyGodotGenericDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.PropertyGodotGenericArray, hint: (global::Godot.PropertyHint)23, hintString: "2/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptPropertyDefVal.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptPropertyDefVal.generated.cs new file mode 100644 index 0000000000..a1b01aed4f --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptPropertyDefVal.generated.cs @@ -0,0 +1,147 @@ +partial class ExportedProperties +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword +#if TOOLS + /// <summary> + /// Get the default values for all properties declared in this class. + /// This method is used by Godot to determine the value that will be + /// used by the inspector when resetting properties. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant> GetGodotPropertyDefaultValues() + { + var values = new global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>(64); + string __NotGenerateComplexLamdaProperty_default_value = default; + values.Add(PropertyName.NotGenerateComplexLamdaProperty, global::Godot.Variant.From<string>(__NotGenerateComplexLamdaProperty_default_value)); + string __NotGenerateLamdaNoFieldProperty_default_value = default; + values.Add(PropertyName.NotGenerateLamdaNoFieldProperty, global::Godot.Variant.From<string>(__NotGenerateLamdaNoFieldProperty_default_value)); + string __NotGenerateComplexReturnProperty_default_value = default; + values.Add(PropertyName.NotGenerateComplexReturnProperty, global::Godot.Variant.From<string>(__NotGenerateComplexReturnProperty_default_value)); + string __NotGenerateReturnsProperty_default_value = default; + values.Add(PropertyName.NotGenerateReturnsProperty, global::Godot.Variant.From<string>(__NotGenerateReturnsProperty_default_value)); + string __FullPropertyString_default_value = "FullPropertyString"; + values.Add(PropertyName.FullPropertyString, global::Godot.Variant.From<string>(__FullPropertyString_default_value)); + string __FullPropertyString_Complex_default_value = new string("FullPropertyString_Complex") + global::System.Convert.ToInt32("1"); + values.Add(PropertyName.FullPropertyString_Complex, global::Godot.Variant.From<string>(__FullPropertyString_Complex_default_value)); + string __LamdaPropertyString_default_value = "LamdaPropertyString"; + values.Add(PropertyName.LamdaPropertyString, global::Godot.Variant.From<string>(__LamdaPropertyString_default_value)); + bool __PropertyBoolean_default_value = true; + values.Add(PropertyName.PropertyBoolean, global::Godot.Variant.From<bool>(__PropertyBoolean_default_value)); + char __PropertyChar_default_value = 'f'; + values.Add(PropertyName.PropertyChar, global::Godot.Variant.From<char>(__PropertyChar_default_value)); + sbyte __PropertySByte_default_value = 10; + values.Add(PropertyName.PropertySByte, global::Godot.Variant.From<sbyte>(__PropertySByte_default_value)); + short __PropertyInt16_default_value = 10; + values.Add(PropertyName.PropertyInt16, global::Godot.Variant.From<short>(__PropertyInt16_default_value)); + int __PropertyInt32_default_value = 10; + values.Add(PropertyName.PropertyInt32, global::Godot.Variant.From<int>(__PropertyInt32_default_value)); + long __PropertyInt64_default_value = 10; + values.Add(PropertyName.PropertyInt64, global::Godot.Variant.From<long>(__PropertyInt64_default_value)); + byte __PropertyByte_default_value = 10; + values.Add(PropertyName.PropertyByte, global::Godot.Variant.From<byte>(__PropertyByte_default_value)); + ushort __PropertyUInt16_default_value = 10; + values.Add(PropertyName.PropertyUInt16, global::Godot.Variant.From<ushort>(__PropertyUInt16_default_value)); + uint __PropertyUInt32_default_value = 10; + values.Add(PropertyName.PropertyUInt32, global::Godot.Variant.From<uint>(__PropertyUInt32_default_value)); + ulong __PropertyUInt64_default_value = 10; + values.Add(PropertyName.PropertyUInt64, global::Godot.Variant.From<ulong>(__PropertyUInt64_default_value)); + float __PropertySingle_default_value = 10; + values.Add(PropertyName.PropertySingle, global::Godot.Variant.From<float>(__PropertySingle_default_value)); + double __PropertyDouble_default_value = 10; + values.Add(PropertyName.PropertyDouble, global::Godot.Variant.From<double>(__PropertyDouble_default_value)); + string __PropertyString_default_value = "foo"; + values.Add(PropertyName.PropertyString, global::Godot.Variant.From<string>(__PropertyString_default_value)); + global::Godot.Vector2 __PropertyVector2_default_value = new(10f, 10f); + values.Add(PropertyName.PropertyVector2, global::Godot.Variant.From<global::Godot.Vector2>(__PropertyVector2_default_value)); + global::Godot.Vector2I __PropertyVector2I_default_value = global::Godot.Vector2I.Up; + values.Add(PropertyName.PropertyVector2I, global::Godot.Variant.From<global::Godot.Vector2I>(__PropertyVector2I_default_value)); + global::Godot.Rect2 __PropertyRect2_default_value = new(new global::Godot.Vector2(10f, 10f), new global::Godot.Vector2(10f, 10f)); + values.Add(PropertyName.PropertyRect2, global::Godot.Variant.From<global::Godot.Rect2>(__PropertyRect2_default_value)); + global::Godot.Rect2I __PropertyRect2I_default_value = new(new global::Godot.Vector2I(10, 10), new global::Godot.Vector2I(10, 10)); + values.Add(PropertyName.PropertyRect2I, global::Godot.Variant.From<global::Godot.Rect2I>(__PropertyRect2I_default_value)); + global::Godot.Transform2D __PropertyTransform2D_default_value = global::Godot.Transform2D.Identity; + values.Add(PropertyName.PropertyTransform2D, global::Godot.Variant.From<global::Godot.Transform2D>(__PropertyTransform2D_default_value)); + global::Godot.Vector3 __PropertyVector3_default_value = new(10f, 10f, 10f); + values.Add(PropertyName.PropertyVector3, global::Godot.Variant.From<global::Godot.Vector3>(__PropertyVector3_default_value)); + global::Godot.Vector3I __PropertyVector3I_default_value = global::Godot.Vector3I.Back; + values.Add(PropertyName.PropertyVector3I, global::Godot.Variant.From<global::Godot.Vector3I>(__PropertyVector3I_default_value)); + global::Godot.Basis __PropertyBasis_default_value = new global::Godot.Basis(global::Godot.Quaternion.Identity); + values.Add(PropertyName.PropertyBasis, global::Godot.Variant.From<global::Godot.Basis>(__PropertyBasis_default_value)); + global::Godot.Quaternion __PropertyQuaternion_default_value = new global::Godot.Quaternion(global::Godot.Basis.Identity); + values.Add(PropertyName.PropertyQuaternion, global::Godot.Variant.From<global::Godot.Quaternion>(__PropertyQuaternion_default_value)); + global::Godot.Transform3D __PropertyTransform3D_default_value = global::Godot.Transform3D.Identity; + values.Add(PropertyName.PropertyTransform3D, global::Godot.Variant.From<global::Godot.Transform3D>(__PropertyTransform3D_default_value)); + global::Godot.Vector4 __PropertyVector4_default_value = new(10f, 10f, 10f, 10f); + values.Add(PropertyName.PropertyVector4, global::Godot.Variant.From<global::Godot.Vector4>(__PropertyVector4_default_value)); + global::Godot.Vector4I __PropertyVector4I_default_value = global::Godot.Vector4I.One; + values.Add(PropertyName.PropertyVector4I, global::Godot.Variant.From<global::Godot.Vector4I>(__PropertyVector4I_default_value)); + global::Godot.Projection __PropertyProjection_default_value = global::Godot.Projection.Identity; + values.Add(PropertyName.PropertyProjection, global::Godot.Variant.From<global::Godot.Projection>(__PropertyProjection_default_value)); + global::Godot.Aabb __PropertyAabb_default_value = new global::Godot.Aabb(10f, 10f, 10f, new global::Godot.Vector3(1f, 1f, 1f)); + values.Add(PropertyName.PropertyAabb, global::Godot.Variant.From<global::Godot.Aabb>(__PropertyAabb_default_value)); + global::Godot.Color __PropertyColor_default_value = global::Godot.Colors.Aquamarine; + values.Add(PropertyName.PropertyColor, global::Godot.Variant.From<global::Godot.Color>(__PropertyColor_default_value)); + global::Godot.Plane __PropertyPlane_default_value = global::Godot.Plane.PlaneXZ; + values.Add(PropertyName.PropertyPlane, global::Godot.Variant.From<global::Godot.Plane>(__PropertyPlane_default_value)); + global::Godot.Callable __PropertyCallable_default_value = new global::Godot.Callable(global::Godot.Engine.GetMainLoop(), "_process"); + values.Add(PropertyName.PropertyCallable, global::Godot.Variant.From<global::Godot.Callable>(__PropertyCallable_default_value)); + global::Godot.Signal __PropertySignal_default_value = new global::Godot.Signal(global::Godot.Engine.GetMainLoop(), "Propertylist_changed"); + values.Add(PropertyName.PropertySignal, global::Godot.Variant.From<global::Godot.Signal>(__PropertySignal_default_value)); + global::ExportedProperties.MyEnum __PropertyEnum_default_value = global::ExportedProperties.MyEnum.C; + values.Add(PropertyName.PropertyEnum, global::Godot.Variant.From<global::ExportedProperties.MyEnum>(__PropertyEnum_default_value)); + global::ExportedProperties.MyFlagsEnum __PropertyFlagsEnum_default_value = global::ExportedProperties.MyFlagsEnum.C; + values.Add(PropertyName.PropertyFlagsEnum, global::Godot.Variant.From<global::ExportedProperties.MyFlagsEnum>(__PropertyFlagsEnum_default_value)); + byte[] __PropertyByteArray_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName.PropertyByteArray, global::Godot.Variant.From<byte[]>(__PropertyByteArray_default_value)); + int[] __PropertyInt32Array_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName.PropertyInt32Array, global::Godot.Variant.From<int[]>(__PropertyInt32Array_default_value)); + long[] __PropertyInt64Array_default_value = { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName.PropertyInt64Array, global::Godot.Variant.From<long[]>(__PropertyInt64Array_default_value)); + float[] __PropertySingleArray_default_value = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + values.Add(PropertyName.PropertySingleArray, global::Godot.Variant.From<float[]>(__PropertySingleArray_default_value)); + double[] __PropertyDoubleArray_default_value = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + values.Add(PropertyName.PropertyDoubleArray, global::Godot.Variant.From<double[]>(__PropertyDoubleArray_default_value)); + string[] __PropertyStringArray_default_value = { "foo", "bar" }; + values.Add(PropertyName.PropertyStringArray, global::Godot.Variant.From<string[]>(__PropertyStringArray_default_value)); + string[] __PropertyStringArrayEnum_default_value = { "foo", "bar" }; + values.Add(PropertyName.PropertyStringArrayEnum, global::Godot.Variant.From<string[]>(__PropertyStringArrayEnum_default_value)); + global::Godot.Vector2[] __PropertyVector2Array_default_value = { global::Godot.Vector2.Up, global::Godot.Vector2.Down, global::Godot.Vector2.Left, global::Godot.Vector2.Right }; + values.Add(PropertyName.PropertyVector2Array, global::Godot.Variant.From<global::Godot.Vector2[]>(__PropertyVector2Array_default_value)); + global::Godot.Vector3[] __PropertyVector3Array_default_value = { global::Godot.Vector3.Up, global::Godot.Vector3.Down, global::Godot.Vector3.Left, global::Godot.Vector3.Right }; + values.Add(PropertyName.PropertyVector3Array, global::Godot.Variant.From<global::Godot.Vector3[]>(__PropertyVector3Array_default_value)); + global::Godot.Color[] __PropertyColorArray_default_value = { global::Godot.Colors.Aqua, global::Godot.Colors.Aquamarine, global::Godot.Colors.Azure, global::Godot.Colors.Beige }; + values.Add(PropertyName.PropertyColorArray, global::Godot.Variant.From<global::Godot.Color[]>(__PropertyColorArray_default_value)); + global::Godot.GodotObject[] __PropertyGodotObjectOrDerivedArray_default_value = { null }; + values.Add(PropertyName.PropertyGodotObjectOrDerivedArray, global::Godot.Variant.CreateFrom(__PropertyGodotObjectOrDerivedArray_default_value)); + global::Godot.StringName[] __field_StringNameArray_default_value = { "foo", "bar" }; + values.Add(PropertyName.field_StringNameArray, global::Godot.Variant.From<global::Godot.StringName[]>(__field_StringNameArray_default_value)); + global::Godot.NodePath[] __field_NodePathArray_default_value = { "foo", "bar" }; + values.Add(PropertyName.field_NodePathArray, global::Godot.Variant.From<global::Godot.NodePath[]>(__field_NodePathArray_default_value)); + global::Godot.Rid[] __field_RidArray_default_value = { default, default, default }; + values.Add(PropertyName.field_RidArray, global::Godot.Variant.From<global::Godot.Rid[]>(__field_RidArray_default_value)); + global::Godot.Variant __PropertyVariant_default_value = "foo"; + values.Add(PropertyName.PropertyVariant, global::Godot.Variant.From<global::Godot.Variant>(__PropertyVariant_default_value)); + global::Godot.GodotObject __PropertyGodotObjectOrDerived_default_value = default; + values.Add(PropertyName.PropertyGodotObjectOrDerived, global::Godot.Variant.From<global::Godot.GodotObject>(__PropertyGodotObjectOrDerived_default_value)); + global::Godot.Texture __PropertyGodotResourceTexture_default_value = default; + values.Add(PropertyName.PropertyGodotResourceTexture, global::Godot.Variant.From<global::Godot.Texture>(__PropertyGodotResourceTexture_default_value)); + global::Godot.StringName __PropertyStringName_default_value = new global::Godot.StringName("foo"); + values.Add(PropertyName.PropertyStringName, global::Godot.Variant.From<global::Godot.StringName>(__PropertyStringName_default_value)); + global::Godot.NodePath __PropertyNodePath_default_value = new global::Godot.NodePath("foo"); + values.Add(PropertyName.PropertyNodePath, global::Godot.Variant.From<global::Godot.NodePath>(__PropertyNodePath_default_value)); + global::Godot.Rid __PropertyRid_default_value = default; + values.Add(PropertyName.PropertyRid, global::Godot.Variant.From<global::Godot.Rid>(__PropertyRid_default_value)); + global::Godot.Collections.Dictionary __PropertyGodotDictionary_default_value = new() { { "foo", 10 }, { global::Godot.Vector2.Up, global::Godot.Colors.Chocolate } }; + values.Add(PropertyName.PropertyGodotDictionary, global::Godot.Variant.From<global::Godot.Collections.Dictionary>(__PropertyGodotDictionary_default_value)); + global::Godot.Collections.Array __PropertyGodotArray_default_value = new() { "foo", 10, global::Godot.Vector2.Up, global::Godot.Colors.Chocolate }; + values.Add(PropertyName.PropertyGodotArray, global::Godot.Variant.From<global::Godot.Collections.Array>(__PropertyGodotArray_default_value)); + global::Godot.Collections.Dictionary<string, bool> __PropertyGodotGenericDictionary_default_value = new() { { "foo", true }, { "bar", false } }; + values.Add(PropertyName.PropertyGodotGenericDictionary, global::Godot.Variant.CreateFrom(__PropertyGodotGenericDictionary_default_value)); + global::Godot.Collections.Array<int> __PropertyGodotGenericArray_default_value = new() { 0, 1, 2, 3, 4, 5, 6 }; + values.Add(PropertyName.PropertyGodotGenericArray, global::Godot.Variant.CreateFrom(__PropertyGodotGenericArray_default_value)); + return values; + } +#endif // TOOLS +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Foo_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Foo_ScriptPath.generated.cs new file mode 100644 index 0000000000..9092ad9938 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Foo_ScriptPath.generated.cs @@ -0,0 +1,5 @@ +using Godot; +[ScriptPathAttribute("res://Foo.cs")] +partial class Foo +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic(Of T)_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic(Of T)_ScriptPath.generated.cs new file mode 100644 index 0000000000..355b01f753 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Generic(Of T)_ScriptPath.generated.cs @@ -0,0 +1,5 @@ +using Godot; +[ScriptPathAttribute("res://Generic.cs")] +partial class Generic<T> +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Methods_ScriptMethods.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Methods_ScriptMethods.generated.cs new file mode 100644 index 0000000000..f757497618 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/Methods_ScriptMethods.generated.cs @@ -0,0 +1,61 @@ +using Godot; +using Godot.NativeInterop; + +partial class Methods +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the methods contained in this class, for fast lookup. + /// </summary> + public new class MethodName : global::Godot.GodotObject.MethodName { + /// <summary> + /// Cached name for the 'MethodWithOverload' method. + /// </summary> + public new static readonly global::Godot.StringName MethodWithOverload = "MethodWithOverload"; + } + /// <summary> + /// Get the method information for all the methods declared in this class. + /// This method is used by Godot to register the available methods in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo> GetGodotMethodList() + { + var methods = new global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>(3); + methods.Add(new(name: MethodName.MethodWithOverload, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: null, defaultArguments: null)); + methods.Add(new(name: MethodName.MethodWithOverload, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)2, name: "a", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + methods.Add(new(name: MethodName.MethodWithOverload, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)2, name: "a", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), new(type: (global::Godot.Variant.Type)2, name: "b", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return methods; + } +#pragma warning restore CS0109 + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool InvokeGodotClassMethod(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret) + { + if (method == MethodName.MethodWithOverload && args.Count == 0) { + MethodWithOverload(); + ret = default; + return true; + } + if (method == MethodName.MethodWithOverload && args.Count == 1) { + MethodWithOverload(global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(args[0])); + ret = default; + return true; + } + if (method == MethodName.MethodWithOverload && args.Count == 2) { + MethodWithOverload(global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(args[0]), global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(args[1])); + ret = default; + return true; + } + return base.InvokeGodotClassMethod(method, args, out ret); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassMethod(in godot_string_name method) + { + if (method == MethodName.MethodWithOverload) { + return true; + } + return base.HasGodotClassMethod(method); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/MixedReadOnlyWriteOnly_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/MixedReadOnlyWriteOnly_ScriptProperties.generated.cs new file mode 100644 index 0000000000..91f808f55e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/MixedReadOnlyWriteOnly_ScriptProperties.generated.cs @@ -0,0 +1,94 @@ +using Godot; +using Godot.NativeInterop; + +partial class MixedReadOnlyWriteOnly +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.GodotObject.PropertyName { + /// <summary> + /// Cached name for the 'ReadOnlyAutoProperty' property. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyAutoProperty = "ReadOnlyAutoProperty"; + /// <summary> + /// Cached name for the 'ReadOnlyProperty' property. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyProperty = "ReadOnlyProperty"; + /// <summary> + /// Cached name for the 'InitOnlyAutoProperty' property. + /// </summary> + public new static readonly global::Godot.StringName InitOnlyAutoProperty = "InitOnlyAutoProperty"; + /// <summary> + /// Cached name for the 'WriteOnlyProperty' property. + /// </summary> + public new static readonly global::Godot.StringName WriteOnlyProperty = "WriteOnlyProperty"; + /// <summary> + /// Cached name for the 'ReadOnlyField' field. + /// </summary> + public new static readonly global::Godot.StringName ReadOnlyField = "ReadOnlyField"; + /// <summary> + /// Cached name for the '_writeOnlyBackingField' field. + /// </summary> + public new static readonly global::Godot.StringName _writeOnlyBackingField = "_writeOnlyBackingField"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName.WriteOnlyProperty) { + this.WriteOnlyProperty = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + else if (name == PropertyName._writeOnlyBackingField) { + this._writeOnlyBackingField = global::Godot.NativeInterop.VariantUtils.ConvertTo<bool>(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName.ReadOnlyAutoProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyAutoProperty); + return true; + } + else if (name == PropertyName.ReadOnlyProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyProperty); + return true; + } + else if (name == PropertyName.InitOnlyAutoProperty) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.InitOnlyAutoProperty); + return true; + } + else if (name == PropertyName.ReadOnlyField) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<string>(this.ReadOnlyField); + return true; + } + else if (name == PropertyName._writeOnlyBackingField) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<bool>(this._writeOnlyBackingField); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyField, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyAutoProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.ReadOnlyProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)4, name: PropertyName.InitOnlyAutoProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName._writeOnlyBackingField, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)1, name: PropertyName.WriteOnlyProperty, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/NamespaceA.SameName_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/NamespaceA.SameName_ScriptPath.generated.cs new file mode 100644 index 0000000000..cad9f2a46b --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/NamespaceA.SameName_ScriptPath.generated.cs @@ -0,0 +1,9 @@ +using Godot; +namespace NamespaceA { + +[ScriptPathAttribute("res://SameName.cs")] +partial class SameName +{ +} + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptMethods.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptMethods.generated.cs new file mode 100644 index 0000000000..328107a237 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptMethods.generated.cs @@ -0,0 +1,52 @@ +using Godot; +using Godot.NativeInterop; + +partial struct OuterClass +{ +partial class NestedClass +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the methods contained in this class, for fast lookup. + /// </summary> + public new class MethodName : global::Godot.RefCounted.MethodName { + /// <summary> + /// Cached name for the '_Get' method. + /// </summary> + public new static readonly global::Godot.StringName _Get = "_Get"; + } + /// <summary> + /// Get the method information for all the methods declared in this class. + /// This method is used by Godot to register the available methods in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo> GetGodotMethodList() + { + var methods = new global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>(1); + methods.Add(new(name: MethodName._Get, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)131078, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)21, name: "property", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return methods; + } +#pragma warning restore CS0109 + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool InvokeGodotClassMethod(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret) + { + if (method == MethodName._Get && args.Count == 1) { + var callRet = _Get(global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName>(args[0])); + ret = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.Variant>(callRet); + return true; + } + return base.InvokeGodotClassMethod(method, args, out ret); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassMethod(in godot_string_name method) + { + if (method == MethodName._Get) { + return true; + } + return base.HasGodotClassMethod(method); + } +} +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptProperties.generated.cs new file mode 100644 index 0000000000..79f014a41e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptProperties.generated.cs @@ -0,0 +1,15 @@ +using Godot; +using Godot.NativeInterop; + +partial struct OuterClass +{ +partial class NestedClass +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.RefCounted.PropertyName { + } +} +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptSerialization.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptSerialization.generated.cs new file mode 100644 index 0000000000..4ecce50178 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/OuterClass.NestedClass_ScriptSerialization.generated.cs @@ -0,0 +1,21 @@ +using Godot; +using Godot.NativeInterop; + +partial struct OuterClass +{ +partial class NestedClass +{ + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info) + { + base.SaveGodotObjectData(info); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info) + { + base.RestoreGodotObjectData(info); + } +} +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptMethods.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptMethods.generated.cs new file mode 100644 index 0000000000..8656f4617e --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptMethods.generated.cs @@ -0,0 +1,62 @@ +using Godot; +using Godot.NativeInterop; + +partial class ScriptBoilerplate +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the methods contained in this class, for fast lookup. + /// </summary> + public new class MethodName : global::Godot.Node.MethodName { + /// <summary> + /// Cached name for the '_Process' method. + /// </summary> + public new static readonly global::Godot.StringName _Process = "_Process"; + /// <summary> + /// Cached name for the 'Bazz' method. + /// </summary> + public new static readonly global::Godot.StringName Bazz = "Bazz"; + } + /// <summary> + /// Get the method information for all the methods declared in this class. + /// This method is used by Godot to register the available methods in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo> GetGodotMethodList() + { + var methods = new global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>(2); + methods.Add(new(name: MethodName._Process, returnVal: new(type: (global::Godot.Variant.Type)0, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)3, name: "delta", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + methods.Add(new(name: MethodName.Bazz, returnVal: new(type: (global::Godot.Variant.Type)2, name: "", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), flags: (global::Godot.MethodFlags)1, arguments: new() { new(type: (global::Godot.Variant.Type)21, name: "name", hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)6, exported: false), }, defaultArguments: null)); + return methods; + } +#pragma warning restore CS0109 + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool InvokeGodotClassMethod(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret) + { + if (method == MethodName._Process && args.Count == 1) { + _Process(global::Godot.NativeInterop.VariantUtils.ConvertTo<double>(args[0])); + ret = default; + return true; + } + if (method == MethodName.Bazz && args.Count == 1) { + var callRet = Bazz(global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.StringName>(args[0])); + ret = global::Godot.NativeInterop.VariantUtils.CreateFrom<int>(callRet); + return true; + } + return base.InvokeGodotClassMethod(method, args, out ret); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool HasGodotClassMethod(in godot_string_name method) + { + if (method == MethodName._Process) { + return true; + } + else if (method == MethodName.Bazz) { + return true; + } + return base.HasGodotClassMethod(method); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptPath.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptPath.generated.cs new file mode 100644 index 0000000000..ffcd29f7cd --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptPath.generated.cs @@ -0,0 +1,5 @@ +using Godot; +[ScriptPathAttribute("res://ScriptBoilerplate.cs")] +partial class ScriptBoilerplate +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptProperties.generated.cs new file mode 100644 index 0000000000..09368b7ab6 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptProperties.generated.cs @@ -0,0 +1,62 @@ +using Godot; +using Godot.NativeInterop; + +partial class ScriptBoilerplate +{ +#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword + /// <summary> + /// Cached StringNames for the properties and fields contained in this class, for fast lookup. + /// </summary> + public new class PropertyName : global::Godot.Node.PropertyName { + /// <summary> + /// Cached name for the '_nodePath' field. + /// </summary> + public new static readonly global::Godot.StringName _nodePath = "_nodePath"; + /// <summary> + /// Cached name for the '_velocity' field. + /// </summary> + public new static readonly global::Godot.StringName _velocity = "_velocity"; + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool SetGodotClassPropertyValue(in godot_string_name name, in godot_variant value) + { + if (name == PropertyName._nodePath) { + this._nodePath = global::Godot.NativeInterop.VariantUtils.ConvertTo<global::Godot.NodePath>(value); + return true; + } + else if (name == PropertyName._velocity) { + this._velocity = global::Godot.NativeInterop.VariantUtils.ConvertTo<int>(value); + return true; + } + return base.SetGodotClassPropertyValue(name, value); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override bool GetGodotClassPropertyValue(in godot_string_name name, out godot_variant value) + { + if (name == PropertyName._nodePath) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<global::Godot.NodePath>(this._nodePath); + return true; + } + else if (name == PropertyName._velocity) { + value = global::Godot.NativeInterop.VariantUtils.CreateFrom<int>(this._velocity); + return true; + } + return base.GetGodotClassPropertyValue(name, out value); + } + /// <summary> + /// Get the property information for all the properties declared in this class. + /// This method is used by Godot to register the available properties in the editor. + /// Do not call this method. + /// </summary> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal new static global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo> GetGodotPropertyList() + { + var properties = new global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>(); + properties.Add(new(type: (global::Godot.Variant.Type)22, name: PropertyName._nodePath, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + properties.Add(new(type: (global::Godot.Variant.Type)2, name: PropertyName._velocity, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4096, exported: false)); + return properties; + } +#pragma warning restore CS0109 +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptSerialization.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptSerialization.generated.cs new file mode 100644 index 0000000000..28bc863b0a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ScriptBoilerplate_ScriptSerialization.generated.cs @@ -0,0 +1,24 @@ +using Godot; +using Godot.NativeInterop; + +partial class ScriptBoilerplate +{ + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info) + { + base.SaveGodotObjectData(info); + info.AddProperty(PropertyName._nodePath, global::Godot.Variant.From<global::Godot.NodePath>(this._nodePath)); + info.AddProperty(PropertyName._velocity, global::Godot.Variant.From<int>(this._velocity)); + } + /// <inheritdoc/> + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info) + { + base.RestoreGodotObjectData(info); + if (info.TryGetProperty(PropertyName._nodePath, out var _value__nodePath)) + this._nodePath = _value__nodePath.As<global::Godot.NodePath>(); + if (info.TryGetProperty(PropertyName._velocity, out var _value__velocity)) + this._velocity = _value__velocity.As<int>(); + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllReadOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllReadOnly.cs new file mode 100644 index 0000000000..2586db1137 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllReadOnly.cs @@ -0,0 +1,9 @@ +using Godot; + +public partial class AllReadOnly : GodotObject +{ + public readonly string ReadOnlyField = "foo"; + public string ReadOnlyAutoProperty { get; } = "foo"; + public string ReadOnlyProperty { get => "foo"; } + public string InitOnlyAutoProperty { get; init; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllWriteOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllWriteOnly.cs new file mode 100644 index 0000000000..e2ebff4876 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/AllWriteOnly.cs @@ -0,0 +1,7 @@ +using Godot; + +public partial class AllWriteOnly : GodotObject +{ + private bool _writeOnlyBackingField = false; + public bool WriteOnlyProperty { set => _writeOnlyBackingField = value; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Bar.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Bar.cs new file mode 100644 index 0000000000..d364e40bf0 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Bar.cs @@ -0,0 +1,14 @@ +using Godot; + +public partial class Bar : GodotObject +{ +} + +// Foo in another file +public partial class Foo +{ +} + +public partial class NotSameNameAsFile : GodotObject +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ClassPartialModifier.GD0001.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ClassPartialModifier.GD0001.cs new file mode 100644 index 0000000000..c8ff673b76 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ClassPartialModifier.GD0001.cs @@ -0,0 +1,6 @@ +using Godot; + +public class {|GD0001:ClassPartialModifier|} : Node +{ + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/EventSignals.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/EventSignals.cs new file mode 100644 index 0000000000..51dc359157 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/EventSignals.cs @@ -0,0 +1,18 @@ +using Godot; + +public partial class EventSignals : GodotObject +{ + [Signal] + public delegate void MySignalEventHandler(string str, int num); + + private struct MyStruct { } + + [Signal] + private delegate void {|GD0201:MyInvalidSignal|}(); + + [Signal] + private delegate void MyInvalidParameterTypeSignalEventHandler(MyStruct {|GD0202:myStruct|}); + + [Signal] + private delegate MyStruct {|GD0203:MyInvalidReturnTypeSignalEventHandler|}(); +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0101.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0101.cs new file mode 100644 index 0000000000..6a6682f493 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0101.cs @@ -0,0 +1,10 @@ +using Godot; + +public partial class ExportDiagnostics_GD0101 : Node +{ + [Export] + public static string {|GD0101:StaticField|}; + + [Export] + public static int {|GD0101:StaticProperty|} { get; set; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0102.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0102.cs new file mode 100644 index 0000000000..c2e4a60811 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0102.cs @@ -0,0 +1,12 @@ +using Godot; + +public partial class ExportDiagnostics_GD0102 : Node +{ + public struct MyStruct { } + + [Export] + public MyStruct {|GD0102:StructField|}; + + [Export] + public MyStruct {|GD0102:StructProperty|} { get; set; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0103.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0103.cs new file mode 100644 index 0000000000..c4b3d3aaac --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0103.cs @@ -0,0 +1,10 @@ +using Godot; + +public partial class ExportDiagnostics_GD0103 : Node +{ + [Export] + public readonly string {|GD0103:ReadOnlyField|}; + + [Export] + public string {|GD0103:ReadOnlyProperty|} { get; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0104.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0104.cs new file mode 100644 index 0000000000..d7d80b6a14 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0104.cs @@ -0,0 +1,7 @@ +using Godot; + +public partial class ExportDiagnostics_GD0104 : Node +{ + [Export] + public string {|GD0104:WriteOnlyProperty|} { set { } } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0105.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0105.cs new file mode 100644 index 0000000000..f6b13e00fa --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0105.cs @@ -0,0 +1,12 @@ +using System; +using Godot; + +public partial class ExportDiagnostics_GD0105 : Node +{ + [Export] + public int {|GD0105:this|}[int index] + { + get { return index; } + set { } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0106.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0106.cs new file mode 100644 index 0000000000..e2e5c68c21 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0106.cs @@ -0,0 +1,18 @@ +using Godot; + +public interface MyInterface +{ + public int MyProperty { get; set; } +} + +public partial class ExportDiagnostics_GD0106_OK : Node, MyInterface +{ + [Export] + public int MyProperty { get; set; } +} + +public partial class ExportDiagnostics_GD0106_KO : Node, MyInterface +{ + [Export] + int MyInterface.{|GD0106:MyProperty|} { get; set; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0107.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0107.cs new file mode 100644 index 0000000000..067783ea66 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportDiagnostics_GD0107.cs @@ -0,0 +1,19 @@ +using Godot; + +public partial class ExportDiagnostics_GD0107_OK : Node +{ + [Export] + public Node NodeField; + + [Export] + public Node NodeProperty { get; set; } +} + +public partial class ExportDiagnostics_GD0107_KO : Resource +{ + [Export] + public Node {|GD0107:NodeField|}; + + [Export] + public Node {|GD0107:NodeProperty|} { get; set; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedFields.cs new file mode 100644 index 0000000000..0938d10afe --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedFields.cs @@ -0,0 +1,102 @@ +using Godot; +using System; +using System.Collections.Generic; + +public partial class ExportedFields : GodotObject +{ + [Export] private Boolean _fieldBoolean = true; + [Export] private Char _fieldChar = 'f'; + [Export] private SByte _fieldSByte = 10; + [Export] private Int16 _fieldInt16 = 10; + [Export] private Int32 _fieldInt32 = 10; + [Export] private Int64 _fieldInt64 = 10; + [Export] private Byte _fieldByte = 10; + [Export] private UInt16 _fieldUInt16 = 10; + [Export] private UInt32 _fieldUInt32 = 10; + [Export] private UInt64 _fieldUInt64 = 10; + [Export] private Single _fieldSingle = 10; + [Export] private Double _fieldDouble = 10; + [Export] private String _fieldString = "foo"; + + // Godot structs + [Export] private Vector2 _fieldVector2 = new(10f, 10f); + [Export] private Vector2I _fieldVector2I = Vector2I.Up; + [Export] private Rect2 _fieldRect2 = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2I _fieldRect2I = new(new Vector2I(10, 10), new Vector2I(10, 10)); + [Export] private Transform2D _fieldTransform2D = Transform2D.Identity; + [Export] private Vector3 _fieldVector3 = new(10f, 10f, 10f); + [Export] private Vector3I _fieldVector3I = Vector3I.Back; + [Export] private Basis _fieldBasis = new Basis(Quaternion.Identity); + [Export] private Quaternion _fieldQuaternion = new Quaternion(Basis.Identity); + [Export] private Transform3D _fieldTransform3D = Transform3D.Identity; + [Export] private Vector4 _fieldVector4 = new(10f, 10f, 10f, 10f); + [Export] private Vector4I _fieldVector4I = Vector4I.One; + [Export] private Projection _fieldProjection = Projection.Identity; + [Export] private Aabb _fieldAabb = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color _fieldColor = Colors.Aquamarine; + [Export] private Plane _fieldPlane = Plane.PlaneXZ; + [Export] private Callable _fieldCallable = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private Signal _fieldSignal = new Signal(Engine.GetMainLoop(), "property_list_changed"); + + // Enums + public enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum _fieldEnum = MyEnum.C; + + [Flags] + public enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum _fieldFlagsEnum = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] _fieldByteArray = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] _fieldInt32Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] _fieldInt64Array = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] _fieldSingleArray = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] _fieldDoubleArray = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] _fieldStringArray = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] _fieldStringArrayEnum = { "foo", "bar" }; + [Export] private Vector2[] _fieldVector2Array = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] _fieldVector3Array = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] _fieldColorArray = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private GodotObject[] _fieldGodotObjectOrDerivedArray = { null }; + [Export] private StringName[] _fieldStringNameArray = { "foo", "bar" }; + [Export] private NodePath[] _fieldNodePathArray = { "foo", "bar" }; + [Export] private Rid[] _fieldRidArray = { default, default, default }; + // Note we use Array and not System.Array. This tests the generated namespace qualification. + [Export] private Int32[] _fieldEmptyInt32Array = Array.Empty<Int32>(); + // Note we use List and not System.Collections.Generic. + [Export] private int[] _fieldArrayFromList = new List<int>(Array.Empty<int>()).ToArray(); + + // Variant + [Export] private Variant _fieldVariant = "foo"; + + // Classes + [Export] private GodotObject _fieldGodotObjectOrDerived; + [Export] private Godot.Texture _fieldGodotResourceTexture; + [Export] private StringName _fieldStringName = new StringName("foo"); + [Export] private NodePath _fieldNodePath = new NodePath("foo"); + [Export] private Rid _fieldRid; + + [Export] + private Godot.Collections.Dictionary _fieldGodotDictionary = new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] + private Godot.Collections.Array _fieldGodotArray = new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] + private Godot.Collections.Dictionary<string, bool> _fieldGodotGenericDictionary = new() { { "foo", true }, { "bar", false } }; + + [Export] + private Godot.Collections.Array<int> _fieldGodotGenericArray = new() { 0, 1, 2, 3, 4, 5, 6 }; +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedProperties.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedProperties.cs new file mode 100644 index 0000000000..9ae23066fc --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ExportedProperties.cs @@ -0,0 +1,186 @@ +using Godot; +using System; + +public partial class ExportedProperties : GodotObject +{ + // Do not generate default value + private String _notGeneratePropertyString = new string("not generate"); + [Export] + public String NotGenerateComplexLamdaProperty + { + get => _notGeneratePropertyString + Convert.ToInt32("1"); + set => _notGeneratePropertyString = value; + } + + [Export] + public String NotGenerateLamdaNoFieldProperty + { + get => new string("not generate"); + set => _notGeneratePropertyString = value; + } + + [Export] + public String NotGenerateComplexReturnProperty + { + get + { + return _notGeneratePropertyString + Convert.ToInt32("1"); + } + set + { + _notGeneratePropertyString = value; + } + } + + private int _notGeneratePropertyInt = 1; + [Export] + public string NotGenerateReturnsProperty + { + get + { + if (_notGeneratePropertyInt == 1) + { + return "a"; + } + else + { + return "b"; + } + } + set + { + _notGeneratePropertyInt = value == "a" ? 1 : 2; + } + } + + // Full Property + private String _fullPropertyString = "FullPropertyString"; + [Export] + public String FullPropertyString + { + get + { + return _fullPropertyString; + } + set + { + _fullPropertyString = value; + } + } + + private String _fullPropertyStringComplex = new string("FullPropertyString_Complex") + Convert.ToInt32("1"); + [Export] + public String FullPropertyString_Complex + { + get + { + return _fullPropertyStringComplex; + } + set + { + _fullPropertyStringComplex = value; + } + } + + // Lambda Property + private String _lamdaPropertyString = "LamdaPropertyString"; + [Export] + public String LamdaPropertyString + { + get => _lamdaPropertyString; + set => _lamdaPropertyString = value; + } + + // Auto Property + [Export] private Boolean PropertyBoolean { get; set; } = true; + [Export] private Char PropertyChar { get; set; } = 'f'; + [Export] private SByte PropertySByte { get; set; } = 10; + [Export] private Int16 PropertyInt16 { get; set; } = 10; + [Export] private Int32 PropertyInt32 { get; set; } = 10; + [Export] private Int64 PropertyInt64 { get; set; } = 10; + [Export] private Byte PropertyByte { get; set; } = 10; + [Export] private UInt16 PropertyUInt16 { get; set; } = 10; + [Export] private UInt32 PropertyUInt32 { get; set; } = 10; + [Export] private UInt64 PropertyUInt64 { get; set; } = 10; + [Export] private Single PropertySingle { get; set; } = 10; + [Export] private Double PropertyDouble { get; set; } = 10; + [Export] private String PropertyString { get; set; } = "foo"; + + // Godot structs + [Export] private Vector2 PropertyVector2 { get; set; } = new(10f, 10f); + [Export] private Vector2I PropertyVector2I { get; set; } = Vector2I.Up; + [Export] private Rect2 PropertyRect2 { get; set; } = new(new Vector2(10f, 10f), new Vector2(10f, 10f)); + [Export] private Rect2I PropertyRect2I { get; set; } = new(new Vector2I(10, 10), new Vector2I(10, 10)); + [Export] private Transform2D PropertyTransform2D { get; set; } = Transform2D.Identity; + [Export] private Vector3 PropertyVector3 { get; set; } = new(10f, 10f, 10f); + [Export] private Vector3I PropertyVector3I { get; set; } = Vector3I.Back; + [Export] private Basis PropertyBasis { get; set; } = new Basis(Quaternion.Identity); + [Export] private Quaternion PropertyQuaternion { get; set; } = new Quaternion(Basis.Identity); + [Export] private Transform3D PropertyTransform3D { get; set; } = Transform3D.Identity; + [Export] private Vector4 PropertyVector4 { get; set; } = new(10f, 10f, 10f, 10f); + [Export] private Vector4I PropertyVector4I { get; set; } = Vector4I.One; + [Export] private Projection PropertyProjection { get; set; } = Projection.Identity; + [Export] private Aabb PropertyAabb { get; set; } = new Aabb(10f, 10f, 10f, new Vector3(1f, 1f, 1f)); + [Export] private Color PropertyColor { get; set; } = Colors.Aquamarine; + [Export] private Plane PropertyPlane { get; set; } = Plane.PlaneXZ; + [Export] private Callable PropertyCallable { get; set; } = new Callable(Engine.GetMainLoop(), "_process"); + [Export] private Signal PropertySignal { get; set; } = new Signal(Engine.GetMainLoop(), "Propertylist_changed"); + + // Enums + public enum MyEnum + { + A, + B, + C + } + + [Export] private MyEnum PropertyEnum { get; set; } = MyEnum.C; + + [Flags] + public enum MyFlagsEnum + { + A, + B, + C + } + + [Export] private MyFlagsEnum PropertyFlagsEnum { get; set; } = MyFlagsEnum.C; + + // Arrays + [Export] private Byte[] PropertyByteArray { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int32[] PropertyInt32Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Int64[] PropertyInt64Array { get; set; } = { 0, 1, 2, 3, 4, 5, 6 }; + [Export] private Single[] PropertySingleArray { get; set; } = { 0f, 1f, 2f, 3f, 4f, 5f, 6f }; + [Export] private Double[] PropertyDoubleArray { get; set; } = { 0d, 1d, 2d, 3d, 4d, 5d, 6d }; + [Export] private String[] PropertyStringArray { get; set; } = { "foo", "bar" }; + [Export(PropertyHint.Enum, "A,B,C")] private String[] PropertyStringArrayEnum { get; set; } = { "foo", "bar" }; + [Export] private Vector2[] PropertyVector2Array { get; set; } = { Vector2.Up, Vector2.Down, Vector2.Left, Vector2.Right }; + [Export] private Vector3[] PropertyVector3Array { get; set; } = { Vector3.Up, Vector3.Down, Vector3.Left, Vector3.Right }; + [Export] private Color[] PropertyColorArray { get; set; } = { Colors.Aqua, Colors.Aquamarine, Colors.Azure, Colors.Beige }; + [Export] private GodotObject[] PropertyGodotObjectOrDerivedArray { get; set; } = { null }; + [Export] private StringName[] field_StringNameArray { get; set; } = { "foo", "bar" }; + [Export] private NodePath[] field_NodePathArray { get; set; } = { "foo", "bar" }; + [Export] private Rid[] field_RidArray { get; set; } = { default, default, default }; + + // Variant + [Export] private Variant PropertyVariant { get; set; } = "foo"; + + // Classes + [Export] private GodotObject PropertyGodotObjectOrDerived { get; set; } + [Export] private Godot.Texture PropertyGodotResourceTexture { get; set; } + [Export] private StringName PropertyStringName { get; set; } = new StringName("foo"); + [Export] private NodePath PropertyNodePath { get; set; } = new NodePath("foo"); + [Export] private Rid PropertyRid { get; set; } + + [Export] + private Godot.Collections.Dictionary PropertyGodotDictionary { get; set; } = new() { { "foo", 10 }, { Vector2.Up, Colors.Chocolate } }; + + [Export] + private Godot.Collections.Array PropertyGodotArray { get; set; } = new() { "foo", 10, Vector2.Up, Colors.Chocolate }; + + [Export] + private Godot.Collections.Dictionary<string, bool> PropertyGodotGenericDictionary { get; set; } = new() { { "foo", true }, { "bar", false } }; + + [Export] + private Godot.Collections.Array<int> PropertyGodotGenericArray { get; set; } = new() { 0, 1, 2, 3, 4, 5, 6 }; +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Foo.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Foo.cs new file mode 100644 index 0000000000..53801990d3 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Foo.cs @@ -0,0 +1,10 @@ +using Godot; + +public partial class Foo : GodotObject +{ +} + +// Foo again in the same file +public partial class Foo +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.GD0003.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.GD0003.cs new file mode 100644 index 0000000000..83e9094a25 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.GD0003.cs @@ -0,0 +1,18 @@ +using Godot; + +public partial class Generic<T> : GodotObject +{ + private int _field; +} + +// Generic again but different generic parameters +public partial class {|GD0003:Generic|}<T, R> : GodotObject +{ + private int _field; +} + +// Generic again but without generic parameters +public partial class {|GD0003:Generic|} : GodotObject +{ + private int _field; +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs new file mode 100644 index 0000000000..ce8a7fe218 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Generic.cs @@ -0,0 +1,6 @@ +using Godot; + +public partial class Generic<T> : GodotObject +{ + private int _field; +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs new file mode 100644 index 0000000000..1908703a71 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs @@ -0,0 +1,22 @@ +using Godot; + +// This works because it inherits from GodotObject. +[GlobalClass] +public partial class CustomGlobalClass1 : GodotObject +{ + +} + +// This works because it inherits from an object that inherits from GodotObject +[GlobalClass] +public partial class CustomGlobalClass2 : Node +{ + +} + +// This raises a GD0401 diagnostic error: global classes must inherit from GodotObject +[GlobalClass] +public partial class {|GD0401:CustomGlobalClass3|} +{ + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs new file mode 100644 index 0000000000..4f7885cf37 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs @@ -0,0 +1,15 @@ +using Godot; + +// This works because it inherits from GodotObject and it doesn't have any generic type parameter. +[GlobalClass] +public partial class CustomGlobalClass : GodotObject +{ + +} + +// This raises a GD0402 diagnostic error: global classes can't have any generic type parameter +[GlobalClass] +public partial class {|GD0402:CustomGlobalClass|}<T> : GodotObject +{ + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Methods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Methods.cs new file mode 100644 index 0000000000..1da9db8204 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/Methods.cs @@ -0,0 +1,26 @@ +using Godot; + +public partial class Methods : GodotObject +{ + private void MethodWithOverload() + { + } + + private void MethodWithOverload(int a) + { + } + + private void MethodWithOverload(int a, int b) + { + } + + // Should be ignored. The previous one is picked. + private void MethodWithOverload(float a, float b) + { + } + + // Generic methods should be ignored. + private void GenericMethod<T>(T t) + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MixedReadOnlyWriteOnly.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MixedReadOnlyWriteOnly.cs new file mode 100644 index 0000000000..190a3fb256 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MixedReadOnlyWriteOnly.cs @@ -0,0 +1,12 @@ +using Godot; + +public partial class MixedReadOnlyWriteOnly : GodotObject +{ + public readonly string ReadOnlyField = "foo"; + public string ReadOnlyAutoProperty { get; } = "foo"; + public string ReadOnlyProperty { get => "foo"; } + public string InitOnlyAutoProperty { get; init; } + + bool _writeOnlyBackingField = false; + public bool WriteOnlyProperty { set => _writeOnlyBackingField = value; } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MoreExportedFields.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MoreExportedFields.cs new file mode 100644 index 0000000000..0cf462f799 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MoreExportedFields.cs @@ -0,0 +1,8 @@ +using Godot; +using System; + +public partial class ExportedFields : GodotObject +{ + // Note we use Array and not System.Array. This tests the generated namespace qualification. + [Export] private Int64[] _fieldEmptyInt64Array = Array.Empty<Int64>(); +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs new file mode 100644 index 0000000000..462da31d66 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs @@ -0,0 +1,653 @@ +using System; +using Godot; +using Godot.Collections; +using Array = Godot.Collections.Array; + +public class MustBeVariantGD0301 +{ + public void MethodCallsError() + { + // This raises a GD0301 diagnostic error: object is not Variant (and Method<T> requires a variant generic type). + Method<{|GD0301:object|}>(); + } + + public void MethodCallsOk() + { + // All these calls are valid because they are Variant types. + Method<bool>(); + Method<char>(); + Method<sbyte>(); + Method<byte>(); + Method<short>(); + Method<ushort>(); + Method<int>(); + Method<uint>(); + Method<long>(); + Method<ulong>(); + Method<float>(); + Method<double>(); + Method<string>(); + Method<Vector2>(); + Method<Vector2I>(); + Method<Rect2>(); + Method<Rect2I>(); + Method<Transform2D>(); + Method<Vector3>(); + Method<Vector3I>(); + Method<Vector4>(); + Method<Vector4I>(); + Method<Basis>(); + Method<Quaternion>(); + Method<Transform3D>(); + Method<Projection>(); + Method<Aabb>(); + Method<Color>(); + Method<Plane>(); + Method<Callable>(); + Method<Signal>(); + Method<GodotObject>(); + Method<StringName>(); + Method<NodePath>(); + Method<Rid>(); + Method<Dictionary>(); + Method<Array>(); + Method<byte[]>(); + Method<int[]>(); + Method<long[]>(); + Method<float[]>(); + Method<double[]>(); + Method<string[]>(); + Method<Vector2[]>(); + Method<Vector3[]>(); + Method<Color[]>(); + Method<GodotObject[]>(); + Method<StringName[]>(); + Method<NodePath[]>(); + Method<Rid[]>(); + } + + public void Method<[MustBeVariant] T>() + { + } + + public void MustBeVariantClasses() + { + new ClassWithGenericVariant<bool>(); + new ClassWithGenericVariant<char>(); + new ClassWithGenericVariant<sbyte>(); + new ClassWithGenericVariant<byte>(); + new ClassWithGenericVariant<short>(); + new ClassWithGenericVariant<ushort>(); + new ClassWithGenericVariant<int>(); + new ClassWithGenericVariant<uint>(); + new ClassWithGenericVariant<long>(); + new ClassWithGenericVariant<ulong>(); + new ClassWithGenericVariant<float>(); + new ClassWithGenericVariant<double>(); + new ClassWithGenericVariant<string>(); + new ClassWithGenericVariant<Vector2>(); + new ClassWithGenericVariant<Vector2I>(); + new ClassWithGenericVariant<Rect2>(); + new ClassWithGenericVariant<Rect2I>(); + new ClassWithGenericVariant<Transform2D>(); + new ClassWithGenericVariant<Vector3>(); + new ClassWithGenericVariant<Vector3I>(); + new ClassWithGenericVariant<Vector4>(); + new ClassWithGenericVariant<Vector4I>(); + new ClassWithGenericVariant<Basis>(); + new ClassWithGenericVariant<Quaternion>(); + new ClassWithGenericVariant<Transform3D>(); + new ClassWithGenericVariant<Projection>(); + new ClassWithGenericVariant<Aabb>(); + new ClassWithGenericVariant<Color>(); + new ClassWithGenericVariant<Plane>(); + new ClassWithGenericVariant<Callable>(); + new ClassWithGenericVariant<Signal>(); + new ClassWithGenericVariant<GodotObject>(); + new ClassWithGenericVariant<StringName>(); + new ClassWithGenericVariant<NodePath>(); + new ClassWithGenericVariant<Rid>(); + new ClassWithGenericVariant<Dictionary>(); + new ClassWithGenericVariant<Array>(); + new ClassWithGenericVariant<byte[]>(); + new ClassWithGenericVariant<int[]>(); + new ClassWithGenericVariant<long[]>(); + new ClassWithGenericVariant<float[]>(); + new ClassWithGenericVariant<double[]>(); + new ClassWithGenericVariant<string[]>(); + new ClassWithGenericVariant<Vector2[]>(); + new ClassWithGenericVariant<Vector3[]>(); + new ClassWithGenericVariant<Color[]>(); + new ClassWithGenericVariant<GodotObject[]>(); + new ClassWithGenericVariant<StringName[]>(); + new ClassWithGenericVariant<NodePath[]>(); + new ClassWithGenericVariant<Rid[]>(); + + // This class fails because generic type is not Variant-compatible. + new ClassWithGenericVariant<{|GD0301:object|}>(); + } +} + +public class ClassWithGenericVariant<[MustBeVariant] T> +{ +} + +public class MustBeVariantAnnotatedMethods +{ + [GenericTypeAttribute<bool>()] + public void MethodWithAttributeBool() + { + } + + [GenericTypeAttribute<char>()] + public void MethodWithAttributeChar() + { + } + + [GenericTypeAttribute<sbyte>()] + public void MethodWithAttributeSByte() + { + } + + [GenericTypeAttribute<byte>()] + public void MethodWithAttributeByte() + { + } + + [GenericTypeAttribute<short>()] + public void MethodWithAttributeInt16() + { + } + + [GenericTypeAttribute<ushort>()] + public void MethodWithAttributeUInt16() + { + } + + [GenericTypeAttribute<int>()] + public void MethodWithAttributeInt32() + { + } + + [GenericTypeAttribute<uint>()] + public void MethodWithAttributeUInt32() + { + } + + [GenericTypeAttribute<long>()] + public void MethodWithAttributeInt64() + { + } + + [GenericTypeAttribute<ulong>()] + public void MethodWithAttributeUInt64() + { + } + + [GenericTypeAttribute<float>()] + public void MethodWithAttributeSingle() + { + } + + [GenericTypeAttribute<double>()] + public void MethodWithAttributeDouble() + { + } + + [GenericTypeAttribute<string>()] + public void MethodWithAttributeString() + { + } + + [GenericTypeAttribute<Vector2>()] + public void MethodWithAttributeVector2() + { + } + + [GenericTypeAttribute<Vector2I>()] + public void MethodWithAttributeVector2I() + { + } + + [GenericTypeAttribute<Rect2>()] + public void MethodWithAttributeRect2() + { + } + + [GenericTypeAttribute<Rect2I>()] + public void MethodWithAttributeRect2I() + { + } + + [GenericTypeAttribute<Transform2D>()] + public void MethodWithAttributeTransform2D() + { + } + + [GenericTypeAttribute<Vector3>()] + public void MethodWithAttributeVector3() + { + } + + [GenericTypeAttribute<Vector3I>()] + public void MethodWithAttributeVector3I() + { + } + + [GenericTypeAttribute<Vector4>()] + public void MethodWithAttributeVector4() + { + } + + [GenericTypeAttribute<Vector4I>()] + public void MethodWithAttributeVector4I() + { + } + + [GenericTypeAttribute<Basis>()] + public void MethodWithAttributeBasis() + { + } + + [GenericTypeAttribute<Quaternion>()] + public void MethodWithAttributeQuaternion() + { + } + + [GenericTypeAttribute<Transform3D>()] + public void MethodWithAttributeTransform3D() + { + } + + [GenericTypeAttribute<Projection>()] + public void MethodWithAttributeProjection() + { + } + + [GenericTypeAttribute<Aabb>()] + public void MethodWithAttributeAabb() + { + } + + [GenericTypeAttribute<Color>()] + public void MethodWithAttributeColor() + { + } + + [GenericTypeAttribute<Plane>()] + public void MethodWithAttributePlane() + { + } + + [GenericTypeAttribute<Callable>()] + public void MethodWithAttributeCallable() + { + } + + [GenericTypeAttribute<Signal>()] + public void MethodWithAttributeSignal() + { + } + + [GenericTypeAttribute<GodotObject>()] + public void MethodWithAttributeGodotObject() + { + } + + [GenericTypeAttribute<StringName>()] + public void MethodWithAttributeStringName() + { + } + + [GenericTypeAttribute<NodePath>()] + public void MethodWithAttributeNodePath() + { + } + + [GenericTypeAttribute<Rid>()] + public void MethodWithAttributeRid() + { + } + + [GenericTypeAttribute<Dictionary>()] + public void MethodWithAttributeDictionary() + { + } + + [GenericTypeAttribute<Array>()] + public void MethodWithAttributeArray() + { + } + + [GenericTypeAttribute<byte[]>()] + public void MethodWithAttributeByteArray() + { + } + + [GenericTypeAttribute<int[]>()] + public void MethodWithAttributeInt32Array() + { + } + + [GenericTypeAttribute<long[]>()] + public void MethodWithAttributeInt64Array() + { + } + + [GenericTypeAttribute<float[]>()] + public void MethodWithAttributeSingleArray() + { + } + + [GenericTypeAttribute<double[]>()] + public void MethodWithAttributeDoubleArray() + { + } + + [GenericTypeAttribute<string[]>()] + public void MethodWithAttributeStringArray() + { + } + + [GenericTypeAttribute<Vector2[]>()] + public void MethodWithAttributeVector2Array() + { + } + + [GenericTypeAttribute<Vector3[]>()] + public void MethodWithAttributeVector3Array() + { + } + + [GenericTypeAttribute<Color[]>()] + public void MethodWithAttributeColorArray() + { + } + + [GenericTypeAttribute<GodotObject[]>()] + public void MethodWithAttributeGodotObjectArray() + { + } + + [GenericTypeAttribute<StringName[]>()] + public void MethodWithAttributeStringNameArray() + { + } + + [GenericTypeAttribute<NodePath[]>()] + public void MethodWithAttributeNodePathArray() + { + } + + [GenericTypeAttribute<Rid[]>()] + public void MethodWithAttributeRidArray() + { + } + + // This method definition fails because generic type is not Variant-compatible. + [GenericTypeAttribute<{|GD0301:object|}>()] + public void MethodWithWrongAttribute() + { + } +} + +[GenericTypeAttribute<bool>()] +public class ClassVariantAnnotatedBool +{ +} + +[GenericTypeAttribute<char>()] +public class ClassVariantAnnotatedChar +{ +} + +[GenericTypeAttribute<sbyte>()] +public class ClassVariantAnnotatedSByte +{ +} + +[GenericTypeAttribute<byte>()] +public class ClassVariantAnnotatedByte +{ +} + +[GenericTypeAttribute<short>()] +public class ClassVariantAnnotatedInt16 +{ +} + +[GenericTypeAttribute<ushort>()] +public class ClassVariantAnnotatedUInt16 +{ +} + +[GenericTypeAttribute<int>()] +public class ClassVariantAnnotatedInt32 +{ +} + +[GenericTypeAttribute<uint>()] +public class ClassVariantAnnotatedUInt32 +{ +} + +[GenericTypeAttribute<long>()] +public class ClassVariantAnnotatedInt64 +{ +} + +[GenericTypeAttribute<ulong>()] +public class ClassVariantAnnotatedUInt64 +{ +} + +[GenericTypeAttribute<float>()] +public class ClassVariantAnnotatedSingle +{ +} + +[GenericTypeAttribute<double>()] +public class ClassVariantAnnotatedDouble +{ +} + +[GenericTypeAttribute<string>()] +public class ClassVariantAnnotatedString +{ +} + +[GenericTypeAttribute<Vector2>()] +public class ClassVariantAnnotatedVector2 +{ +} + +[GenericTypeAttribute<Vector2I>()] +public class ClassVariantAnnotatedVector2I +{ +} + +[GenericTypeAttribute<Rect2>()] +public class ClassVariantAnnotatedRect2 +{ +} + +[GenericTypeAttribute<Rect2I>()] +public class ClassVariantAnnotatedRect2I +{ +} + +[GenericTypeAttribute<Transform2D>()] +public class ClassVariantAnnotatedTransform2D +{ +} + +[GenericTypeAttribute<Vector3>()] +public class ClassVariantAnnotatedVector3 +{ +} + +[GenericTypeAttribute<Vector3I>()] +public class ClassVariantAnnotatedVector3I +{ +} + +[GenericTypeAttribute<Vector4>()] +public class ClassVariantAnnotatedVector4 +{ +} + +[GenericTypeAttribute<Vector4I>()] +public class ClassVariantAnnotatedVector4I +{ +} + +[GenericTypeAttribute<Basis>()] +public class ClassVariantAnnotatedBasis +{ +} + +[GenericTypeAttribute<Quaternion>()] +public class ClassVariantAnnotatedQuaternion +{ +} + +[GenericTypeAttribute<Transform3D>()] +public class ClassVariantAnnotatedTransform3D +{ +} + +[GenericTypeAttribute<Projection>()] +public class ClassVariantAnnotatedProjection +{ +} + +[GenericTypeAttribute<Aabb>()] +public class ClassVariantAnnotatedAabb +{ +} + +[GenericTypeAttribute<Color>()] +public class ClassVariantAnnotatedColor +{ +} + +[GenericTypeAttribute<Plane>()] +public class ClassVariantAnnotatedPlane +{ +} + +[GenericTypeAttribute<Callable>()] +public class ClassVariantAnnotatedCallable +{ +} + +[GenericTypeAttribute<Signal>()] +public class ClassVariantAnnotatedSignal +{ +} + +[GenericTypeAttribute<GodotObject>()] +public class ClassVariantAnnotatedGodotObject +{ +} + +[GenericTypeAttribute<StringName>()] +public class ClassVariantAnnotatedStringName +{ +} + +[GenericTypeAttribute<NodePath>()] +public class ClassVariantAnnotatedNodePath +{ +} + +[GenericTypeAttribute<Rid>()] +public class ClassVariantAnnotatedRid +{ +} + +[GenericTypeAttribute<Dictionary>()] +public class ClassVariantAnnotatedDictionary +{ +} + +[GenericTypeAttribute<Array>()] +public class ClassVariantAnnotatedArray +{ +} + +[GenericTypeAttribute<byte[]>()] +public class ClassVariantAnnotatedByteArray +{ +} + +[GenericTypeAttribute<int[]>()] +public class ClassVariantAnnotatedInt32Array +{ +} + +[GenericTypeAttribute<long[]>()] +public class ClassVariantAnnotatedInt64Array +{ +} + +[GenericTypeAttribute<float[]>()] +public class ClassVariantAnnotatedSingleArray +{ +} + +[GenericTypeAttribute<double[]>()] +public class ClassVariantAnnotatedDoubleArray +{ +} + +[GenericTypeAttribute<string[]>()] +public class ClassVariantAnnotatedStringArray +{ +} + +[GenericTypeAttribute<Vector2[]>()] +public class ClassVariantAnnotatedVector2Array +{ +} + +[GenericTypeAttribute<Vector3[]>()] +public class ClassVariantAnnotatedVector3Array +{ +} + +[GenericTypeAttribute<Color[]>()] +public class ClassVariantAnnotatedColorArray +{ +} + +[GenericTypeAttribute<GodotObject[]>()] +public class ClassVariantAnnotatedGodotObjectArray +{ +} + +[GenericTypeAttribute<StringName[]>()] +public class ClassVariantAnnotatedStringNameArray +{ +} + +[GenericTypeAttribute<NodePath[]>()] +public class ClassVariantAnnotatedNodePathArray +{ +} + +[GenericTypeAttribute<Rid[]>()] +public class ClassVariantAnnotatedRidArray +{ +} + +// This class definition fails because generic type is not Variant-compatible. +[GenericTypeAttribute<{|GD0301:object|}>()] +public class ClassNonVariantAnnotated +{ +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class GenericTypeAttribute<[MustBeVariant] T> : Attribute +{ +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs new file mode 100644 index 0000000000..ce182e8c62 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs @@ -0,0 +1,27 @@ +using Godot; + +public class MustBeVariantGD0302 +{ + public void MethodOk<[MustBeVariant] T>() + { + // T is guaranteed to be a Variant-compatible type because it's annotated with the [MustBeVariant] attribute, so it can be used here. + new ExampleClass<T>(); + Method<T>(); + } + + public void MethodFail<T>() + { + // These two calls raise a GD0302 diagnostic error: T is not valid here because it may not a Variant type and method call and class require it. + new ExampleClass<{|GD0302:T|}>(); + Method<{|GD0302:T|}>(); + } + + public void Method<[MustBeVariant] T>() + { + } +} + +public class ExampleClass<[MustBeVariant] T> +{ + +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/OuterClassPartialModifierAnalyzer.GD0002.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/OuterClassPartialModifierAnalyzer.GD0002.cs new file mode 100644 index 0000000000..49c898e359 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/OuterClassPartialModifierAnalyzer.GD0002.cs @@ -0,0 +1,11 @@ +using Godot; + +public class {|GD0002:OuterOuterClassPartialModifierAnalyzer|} +{ + public class {|GD0002:OuterClassPartialModifierAnalyzer|} + { + // MyNode is contained in a non-partial type so the source generators + // can't enhance this type to work with Godot. + public partial class MyNode : Node { } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/SameName.GD0003.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/SameName.GD0003.cs new file mode 100644 index 0000000000..3f4f79fc49 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/SameName.GD0003.cs @@ -0,0 +1,18 @@ +using Godot; + +namespace NamespaceA +{ + partial class SameName : GodotObject + { + private int _field; + } +} + +// SameName again but different namespace +namespace NamespaceB +{ + partial class {|GD0003:SameName|} : GodotObject + { + private int _field; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ScriptBoilerplate.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ScriptBoilerplate.cs new file mode 100644 index 0000000000..b431522e7c --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/ScriptBoilerplate.cs @@ -0,0 +1,33 @@ +using Godot; + +public partial class ScriptBoilerplate : Node +{ + private NodePath _nodePath; + private int _velocity; + + public override void _Process(double delta) + { + _ = delta; + + base._Process(delta); + } + + public int Bazz(StringName name) + { + _ = name; + return 1; + } + + public void IgnoreThisMethodWithByRefParams(ref int a) + { + _ = a; + } +} + +public partial struct OuterClass +{ + public partial class NestedClass : RefCounted + { + public override Variant _Get(StringName property) => default; + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Shipped.md b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..19a9aedc40 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,30 @@ +## Release 4.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +GD0001 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0001.html) +GD0002 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0002.html) +GD0101 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0101.html) +GD0102 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0102.html) +GD0103 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0103.html) +GD0104 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0104.html) +GD0105 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0105.html) +GD0106 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0106.html) +GD0201 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0201.html) +GD0202 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0202.html) +GD0203 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0203.html) +GD0301 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0301.html) +GD0302 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0302.html) +GD0303 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0303.html) + +## Release 4.2 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +GD0107 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0107.html) +GD0401 | Usage | Error | GlobalClassAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0401.html) +GD0402 | Usage | Error | GlobalClassAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0402.html) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Unshipped.md b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..7286853f7a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,5 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +GD0003 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0003.html) diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ClassPartialModifierAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ClassPartialModifierAnalyzer.cs new file mode 100644 index 0000000000..e4bdb8db84 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ClassPartialModifierAnalyzer.cs @@ -0,0 +1,112 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Godot.SourceGenerators +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class ClassPartialModifierAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => + ImmutableArray.Create(Common.ClassPartialModifierRule, Common.OuterClassPartialModifierRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclaration) + return; + + if (context.ContainingSymbol is not INamedTypeSymbol typeSymbol) + return; + + if (!typeSymbol.InheritsFrom("GodotSharp", GodotClasses.GodotObject)) + return; + + if (!classDeclaration.IsPartial()) + context.ReportDiagnostic(Diagnostic.Create( + Common.ClassPartialModifierRule, + classDeclaration.Identifier.GetLocation(), + typeSymbol.ToDisplayString())); + + var outerClassDeclaration = context.Node.Parent as ClassDeclarationSyntax; + while (outerClassDeclaration is not null) + { + var outerClassTypeSymbol = context.SemanticModel.GetDeclaredSymbol(outerClassDeclaration); + if (outerClassTypeSymbol == null) + return; + + if (!outerClassDeclaration.IsPartial()) + context.ReportDiagnostic(Diagnostic.Create( + Common.OuterClassPartialModifierRule, + outerClassDeclaration.Identifier.GetLocation(), + outerClassTypeSymbol.ToDisplayString())); + + outerClassDeclaration = outerClassDeclaration.Parent as ClassDeclarationSyntax; + } + } + } + + [ExportCodeFixProvider(LanguageNames.CSharp)] + public sealed class ClassPartialModifierCodeFixProvider : CodeFixProvider + { + public override ImmutableArray<string> FixableDiagnosticIds => + ImmutableArray.Create(Common.ClassPartialModifierRule.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + // Get the syntax root of the document. + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the diagnostic to fix. + var diagnostic = context.Diagnostics.First(); + + // Get the location of code issue. + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Use that location to find the containing class declaration. + var classDeclaration = root?.FindToken(diagnosticSpan.Start) + .Parent? + .AncestorsAndSelf() + .OfType<ClassDeclarationSyntax>() + .First(); + + if (classDeclaration == null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + "Add partial modifier", + cancellationToken => AddPartialModifierAsync(context.Document, classDeclaration, cancellationToken), + classDeclaration.ToFullString()), + context.Diagnostics); + } + + private static async Task<Document> AddPartialModifierAsync(Document document, + ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken) + { + // Create a new partial modifier. + var partialModifier = SyntaxFactory.Token(SyntaxKind.PartialKeyword); + var modifiedClassDeclaration = classDeclaration.AddModifiers(partialModifier); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // Replace the old class declaration with the modified one in the syntax root. + var newRoot = root!.ReplaceNode(classDeclaration, modifiedClassDeclaration); + var newDocument = document.WithSyntaxRoot(newRoot); + return newDocument; + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/CodeAnalysisAttributes.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/CodeAnalysisAttributes.cs new file mode 100644 index 0000000000..fa591bc873 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/CodeAnalysisAttributes.cs @@ -0,0 +1,7 @@ +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] + public sealed class NotNullAttribute : Attribute + { + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs index 72614dd7e0..ad7962e7df 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Common.cs @@ -1,448 +1,190 @@ -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; namespace Godot.SourceGenerators { - public static class Common + public static partial class Common { - public static void ReportNonPartialGodotScriptClass( - GeneratorExecutionContext context, - ClassDeclarationSyntax cds, INamedTypeSymbol symbol - ) - { - string message = - "Missing partial modifier on declaration of type '" + - $"{symbol.FullQualifiedNameOmitGlobal()}' which is a subclass of '{GodotClasses.GodotObject}'"; + private static readonly string _helpLinkFormat = $"{VersionDocsUrl}/tutorials/scripting/c_sharp/diagnostics/{{0}}.html"; - string description = $"{message}. Subclasses of '{GodotClasses.GodotObject}' " + - "must be declared with the partial modifier."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0001", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - cds.GetLocation(), - cds.SyntaxTree.FilePath)); - } - - public static void ReportNonPartialGodotScriptOuterClass( - GeneratorExecutionContext context, - TypeDeclarationSyntax outerTypeDeclSyntax - ) - { - var outerSymbol = context.Compilation - .GetSemanticModel(outerTypeDeclSyntax.SyntaxTree) - .GetDeclaredSymbol(outerTypeDeclSyntax); - - string fullQualifiedName = outerSymbol is INamedTypeSymbol namedTypeSymbol ? - namedTypeSymbol.FullQualifiedNameOmitGlobal() : - "type not found"; - - string message = - $"Missing partial modifier on declaration of type '{fullQualifiedName}', " + - $"which contains one or more subclasses of '{GodotClasses.GodotObject}'"; - - string description = $"{message}. Subclasses of '{GodotClasses.GodotObject}' and their " + - "containing types must be declared with the partial modifier."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0002", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - outerTypeDeclSyntax.GetLocation(), - outerTypeDeclSyntax.SyntaxTree.FilePath)); - } - - public static void ReportExportedMemberIsStatic( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - bool isField = exportedMemberSymbol is IFieldSymbol; - - string message = $"Attempted to export static {(isField ? "field" : "property")}: " + - $"'{exportedMemberSymbol.ToDisplayString()}'"; - - string description = $"{message}. Only instance fields and properties can be exported." + - " Remove the 'static' modifier or the '[Export]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0101", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } - - public static void ReportExportedMemberTypeNotSupported( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - bool isField = exportedMemberSymbol is IFieldSymbol; - - string message = $"The type of the exported {(isField ? "field" : "property")} " + - $"is not supported: '{exportedMemberSymbol.ToDisplayString()}'"; - - string description = $"{message}. Use a supported type or remove the '[Export]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0102", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } - - public static void ReportExportedMemberIsReadOnly( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - bool isField = exportedMemberSymbol is IFieldSymbol; - - string message = $"The exported {(isField ? "field" : "property")} " + - $"is read-only: '{exportedMemberSymbol.ToDisplayString()}'"; - - string description = isField ? - $"{message}. Exported fields cannot be read-only." : - $"{message}. Exported properties must be writable."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0103", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } - - public static void ReportExportedMemberIsWriteOnly( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - - string message = $"The exported property is write-only: '{exportedMemberSymbol.ToDisplayString()}'"; - - string description = $"{message}. Exported properties must be readable."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0104", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } - - public static void ReportExportedMemberIsIndexer( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - - string message = $"Attempted to export indexer property: " + - $"'{exportedMemberSymbol.ToDisplayString()}'"; - - string description = $"{message}. Indexer properties can't be exported." + - " Remove the '[Export]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0105", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } - - public static void ReportExportedMemberIsExplicitInterfaceImplementation( - GeneratorExecutionContext context, - ISymbol exportedMemberSymbol - ) - { - var locations = exportedMemberSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); - - string message = $"Attempted to export explicit interface property implementation: " + - $"'{exportedMemberSymbol.ToDisplayString()}'"; - - string description = $"{message}. Explicit interface implementations can't be exported." + - " Remove the '[Export]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0106", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } + internal static readonly DiagnosticDescriptor ClassPartialModifierRule = + new DiagnosticDescriptor(id: "GD0001", + title: $"Missing partial modifier on declaration of type that derives from '{GodotClasses.GodotObject}'", + messageFormat: $"Missing partial modifier on declaration of type '{{0}}' that derives from '{GodotClasses.GodotObject}'", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + $"Classes that derive from '{GodotClasses.GodotObject}' must be declared with the partial modifier.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0001")); - public static void ReportSignalDelegateMissingSuffix( - GeneratorExecutionContext context, - INamedTypeSymbol delegateSymbol) - { - var locations = delegateSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + internal static readonly DiagnosticDescriptor OuterClassPartialModifierRule = + new DiagnosticDescriptor(id: "GD0002", + title: $"Missing partial modifier on declaration of type which contains nested classes that derive from '{GodotClasses.GodotObject}'", + messageFormat: $"Missing partial modifier on declaration of type '{{0}}' which contains nested classes that derive from '{GodotClasses.GodotObject}'", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + $"Classes that derive from '{GodotClasses.GodotObject}' and their containing types must be declared with the partial modifier.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0002")); - string message = "The name of the delegate must end with 'EventHandler': " + - delegateSymbol.ToDisplayString() + - $". Did you mean '{delegateSymbol.Name}EventHandler'?"; + public static readonly DiagnosticDescriptor MultipleClassesInGodotScriptRule = + new DiagnosticDescriptor(id: "GD0003", + title: "Found multiple classes with the same name in the same script file", + messageFormat: "Found multiple classes with the name '{0}' in the same script file", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "Found multiple classes with the same name in the same script file. A script file must only contain one class with a name that matches the file name.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0003")); - string description = $"{message}. Rename the delegate accordingly or remove the '[Signal]' attribute."; + public static readonly DiagnosticDescriptor ExportedMemberIsStaticRule = + new DiagnosticDescriptor(id: "GD0101", + title: "The exported member is static", + messageFormat: "The exported member '{0}' is static", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The exported member is static. Only instance fields and properties can be exported. Remove the 'static' modifier, or the '[Export]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0101")); - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0201", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } + public static readonly DiagnosticDescriptor ExportedMemberTypeIsNotSupportedRule = + new DiagnosticDescriptor(id: "GD0102", + title: "The type of the exported member is not supported", + messageFormat: "The type of the exported member '{0}' is not supported", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The type of the exported member is not supported. Use a supported type, or remove the '[Export]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0102")); - public static void ReportSignalParameterTypeNotSupported( - GeneratorExecutionContext context, - IParameterSymbol parameterSymbol) - { - var locations = parameterSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + public static readonly DiagnosticDescriptor ExportedMemberIsReadOnlyRule = + new DiagnosticDescriptor(id: "GD0103", + title: "The exported member is read-only", + messageFormat: "The exported member '{0}' is read-only", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The exported member is read-only. Exported member must be writable.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0103")); - string message = "The parameter of the delegate signature of the signal " + - $"is not supported: '{parameterSymbol.ToDisplayString()}'"; + public static readonly DiagnosticDescriptor ExportedPropertyIsWriteOnlyRule = + new DiagnosticDescriptor(id: "GD0104", + title: "The exported property is write-only", + messageFormat: "The exported property '{0}' is write-only", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The exported property is write-only. Exported properties must be readable.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0104")); - string description = $"{message}. Use supported types only or remove the '[Signal]' attribute."; + public static readonly DiagnosticDescriptor ExportedMemberIsIndexerRule = + new DiagnosticDescriptor(id: "GD0105", + title: "The exported property is an indexer", + messageFormat: "The exported property '{0}' is an indexer", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The exported property is an indexer. Remove the '[Export]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0105")); - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0202", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } + public static readonly DiagnosticDescriptor ExportedMemberIsExplicitInterfaceImplementationRule = + new DiagnosticDescriptor(id: "GD0106", + title: "The exported property is an explicit interface implementation", + messageFormat: "The exported property '{0}' is an explicit interface implementation", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The exported property is an explicit interface implementation. Remove the '[Export]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0106")); - public static void ReportSignalDelegateSignatureMustReturnVoid( - GeneratorExecutionContext context, - INamedTypeSymbol delegateSymbol) - { - var locations = delegateSymbol.Locations; - var location = locations.FirstOrDefault(l => l.SourceTree != null) ?? locations.FirstOrDefault(); + public static readonly DiagnosticDescriptor OnlyNodesShouldExportNodesRule = + new DiagnosticDescriptor(id: "GD0107", + title: "Types not derived from Node should not export Node members", + messageFormat: "Types not derived from Node should not export Node members", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "Types not derived from Node should not export Node members. Node export is only supported in Node-derived classes.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0107")); - string message = "The delegate signature of the signal " + - $"must return void: '{delegateSymbol.ToDisplayString()}'"; + public static readonly DiagnosticDescriptor SignalDelegateMissingSuffixRule = + new DiagnosticDescriptor(id: "GD0201", + title: "The name of the delegate must end with 'EventHandler'", + messageFormat: "The name of the delegate '{0}' must end with 'EventHandler'", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The name of the delegate must end with 'EventHandler'. Rename the delegate accordingly, or remove the '[Signal]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0201")); - string description = $"{message}. Return void or remove the '[Signal]' attribute."; + public static readonly DiagnosticDescriptor SignalParameterTypeNotSupportedRule = + new DiagnosticDescriptor(id: "GD0202", + title: "The parameter of the delegate signature of the signal is not supported", + messageFormat: "The parameter of the delegate signature of the signal '{0}' is not supported", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The parameter of the delegate signature of the signal is not supported. Use supported types only, or remove the '[Signal]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0202")); - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0203", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - location, - location?.SourceTree?.FilePath)); - } + public static readonly DiagnosticDescriptor SignalDelegateSignatureMustReturnVoidRule = + new DiagnosticDescriptor(id: "GD0203", + title: "The delegate signature of the signal must return void", + messageFormat: "The delegate signature of the signal '{0}' must return void", + category: "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + "The delegate signature of the signal must return void. Return void, or remove the '[Signal]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0203")); public static readonly DiagnosticDescriptor GenericTypeArgumentMustBeVariantRule = new DiagnosticDescriptor(id: "GD0301", title: "The generic type argument must be a Variant compatible type", - messageFormat: "The generic type argument must be a Variant compatible type: {0}", + messageFormat: "The generic type argument '{0}' must be a Variant compatible type", category: "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, - "The generic type argument must be a Variant compatible type. Use a Variant compatible type as the generic type argument."); - - public static void ReportGenericTypeArgumentMustBeVariant( - SyntaxNodeAnalysisContext context, - SyntaxNode typeArgumentSyntax, - ISymbol typeArgumentSymbol) - { - string message = "The generic type argument " + - $"must be a Variant compatible type: '{typeArgumentSymbol.ToDisplayString()}'"; - - string description = $"{message}. Use a Variant compatible type as the generic type argument."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0301", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - typeArgumentSyntax.GetLocation(), - typeArgumentSyntax.SyntaxTree.FilePath)); - } + "The generic type argument must be a Variant compatible type. Use a Variant compatible type as the generic type argument.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0301")); public static readonly DiagnosticDescriptor GenericTypeParameterMustBeVariantAnnotatedRule = new DiagnosticDescriptor(id: "GD0302", - title: "The generic type parameter must be annotated with the MustBeVariant attribute", - messageFormat: "The generic type argument must be a Variant type: {0}", + title: "The generic type parameter must be annotated with the '[MustBeVariant]' attribute", + messageFormat: "The generic type parameter '{0}' must be annotated with the '[MustBeVariant]' attribute", category: "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, - "The generic type argument must be a Variant type. Use a Variant type as the generic type argument."); - - public static void ReportGenericTypeParameterMustBeVariantAnnotated( - SyntaxNodeAnalysisContext context, - SyntaxNode typeArgumentSyntax, - ISymbol typeArgumentSymbol) - { - string message = "The generic type parameter must be annotated with the MustBeVariant attribute"; - - string description = $"{message}. Add the MustBeVariant attribute to the generic type parameter."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0302", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - typeArgumentSyntax.GetLocation(), - typeArgumentSyntax.SyntaxTree.FilePath)); - } + "The generic type parameter must be annotated with the '[MustBeVariant]' attribute. Add the '[MustBeVariant]' attribute to the generic type parameter.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0302")); public static readonly DiagnosticDescriptor TypeArgumentParentSymbolUnhandledRule = new DiagnosticDescriptor(id: "GD0303", - title: "The generic type parameter must be annotated with the MustBeVariant attribute", - messageFormat: "The generic type argument must be a Variant type: {0}", + title: "The parent symbol of a type argument that must be Variant compatible was not handled", + messageFormat: "The parent symbol '{0}' of a type argument that must be Variant compatible was not handled", category: "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, - "The generic type argument must be a Variant type. Use a Variant type as the generic type argument."); - - public static void ReportTypeArgumentParentSymbolUnhandled( - SyntaxNodeAnalysisContext context, - SyntaxNode typeArgumentSyntax, - ISymbol parentSymbol) - { - string message = $"Symbol '{parentSymbol.ToDisplayString()}' parent of a type argument " + - "that must be Variant compatible was not handled."; - - string description = $"{message}. Handle type arguments that are children of the unhandled symbol type."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0303", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - typeArgumentSyntax.GetLocation(), - typeArgumentSyntax.SyntaxTree.FilePath)); - } + "The parent symbol of a type argument that must be Variant compatible was not handled. This is an issue in the engine, and should be reported.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0303")); public static readonly DiagnosticDescriptor GlobalClassMustDeriveFromGodotObjectRule = new DiagnosticDescriptor(id: "GD0401", - title: "The class must derive from GodotObject or a derived class", - messageFormat: "The class '{0}' must derive from GodotObject or a derived class.", + title: $"The class must derive from {GodotClasses.GodotObject} or a derived class", + messageFormat: $"The class '{{0}}' must derive from {GodotClasses.GodotObject} or a derived class", category: "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, - "The class must derive from GodotObject or a derived class. Change the base class or remove the '[GlobalClass]' attribute."); - - public static void ReportGlobalClassMustDeriveFromGodotObject( - SyntaxNodeAnalysisContext context, - SyntaxNode classSyntax, - ISymbol typeSymbol) - { - string message = $"The class '{typeSymbol.ToDisplayString()}' must derive from GodotObject or a derived class"; - - string description = $"{message}. Change the base class or remove the '[GlobalClass]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0401", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - classSyntax.GetLocation(), - classSyntax.SyntaxTree.FilePath)); - } + $"The class must derive from {GodotClasses.GodotObject} or a derived class. Change the base type, or remove the '[GlobalClass]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0401")); public static readonly DiagnosticDescriptor GlobalClassMustNotBeGenericRule = new DiagnosticDescriptor(id: "GD0402", - title: "The class must not contain generic arguments", - messageFormat: "The class '{0}' must not contain generic arguments", + title: "The class must not be generic", + messageFormat: "The class '{0}' must not be generic", category: "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, - "The class must be a non-generic type. Remove the generic arguments or the '[GlobalClass]' attribute."); - - public static void ReportGlobalClassMustNotBeGeneric( - SyntaxNodeAnalysisContext context, - SyntaxNode classSyntax, - ISymbol typeSymbol) - { - string message = $"The class '{typeSymbol.ToDisplayString()}' must not contain generic arguments"; - - string description = $"{message}. Remove the generic arguments or the '[GlobalClass]' attribute."; - - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor(id: "GD0402", - title: message, - messageFormat: message, - category: "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description), - classSyntax.GetLocation(), - classSyntax.SyntaxTree.FilePath)); - } + "The class must not be generic. Make the class non-generic, or remove the '[GlobalClass]' attribute.", + helpLinkUri: string.Format(_helpLinkFormat, "GD0402")); } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs index 5866db5144..9784bd0b78 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ExtensionMethods.cs @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Godot.SourceGenerators { - static class ExtensionMethods + internal static class ExtensionMethods { public static bool TryGetGlobalAnalyzerProperty( this GeneratorExecutionContext context, string property, out string? value @@ -32,7 +32,7 @@ namespace Godot.SourceGenerators disabledGenerators != null && disabledGenerators.Split(';').Contains(generatorName)); - public static bool InheritsFrom(this INamedTypeSymbol? symbol, string assemblyName, string typeFullName) + public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, string typeFullName) { while (symbol != null) { @@ -329,6 +329,11 @@ namespace Godot.SourceGenerators } } + public static Location? FirstLocationWithSourceTreeOrDefault(this IEnumerable<Location> locations) + { + return locations.FirstOrDefault(location => location.SourceTree != null) ?? locations.FirstOrDefault(); + } + public static string Path(this Location location) => location.SourceTree?.GetLineSpan(location.SourceSpan).Path ?? location.GetLineSpan().Path; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GlobalClassAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GlobalClassAnalyzer.cs index bcb35dae8a..22af25b9c4 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GlobalClassAnalyzer.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GlobalClassAnalyzer.cs @@ -1,15 +1,13 @@ using System.Collections.Immutable; using System.Linq; - using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace Godot.SourceGenerators { [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class GlobalClassAnalyzer : DiagnosticAnalyzer + public sealed class GlobalClassAnalyzer : DiagnosticAnalyzer { public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( @@ -23,20 +21,30 @@ namespace Godot.SourceGenerators context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); } - private void AnalyzeNode(SyntaxNodeAnalysisContext context) + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) { - var typeClassDecl = (ClassDeclarationSyntax)context.Node; - // Return if not a type symbol or the type is not a global class. if (context.ContainingSymbol is not INamedTypeSymbol typeSymbol || !typeSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotGlobalClassAttribute() ?? false)) return; if (typeSymbol.IsGenericType) - Common.ReportGlobalClassMustNotBeGeneric(context, typeClassDecl, typeSymbol); + { + context.ReportDiagnostic(Diagnostic.Create( + Common.GlobalClassMustNotBeGenericRule, + typeSymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + typeSymbol.ToDisplayString() + )); + } if (!typeSymbol.InheritsFrom("GodotSharp", GodotClasses.GodotObject)) - Common.ReportGlobalClassMustDeriveFromGodotObject(context, typeClassDecl, typeSymbol); + { + context.ReportDiagnostic(Diagnostic.Create( + Common.GlobalClassMustDeriveFromGodotObjectRule, + typeSymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + typeSymbol.ToDisplayString() + )); + } } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj index 23879e0e53..8a3b17ac49 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Godot.SourceGenerators.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>9.0</LangVersion> + <LangVersion>10</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> <PropertyGroup> @@ -9,7 +9,7 @@ <Authors>Godot Engine contributors</Authors> <PackageId>Godot.SourceGenerators</PackageId> - <Version>4.2.0</Version> + <Version>4.3.0</Version> <PackageVersion>$(PackageVersion_Godot_SourceGenerators)</PackageVersion> <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> @@ -20,9 +20,13 @@ <IncludeBuildOutput>false</IncludeBuildOutput> <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking> </PropertyGroup> + <!-- Analyzer release tracking --> <ItemGroup> - <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" /> - <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> + <AdditionalFiles Include="AnalyzerReleases.Shipped.md" /> + <AdditionalFiles Include="AnalyzerReleases.Unshipped.md" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.11.0" PrivateAssets="all" /> </ItemGroup> <ItemGroup> <!-- Package the generator in the analyzer directory of the nuget package --> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs index e80837dbe0..af39a54b0b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotClasses.cs @@ -3,6 +3,7 @@ namespace Godot.SourceGenerators public static class GodotClasses { public const string GodotObject = "Godot.GodotObject"; + public const string Node = "Godot.Node"; public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute"; public const string ExportAttr = "Godot.ExportAttribute"; public const string ExportCategoryAttr = "Godot.ExportCategoryAttribute"; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Helper.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Helper.cs new file mode 100644 index 0000000000..aecf127686 --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/Helper.cs @@ -0,0 +1,15 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Godot.SourceGenerators +{ + public static class Helper + { + [Conditional("DEBUG")] + public static void ThrowIfNull([NotNull] object? value) + { + _ = value ?? throw new ArgumentNullException(); + } + } +} diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs index 2a9758516c..95eaca4d3d 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MustBeVariantAnalyzer.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -9,7 +8,7 @@ using Microsoft.CodeAnalysis.Diagnostics; namespace Godot.SourceGenerators { [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class MustBeVariantAnalyzer : DiagnosticAnalyzer + public sealed class MustBeVariantAnalyzer : DiagnosticAnalyzer { public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( @@ -34,7 +33,7 @@ namespace Godot.SourceGenerators // Method invocation or variable declaration that contained the type arguments var parentSyntax = context.Node.Parent; - Debug.Assert(parentSyntax != null); + Helper.ThrowIfNull(parentSyntax); var sm = context.SemanticModel; @@ -49,9 +48,10 @@ namespace Godot.SourceGenerators continue; var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol; - Debug.Assert(typeSymbol != null); + Helper.ThrowIfNull(typeSymbol); var parentSymbol = sm.GetSymbolInfo(parentSyntax).Symbol; + Helper.ThrowIfNull(parentSymbol); if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i)) { @@ -62,17 +62,24 @@ namespace Godot.SourceGenerators { if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false)) { - Common.ReportGenericTypeParameterMustBeVariantAnnotated(context, typeSyntax, typeSymbol); + context.ReportDiagnostic(Diagnostic.Create( + Common.GenericTypeParameterMustBeVariantAnnotatedRule, + typeSyntax.GetLocation(), + typeSymbol.ToDisplayString() + )); } continue; } var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache); - if (marshalType == null) + if (marshalType is null) { - Common.ReportGenericTypeArgumentMustBeVariant(context, typeSyntax, typeSymbol); - continue; + context.ReportDiagnostic(Diagnostic.Create( + Common.GenericTypeArgumentMustBeVariantRule, + typeSyntax.GetLocation(), + typeSymbol.ToDisplayString() + )); } } } @@ -106,24 +113,45 @@ namespace Godot.SourceGenerators /// <param name="parentSymbol">The symbol retrieved for the parent node syntax.</param> /// <param name="typeArgumentSyntax">The type node syntax of the argument type to check.</param> /// <param name="typeArgumentSymbol">The symbol retrieved for the type node syntax.</param> + /// <param name="typeArgumentIndex"></param> /// <returns><see langword="true"/> if the type must be variant and must be analyzed.</returns> - private bool ShouldCheckTypeArgument(SyntaxNodeAnalysisContext context, SyntaxNode parentSyntax, ISymbol parentSymbol, TypeSyntax typeArgumentSyntax, ITypeSymbol typeArgumentSymbol, int typeArgumentIndex) + private bool ShouldCheckTypeArgument( + SyntaxNodeAnalysisContext context, + SyntaxNode parentSyntax, + ISymbol parentSymbol, + TypeSyntax typeArgumentSyntax, + ITypeSymbol typeArgumentSymbol, + int typeArgumentIndex) { - var typeParamSymbol = parentSymbol switch + ITypeParameterSymbol? typeParamSymbol = parentSymbol switch { - IMethodSymbol methodSymbol => methodSymbol.TypeParameters[typeArgumentIndex], - INamedTypeSymbol typeSymbol => typeSymbol.TypeParameters[typeArgumentIndex], - _ => null, + IMethodSymbol methodSymbol when parentSyntax.Parent is AttributeSyntax && + methodSymbol.ContainingType.TypeParameters.Length > 0 + => methodSymbol.ContainingType.TypeParameters[typeArgumentIndex], + + IMethodSymbol { TypeParameters.Length: > 0 } methodSymbol + => methodSymbol.TypeParameters[typeArgumentIndex], + + INamedTypeSymbol { TypeParameters.Length: > 0 } typeSymbol + => typeSymbol.TypeParameters[typeArgumentIndex], + + _ + => null }; - if (typeParamSymbol == null) + if (typeParamSymbol != null) { - Common.ReportTypeArgumentParentSymbolUnhandled(context, typeArgumentSyntax, parentSymbol); - return false; + return typeParamSymbol.GetAttributes() + .Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false); } - return typeParamSymbol.GetAttributes() - .Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false); + context.ReportDiagnostic(Diagnostic.Create( + Common.TypeArgumentParentSymbolUnhandledRule, + typeArgumentSyntax.GetLocation(), + parentSymbol.ToDisplayString() + )); + + return false; } } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs index 7b643914bb..f314f7dada 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs @@ -30,16 +30,13 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) { - if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _)) { - Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); return false; } return true; } - - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) .Select(x => x.symbol) @@ -105,16 +102,20 @@ namespace Godot.SourceGenerators if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -140,7 +141,7 @@ namespace Godot.SourceGenerators .Append(" /// </summary>\n"); source.Append( - $" public new class MethodName : {symbol.BaseType.FullQualifiedNameIncludeGlobal()}.MethodName {{\n"); + $" public new class MethodName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.MethodName {{\n"); // Generate cached StringNames for methods and properties, for fast lookup @@ -170,7 +171,7 @@ namespace Godot.SourceGenerators if (godotClassMethods.Length > 0) { - const string listType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; + const string ListType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; source.Append(" /// <summary>\n") .Append(" /// Get the method information for all the methods declared in this class.\n") @@ -181,11 +182,11 @@ namespace Godot.SourceGenerators source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n"); source.Append(" internal new static ") - .Append(listType) + .Append(ListType) .Append(" GetGodotMethodList()\n {\n"); source.Append(" var methods = new ") - .Append(listType) + .Append(ListType) .Append("(") .Append(godotClassMethods.Length) .Append(");\n"); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs index 01aafe9c74..59cf9946c9 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPathAttributeGenerator.cs @@ -48,21 +48,19 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) return true; - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) ) .Where(x => // Ignore classes whose name is not the same as the file name - Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name && - // Ignore generic classes - !x.symbol.IsGenericType) - .GroupBy(x => x.symbol) - .ToDictionary(g => g.Key, g => g.Select(x => x.cds)); + Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name) + .GroupBy<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol), INamedTypeSymbol>(x => x.symbol, SymbolEqualityComparer.Default) + .ToDictionary<IGrouping<INamedTypeSymbol, (ClassDeclarationSyntax cds, INamedTypeSymbol symbol)>, INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>>(g => g.Key, g => g.Select(x => x.cds), SymbolEqualityComparer.Default); + var usedPaths = new HashSet<string>(); foreach (var godotClass in godotClasses) { - VisitGodotScriptClass(context, godotProjectDir, + VisitGodotScriptClass(context, godotProjectDir, usedPaths, symbol: godotClass.Key, classDeclarations: godotClass.Value); } @@ -76,6 +74,7 @@ namespace Godot.SourceGenerators private static void VisitGodotScriptClass( GeneratorExecutionContext context, string godotProjectDir, + HashSet<string> usedPaths, INamedTypeSymbol symbol, IEnumerable<ClassDeclarationSyntax> classDeclarations ) @@ -95,8 +94,19 @@ namespace Godot.SourceGenerators if (attributes.Length != 0) attributes.Append("\n"); + string scriptPath = RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir); + if (!usedPaths.Add(scriptPath)) + { + context.ReportDiagnostic(Diagnostic.Create( + Common.MultipleClassesInGodotScriptRule, + cds.Identifier.GetLocation(), + symbol.Name + )); + return; + } + attributes.Append(@"[ScriptPathAttribute(""res://"); - attributes.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir)); + attributes.Append(scriptPath); attributes.Append(@""")]"); } @@ -160,6 +170,8 @@ namespace Godot.SourceGenerators first = false; sourceBuilder.Append("typeof("); sourceBuilder.Append(qualifiedName); + if (godotClass.Key.IsGenericType) + sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>"); sourceBuilder.Append(")"); } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index 219ab7aa44..a0e410e31a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -30,16 +30,13 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) { - if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _)) { - Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); return false; } return true; } - - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) .Select(x => x.symbol) @@ -91,16 +88,20 @@ namespace Godot.SourceGenerators if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -129,7 +130,7 @@ namespace Godot.SourceGenerators .Append(" /// </summary>\n"); source.Append( - $" public new class PropertyName : {symbol.BaseType.FullQualifiedNameIncludeGlobal()}.PropertyName {{\n"); + $" public new class PropertyName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.PropertyName {{\n"); // Generate cached StringNames for methods and properties, for fast lookup @@ -245,7 +246,7 @@ namespace Godot.SourceGenerators } // Generate GetGodotPropertyList - const string dictionaryType = "global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>"; + const string DictionaryType = "global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>"; source.Append(" /// <summary>\n") .Append(" /// Get the property information for all the properties declared in this class.\n") @@ -256,11 +257,11 @@ namespace Godot.SourceGenerators source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n"); source.Append(" internal new static ") - .Append(dictionaryType) + .Append(DictionaryType) .Append(" GetGodotPropertyList()\n {\n"); source.Append(" var properties = new ") - .Append(dictionaryType) + .Append(DictionaryType) .Append("();\n"); // To retain the definition order (and display categories correctly), we want to @@ -439,14 +440,22 @@ namespace Godot.SourceGenerators if (propertySymbol.GetMethod == null) { // This should never happen, as we filtered WriteOnly properties, but just in case. - Common.ReportExportedMemberIsWriteOnly(context, propertySymbol); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedPropertyIsWriteOnlyRule, + propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + propertySymbol.ToDisplayString() + )); return null; } if (propertySymbol.SetMethod == null || propertySymbol.SetMethod.IsInitOnly) { // This should never happen, as we filtered ReadOnly properties, but just in case. - Common.ReportExportedMemberIsReadOnly(context, propertySymbol); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsReadOnlyRule, + propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + propertySymbol.ToDisplayString() + )); return null; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs index 4df16d05f0..d13a828875 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertyDefValGenerator.cs @@ -31,16 +31,14 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) { - if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _)) { - Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); return false; } return true; } - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) .Select(x => x.symbol) @@ -66,11 +64,13 @@ namespace Godot.SourceGenerators ) { INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace; - string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ? - namespaceSymbol.FullQualifiedNameOmitGlobal() : - string.Empty; + string classNs = namespaceSymbol is { IsGlobalNamespace: false } + ? namespaceSymbol.FullQualifiedNameOmitGlobal() + : string.Empty; bool hasNamespace = classNs.Length != 0; + bool isNode = symbol.InheritsFrom("GodotSharp", GodotClasses.Node); + bool isInnerClass = symbol.ContainingType != null; string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint() @@ -88,16 +88,20 @@ namespace Godot.SourceGenerators if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -110,14 +114,14 @@ namespace Godot.SourceGenerators var members = symbol.GetMembers(); var exportedProperties = members - .Where(s => !s.IsStatic && s.Kind == SymbolKind.Property) + .Where(s => s.Kind == SymbolKind.Property) .Cast<IPropertySymbol>() .Where(s => s.GetAttributes() .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) .ToArray(); var exportedFields = members - .Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) + .Where(s => s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared) .Cast<IFieldSymbol>() .Where(s => s.GetAttributes() .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false)) @@ -127,33 +131,55 @@ namespace Godot.SourceGenerators { if (property.IsStatic) { - Common.ReportExportedMemberIsStatic(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsStaticRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } if (property.IsIndexer) { - Common.ReportExportedMemberIsIndexer(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsIndexerRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } - // TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. - // Ignore properties without a getter, without a setter or with an init-only setter. Godot properties must be both readable and writable. + // TODO: We should still restore read-only properties after reloading assembly. + // Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload. + // Ignore properties without a getter, without a setter or with an init-only setter. + // Godot properties must be both readable and writable. if (property.IsWriteOnly) { - Common.ReportExportedMemberIsWriteOnly(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedPropertyIsWriteOnlyRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } if (property.IsReadOnly || property.SetMethod!.IsInitOnly) { - Common.ReportExportedMemberIsReadOnly(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsReadOnlyRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } if (property.ExplicitInterfaceImplementations.Length > 0) { - Common.ReportExportedMemberIsExplicitInterfaceImplementation(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsExplicitInterfaceImplementationRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } @@ -162,10 +188,26 @@ namespace Godot.SourceGenerators if (marshalType == null) { - Common.ReportExportedMemberTypeNotSupported(context, property); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberTypeIsNotSupportedRule, + property.Locations.FirstLocationWithSourceTreeOrDefault(), + property.ToDisplayString() + )); continue; } + if (marshalType == MarshalType.GodotObjectOrDerived) + { + if (!isNode && propertyType.InheritsFrom("GodotSharp", GodotClasses.Node)) + { + context.ReportDiagnostic(Diagnostic.Create( + Common.OnlyNodesShouldExportNodesRule, + property.Locations.FirstLocationWithSourceTreeOrDefault() + )); + continue; + } + } + var propertyDeclarationSyntax = property.DeclaringSyntaxReferences .Select(r => r.GetSyntax() as PropertyDeclarationSyntax).FirstOrDefault(); @@ -181,7 +223,7 @@ namespace Godot.SourceGenerators else { var propertyGet = propertyDeclarationSyntax.AccessorList?.Accessors - .Where(a => a.Keyword.IsKind(SyntaxKind.GetKeyword)).FirstOrDefault(); + .FirstOrDefault(a => a.Keyword.IsKind(SyntaxKind.GetKeyword)); if (propertyGet != null) { if (propertyGet.ExpressionBody != null) @@ -240,7 +282,11 @@ namespace Godot.SourceGenerators { if (field.IsStatic) { - Common.ReportExportedMemberIsStatic(context, field); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsStaticRule, + field.Locations.FirstLocationWithSourceTreeOrDefault(), + field.ToDisplayString() + )); continue; } @@ -248,7 +294,11 @@ namespace Godot.SourceGenerators // Ignore properties without a getter or without a setter. Godot properties must be both readable and writable. if (field.IsReadOnly) { - Common.ReportExportedMemberIsReadOnly(context, field); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberIsReadOnlyRule, + field.Locations.FirstLocationWithSourceTreeOrDefault(), + field.ToDisplayString() + )); continue; } @@ -257,10 +307,26 @@ namespace Godot.SourceGenerators if (marshalType == null) { - Common.ReportExportedMemberTypeNotSupported(context, field); + context.ReportDiagnostic(Diagnostic.Create( + Common.ExportedMemberTypeIsNotSupportedRule, + field.Locations.FirstLocationWithSourceTreeOrDefault(), + field.ToDisplayString() + )); continue; } + if (marshalType == MarshalType.GodotObjectOrDerived) + { + if (!isNode && fieldType.InheritsFrom("GodotSharp", GodotClasses.Node)) + { + context.ReportDiagnostic(Diagnostic.Create( + Common.OnlyNodesShouldExportNodesRule, + field.Locations.FirstLocationWithSourceTreeOrDefault() + )); + continue; + } + } + EqualsValueClauseSyntax? initializer = field.DeclaringSyntaxReferences .Select(r => r.GetSyntax()) .OfType<VariableDeclaratorSyntax>() @@ -285,7 +351,7 @@ namespace Godot.SourceGenerators { source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n"); - const string dictionaryType = + const string DictionaryType = "global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>"; source.Append("#if TOOLS\n"); @@ -300,11 +366,11 @@ namespace Godot.SourceGenerators source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n"); source.Append(" internal new static "); - source.Append(dictionaryType); + source.Append(DictionaryType); source.Append(" GetGodotPropertyDefaultValues()\n {\n"); source.Append(" var values = new "); - source.Append(dictionaryType); + source.Append(DictionaryType); source.Append("("); source.Append(exportedMembers.Count); source.Append(");\n"); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs index 9de99414b6..df0484333a 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSerializationGenerator.cs @@ -30,16 +30,14 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) { - if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _)) { - Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); return false; } return true; } - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) .Select(x => x.symbol) @@ -91,16 +89,20 @@ namespace Godot.SourceGenerators if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs index 8f2774d5ae..107bd93faa 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptSignalsGenerator.cs @@ -37,16 +37,14 @@ namespace Godot.SourceGenerators { if (x.cds.IsPartial()) { - if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial)) + if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out _)) { - Common.ReportNonPartialGodotScriptOuterClass(context, typeMissingPartial!); return false; } return true; } - Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol); return false; }) .Select(x => x.symbol) @@ -100,16 +98,20 @@ namespace Godot.SourceGenerators if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -132,7 +134,11 @@ namespace Godot.SourceGenerators { if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix)) { - Common.ReportSignalDelegateMissingSuffix(context, signalDelegateSymbol); + context.ReportDiagnostic(Diagnostic.Create( + Common.SignalDelegateMissingSuffixRule, + signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + signalDelegateSymbol.ToDisplayString() + )); continue; } @@ -150,21 +156,32 @@ namespace Godot.SourceGenerators { if (parameter.RefKind != RefKind.None) { - Common.ReportSignalParameterTypeNotSupported(context, parameter); + context.ReportDiagnostic(Diagnostic.Create( + Common.SignalParameterTypeNotSupportedRule, + parameter.Locations.FirstLocationWithSourceTreeOrDefault(), + parameter.ToDisplayString() + )); continue; } var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache); - if (marshalType == null) { - Common.ReportSignalParameterTypeNotSupported(context, parameter); + context.ReportDiagnostic(Diagnostic.Create( + Common.SignalParameterTypeNotSupportedRule, + parameter.Locations.FirstLocationWithSourceTreeOrDefault(), + parameter.ToDisplayString() + )); } } if (!methodSymbol.ReturnsVoid) { - Common.ReportSignalDelegateSignatureMustReturnVoid(context, signalDelegateSymbol); + context.ReportDiagnostic(Diagnostic.Create( + Common.SignalDelegateSignatureMustReturnVoidRule, + signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(), + signalDelegateSymbol.ToDisplayString() + )); } } @@ -181,7 +198,7 @@ namespace Godot.SourceGenerators .Append(" /// </summary>\n"); source.Append( - $" public new class SignalName : {symbol.BaseType.FullQualifiedNameIncludeGlobal()}.SignalName {{\n"); + $" public new class SignalName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.SignalName {{\n"); // Generate cached StringNames for methods and properties, for fast lookup @@ -208,7 +225,7 @@ namespace Godot.SourceGenerators if (godotSignalDelegates.Count > 0) { - const string listType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; + const string ListType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>"; source.Append(" /// <summary>\n") .Append(" /// Get the signal information for all the signals declared in this class.\n") @@ -219,11 +236,11 @@ namespace Godot.SourceGenerators source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n"); source.Append(" internal new static ") - .Append(listType) + .Append(ListType) .Append(" GetGodotSignalList()\n {\n"); source.Append(" var signals = new ") - .Append(listType) + .Append(ListType) .Append("(") .Append(godotSignalDelegates.Count) .Append(");\n"); diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index 01aa65bfc3..b699765b8e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -7,11 +7,11 @@ namespace GodotTools.BuildLogger { public class GodotBuildLogger : ILogger { - public string Parameters { get; set; } + public string? Parameters { get; set; } public LoggerVerbosity Verbosity { get; set; } - private StreamWriter _logStreamWriter; - private StreamWriter _issuesStreamWriter; + private StreamWriter _logStreamWriter = StreamWriter.Null; + private StreamWriter _issuesStreamWriter = StreamWriter.Null; private int _indent; public void Initialize(IEventSource eventSource) @@ -165,7 +165,7 @@ namespace GodotTools.BuildLogger bool hasSpecialChar = value.IndexOfAny(new[] { '\"', '\n', '\r', delimiter }) != -1; if (hasSpecialChar) - return "\"" + value.Replace("\"", "\"\"") + "\""; + return "\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; return value; } diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj index 9e36497b06..fd836f9ad2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotTools.BuildLogger.csproj @@ -1,8 +1,9 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid> - <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" ExcludeAssets="runtime" /> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj index cfd5c88a58..0755484465 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.Core/GodotTools.Core.csproj @@ -3,5 +3,6 @@ <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid> <TargetFramework>net6.0</TargetFramework> <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> </PropertyGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/ProcessExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/ProcessExtensions.cs index a4d7dedbd5..4dd18b4c03 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/ProcessExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/ProcessExtensions.cs @@ -11,7 +11,7 @@ namespace GodotTools.Core { var tcs = new TaskCompletionSource<bool>(); - void ProcessExited(object sender, EventArgs e) + void ProcessExited(object? sender, EventArgs e) { tcs.TrySetResult(true); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index d86a77d222..2840fa375c 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -7,7 +7,7 @@ namespace GodotTools.Core { public static class StringExtensions { - private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory); + private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory)!; public static string RelativeToPath(this string path, string dir) { @@ -15,7 +15,7 @@ namespace GodotTools.Core dir = Path.Combine(dir, " ").TrimEnd(); if (Path.DirectorySeparatorChar == '\\') - dir = dir.Replace("/", "\\") + "\\"; + dir = dir.Replace("/", "\\", StringComparison.Ordinal) + "\\"; var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute); var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute); @@ -26,9 +26,6 @@ namespace GodotTools.Core public static string NormalizePath(this string path) { - if (string.IsNullOrEmpty(path)) - return path; - bool rooted = path.IsAbsolutePath(); path = path.Replace('\\', '/'); diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs index 3cb6a6687e..67d55d3897 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/ForwarderMessageHandler.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -28,7 +29,7 @@ namespace GodotTools.IdeMessaging.CLI { await outputWriter.WriteLineAsync("======= Request ======="); await outputWriter.WriteLineAsync(id); - await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString()); + await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString(CultureInfo.InvariantCulture)); await outputWriter.WriteLineAsync(content.Body); await outputWriter.WriteLineAsync("======================="); await outputWriter.FlushAsync(); @@ -41,7 +42,7 @@ namespace GodotTools.IdeMessaging.CLI { await outputWriter.WriteLineAsync("======= Response ======="); await outputWriter.WriteLineAsync(id); - await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString()); + await outputWriter.WriteLineAsync(content.Body.Count(c => c == '\n').ToString(CultureInfo.InvariantCulture)); await outputWriter.WriteLineAsync(content.Body); await outputWriter.WriteLineAsync("========================"); await outputWriter.FlushAsync(); diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj index d2132115f3..3bf678e9f9 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/GodotTools.IdeMessaging.CLI.csproj @@ -2,13 +2,11 @@ <PropertyGroup> <ProjectGuid>{B06C2951-C8E3-4F28-80B2-717CF327EB19}</ProjectGuid> <OutputType>Exe</OutputType> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> + <TargetFramework>net6.0</TargetFramework> + <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> </ItemGroup> <ItemGroup> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs index 450c4bf0cb..e344aa4a37 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging.CLI/Program.cs @@ -64,14 +64,14 @@ namespace GodotTools.IdeMessaging.CLI while (!fwdClient.IsDisposed) { - string firstLine = await inputReader.ReadLineAsync(); + string? firstLine = await inputReader.ReadLineAsync(); if (firstLine == null || firstLine == "QUIT") goto ExitMainLoop; string messageId = firstLine; - string messageArgcLine = await inputReader.ReadLineAsync(); + string? messageArgcLine = await inputReader.ReadLineAsync(); if (messageArgcLine == null) { @@ -89,7 +89,7 @@ namespace GodotTools.IdeMessaging.CLI for (int i = 0; i < messageArgc; i++) { - string bodyLine = await inputReader.ReadLineAsync(); + string? bodyLine = await inputReader.ReadLineAsync(); if (bodyLine == null) { @@ -126,29 +126,29 @@ namespace GodotTools.IdeMessaging.CLI } } - private static async Task<Response> SendRequest(Client client, string id, MessageContent content) + private static async Task<Response?> SendRequest(Client client, string id, MessageContent content) { - var handlers = new Dictionary<string, Func<Task<Response>>> + var handlers = new Dictionary<string, Func<Task<Response?>>> { [PlayRequest.Id] = async () => { var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body); - return await client.SendRequest<PlayResponse>(request); + return await client.SendRequest<PlayResponse>(request!); }, [DebugPlayRequest.Id] = async () => { var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body); - return await client.SendRequest<DebugPlayResponse>(request); + return await client.SendRequest<DebugPlayResponse>(request!); }, [ReloadScriptsRequest.Id] = async () => { var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body); - return await client.SendRequest<ReloadScriptsResponse>(request); + return await client.SendRequest<ReloadScriptsResponse>(request!); }, [CodeCompletionRequest.Id] = async () => { var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body); - return await client.SendRequest<CodeCompletionResponse>(request); + return await client.SendRequest<CodeCompletionResponse>(request!); } }; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs index 72e2a1fc0d..7bfa07be0b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Client.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.IO; using System.Net; @@ -27,7 +28,7 @@ namespace GodotTools.IdeMessaging private readonly IMessageHandler messageHandler; - private Peer peer; + private Peer? peer; private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1); private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>(); @@ -53,6 +54,7 @@ namespace GodotTools.IdeMessaging public bool IsDisposed { get; private set; } // ReSharper disable once MemberCanBePrivate.Global + [MemberNotNullWhen(true, "peer")] public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected; // ReSharper disable once EventNeverSubscribedTo.Global @@ -111,7 +113,7 @@ namespace GodotTools.IdeMessaging if (disposing) { peer?.Dispose(); - fsWatcher?.Dispose(); + fsWatcher.Dispose(); } } @@ -215,12 +217,12 @@ namespace GodotTools.IdeMessaging using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var reader = new StreamReader(fileStream)) { - string portStr = reader.ReadLine(); + string? portStr = reader.ReadLine(); if (portStr == null) return null; - string editorExecutablePath = reader.ReadLine(); + string? editorExecutablePath = reader.ReadLine(); if (editorExecutablePath == null) return null; @@ -333,7 +335,7 @@ namespace GodotTools.IdeMessaging } } - public async Task<TResponse> SendRequest<TResponse>(Request request) + public async Task<TResponse?> SendRequest<TResponse>(Request request) where TResponse : Response, new() { if (!IsConnected) @@ -346,7 +348,7 @@ namespace GodotTools.IdeMessaging return await peer.SendRequest<TResponse>(request.Id, body); } - public async Task<TResponse> SendRequest<TResponse>(string id, string body) + public async Task<TResponse?> SendRequest<TResponse>(string id, string body) where TResponse : Response, new() { if (!IsConnected) diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs index 43041be7be..7fef628c79 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientHandshake.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace GodotTools.IdeMessaging @@ -9,7 +10,7 @@ namespace GodotTools.IdeMessaging public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}"; - public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) + public bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger) { identity = null; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs index 64bcfd824c..0321aa1b57 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ClientMessageHandler.cs @@ -42,7 +42,7 @@ namespace GodotTools.IdeMessaging [OpenFileRequest.Id] = async (peer, content) => { var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body); - return await HandleOpenFile(request); + return await HandleOpenFile(request!); } }; } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/CodeAnalysisAttributes.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/CodeAnalysisAttributes.cs new file mode 100644 index 0000000000..a4a7ee82df --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/CodeAnalysisAttributes.cs @@ -0,0 +1,142 @@ +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary> + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute + { } + + /// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary> + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute + { } + + /// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary> + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute + { } + + /// <summary>Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.</summary> + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { } + + /// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary> + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// <summary>Initializes the attribute with the specified return value condition.</summary> + /// <param name="returnValue"> + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// </param> + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// <summary>Gets the return value condition.</summary> + public bool ReturnValue { get; } + } + + /// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary> + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// <summary>Initializes the attribute with the specified return value condition.</summary> + /// <param name="returnValue"> + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// </param> + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// <summary>Gets the return value condition.</summary> + public bool ReturnValue { get; } + } + + /// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary> + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// <summary>Initializes the attribute with the associated parameter name.</summary> + /// <param name="parameterName"> + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// </param> + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// <summary>Gets the associated parameter name.</summary> + public string ParameterName { get; } + } + + /// <summary>Applied to a method that will never return under any circumstance.</summary> + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { } + + /// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary> + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// <summary>Initializes the attribute with the specified parameter value.</summary> + /// <param name="parameterValue"> + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// </param> + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// <summary>Gets the condition parameter value.</summary> + public bool ParameterValue { get; } + } + + /// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary> + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// <summary>Initializes the attribute with a field or property member.</summary> + /// <param name="member"> + /// The field or property member that is promised to be not-null. + /// </param> + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// <summary>Initializes the attribute with the list of field and property members.</summary> + /// <param name="members"> + /// The list of field and property members that are promised to be not-null. + /// </param> + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// <summary>Gets field or property member names.</summary> + public string[] Members { get; } + } + + /// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary> + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary> + /// <param name="returnValue"> + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// </param> + /// <param name="member"> + /// The field or property member that is promised to be not-null. + /// </param> + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary> + /// <param name="returnValue"> + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// </param> + /// <param name="members"> + /// The list of field and property members that are promised to be not-null. + /// </param> + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// <summary>Gets the return value condition.</summary> + public bool ReturnValue { get; } + + /// <summary>Gets field or property member names.</summary> + public string[] Members { get; } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs index 2448a2953b..f11a7cc149 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotIdeMetadata.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace GodotTools.IdeMessaging { public readonly struct GodotIdeMetadata @@ -23,7 +25,7 @@ namespace GodotTools.IdeMessaging return !(a == b); } - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { return obj is GodotIdeMetadata metadata && metadata == this; } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj index 02f1764f32..be6af398e2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/GodotTools.IdeMessaging.csproj @@ -2,9 +2,10 @@ <PropertyGroup> <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid> <TargetFramework>netstandard2.0</TargetFramework> - <LangVersion>7.2</LangVersion> + <LangVersion>9</LangVersion> + <Nullable>enable</Nullable> <PackageId>GodotTools.IdeMessaging</PackageId> - <Version>1.1.1</Version> + <Version>1.1.2</Version> <AssemblyVersion>$(Version)</AssemblyVersion> <Authors>Godot Engine contributors</Authors> <Company /> diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs index 6387145a28..8ec60876cc 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/IHandshake.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + namespace GodotTools.IdeMessaging { public interface IHandshake { string GetHandshakeLine(string identity); - bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger); + bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger); } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs index a00575a2a1..caf1ae2705 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/MessageDecoder.cs @@ -8,7 +8,7 @@ namespace GodotTools.IdeMessaging private class DecodedMessage { public MessageKind? Kind; - public string Id; + public string? Id; public MessageStatus? Status; public readonly StringBuilder Body = new StringBuilder(); public uint? PendingBodyLines; @@ -41,7 +41,7 @@ namespace GodotTools.IdeMessaging private readonly DecodedMessage decodingMessage = new DecodedMessage(); - public State Decode(string messageLine, out Message decodedMessage) + public State Decode(string messageLine, out Message? decodedMessage) { decodedMessage = null; diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs index dd3913b4f3..5e453dcfab 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Peer.cs @@ -46,11 +46,11 @@ namespace GodotTools.IdeMessaging private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1); - private string remoteIdentity = string.Empty; - public string RemoteIdentity => remoteIdentity; + private string? remoteIdentity; + public string RemoteIdentity => remoteIdentity ??= string.Empty; - public event Action Connected; - public event Action Disconnected; + public event Action? Connected; + public event Action? Disconnected; private ILogger Logger { get; } @@ -87,7 +87,7 @@ namespace GodotTools.IdeMessaging { var decoder = new MessageDecoder(); - string messageLine; + string? messageLine; while ((messageLine = await ReadLine()) != null) { var state = decoder.Decode(messageLine, out var msg); @@ -105,7 +105,7 @@ namespace GodotTools.IdeMessaging try { - if (msg.Kind == MessageKind.Request) + if (msg!.Kind == MessageKind.Request) { var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger); await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent)); @@ -163,9 +163,9 @@ namespace GodotTools.IdeMessaging return false; } - string peerHandshake = await readHandshakeTask; + string? peerHandshake = await readHandshakeTask; - if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger)) + if (peerHandshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger)) { Logger.LogError("Received invalid handshake: " + peerHandshake); return false; @@ -179,7 +179,7 @@ namespace GodotTools.IdeMessaging return true; } - private async Task<string> ReadLine() + private async Task<string?> ReadLine() { try { @@ -216,7 +216,7 @@ namespace GodotTools.IdeMessaging return WriteLine(builder.ToString()); } - public async Task<TResponse> SendRequest<TResponse>(string id, string body) + public async Task<TResponse?> SendRequest<TResponse>(string id, string body) where TResponse : Response, new() { ResponseAwaiter responseAwaiter; @@ -243,7 +243,7 @@ namespace GodotTools.IdeMessaging private async Task<bool> WriteLine(string text) { - if (clientWriter == null || IsDisposed || !IsTcpClientConnected) + if (IsDisposed || !IsTcpClientConnected) return false; using (await writeSem.UseAsync()) @@ -290,9 +290,9 @@ namespace GodotTools.IdeMessaging Disconnected?.Invoke(); } - clientReader?.Dispose(); - clientWriter?.Dispose(); - ((IDisposable)tcpClient)?.Dispose(); + clientReader.Dispose(); + clientWriter.Dispose(); + ((IDisposable)tcpClient).Dispose(); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs index e93db9377b..452593b673 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Requests/Requests.cs @@ -2,6 +2,7 @@ // ReSharper disable UnusedMember.Global // ReSharper disable UnusedAutoPropertyAccessor.Global +using System; using Newtonsoft.Json; namespace GodotTools.IdeMessaging.Requests @@ -38,7 +39,7 @@ namespace GodotTools.IdeMessaging.Requests } public CompletionKind Kind { get; set; } - public string ScriptFile { get; set; } + public string ScriptFile { get; set; } = string.Empty; public new const string Id = "CodeCompletion"; @@ -50,8 +51,8 @@ namespace GodotTools.IdeMessaging.Requests public sealed class CodeCompletionResponse : Response { public CodeCompletionRequest.CompletionKind Kind; - public string ScriptFile { get; set; } - public string[] Suggestions { get; set; } + public string ScriptFile { get; set; } = string.Empty; + public string[] Suggestions { get; set; } = Array.Empty<string>(); } public sealed class PlayRequest : Request @@ -82,7 +83,7 @@ namespace GodotTools.IdeMessaging.Requests public sealed class DebugPlayRequest : Request { - public string DebuggerHost { get; set; } + public string DebuggerHost { get; set; } = string.Empty; public int DebuggerPort { get; set; } public bool? BuildBeforePlaying { get; set; } @@ -99,7 +100,7 @@ namespace GodotTools.IdeMessaging.Requests public sealed class OpenFileRequest : Request { - public string File { get; set; } + public string File { get; set; } = string.Empty; public int? Line { get; set; } public int? Column { get; set; } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs index a57c82b608..b09575d5d2 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/ResponseAwaiter.cs @@ -15,7 +15,7 @@ namespace GodotTools.IdeMessaging public override void SetResult(MessageContent content) { if (content.Status == MessageStatus.Ok) - SetResult(JsonConvert.DeserializeObject<T>(content.Body)); + SetResult(JsonConvert.DeserializeObject<T>(content.Body)!); else SetResult(new T { Status = content.Status }); } diff --git a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs index a285d5fa97..6dde1828a9 100644 --- a/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs +++ b/modules/mono/editor/GodotTools/GodotTools.IdeMessaging/Utils/NotifyAwaiter.cs @@ -1,3 +1,6 @@ +// ReSharper disable ParameterHidesMember +// ReSharper disable UnusedMember.Global + using System; using System.Runtime.CompilerServices; @@ -5,9 +8,9 @@ namespace GodotTools.IdeMessaging.Utils { public class NotifyAwaiter<T> : INotifyCompletion { - private Action continuation; - private Exception exception; - private T result; + private Action? continuation; + private Exception? exception; + private T? result; public bool IsCompleted { get; private set; } @@ -15,7 +18,7 @@ namespace GodotTools.IdeMessaging.Utils { if (exception != null) throw exception; - return result; + return result!; } public void OnCompleted(Action continuation) diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj index c05096bdcc..09908c85a1 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/GodotTools.OpenVisualStudio.csproj @@ -1,12 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid> - <OutputType>Exe</OutputType> - <TargetFramework>net472</TargetFramework> - <LangVersion>7.2</LangVersion> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> - <PackageReference Include="EnvDTE" Version="8.0.2" /> - </ItemGroup> + <PropertyGroup> + <ProjectGuid>{EAFFF236-FA96-4A4D-BD23-0E51EF988277}</ProjectGuid> + <OutputType>Exe</OutputType> + <TargetFramework>net6.0-windows</TargetFramework> + <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> + <SelfContained>False</SelfContained> + </PropertyGroup> + <PropertyGroup Condition="Exists('$(SolutionDir)/../../../../bin/GodotSharp/Api/Debug/GodotSharp.dll') And ('$(GodotPlatform)' == 'windows' Or ('$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT'))"> + <OutputPath>$(SolutionDir)/../../../../bin/GodotSharp/Tools</OutputPath> + <AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath> + <AppendRuntimeIdentifierToOutputPath>False</AppendRuntimeIdentifierToOutputPath> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="EnvDTE" Version="17.8.37221" /> + </ItemGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs index cc0deb6cc0..5bf07f626b 100644 --- a/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs +++ b/modules/mono/editor/GodotTools/GodotTools.OpenVisualStudio/Program.cs @@ -53,6 +53,12 @@ namespace GodotTools.OpenVisualStudio { // Launch of VS 2022 failed, fallback to 2019 dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0"); + + if (dte == null) + { + Console.Error.WriteLine("Visual Studio not found"); + return 1; + } } dte.UserControl = true; @@ -129,7 +135,7 @@ namespace GodotTools.OpenVisualStudio { var mainWindow = dte.MainWindow; mainWindow.Activate(); - SetForegroundWindow(new IntPtr(mainWindow.HWnd)); + SetForegroundWindow(mainWindow.HWnd); MessageFilter.Revoke(); } @@ -137,12 +143,12 @@ namespace GodotTools.OpenVisualStudio return 0; } - private static DTE TryVisualStudioLaunch(string version) + private static DTE? TryVisualStudioLaunch(string version) { try { var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true); - var dte = (DTE)Activator.CreateInstance(visualStudioDteType); + var dte = (DTE?)Activator.CreateInstance(visualStudioDteType!); return dte; } @@ -152,7 +158,7 @@ namespace GodotTools.OpenVisualStudio } } - private static DTE FindInstanceEditingSolution(string solutionPath) + private static DTE? FindInstanceEditingSolution(string solutionPath) { if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0) return null; @@ -204,7 +210,7 @@ namespace GodotTools.OpenVisualStudio return null; } - static string NormalizePath(string path) + private static string NormalizePath(string path) { return new Uri(Path.GetFullPath(path)).LocalPath .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) @@ -218,7 +224,7 @@ namespace GodotTools.OpenVisualStudio // Class containing the IOleMessageFilter // thread error-handling functions - private static IOleMessageFilter _oldFilter; + private static IOleMessageFilter? _oldFilter; // Start the filter public static void Register() @@ -268,7 +274,7 @@ namespace GodotTools.OpenVisualStudio // Implement the IOleMessageFilter interface [DllImport("ole32.dll")] - private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter); + private static extern int CoRegisterMessageFilter(IOleMessageFilter? newFilter, out IOleMessageFilter? oldFilter); } [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ApiAssembliesInfo.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ApiAssembliesInfo.cs deleted file mode 100644 index 345a472185..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ApiAssembliesInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace GodotTools -{ - public static class ApiAssemblyNames - { - public const string SolutionName = "GodotSharp"; - public const string Core = "GodotSharp"; - public const string Editor = "GodotSharpEditor"; - } - - public enum ApiAssemblyType - { - Core, - Editor - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs index 355b21d63a..2944dc0d82 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/DotNetSolution.cs @@ -1,5 +1,7 @@ using GodotTools.Core; +using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -34,22 +36,23 @@ EndProject"; @" {{{0}}}.{1}|Any CPU.ActiveCfg = {1}|Any CPU {{{0}}}.{1}|Any CPU.Build.0 = {1}|Any CPU"; - private string _directoryPath; private readonly Dictionary<string, ProjectInfo> _projects = new Dictionary<string, ProjectInfo>(); public string Name { get; } - - public string DirectoryPath - { - get => _directoryPath; - set => _directoryPath = value.IsAbsolutePath() ? value : Path.GetFullPath(value); - } + public string DirectoryPath { get; } public class ProjectInfo { - public string Guid; - public string PathRelativeToSolution; - public List<string> Configs = new List<string>(); + public string Guid { get; } + public string PathRelativeToSolution { get; } + public List<string> Configs { get; } + + public ProjectInfo(string guid, string pathRelativeToSolution, List<string> configs) + { + Guid = guid; + PathRelativeToSolution = pathRelativeToSolution; + Configs = configs; + } } public void AddNewProject(string name, ProjectInfo projectInfo) @@ -91,8 +94,8 @@ EndProject"; if (!isFirstProject) projectsDecl += "\n"; - projectsDecl += string.Format(_projectDeclaration, - name, projectInfo.PathRelativeToSolution.Replace("/", "\\"), projectInfo.Guid); + projectsDecl += string.Format(CultureInfo.InvariantCulture, _projectDeclaration, + name, projectInfo.PathRelativeToSolution.Replace("/", "\\", StringComparison.Ordinal), projectInfo.Guid); for (int i = 0; i < projectInfo.Configs.Count; i++) { @@ -104,22 +107,23 @@ EndProject"; projPlatformsCfg += "\n"; } - slnPlatformsCfg += string.Format(_solutionPlatformsConfig, config); - projPlatformsCfg += string.Format(_projectPlatformsConfig, projectInfo.Guid, config); + slnPlatformsCfg += string.Format(CultureInfo.InvariantCulture, _solutionPlatformsConfig, config); + projPlatformsCfg += string.Format(CultureInfo.InvariantCulture, _projectPlatformsConfig, projectInfo.Guid, config); } isFirstProject = false; } string solutionPath = Path.Combine(DirectoryPath, Name + ".sln"); - string content = string.Format(_solutionTemplate, projectsDecl, slnPlatformsCfg, projPlatformsCfg); + string content = string.Format(CultureInfo.InvariantCulture, _solutionTemplate, projectsDecl, slnPlatformsCfg, projPlatformsCfg); File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM } - public DotNetSolution(string name) + public DotNetSolution(string name, string directoryPath) { Name = name; + DirectoryPath = directoryPath.IsAbsolutePath() ? directoryPath : Path.GetFullPath(directoryPath); } public static void MigrateFromOldConfigNames(string slnPath) diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index bde14b2b40..623475e11a 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -3,6 +3,7 @@ <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid> <TargetFramework>net6.0</TargetFramework> <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" /> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index 7b1d5c228a..794443a69c 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using Microsoft.Build.Construction; +using Microsoft.Build.Locator; namespace GodotTools.ProjectEditor { @@ -19,17 +21,20 @@ namespace GodotTools.ProjectEditor public static class ProjectUtils { - public static void MSBuildLocatorRegisterDefaults(out Version version, out string path) + public static void MSBuildLocatorRegisterLatest(out Version version, out string path) { - var instance = Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); + var instance = MSBuildLocator.QueryVisualStudioInstances() + .OrderByDescending(x => x.Version) + .First(); + MSBuildLocator.RegisterInstance(instance); version = instance.Version; path = instance.MSBuildPath; } public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) - => Microsoft.Build.Locator.MSBuildLocator.RegisterMSBuildPath(msbuildPath); + => MSBuildLocator.RegisterMSBuildPath(msbuildPath); - public static MSBuildProject Open(string path) + public static MSBuildProject? Open(string path) { var root = ProjectRootElement.Open(path); return root != null ? new MSBuildProject(root) : null; diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs index 6e0c63dd43..79eb9e2ce2 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildDiagnostic.cs @@ -1,5 +1,3 @@ -#nullable enable - namespace GodotTools.Build { public class BuildDiagnostic diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs index 7c02f29606..dc9e2c5920 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildInfo.cs @@ -1,11 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using Godot; using Godot.Collections; using GodotTools.Internals; using Path = System.IO.Path; -#nullable enable - namespace GodotTools.Build { [Serializable] @@ -25,7 +24,7 @@ namespace GodotTools.Build public string LogsDirPath => GodotSharpDirs.LogsDirPathFor(Solution, Configuration); - public override bool Equals(object? obj) + public override bool Equals([NotNullWhen(true)] object? obj) { return obj is BuildInfo other && other.Solution == Solution && @@ -39,21 +38,18 @@ namespace GodotTools.Build public override int GetHashCode() { - unchecked - { - int hash = 17; - hash = (hash * 29) + Solution.GetHashCode(); - hash = (hash * 29) + Project.GetHashCode(); - hash = (hash * 29) + Configuration.GetHashCode(); - hash = (hash * 29) + (RuntimeIdentifier?.GetHashCode() ?? 0); - hash = (hash * 29) + (PublishOutputDir?.GetHashCode() ?? 0); - hash = (hash * 29) + Restore.GetHashCode(); - hash = (hash * 29) + Rebuild.GetHashCode(); - hash = (hash * 29) + OnlyClean.GetHashCode(); - hash = (hash * 29) + CustomProperties.GetHashCode(); - hash = (hash * 29) + LogsDirPath.GetHashCode(); - return hash; - } + var hash = new HashCode(); + hash.Add(Solution); + hash.Add(Project); + hash.Add(Configuration); + hash.Add(RuntimeIdentifier); + hash.Add(PublishOutputDir); + hash.Add(Restore); + hash.Add(Rebuild); + hash.Add(OnlyClean); + hash.Add(CustomProperties); + hash.Add(LogsDirPath); + return hash.ToHashCode(); } // Needed for instantiation from Godot, after reloading assemblies diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index 7cf98b8f1f..ebb2677361 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -11,18 +11,32 @@ namespace GodotTools.Build { public static class BuildManager { - private static BuildInfo _buildInProgress; + private static BuildInfo? _buildInProgress; public const string MsBuildIssuesFileName = "msbuild_issues.csv"; private const string MsBuildLogFileName = "msbuild_log.txt"; public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); - public static event BuildLaunchFailedEventHandler BuildLaunchFailed; - public static event Action<BuildInfo> BuildStarted; - public static event Action<BuildResult> BuildFinished; - public static event Action<string> StdOutputReceived; - public static event Action<string> StdErrorReceived; + public static event BuildLaunchFailedEventHandler? BuildLaunchFailed; + public static event Action<BuildInfo>? BuildStarted; + public static event Action<BuildResult>? BuildFinished; + public static event Action<string?>? StdOutputReceived; + public static event Action<string?>? StdErrorReceived; + + public static DateTime LastValidBuildDateTime { get; private set; } + + static BuildManager() + { + UpdateLastValidBuildDateTime(); + } + + public static void UpdateLastValidBuildDateTime() + { + var dllName = $"{GodotSharpDirs.ProjectAssemblyName}.dll"; + var path = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "Debug", dllName); + LastValidBuildDateTime = File.GetLastWriteTime(path); + } private static void RemoveOldIssuesFile(BuildInfo buildInfo) { @@ -260,8 +274,8 @@ namespace GodotTools.Build } private static BuildInfo CreateBuildInfo( - [DisallowNull] string configuration, - [AllowNull] string platform = null, + string configuration, + string? platform = null, bool rebuild = false, bool onlyClean = false ) @@ -280,10 +294,10 @@ namespace GodotTools.Build } private static BuildInfo CreatePublishBuildInfo( - [DisallowNull] string configuration, - [DisallowNull] string platform, - [DisallowNull] string runtimeIdentifier, - [DisallowNull] string publishOutputDir, + string configuration, + string platform, + string runtimeIdentifier, + string publishOutputDir, bool includeDebugSymbols = true ) { @@ -305,20 +319,20 @@ namespace GodotTools.Build } public static bool BuildProjectBlocking( - [DisallowNull] string configuration, - [AllowNull] string platform = null, + string configuration, + string? platform = null, bool rebuild = false ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild)); public static bool CleanProjectBlocking( - [DisallowNull] string configuration, - [AllowNull] string platform = null + string configuration, + string? platform = null ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false, onlyClean: true)); public static bool PublishProjectBlocking( - [DisallowNull] string configuration, - [DisallowNull] string platform, - [DisallowNull] string runtimeIdentifier, + string configuration, + string platform, + string runtimeIdentifier, string publishOutputDir, bool includeDebugSymbols = true ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs index f9e85c36e5..5cf6581fc4 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs @@ -1,8 +1,6 @@ using Godot; using static GodotTools.Internals.Globals; -#nullable enable - namespace GodotTools.Build { public partial class BuildOutputView : HBoxContainer diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs index 9c165e5767..a0db1cdf03 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsFilter.cs @@ -1,6 +1,5 @@ using Godot; - -#nullable enable +using System.Globalization; namespace GodotTools.Build { @@ -18,7 +17,7 @@ namespace GodotTools.Build set { _problemsCount = value; - ToggleButton.Text = _problemsCount.ToString(); + ToggleButton.Text = _problemsCount.ToString(CultureInfo.InvariantCulture); } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs index b23b3f42ef..b6d6d9ebf8 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildProblemsView.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -8,8 +9,6 @@ using GodotTools.Internals; using static GodotTools.Internals.Globals; using FileAccess = Godot.FileAccess; -#nullable enable - namespace GodotTools.Build { public partial class BuildProblemsView : HBoxContainer @@ -85,8 +84,8 @@ namespace GodotTools.Build "error" or _ => BuildDiagnostic.DiagnosticType.Error, }, File = csvColumns[1], - Line = int.Parse(csvColumns[2]), - Column = int.Parse(csvColumns[3]), + Line = int.Parse(csvColumns[2], CultureInfo.InvariantCulture), + Column = int.Parse(csvColumns[3], CultureInfo.InvariantCulture), Code = csvColumns[4], Message = csvColumns[5], ProjectFile = csvColumns[6], @@ -95,7 +94,7 @@ namespace GodotTools.Build // If there's no ProjectFile but the File is a csproj, then use that. if (string.IsNullOrEmpty(diagnostic.ProjectFile) && !string.IsNullOrEmpty(diagnostic.File) && - diagnostic.File.EndsWith(".csproj")) + diagnostic.File.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) { diagnostic.ProjectFile = diagnostic.File; } @@ -153,9 +152,9 @@ namespace GodotTools.Build foreach (var diagnostic in selectedDiagnostics) { if (!string.IsNullOrEmpty(diagnostic.Code)) - sb.Append($"{diagnostic.Code}: "); + sb.Append(CultureInfo.InvariantCulture, $"{diagnostic.Code}: "); - sb.AppendLine($"{diagnostic.Message} {diagnostic.File}({diagnostic.Line},{diagnostic.Column})"); + sb.AppendLine(CultureInfo.InvariantCulture, $"{diagnostic.Message} {diagnostic.File}({diagnostic.Line},{diagnostic.Column})"); } string text = sb.ToString(); @@ -244,14 +243,16 @@ namespace GodotTools.Build if (string.IsNullOrEmpty(projectDir)) return; - string file = Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath()); + string? file = !string.IsNullOrEmpty(diagnostic.File) ? + Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath()) : + null; if (!File.Exists(file)) return; file = ProjectSettings.LocalizePath(file); - if (file.StartsWith("res://")) + if (file.StartsWith("res://", StringComparison.Ordinal)) { var script = (Script)ResourceLoader.Load(file, typeHint: Internal.CSharpLanguageType); @@ -308,14 +309,14 @@ namespace GodotTools.Build return false; string searchText = _searchBox.Text; - if (!string.IsNullOrEmpty(searchText) && - (!diagnostic.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase) || - !(diagnostic.File?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false))) - { - return false; - } - - return true; + if (string.IsNullOrEmpty(searchText)) + return true; + if (diagnostic.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase)) + return true; + if (diagnostic.File?.Contains(searchText, StringComparison.OrdinalIgnoreCase) ?? false) + return true; + + return false; } private Color? GetProblemItemColor(BuildDiagnostic diagnostic) @@ -426,7 +427,7 @@ namespace GodotTools.Build ? Path.GetRelativePath(projectDir, file) : "Unknown file".TTR(); - string fileItemText = string.Format("{0} ({1} issues)".TTR(), relativeFilePath, fileDiagnostics.Length); + string fileItemText = string.Format(CultureInfo.InvariantCulture, "{0} ({1} issues)".TTR(), relativeFilePath, fileDiagnostics.Length); var fileItem = _problemsTree.CreateItem(projectItem); fileItem.SetText(0, fileItemText); @@ -468,10 +469,10 @@ namespace GodotTools.Build shortMessage = shortMessage[..lineBreakIdx]; text.Append(shortMessage); - tooltip.Append($"Message: {diagnostic.Message}"); + tooltip.Append(CultureInfo.InvariantCulture, $"Message: {diagnostic.Message}"); if (!string.IsNullOrEmpty(diagnostic.Code)) - tooltip.Append($"\nCode: {diagnostic.Code}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nCode: {diagnostic.Code}"); string type = diagnostic.Type switch { @@ -481,7 +482,7 @@ namespace GodotTools.Build BuildDiagnostic.DiagnosticType.Error => "error", _ => "unknown", }; - tooltip.Append($"\nType: {type}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nType: {type}"); if (!string.IsNullOrEmpty(diagnostic.File)) { @@ -491,15 +492,15 @@ namespace GodotTools.Build text.Append(diagnostic.File); } - text.Append($"({diagnostic.Line},{diagnostic.Column})"); + text.Append(CultureInfo.InvariantCulture, $"({diagnostic.Line},{diagnostic.Column})"); - tooltip.Append($"\nFile: {diagnostic.File}"); - tooltip.Append($"\nLine: {diagnostic.Line}"); - tooltip.Append($"\nColumn: {diagnostic.Column}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nFile: {diagnostic.File}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nLine: {diagnostic.Line}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nColumn: {diagnostic.Column}"); } if (!string.IsNullOrEmpty(diagnostic.ProjectFile)) - tooltip.Append($"\nProject: {diagnostic.ProjectFile}"); + tooltip.Append(CultureInfo.InvariantCulture, $"\nProject: {diagnostic.ProjectFile}"); return new ProblemItem() { @@ -516,7 +517,7 @@ namespace GodotTools.Build public override void _Ready() { - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var editorSettings = EditorInterface.Singleton.GetEditorSettings(); _layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>(); Name = "Problems".TTR(); @@ -655,7 +656,7 @@ namespace GodotTools.Build switch ((long)what) { case EditorSettings.NotificationEditorSettingsChanged: - var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); + var editorSettings = EditorInterface.Singleton.GetEditorSettings(); _layout = editorSettings.GetSetting(GodotSharpEditor.Settings.ProblemsLayout).As<ProblemsLayout>(); _toggleLayoutButton.ButtonPressed = GetToggleLayoutPressedState(); UpdateProblemsView(); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index 57b5598a78..5ae4dfa076 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -17,10 +17,10 @@ namespace GodotTools.Build { public static class BuildSystem { - private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, - Action<string> stdErrHandler) + private static Process LaunchBuild(BuildInfo buildInfo, Action<string?>? stdOutHandler, + Action<string?>? stdErrHandler) { - string dotnetPath = DotNetFinder.FindDotNetExe(); + string? dotnetPath = DotNetFinder.FindDotNetExe(); if (dotnetPath == null) throw new FileNotFoundException("Cannot find the dotnet executable."); @@ -67,7 +67,7 @@ namespace GodotTools.Build return process; } - public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int Build(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler) { using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { @@ -77,8 +77,8 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, - Action<string> stdErrHandler) + public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string?>? stdOutHandler, + Action<string?>? stdErrHandler) { using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) { @@ -88,10 +88,10 @@ namespace GodotTools.Build } } - private static Process LaunchPublish(BuildInfo buildInfo, Action<string> stdOutHandler, - Action<string> stdErrHandler) + private static Process LaunchPublish(BuildInfo buildInfo, Action<string?>? stdOutHandler, + Action<string?>? stdErrHandler) { - string dotnetPath = DotNetFinder.FindDotNetExe(); + string? dotnetPath = DotNetFinder.FindDotNetExe(); if (dotnetPath == null) throw new FileNotFoundException("Cannot find the dotnet executable."); @@ -137,7 +137,7 @@ namespace GodotTools.Build return process; } - public static int Publish(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int Publish(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler) { using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler)) { @@ -297,7 +297,7 @@ namespace GodotTools.Build } private static Process DoGenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, - Action<string> stdOutHandler, Action<string> stdErrHandler) + Action<string?>? stdOutHandler, Action<string?>? stdErrHandler) { if (Directory.Exists(xcFrameworkPath)) { @@ -341,7 +341,7 @@ namespace GodotTools.Build return process; } - public static int GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, Action<string> stdOutHandler, Action<string> stdErrHandler) + public static int GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler) { using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler)) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs index cfe79cf3e1..4d1456b70c 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/DotNetFinder.cs @@ -5,15 +5,13 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using System.Text; -using JetBrains.Annotations; using OS = GodotTools.Utils.OS; namespace GodotTools.Build { public static class DotNetFinder { - [CanBeNull] - public static string FindDotNetExe() + public static string? FindDotNetExe() { // In the future, this method may do more than just search in PATH. We could look in // known locations or use Godot's linked nethost to search from the hostfxr location. @@ -40,14 +38,14 @@ namespace GodotTools.Build public static bool TryFindDotNetSdk( Version expectedVersion, - [NotNullWhen(true)] out Version version, - [NotNullWhen(true)] out string path + [NotNullWhen(true)] out Version? version, + [NotNullWhen(true)] out string? path ) { version = null; path = null; - string dotNetExe = FindDotNetExe(); + string? dotNetExe = FindDotNetExe(); if (string.IsNullOrEmpty(dotNetExe)) return false; @@ -86,8 +84,8 @@ namespace GodotTools.Build process.BeginOutputReadLine(); process.WaitForExit(); - Version latestVersionMatch = null; - string matchPath = null; + Version? latestVersionMatch = null; + string? matchPath = null; foreach (var line in lines) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs index bae87dd1dd..986da47860 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MSBuildPanel.cs @@ -5,8 +5,6 @@ using GodotTools.Internals; using static GodotTools.Internals.Globals; using File = GodotTools.Utils.File; -#nullable enable - namespace GodotTools.Build { public partial class MSBuildPanel : MarginContainer, ISerializationListener @@ -89,7 +87,10 @@ namespace GodotTools.Build GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer(); if (Internal.IsAssembliesReloadingNeeded()) + { + BuildManager.UpdateLastValidBuildDateTime(); Internal.ReloadAssemblies(softReload: false); + } } private void RebuildProject() @@ -107,7 +108,10 @@ namespace GodotTools.Build GodotSharpEditor.Instance.GetNode<HotReloadAssemblyWatcher>("HotReloadAssemblyWatcher").RestartTimer(); if (Internal.IsAssembliesReloadingNeeded()) + { + BuildManager.UpdateLastValidBuildDateTime(); Internal.ReloadAssemblies(softReload: false); + } } private void CleanProject() @@ -177,7 +181,7 @@ namespace GodotTools.Build } } - private void StdOutputReceived(string text) + private void StdOutputReceived(string? text) { lock (_pendingBuildLogTextLock) { @@ -187,7 +191,7 @@ namespace GodotTools.Build } } - private void StdErrorReceived(string text) + private void StdErrorReceived(string? text) { lock (_pendingBuildLogTextLock) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs b/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs deleted file mode 100644 index b16adb6f55..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/Export/AotBuilder.cs +++ /dev/null @@ -1,623 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using GodotTools.Internals; -using Directory = GodotTools.Utils.Directory; -using File = GodotTools.Utils.File; -using OS = GodotTools.Utils.OS; -using Path = System.IO.Path; - -namespace GodotTools.Export -{ - public struct AotOptions - { - public bool EnableLLVM; - public bool LLVMOnly; - public string LLVMPath; - public string LLVMOutputPath; - - public bool FullAot; - - private bool _useInterpreter; - public bool UseInterpreter { readonly get => _useInterpreter && !LLVMOnly; set => _useInterpreter = value; } - - public string[] ExtraAotOptions; - public string[] ExtraOptimizerOptions; - - public string ToolchainPath; - } - - public static class AotBuilder - { - public static void CompileAssemblies(ExportPlugin exporter, AotOptions aotOpts, string[] features, string platform, bool isDebug, string bclDir, string outputDir, string outputDataDir, IDictionary<string, string> assemblies) - { - // TODO: WASM - - string aotTempDir = Path.Combine(Path.GetTempPath(), $"godot-aot-{Process.GetCurrentProcess().Id}"); - - if (!Directory.Exists(aotTempDir)) - Directory.CreateDirectory(aotTempDir); - - var assembliesPrepared = new Dictionary<string, string>(); - - foreach (var dependency in assemblies) - { - string assemblyName = dependency.Key; - string assemblyPath = dependency.Value; - - string assemblyPathInBcl = Path.Combine(bclDir, assemblyName + ".dll"); - - if (File.Exists(assemblyPathInBcl)) - { - // Don't create teporaries for assemblies from the BCL - assembliesPrepared.Add(assemblyName, assemblyPathInBcl); - } - else - { - string tempAssemblyPath = Path.Combine(aotTempDir, assemblyName + ".dll"); - File.Copy(assemblyPath, tempAssemblyPath); - assembliesPrepared.Add(assemblyName, tempAssemblyPath); - } - } - - if (platform == OS.Platforms.iOS) - { - string[] architectures = GetEnablediOSArchs(features).ToArray(); - CompileAssembliesForiOS(exporter, isDebug, architectures, aotOpts, aotTempDir, assembliesPrepared, bclDir); - } - else if (platform == OS.Platforms.Android) - { - string[] abis = GetEnabledAndroidAbis(features).ToArray(); - CompileAssembliesForAndroid(exporter, isDebug, abis, aotOpts, aotTempDir, assembliesPrepared, bclDir); - } - else - { - string arch = ""; - if (features.Contains("x86_64")) - { - arch = "x86_64"; - } - else if (features.Contains("x86_32")) - { - arch = "x86_32"; - } - else if (features.Contains("arm64")) - { - arch = "arm64"; - } - else if (features.Contains("arm32")) - { - arch = "arm32"; - } - CompileAssembliesForDesktop(exporter, platform, isDebug, arch, aotOpts, aotTempDir, outputDataDir, assembliesPrepared, bclDir); - } - } - - public static void CompileAssembliesForAndroid(ExportPlugin exporter, bool isDebug, string[] abis, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir) - { - - foreach (var assembly in assemblies) - { - string assemblyName = assembly.Key; - string assemblyPath = assembly.Value; - - // Not sure if the 'lib' prefix is an Android thing or just Godot being picky, - // but we use '-aot-' as well just in case to avoid conflicts with other libs. - string outputFileName = "lib-aot-" + assemblyName + ".dll.so"; - - foreach (string abi in abis) - { - string aotAbiTempDir = Path.Combine(aotTempDir, abi); - string soFilePath = Path.Combine(aotAbiTempDir, outputFileName); - - var compilerArgs = GetAotCompilerArgs(OS.Platforms.Android, isDebug, abi, aotOpts, assemblyPath, soFilePath); - - // Make sure the output directory exists - Directory.CreateDirectory(aotAbiTempDir); - - string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.Android}-{abi}"); - - ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); - - // The Godot exporter expects us to pass the abi in the tags parameter - exporter.AddSharedObject(soFilePath, tags: new[] { abi }, ""); - } - } - } - - public static void CompileAssembliesForDesktop(ExportPlugin exporter, string platform, bool isDebug, string arch, AotOptions aotOpts, string aotTempDir, string outputDataDir, IDictionary<string, string> assemblies, string bclDir) - { - foreach (var assembly in assemblies) - { - string assemblyName = assembly.Key; - string assemblyPath = assembly.Value; - - string outputFileExtension = platform == OS.Platforms.Windows ? ".dll" : - platform == OS.Platforms.MacOS ? ".dylib" : - ".so"; - - string outputFileName = assemblyName + ".dll" + outputFileExtension; - string tempOutputFilePath = Path.Combine(aotTempDir, outputFileName); - - var compilerArgs = GetAotCompilerArgs(platform, isDebug, arch, aotOpts, assemblyPath, tempOutputFilePath); - - string compilerDirPath = GetMonoCrossDesktopDirName(platform, arch); - - ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); - - if (platform == OS.Platforms.MacOS) - { - exporter.AddSharedObject(tempOutputFilePath, tags: null, ""); - } - else - { - string libDir = platform == OS.Platforms.Windows ? "bin" : "lib"; - string outputDataLibDir = Path.Combine(outputDataDir, "Mono", libDir); - File.Copy(tempOutputFilePath, Path.Combine(outputDataLibDir, outputFileName)); - } - } - } - - public static void CompileAssembliesForiOS(ExportPlugin exporter, bool isDebug, string[] architectures, AotOptions aotOpts, string aotTempDir, IDictionary<string, string> assemblies, string bclDir) - { - var cppCode = new StringBuilder(); - var aotModuleInfoSymbols = new List<string>(assemblies.Count); - - // {arch: paths} - var objFilePathsForiOSArch = architectures.ToDictionary(arch => arch, arch => new List<string>(assemblies.Count)); - - foreach (var assembly in assemblies) - { - string assemblyName = assembly.Key; - string assemblyPath = assembly.Value; - - string asmFileName = assemblyName + ".dll.S"; - string objFileName = assemblyName + ".dll.o"; - - foreach (string arch in architectures) - { - string aotArchTempDir = Path.Combine(aotTempDir, arch); - string asmFilePath = Path.Combine(aotArchTempDir, asmFileName); - - var compilerArgs = GetAotCompilerArgs(OS.Platforms.iOS, isDebug, arch, aotOpts, assemblyPath, asmFilePath); - - // Make sure the output directory exists - Directory.CreateDirectory(aotArchTempDir); - - string compilerDirPath = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "aot-compilers", $"{OS.Platforms.iOS}-{arch}"); - - ExecuteCompiler(FindCrossCompiler(compilerDirPath), compilerArgs, bclDir); - - // Assembling - bool isSim = arch == "i386" || arch == "x86_64"; // Shouldn't really happen as we don't do AOT for the simulator - string versionMinName = isSim ? "iphonesimulator" : "iphoneos"; - string iOSPlatformName = isSim ? "iPhoneSimulator" : "iPhoneOS"; - const string versionMin = "10.0"; // TODO: Turn this hard-coded version into an exporter setting - string iOSSdkPath = Path.Combine(XcodeHelper.XcodePath, - $"Contents/Developer/Platforms/{iOSPlatformName}.platform/Developer/SDKs/{iOSPlatformName}.sdk"); - - string objFilePath = Path.Combine(aotArchTempDir, objFileName); - - var clangArgs = new List<string>() - { - "-isysroot", iOSSdkPath, - "-Qunused-arguments", - $"-m{versionMinName}-version-min={versionMin}", - "-arch", arch, - "-c", - "-o", objFilePath, - "-x", "assembler" - }; - - if (isDebug) - clangArgs.Add("-DDEBUG"); - - clangArgs.Add(asmFilePath); - - int clangExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("clang"), clangArgs); - if (clangExitCode != 0) - throw new InvalidOperationException($"Command 'clang' exited with code: {clangExitCode}."); - - objFilePathsForiOSArch[arch].Add(objFilePath); - } - - aotModuleInfoSymbols.Add($"mono_aot_module_{AssemblyNameToAotSymbol(assemblyName)}_info"); - } - - // Generate driver code - cppCode.AppendLine("#if defined(__arm__) || defined(__arm64__) || defined(__aarch64__)"); - cppCode.AppendLine("#define IOS_DEVICE"); - cppCode.AppendLine("#endif"); - - cppCode.AppendLine("#ifdef IOS_DEVICE"); - cppCode.AppendLine("extern \"C\" {"); - cppCode.AppendLine("// Mono API"); - cppCode.AppendLine(@" -typedef enum { -MONO_AOT_MODE_NONE, -MONO_AOT_MODE_NORMAL, -MONO_AOT_MODE_HYBRID, -MONO_AOT_MODE_FULL, -MONO_AOT_MODE_LLVMONLY, -MONO_AOT_MODE_INTERP, -MONO_AOT_MODE_INTERP_LLVMONLY, -MONO_AOT_MODE_LLVMONLY_INTERP, -MONO_AOT_MODE_LAST = 1000, -} MonoAotMode;"); - cppCode.AppendLine("void mono_jit_set_aot_mode(MonoAotMode);"); - cppCode.AppendLine("void mono_aot_register_module(void *);"); - - if (aotOpts.UseInterpreter) - { - cppCode.AppendLine("void mono_ee_interp_init(const char *);"); - cppCode.AppendLine("void mono_icall_table_init();"); - cppCode.AppendLine("void mono_marshal_ilgen_init();"); - cppCode.AppendLine("void mono_method_builder_ilgen_init();"); - cppCode.AppendLine("void mono_sgen_mono_ilgen_init();"); - } - - foreach (string symbol in aotModuleInfoSymbols) - cppCode.AppendLine($"extern void *{symbol};"); - - cppCode.AppendLine("void gd_mono_setup_aot() {"); - - foreach (string symbol in aotModuleInfoSymbols) - cppCode.AppendLine($"\tmono_aot_register_module({symbol});"); - - if (aotOpts.UseInterpreter) - { - cppCode.AppendLine("\tmono_icall_table_init();"); - cppCode.AppendLine("\tmono_marshal_ilgen_init();"); - cppCode.AppendLine("\tmono_method_builder_ilgen_init();"); - cppCode.AppendLine("\tmono_sgen_mono_ilgen_init();"); - cppCode.AppendLine("\tmono_ee_interp_init(0);"); - } - - string aotModeStr = null; - - if (aotOpts.LLVMOnly) - { - aotModeStr = "MONO_AOT_MODE_LLVMONLY"; // --aot=llvmonly - } - else - { - if (aotOpts.UseInterpreter) - aotModeStr = "MONO_AOT_MODE_INTERP"; // --aot=interp or --aot=interp,full - else if (aotOpts.FullAot) - aotModeStr = "MONO_AOT_MODE_FULL"; // --aot=full - } - - // One of the options above is always set for iOS - Debug.Assert(aotModeStr != null); - - cppCode.AppendLine($"\tmono_jit_set_aot_mode({aotModeStr});"); - - cppCode.AppendLine("} // gd_mono_setup_aot"); - cppCode.AppendLine("} // extern \"C\""); - cppCode.AppendLine("#endif // IOS_DEVICE"); - - // Add the driver code to the Xcode project - exporter.AddIosCppCode(cppCode.ToString()); - - // Archive the AOT object files into a static library - - var arFilePathsForAllArchs = new List<string>(); - string projectAssemblyName = GodotSharpDirs.ProjectAssemblyName; - - foreach (var archPathsPair in objFilePathsForiOSArch) - { - string arch = archPathsPair.Key; - var objFilePaths = archPathsPair.Value; - - string arOutputFilePath = Path.Combine(aotTempDir, $"lib-aot-{projectAssemblyName}.{arch}.a"); - - var arArgs = new List<string>() - { - "cr", - arOutputFilePath - }; - - foreach (string objFilePath in objFilePaths) - arArgs.Add(objFilePath); - - int arExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("ar"), arArgs); - if (arExitCode != 0) - throw new InvalidOperationException($"Command 'ar' exited with code: {arExitCode}."); - - arFilePathsForAllArchs.Add(arOutputFilePath); - } - - // It's lipo time - - string fatOutputFileName = $"lib-aot-{projectAssemblyName}.fat.a"; - string fatOutputFilePath = Path.Combine(aotTempDir, fatOutputFileName); - - var lipoArgs = new List<string>(); - lipoArgs.Add("-create"); - lipoArgs.AddRange(arFilePathsForAllArchs); - lipoArgs.Add("-output"); - lipoArgs.Add(fatOutputFilePath); - - int lipoExitCode = OS.ExecuteCommand(XcodeHelper.FindXcodeTool("lipo"), lipoArgs); - if (lipoExitCode != 0) - throw new InvalidOperationException($"Command 'lipo' exited with code: {lipoExitCode}."); - - // TODO: Add the AOT lib and interpreter libs as device only to suppress warnings when targeting the simulator - - // Add the fat AOT static library to the Xcode project - exporter.AddIosProjectStaticLib(fatOutputFilePath); - - // Add the required Mono libraries to the Xcode project - - string MonoLibFile(string libFileName) => libFileName + ".ios.fat.a"; - - string MonoLibFromTemplate(string libFileName) => - Path.Combine(Internal.FullExportTemplatesDir, "ios-mono-libs", MonoLibFile(libFileName)); - - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmonosgen-2.0")); - - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-native")); - - if (aotOpts.UseInterpreter) - { - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ee-interp")); - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-icall-table")); - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-ilgen")); - } - - // TODO: Turn into an exporter option - bool enableProfiling = false; - if (enableProfiling) - exporter.AddIosProjectStaticLib(MonoLibFromTemplate("libmono-profiler-log")); - - // Add frameworks required by Mono to the Xcode project - exporter.AddIosFramework("libiconv.tbd"); - exporter.AddIosFramework("GSS.framework"); - exporter.AddIosFramework("CFNetwork.framework"); - - // Force load and export dynamic are needed for the linker to not strip required symbols. - // In theory we shouldn't be relying on this for P/Invoked functions (as is the case with - // functions in System.Native/libmono-native). Instead, we should use cecil to search for - // DllImports in assemblies and pass them to 'ld' as '-u/--undefined {pinvoke_symbol}'. - exporter.AddIosLinkerFlags("-rdynamic"); - exporter.AddIosLinkerFlags($"-force_load \"$(SRCROOT)/{MonoLibFile("libmono-native")}\""); - } - - /// Converts an assembly name to a valid symbol name in the same way the AOT compiler does - private static string AssemblyNameToAotSymbol(string assemblyName) - { - var builder = new StringBuilder(); - - foreach (var charByte in Encoding.UTF8.GetBytes(assemblyName)) - { - char @char = (char)charByte; - builder.Append(Char.IsLetterOrDigit(@char) || @char == '_' ? @char : '_'); - } - - return builder.ToString(); - } - - private static IEnumerable<string> GetAotCompilerArgs(string platform, bool isDebug, string target, AotOptions aotOpts, string assemblyPath, string outputFilePath) - { - // TODO: LLVM - - bool aotSoftDebug = isDebug && !aotOpts.EnableLLVM; - bool aotDwarfDebug = platform == OS.Platforms.iOS; - - var aotOptions = new List<string>(); - var optimizerOptions = new List<string>(); - - if (aotOpts.LLVMOnly) - { - aotOptions.Add("llvmonly"); - } - else - { - // Can be both 'interp' and 'full' - if (aotOpts.UseInterpreter) - aotOptions.Add("interp"); - if (aotOpts.FullAot) - aotOptions.Add("full"); - } - - aotOptions.Add(aotSoftDebug ? "soft-debug" : "nodebug"); - - if (aotDwarfDebug) - aotOptions.Add("dwarfdebug"); - - if (platform == OS.Platforms.Android) - { - string abi = target; - - string androidToolchain = aotOpts.ToolchainPath; - - if (string.IsNullOrEmpty(androidToolchain)) - { - androidToolchain = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "android-toolchains", $"{abi}"); // TODO: $"{abi}-{apiLevel}{(clang?"clang":"")}" - - if (!Directory.Exists(androidToolchain)) - throw new FileNotFoundException("Missing android toolchain. Specify one in the AOT export settings."); - } - else if (!Directory.Exists(androidToolchain)) - { - throw new FileNotFoundException($"Android toolchain not found: '{androidToolchain}'."); - } - - var androidToolPrefixes = new Dictionary<string, string> - { - ["arm32"] = "arm-linux-androideabi-", - ["arm64"] = "aarch64-linux-android-", - ["x86_32"] = "i686-linux-android-", - ["x86_64"] = "x86_64-linux-android-" - }; - - aotOptions.Add("tool-prefix=" + Path.Combine(androidToolchain, "bin", androidToolPrefixes[abi])); - - string triple = GetAndroidTriple(abi); - aotOptions.Add($"mtriple={triple}"); - } - else if (platform == OS.Platforms.iOS) - { - if (!aotOpts.LLVMOnly && !aotOpts.UseInterpreter) - optimizerOptions.Add("gsharedvt"); - - aotOptions.Add("static"); - - // I couldn't get the Mono cross-compiler to do assembling, so we'll have to do it ourselves - aotOptions.Add("asmonly"); - - aotOptions.Add("direct-icalls"); - - if (aotSoftDebug) - aotOptions.Add("no-direct-calls"); - - if (aotOpts.LLVMOnly || !aotOpts.UseInterpreter) - aotOptions.Add("direct-pinvoke"); - - string arch = target; - aotOptions.Add($"mtriple={arch}-ios"); - } - - aotOptions.Add($"outfile={outputFilePath}"); - - if (aotOpts.EnableLLVM) - { - aotOptions.Add($"llvm-path={aotOpts.LLVMPath}"); - aotOptions.Add($"llvm-outfile={aotOpts.LLVMOutputPath}"); - } - - if (aotOpts.ExtraAotOptions.Length > 0) - aotOptions.AddRange(aotOpts.ExtraAotOptions); - - if (aotOpts.ExtraOptimizerOptions.Length > 0) - optimizerOptions.AddRange(aotOpts.ExtraOptimizerOptions); - - string EscapeOption(string option) => option.Contains(',') ? $"\"{option}\"" : option; - string OptionsToString(IEnumerable<string> options) => string.Join(",", options.Select(EscapeOption)); - - var runtimeArgs = new List<string>(); - - // The '--debug' runtime option is required when using the 'soft-debug' and 'dwarfdebug' AOT options - if (aotSoftDebug || aotDwarfDebug) - runtimeArgs.Add("--debug"); - - if (aotOpts.EnableLLVM) - runtimeArgs.Add("--llvm"); - - runtimeArgs.Add(aotOptions.Count > 0 ? $"--aot={OptionsToString(aotOptions)}" : "--aot"); - - if (optimizerOptions.Count > 0) - runtimeArgs.Add($"-O={OptionsToString(optimizerOptions)}"); - - runtimeArgs.Add(assemblyPath); - - return runtimeArgs; - } - - private static void ExecuteCompiler(string compiler, IEnumerable<string> compilerArgs, string bclDir) - { - // TODO: Once we move to .NET Standard 2.1 we can use ProcessStartInfo.ArgumentList instead - string CmdLineArgsToString(IEnumerable<string> args) - { - // Not perfect, but as long as we are careful... - return string.Join(" ", args.Select(arg => arg.Contains(" ") ? $@"""{arg}""" : arg)); - } - - using (var process = new Process()) - { - process.StartInfo = new ProcessStartInfo(compiler, CmdLineArgsToString(compilerArgs)) - { - UseShellExecute = false - }; - - process.StartInfo.EnvironmentVariables.Remove("MONO_ENV_OPTIONS"); - process.StartInfo.EnvironmentVariables.Remove("MONO_THREADS_SUSPEND"); - process.StartInfo.EnvironmentVariables.Add("MONO_PATH", bclDir); - - Console.WriteLine($"Running: \"{process.StartInfo.FileName}\" {process.StartInfo.Arguments}"); - - if (!process.Start()) - throw new InvalidOperationException("Failed to start process for Mono AOT compiler."); - - process.WaitForExit(); - - if (process.ExitCode != 0) - throw new InvalidOperationException($"Mono AOT compiler exited with code: {process.ExitCode}."); - } - } - - private static IEnumerable<string> GetEnablediOSArchs(string[] features) - { - var iosArchs = new[] - { - "arm64" - }; - - return iosArchs.Where(features.Contains); - } - - private static IEnumerable<string> GetEnabledAndroidAbis(string[] features) - { - var androidAbis = new[] - { - "arm32", - "arm64", - "x86_32", - "x86_64" - }; - - return androidAbis.Where(features.Contains); - } - - private static string GetAndroidTriple(string abi) - { - var abiArchs = new Dictionary<string, string> - { - ["arm32"] = "armv7", - ["arm64"] = "aarch64-v8a", - ["x86_32"] = "i686", - ["x86_64"] = "x86_64" - }; - - string arch = abiArchs[abi]; - - return $"{arch}-linux-android"; - } - - private static string GetMonoCrossDesktopDirName(string platform, string arch) - { - switch (platform) - { - case OS.Platforms.Windows: - { - return $"windows-{arch}"; - } - case OS.Platforms.MacOS: - { - return $"{platform}-{arch}"; - } - case OS.Platforms.LinuxBSD: - { - return $"linux-{arch}"; - } - default: - throw new NotSupportedException($"Platform not supported: {platform}"); - } - } - - // TODO: Replace this for a specific path for each platform - private static string FindCrossCompiler(string monoCrossBin) - { - string exeExt = OS.IsWindows ? ".exe" : string.Empty; - - var files = new DirectoryInfo(monoCrossBin).GetFiles($"*mono-sgen{exeExt}", SearchOption.TopDirectoryOnly); - if (files.Length > 0) - return Path.Combine(monoCrossBin, files[0].Name); - - throw new FileNotFoundException($"Cannot find the mono runtime executable in {monoCrossBin}"); - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 91e5118990..46020fda89 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -1,6 +1,7 @@ using Godot; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -11,6 +12,7 @@ using Directory = GodotTools.Utils.Directory; using File = GodotTools.Utils.File; using OS = GodotTools.Utils.OS; using Path = System.IO.Path; +using System.Globalization; namespace GodotTools.Export { @@ -73,7 +75,7 @@ namespace GodotTools.Export }; } - private string _maybeLastExportError; + private string? _maybeLastExportError; // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) @@ -135,7 +137,7 @@ namespace GodotTools.Export if (!ProjectContainsDotNet()) return; - if (!DeterminePlatformFromFeatures(features, out string platform)) + if (!DeterminePlatformFromFeatures(features, out string? platform)) throw new NotSupportedException("Target platform not supported."); if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS } @@ -194,7 +196,7 @@ namespace GodotTools.Export BundleOutputs = false, IncludeDebugSymbols = publishConfig.IncludeDebugSymbols, RidOS = OS.DotNetOS.iOSSimulator, - UseTempDir = true, + UseTempDir = false, }); } @@ -276,7 +278,7 @@ namespace GodotTools.Export if (platform == OS.Platforms.iOS) { // Exclude dsym folders. - return !dir.EndsWith(".dsym", StringComparison.InvariantCultureIgnoreCase); + return !dir.EndsWith(".dsym", StringComparison.OrdinalIgnoreCase); } return true; @@ -296,7 +298,7 @@ namespace GodotTools.Export if (platform == OS.Platforms.iOS) { // Don't recurse into dsym folders. - return !dir.EndsWith(".dsym", StringComparison.InvariantCultureIgnoreCase); + return !dir.EndsWith(".dsym", StringComparison.OrdinalIgnoreCase); } return true; @@ -312,13 +314,13 @@ namespace GodotTools.Export byte[] fileData = File.ReadAllBytes(path); string hash = Convert.ToBase64String(SHA512.HashData(fileData)); - manifest.Append($"{filePath}\t{hash}\n"); + manifest.Append(CultureInfo.InvariantCulture, $"{filePath}\t{hash}\n"); AddFile($"res://.godot/mono/publish/{arch}/{filePath}", fileData, false); } else { - if (platform == OS.Platforms.iOS && path.EndsWith(".dat")) + if (platform == OS.Platforms.iOS && path.EndsWith(".dat", StringComparison.OrdinalIgnoreCase)) { AddIosBundleFile(path); } @@ -327,7 +329,7 @@ namespace GodotTools.Export AddSharedObject(path, tags: null, Path.Join(projectDataDirName, Path.GetRelativePath(publishOutputDir, - Path.GetDirectoryName(path)))); + Path.GetDirectoryName(path)!))); } } } @@ -361,7 +363,7 @@ namespace GodotTools.Export } var xcFrameworkPath = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig, - $"{GodotSharpDirs.ProjectAssemblyName}.xcframework"); + $"{GodotSharpDirs.ProjectAssemblyName}_aot.xcframework"); if (!BuildManager.GenerateXCFrameworkBlocking(outputPaths, Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, publishConfig.BuildConfig, xcFrameworkPath))) { @@ -389,10 +391,10 @@ namespace GodotTools.Export if (filterDir(dir)) { addEntry(dir, false); - } - else if (recurseDir(dir)) - { - RecursePublishContents(dir, filterDir, filterFile, recurseDir, addEntry); + if (recurseDir(dir)) + { + RecursePublishContents(dir, filterDir, filterFile, recurseDir, addEntry); + } } } } @@ -450,7 +452,7 @@ namespace GodotTools.Export } } - private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) + private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, [NotNullWhen(true)] out string? platform) { foreach (var feature in features) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs index 4f5bebfb42..023f46b685 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/XcodeHelper.cs @@ -5,7 +5,7 @@ namespace GodotTools.Export { public static class XcodeHelper { - private static string _XcodePath = null; + private static string? _XcodePath = null; public static string XcodePath { @@ -23,7 +23,7 @@ namespace GodotTools.Export } } - private static string FindSelectedXcode() + private static string? FindSelectedXcode() { var outputWrapper = new Godot.Collections.Array(); @@ -40,9 +40,9 @@ namespace GodotTools.Export return null; } - public static string FindXcode() + public static string? FindXcode() { - string selectedXcode = FindSelectedXcode(); + string? selectedXcode = FindSelectedXcode(); if (selectedXcode != null) { if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer"))) @@ -50,10 +50,10 @@ namespace GodotTools.Export // The path already pointed to Contents/Developer var dirInfo = new DirectoryInfo(selectedXcode); - if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents") + if (dirInfo is not { Parent.Name: "Contents", Name: "Developer" }) { Console.WriteLine(Path.GetDirectoryName(selectedXcode)); - Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name); + Console.WriteLine(System.IO.Directory.GetParent(selectedXcode)?.Name); Console.Error.WriteLine("Unrecognized path for selected Xcode"); } else diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index a00c812c79..bf6cab11c7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -4,11 +4,13 @@ using GodotTools.Export; using GodotTools.Utils; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using GodotTools.Build; using GodotTools.Ides; using GodotTools.Ides.Rider; +using GodotTools.Inspector; using GodotTools.Internals; using GodotTools.ProjectEditor; using JetBrains.Annotations; @@ -33,6 +35,7 @@ namespace GodotTools public const string ProblemsLayout = "dotnet/build/problems_layout"; } +#nullable disable private EditorSettings _editorSettings; private PopupMenu _menuPopup; @@ -45,10 +48,12 @@ namespace GodotTools // TODO Use WeakReference once we have proper serialization. private WeakRef _exportPluginWeak; + private WeakRef _inspectorPluginWeak; public GodotIdeManager GodotIdeManager { get; private set; } public MSBuildPanel MSBuildPanel { get; private set; } +#nullable enable public bool SkipBuildBeforePlaying { get; set; } = false; @@ -65,29 +70,23 @@ namespace GodotTools private bool CreateProjectSolution() { - string errorMessage = null; + string? errorMessage = null; using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2)) { pr.Step("Generating C# project...".TTR()); - string csprojDir = Path.GetDirectoryName(GodotSharpDirs.ProjectCsProjPath); - string slnDir = Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath); + string csprojDir = Path.GetDirectoryName(GodotSharpDirs.ProjectCsProjPath)!; + string slnDir = Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)!; string name = GodotSharpDirs.ProjectAssemblyName; string guid = CsProjOperations.GenerateGameProject(csprojDir, name); if (guid.Length > 0) { - var solution = new DotNetSolution(name) - { - DirectoryPath = slnDir - }; + var solution = new DotNetSolution(name, slnDir); - var projectInfo = new DotNetSolution.ProjectInfo - { - Guid = guid, - PathRelativeToSolution = Path.GetRelativePath(slnDir, GodotSharpDirs.ProjectCsProjPath), - Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" } - }; + var projectInfo = new DotNetSolution.ProjectInfo(guid, + Path.GetRelativePath(slnDir, GodotSharpDirs.ProjectCsProjPath), + new List<string> { "Debug", "ExportDebug", "ExportRelease" }); solution.AddNewProject(name, projectInfo); @@ -204,10 +203,10 @@ namespace GodotTools var insideQuotes = false; var hasFileFlag = false; - execArgs = execArgs.ReplaceN("{line}", line.ToString()); - execArgs = execArgs.ReplaceN("{col}", col.ToString()); + execArgs = execArgs.ReplaceN("{line}", line.ToString(CultureInfo.InvariantCulture)); + execArgs = execArgs.ReplaceN("{col}", col.ToString(CultureInfo.InvariantCulture)); execArgs = execArgs.StripEdges(true, true); - execArgs = execArgs.Replace("\\\\", "\\"); + execArgs = execArgs.Replace("\\\\", "\\", StringComparison.Ordinal); for (int i = 0; i < execArgs.Length; ++i) { @@ -227,7 +226,7 @@ namespace GodotTools } var arg = execArgs.Substr(from, numChars); - if (arg.Contains("{file}")) + if (arg.Contains("{file}", StringComparison.OrdinalIgnoreCase)) { hasFileFlag = true; } @@ -340,7 +339,7 @@ namespace GodotTools } } - args.Add(Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)); + args.Add(Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)!); string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); @@ -456,8 +455,8 @@ namespace GodotTools var dotNetSdkSearchVersion = Environment.Version; // First we try to find the .NET Sdk ourselves to make sure we get the - // correct version first (`RegisterDefaults` always picks the latest). - if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string sdkPath)) + // correct version first, otherwise pick the latest. + if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string? sdkPath)) { if (Godot.OS.IsStdOutVerbose()) Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); @@ -468,7 +467,7 @@ namespace GodotTools { try { - ProjectUtils.MSBuildLocatorRegisterDefaults(out sdkVersion, out sdkPath); + ProjectUtils.MSBuildLocatorRegisterLatest(out sdkVersion, out sdkPath); if (Godot.OS.IsStdOutVerbose()) Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); } @@ -497,18 +496,22 @@ namespace GodotTools AddChild(new HotReloadAssemblyWatcher { Name = "HotReloadAssemblyWatcher" }); - _menuPopup = new PopupMenu(); + _menuPopup = new PopupMenu + { + Name = "CSharpTools", + }; _menuPopup.Hide(); AddToolSubmenuItem("C#", _menuPopup); _toolBarBuildButton = new Button { - Flat = true, + Flat = false, Icon = EditorInterface.Singleton.GetEditorTheme().GetIcon("BuildCSharp", "EditorIcons"), FocusMode = Control.FocusModeEnum.None, Shortcut = EditorDefShortcut("mono/build_solution", "Build Project".TTR(), (Key)KeyModifierMask.MaskAlt | Key.B), ShortcutInTooltip = true, + ThemeTypeVariation = "RunBarButton", }; EditorShortcutOverride("mono/build_solution", "macos", (Key)KeyModifierMask.MaskMeta | (Key)KeyModifierMask.MaskCtrl | Key.B); @@ -612,6 +615,11 @@ namespace GodotTools AddExportPlugin(exportPlugin); _exportPluginWeak = WeakRef(exportPlugin); + // Inspector plugin + var inspectorPlugin = new InspectorPlugin(); + AddInspectorPlugin(inspectorPlugin); + _inspectorPluginWeak = WeakRef(inspectorPlugin); + BuildManager.Initialize(); RiderPathManager.Initialize(); @@ -624,6 +632,15 @@ namespace GodotTools base._DisablePlugin(); _editorSettings.SettingsChanged -= OnSettingsChanged; + + // Custom signals aren't automatically disconnected currently. + MSBuildPanel.BuildStateChanged -= BuildStateChanged; + } + + public override void _ExitTree() + { + _errorDialog?.QueueFree(); + _confirmCreateSlnDialog?.QueueFree(); } private void OnSettingsChanged() @@ -652,6 +669,13 @@ namespace GodotTools _exportPluginWeak.Dispose(); } + if (IsInstanceValid(_inspectorPluginWeak)) + { + (_inspectorPluginWeak.GetRef().AsGodotObject() as InspectorPlugin)?.Dispose(); + + _inspectorPluginWeak.Dispose(); + } + GodotIdeManager?.Dispose(); } @@ -668,8 +692,9 @@ namespace GodotTools } // Singleton - +#nullable disable public static GodotSharpEditor Instance { get; private set; } +#nullable enable [UsedImplicitly] private static IntPtr InternalCreateInstance(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 56ae37b4dd..35b3f5a710 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -4,6 +4,7 @@ <TargetFramework>net6.0</TargetFramework> <EnableDynamicLoading>true</EnableDynamicLoading> <LangVersion>10</LangVersion> + <Nullable>enable</Nullable> <!-- The Godot editor uses the Debug Godot API assemblies --> <GodotApiConfiguration>Debug</GodotApiConfiguration> <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> @@ -28,7 +29,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" /> - <PackageReference Include="JetBrains.Rider.PathLocator" Version="1.0.4" /> + <PackageReference Include="JetBrains.Rider.PathLocator" Version="1.0.9" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <Reference Include="GodotSharp"> @@ -49,7 +50,5 @@ <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> <ProjectReference Include="..\GodotTools.ProjectEditor\GodotTools.ProjectEditor.csproj" /> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> - <!-- Include it if this is an SCons build targeting Windows, or if it's not an SCons build but we're on Windows --> - <ProjectReference Include="..\GodotTools.OpenVisualStudio\GodotTools.OpenVisualStudio.csproj" Condition=" '$(GodotPlatform)' == 'windows' Or ( '$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT' ) " /> </ItemGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs index eb42f01b3a..638b7d6420 100644 --- a/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs +++ b/modules/mono/editor/GodotTools/GodotTools/HotReloadAssemblyWatcher.cs @@ -1,13 +1,15 @@ using Godot; +using GodotTools.Build; using GodotTools.Internals; using JetBrains.Annotations; -using static GodotTools.Internals.Globals; namespace GodotTools { public partial class HotReloadAssemblyWatcher : Node { +#nullable disable private Timer _watchTimer; +#nullable enable public override void _Notification(int what) { @@ -16,14 +18,20 @@ namespace GodotTools RestartTimer(); if (Internal.IsAssembliesReloadingNeeded()) + { + BuildManager.UpdateLastValidBuildDateTime(); Internal.ReloadAssemblies(softReload: false); + } } } private void TimerTimeout() { if (Internal.IsAssembliesReloadingNeeded()) + { + BuildManager.UpdateLastValidBuildDateTime(); Internal.ReloadAssemblies(softReload: false); + } } [UsedImplicitly] diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs index 65b77112aa..6563bfbb06 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/GodotIdeManager.cs @@ -10,10 +10,10 @@ namespace GodotTools.Ides { public sealed partial class GodotIdeManager : Node, ISerializationListener { - private MessagingServer _messagingServer; + private MessagingServer? _messagingServer; - private MonoDevelop.Instance _monoDevelInstance; - private MonoDevelop.Instance _vsForMacInstance; + private MonoDevelop.Instance? _monoDevelInstance; + private MonoDevelop.Instance? _vsForMacInstance; private MessagingServer GetRunningOrNewServer() { @@ -59,7 +59,7 @@ namespace GodotTools.Ides switch (editorId) { case ExternalEditorId.None: - return null; + return string.Empty; case ExternalEditorId.VisualStudio: return "VisualStudio"; case ExternalEditorId.VsCode: diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 51c7a8aa22..c5acc5e2db 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -97,9 +98,9 @@ namespace GodotTools.Ides foreach (var connection in Peers) connection.Dispose(); Peers.Clear(); - _listener?.Stop(); + _listener.Stop(); - _metaFile?.Dispose(); + _metaFile.Dispose(); File.Delete(_metaFilePath); } @@ -122,7 +123,7 @@ namespace GodotTools.Ides _listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0)); _listener.Start(); - int port = ((IPEndPoint)_listener.Server.LocalEndPoint).Port; + int port = ((IPEndPoint?)_listener.Server.LocalEndPoint)?.Port ?? 0; using (var metaFileWriter = new StreamWriter(_metaFile, Encoding.UTF8)) { metaFileWriter.WriteLine(port); @@ -235,7 +236,7 @@ namespace GodotTools.Ides public string GetHandshakeLine(string identity) => $"{_serverHandshakeBase},{identity}"; - public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) + public bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger) { identity = null; @@ -311,12 +312,12 @@ namespace GodotTools.Ides [DebugPlayRequest.Id] = async (peer, content) => { var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body); - return await HandleDebugPlay(request); + return await HandleDebugPlay(request!); }, [StopPlayRequest.Id] = async (peer, content) => { var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body); - return await HandleStopPlay(request); + return await HandleStopPlay(request!); }, [ReloadScriptsRequest.Id] = async (peer, content) => { @@ -326,7 +327,7 @@ namespace GodotTools.Ides [CodeCompletionRequest.Id] = async (peer, content) => { var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body); - return await HandleCodeCompletionRequest(request); + return await HandleCodeCompletionRequest(request!); } }; } @@ -383,7 +384,7 @@ namespace GodotTools.Ides { // This is needed if the "resource path" part of the path is case insensitive. // However, it doesn't fix resource loading if the rest of the path is also case insensitive. - string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); + string? scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); // The node API can only be called from the main thread. await Godot.Engine.GetMainLoop().ToSignal(Godot.Engine.GetMainLoop(), "process_frame"); diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs index 7a0983a8cb..66983156d0 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MonoDevelop/Instance.cs @@ -13,7 +13,7 @@ namespace GodotTools.Ides.MonoDevelop private readonly string _solutionFile; private readonly EditorId _editorId; - private Process _process; + private Process? _process; public bool IsRunning => _process != null && !_process.HasExited; public bool IsDisposed { get; private set; } @@ -24,7 +24,7 @@ namespace GodotTools.Ides.MonoDevelop var args = new List<string>(); - string command; + string? command; if (OS.IsMacOS) { @@ -136,6 +136,8 @@ namespace GodotTools.Ides.MonoDevelop {EditorId.MonoDevelop, "monodevelop"} }; } + ExecutableNames ??= new Dictionary<EditorId, string>(); + BundleIds ??= new Dictionary<EditorId, string>(); } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderLocatorEnvironment.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderLocatorEnvironment.cs index 61c1581281..77e89dfb7f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderLocatorEnvironment.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderLocatorEnvironment.cs @@ -20,12 +20,12 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment } } - public T FromJson<T>(string json) + public T? FromJson<T>(string json) { return JsonConvert.DeserializeObject<T>(json); } - public void Info(string message, Exception e = null) + public void Info(string message, Exception? e = null) { if (e == null) GD.Print(message); @@ -33,7 +33,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment GD.Print(message, e); } - public void Warn(string message, Exception e = null) + public void Warn(string message, Exception? e = null) { if (e == null) GD.PushWarning(message); @@ -41,7 +41,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment GD.PushWarning(message, e); } - public void Error(string message, Exception e = null) + public void Error(string message, Exception? e = null) { if (e == null) GD.PushError(message); @@ -49,7 +49,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment GD.PushError(message, e); } - public void Verbose(string message, Exception e = null) + public void Verbose(string message, Exception? e = null) { // do nothing, since IDK how to write only to the log, without spamming the output } diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs index a0ab381b9b..5c54d2d9ba 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/Rider/RiderPathManager.cs @@ -5,8 +5,6 @@ using Godot; using GodotTools.Internals; using JetBrains.Rider.PathLocator; -#nullable enable - namespace GodotTools.Ides.Rider { public static class RiderPathManager diff --git a/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorOutOfSyncWarning.cs b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorOutOfSyncWarning.cs new file mode 100644 index 0000000000..c5c451f143 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorOutOfSyncWarning.cs @@ -0,0 +1,37 @@ +using Godot; +using GodotTools.Internals; + +namespace GodotTools.Inspector +{ + public partial class InspectorOutOfSyncWarning : HBoxContainer + { + public override void _Ready() + { + SetAnchorsPreset(LayoutPreset.TopWide); + + var iconTexture = GetThemeIcon("StatusWarning", "EditorIcons"); + + var icon = new TextureRect() + { + Texture = iconTexture, + ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional, + CustomMinimumSize = iconTexture.GetSize(), + }; + + icon.SizeFlagsVertical = SizeFlags.ShrinkCenter; + + var label = new Label() + { + Text = "This inspector might be out of date. Please build the C# project.".TTR(), + AutowrapMode = TextServer.AutowrapMode.WordSmart, + CustomMinimumSize = new Vector2(100f, 0f), + }; + + label.AddThemeColorOverride("font_color", GetThemeColor("warning_color", "Editor")); + label.SizeFlagsHorizontal = SizeFlags.Fill | SizeFlags.Expand; + + AddChild(icon); + AddChild(label); + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs new file mode 100644 index 0000000000..8aeb19e08b --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Inspector/InspectorPlugin.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Godot; +using GodotTools.Build; +using GodotTools.Utils; + +namespace GodotTools.Inspector +{ + public partial class InspectorPlugin : EditorInspectorPlugin + { + public override bool _CanHandle(GodotObject godotObject) + { + foreach (var script in EnumerateScripts(godotObject)) + { + if (script is CSharpScript) + { + return true; + } + } + return false; + } + + public override void _ParseBegin(GodotObject godotObject) + { + foreach (var script in EnumerateScripts(godotObject)) + { + if (script is not CSharpScript) + continue; + + string scriptPath = script.ResourcePath; + + if (string.IsNullOrEmpty(scriptPath)) + { + // Generic types used empty paths in older versions of Godot + // so we assume your project is out of sync. + AddCustomControl(new InspectorOutOfSyncWarning()); + break; + } + + if (scriptPath.StartsWith("csharp://")) + { + // This is a virtual path used by generic types, extract the real path. + var scriptPathSpan = scriptPath.AsSpan("csharp://".Length); + scriptPathSpan = scriptPathSpan[..scriptPathSpan.IndexOf(':')]; + scriptPath = $"res://{scriptPathSpan}"; + } + + if (File.GetLastWriteTime(scriptPath) > BuildManager.LastValidBuildDateTime) + { + AddCustomControl(new InspectorOutOfSyncWarning()); + break; + } + } + } + + private static IEnumerable<Script> EnumerateScripts(GodotObject godotObject) + { + var script = godotObject.GetScript().As<Script>(); + while (script != null) + { + yield return script; + script = script.GetBaseScript(); + } + } + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs index 67891a0594..3b761577ae 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/GodotSharpDirs.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.IO; using Godot; using Godot.NativeInterop; @@ -59,19 +60,20 @@ namespace GodotTools.Internals } } + [MemberNotNull("_projectAssemblyName", "_projectSlnPath", "_projectCsProjPath")] public static void DetermineProjectLocation() { - _projectAssemblyName = (string)ProjectSettings.GetSetting("dotnet/project/assembly_name"); + _projectAssemblyName = (string?)ProjectSettings.GetSetting("dotnet/project/assembly_name"); if (string.IsNullOrEmpty(_projectAssemblyName)) { _projectAssemblyName = CSharpProjectName; ProjectSettings.SetSetting("dotnet/project/assembly_name", _projectAssemblyName); } - string slnParentDir = (string)ProjectSettings.GetSetting("dotnet/project/solution_directory"); + string? slnParentDir = (string?)ProjectSettings.GetSetting("dotnet/project/solution_directory"); if (string.IsNullOrEmpty(slnParentDir)) slnParentDir = "res://"; - else if (!slnParentDir.StartsWith("res://")) + else if (!slnParentDir.StartsWith("res://", System.StringComparison.Ordinal)) slnParentDir = "res://" + slnParentDir; // The csproj should be in the same folder as project.godot. @@ -84,9 +86,9 @@ namespace GodotTools.Internals string.Concat(_projectAssemblyName, ".csproj")); } - private static string _projectAssemblyName; - private static string _projectSlnPath; - private static string _projectCsProjPath; + private static string? _projectAssemblyName; + private static string? _projectSlnPath; + private static string? _projectCsProjPath; public static string ProjectAssemblyName { diff --git a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs index 90c443ebb8..175bb78051 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Internals/Internal.cs @@ -1,3 +1,6 @@ +#pragma warning disable IDE1006 // Naming rule violation +// ReSharper disable InconsistentNaming + using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -8,7 +11,6 @@ using GodotTools.IdeMessaging.Requests; namespace GodotTools.Internals { - [SuppressMessage("ReSharper", "InconsistentNaming")] [GenerateUnmanagedCallbacks(typeof(InternalUnmanagedCallbacks))] internal static partial class Internal { diff --git a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs b/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs deleted file mode 100644 index 9a8fdcc7c5..0000000000 --- a/modules/mono/editor/GodotTools/GodotTools/PlaySettings.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace GodotTools -{ - public readonly struct PlaySettings - { - public bool HasDebugger { get; } - public string DebuggerHost { get; } - public int DebuggerPort { get; } - - public bool BuildBeforePlaying { get; } - - public PlaySettings(string debuggerHost, int debuggerPort, bool buildBeforePlaying) - { - HasDebugger = true; - DebuggerHost = debuggerHost; - DebuggerPort = debuggerPort; - BuildBeforePlaying = buildBeforePlaying; - } - } -} diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs index e3c2c822a5..2abca985da 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/CollectionExtensions.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace GodotTools.Utils { public static class CollectionExtensions { - public static T SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T> predicate, T orElse = null) + [return: NotNullIfNotNull("orElse")] + public static T? SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T?> predicate, T? orElse = null) where T : class { foreach (T elem in enumerable) { - T result = predicate(elem); + T? result = predicate(elem); if (result != null) return result; } @@ -21,7 +23,7 @@ namespace GodotTools.Utils public static IEnumerable<string> EnumerateLines(this TextReader textReader) { - string line; + string? line; while ((line = textReader.ReadLine()) != null) yield return line; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs index 89bda704bb..fee9646931 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/FsPathUtils.cs @@ -30,8 +30,7 @@ namespace GodotTools.Utils return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm); } - [return: MaybeNull] - public static string LocalizePathWithCaseChecked(string path) + public static string? LocalizePathWithCaseChecked(string path) { string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar; string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar; diff --git a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs index c24b730c89..355264e4b3 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Utils/OS.cs @@ -150,8 +150,7 @@ namespace GodotTools.Utils public static char PathSep => IsWindows ? ';' : ':'; - [return: MaybeNull] - public static string PathWhich([NotNull] string name) + public static string? PathWhich(string name) { if (IsWindows) return PathWhichWindows(name); @@ -159,12 +158,11 @@ namespace GodotTools.Utils return PathWhichUnix(name); } - [return: MaybeNull] - private static string PathWhichWindows([NotNull] string name) + private static string? PathWhichWindows(string name) { string[] windowsExts = Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); - string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); + string[]? pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); char[] invalidPathChars = Path.GetInvalidPathChars(); var searchDirs = new List<string>(); @@ -196,10 +194,9 @@ namespace GodotTools.Utils select path + ext).FirstOrDefault(File.Exists); } - [return: MaybeNull] - private static string PathWhichUnix([NotNull] string name) + private static string? PathWhichUnix(string name) { - string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); + string[]? pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); char[] invalidPathChars = Path.GetInvalidPathChars(); var searchDirs = new List<string>(); @@ -238,7 +235,7 @@ namespace GodotTools.Utils foreach (string arg in arguments) startInfo.ArgumentList.Add(arg); - using Process process = Process.Start(startInfo); + using Process? process = Process.Start(startInfo); if (process == null) throw new InvalidOperationException("No process was started."); @@ -277,7 +274,7 @@ namespace GodotTools.Utils if (builder.Length > 0) builder.Append(' '); - if (fileName.Contains(' ')) + if (fileName.Contains(' ', StringComparison.Ordinal)) { builder.Append('"'); builder.Append(fileName); @@ -300,7 +297,7 @@ namespace GodotTools.Utils if (builder.Length > 0) builder.Append(' '); - if (argument.Contains(' ')) + if (argument.Contains(' ', StringComparison.Ordinal)) { builder.Append('"'); builder.Append(argument); @@ -315,7 +312,7 @@ namespace GodotTools.Utils public static StringBuilder GetCommandLineDisplay( this ProcessStartInfo startInfo, - StringBuilder optionalBuilder = null + StringBuilder? optionalBuilder = null ) { var builder = optionalBuilder ?? new StringBuilder(); diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 149322ba38..5cb177676c 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -69,6 +69,7 @@ StringBuilder &operator<<(StringBuilder &r_sb, const char *p_cstring) { #define OPEN_BLOCK_L1 INDENT1 OPEN_BLOCK #define OPEN_BLOCK_L2 INDENT2 OPEN_BLOCK +#define OPEN_BLOCK_L3 INDENT3 OPEN_BLOCK #define CLOSE_BLOCK_L1 INDENT1 CLOSE_BLOCK #define CLOSE_BLOCK_L2 INDENT2 CLOSE_BLOCK #define CLOSE_BLOCK_L3 INDENT3 CLOSE_BLOCK @@ -153,8 +154,268 @@ static String fix_doc_description(const String &p_bbcode) { .strip_edges(); } +String BindingsGenerator::bbcode_to_text(const String &p_bbcode, const TypeInterface *p_itype) { + // Based on the version in EditorHelp. + + if (p_bbcode.is_empty()) { + return String(); + } + + DocTools *doc = EditorHelp::get_doc_data(); + + String bbcode = p_bbcode; + + StringBuilder output; + + List<String> tag_stack; + bool code_tag = false; + + int pos = 0; + while (pos < bbcode.length()) { + int brk_pos = bbcode.find("[", pos); + + if (brk_pos < 0) { + brk_pos = bbcode.length(); + } + + if (brk_pos > pos) { + String text = bbcode.substr(pos, brk_pos - pos); + if (code_tag || tag_stack.size() > 0) { + output.append("'" + text + "'"); + } else { + output.append(text); + } + } + + if (brk_pos == bbcode.length()) { + // Nothing else to add. + break; + } + + int brk_end = bbcode.find("]", brk_pos + 1); + + if (brk_end == -1) { + String text = bbcode.substr(brk_pos, bbcode.length() - brk_pos); + if (code_tag || tag_stack.size() > 0) { + output.append("'" + text + "'"); + } + + break; + } + + String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1); + + if (tag.begins_with("/")) { + bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length()); + + if (!tag_ok) { + output.append("]"); + pos = brk_pos + 1; + continue; + } + + tag_stack.pop_front(); + pos = brk_end + 1; + code_tag = false; + } else if (code_tag) { + output.append("["); + pos = brk_pos + 1; + } else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("theme_item ") || tag.begins_with("param ")) { + const int tag_end = tag.find(" "); + const String link_tag = tag.substr(0, tag_end); + const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" "); + + const Vector<String> link_target_parts = link_target.split("."); + + if (link_target_parts.size() <= 0 || link_target_parts.size() > 2) { + ERR_PRINT("Invalid reference format: '" + tag + "'."); + + output.append(tag); + + pos = brk_end + 1; + continue; + } + + const TypeInterface *target_itype; + StringName target_cname; + + if (link_target_parts.size() == 2) { + target_itype = _get_type_or_null(TypeReference(link_target_parts[0])); + if (!target_itype) { + target_itype = _get_type_or_null(TypeReference("_" + link_target_parts[0])); + } + target_cname = link_target_parts[1]; + } else { + target_itype = p_itype; + target_cname = link_target_parts[0]; + } + + if (link_tag == "method") { + _append_text_method(output, target_itype, target_cname, link_target, link_target_parts); + } else if (link_tag == "constructor") { + // TODO: Support constructors? + _append_text_undeclared(output, link_target); + } else if (link_tag == "operator") { + // TODO: Support operators? + _append_text_undeclared(output, link_target); + } else if (link_tag == "member") { + _append_text_member(output, target_itype, target_cname, link_target, link_target_parts); + } else if (link_tag == "signal") { + _append_text_signal(output, target_itype, target_cname, link_target, link_target_parts); + } else if (link_tag == "enum") { + _append_text_enum(output, target_itype, target_cname, link_target, link_target_parts); + } else if (link_tag == "constant") { + _append_text_constant(output, target_itype, target_cname, link_target, link_target_parts); + } else if (link_tag == "param") { + _append_text_param(output, link_target); + } else if (link_tag == "theme_item") { + // We do not declare theme_items in any way in C#, so there is nothing to reference. + _append_text_undeclared(output, link_target); + } + + pos = brk_end + 1; + } else if (doc->class_list.has(tag)) { + if (tag == "Array" || tag == "Dictionary") { + output.append("'" BINDINGS_NAMESPACE_COLLECTIONS "."); + output.append(tag); + output.append("'"); + } else if (tag == "bool" || tag == "int") { + output.append(tag); + } else if (tag == "float") { + output.append( +#ifdef REAL_T_IS_DOUBLE + "double" +#else + "float" +#endif + ); + } else if (tag == "Variant") { + output.append("'Godot.Variant'"); + } else if (tag == "String") { + output.append("string"); + } else if (tag == "Nil") { + output.append("null"); + } else if (tag.begins_with("@")) { + // @GlobalScope, @GDScript, etc. + output.append("'" + tag + "'"); + } else if (tag == "PackedByteArray") { + output.append("byte[]"); + } else if (tag == "PackedInt32Array") { + output.append("int[]"); + } else if (tag == "PackedInt64Array") { + output.append("long[]"); + } else if (tag == "PackedFloat32Array") { + output.append("float[]"); + } else if (tag == "PackedFloat64Array") { + output.append("double[]"); + } else if (tag == "PackedStringArray") { + output.append("string[]"); + } else if (tag == "PackedVector2Array") { + output.append("'" BINDINGS_NAMESPACE ".Vector2[]'"); + } else if (tag == "PackedVector3Array") { + output.append("'" BINDINGS_NAMESPACE ".Vector3[]'"); + } else if (tag == "PackedColorArray") { + output.append("'" BINDINGS_NAMESPACE ".Color[]'"); + } else { + const TypeInterface *target_itype = _get_type_or_null(TypeReference(tag)); + + if (!target_itype) { + target_itype = _get_type_or_null(TypeReference("_" + tag)); + } + + if (target_itype) { + output.append("'" + target_itype->proxy_name + "'"); + } else { + ERR_PRINT("Cannot resolve type reference in documentation: '" + tag + "'."); + output.append("'" + tag + "'"); + } + } + + pos = brk_end + 1; + } else if (tag == "b") { + // Bold is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "i") { + // Italic is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "code" || tag.begins_with("code ")) { + code_tag = true; + pos = brk_end + 1; + tag_stack.push_front("code"); + } else if (tag == "kbd") { + // Keyboard combinations are not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "center") { + // Center alignment is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "br") { + // Break is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "u") { + // Underline is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "s") { + // Strikethrough is not supported. + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag == "url") { + int end = bbcode.find("[", brk_end); + if (end == -1) { + end = bbcode.length(); + } + String url = bbcode.substr(brk_end + 1, end - brk_end - 1); + // Not supported. Just append the url. + output.append(url); + + pos = brk_end + 1; + tag_stack.push_front(tag); + } else if (tag.begins_with("url=")) { + String url = tag.substr(4, tag.length()); + // Not supported. Just append the url. + output.append(url); + + pos = brk_end + 1; + tag_stack.push_front("url"); + } else if (tag == "img") { + int end = bbcode.find("[", brk_end); + if (end == -1) { + end = bbcode.length(); + } + String image = bbcode.substr(brk_end + 1, end - brk_end - 1); + + // Not supported. Just append the bbcode. + output.append("[img]"); + output.append(image); + output.append("[/img]"); + + pos = end; + tag_stack.push_front(tag); + } else if (tag.begins_with("color=")) { + // Not supported. + pos = brk_end + 1; + tag_stack.push_front("color"); + } else if (tag.begins_with("font=")) { + // Not supported. + pos = brk_end + 1; + tag_stack.push_front("font"); + } else { + // Ignore unrecognized tag. + output.append("["); + pos = brk_pos + 1; + } + } + + return output.as_string(); +} + String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterface *p_itype, bool p_is_signal) { - // Based on the version in EditorHelp + // Based on the version in EditorHelp. if (p_bbcode.is_empty()) { return String(); @@ -203,7 +464,8 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } if (brk_pos == bbcode.length()) { - break; // nothing else to add + // Nothing else to add. + break; } int brk_end = bbcode.find("]", brk_pos + 1); @@ -319,7 +581,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } else if (link_tag == "param") { _append_xml_param(xml_output, link_target, p_is_signal); } else if (link_tag == "theme_item") { - // We do not declare theme_items in any way in C#, so there is nothing to reference + // We do not declare theme_items in any way in C#, so there is nothing to reference. _append_xml_undeclared(xml_output, link_target); } @@ -348,7 +610,7 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf } else if (tag == "Nil") { xml_output.append("<see langword=\"null\"/>"); } else if (tag.begins_with("@")) { - // @GlobalScope, @GDScript, etc + // @GlobalScope, @GDScript, etc. xml_output.append("<c>"); xml_output.append(tag); xml_output.append("</c>"); @@ -448,22 +710,22 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf pos = brk_end + 1; tag_stack.push_front("csharp"); } else if (tag == "kbd") { - // keyboard combinations are not supported in xml comments + // Keyboard combinations are not supported in xml comments. pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "center") { - // center alignment is not supported in xml comments + // Center alignment is not supported in xml comments. pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "br") { xml_output.append("\n"); // FIXME: Should use <para> instead. Luckily this tag isn't used for now. pos = brk_end + 1; } else if (tag == "u") { - // underline is not supported in Rider xml comments + // Underline is not supported in Rider xml comments. pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "s") { - // strikethrough is not supported in xml comments + // Strikethrough is not supported in xml comments. pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "url") { @@ -511,7 +773,8 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf tag_stack.push_front("font"); } else { if (!line_del) { - xml_output.append("["); // ignore + // Ignore unrecognized tag. + xml_output.append("["); } pos = brk_pos + 1; } @@ -522,6 +785,285 @@ String BindingsGenerator::bbcode_to_xml(const String &p_bbcode, const TypeInterf return xml_output.as_string(); } +void BindingsGenerator::_append_text_method(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { + if (p_link_target_parts[0] == name_cache.type_at_GlobalScope) { + if (OS::get_singleton()->is_stdout_verbose()) { + OS::get_singleton()->print("Cannot resolve @GlobalScope method reference in documentation: %s\n", p_link_target.utf8().get_data()); + } + + // TODO Map what we can + _append_text_undeclared(p_output, p_link_target); + } else if (!p_target_itype || !p_target_itype->is_object_type) { + if (OS::get_singleton()->is_stdout_verbose()) { + if (p_target_itype) { + OS::get_singleton()->print("Cannot resolve method reference for non-GodotObject type in documentation: %s\n", p_link_target.utf8().get_data()); + } else { + OS::get_singleton()->print("Cannot resolve type from method reference in documentation: %s\n", p_link_target.utf8().get_data()); + } + } + + // TODO Map what we can + _append_text_undeclared(p_output, p_link_target); + } else { + if (p_target_cname == "_init") { + // The _init method is not declared in C#, reference the constructor instead + p_output.append("'new " BINDINGS_NAMESPACE "."); + p_output.append(p_target_itype->proxy_name); + p_output.append("()'"); + } else { + const MethodInterface *target_imethod = p_target_itype->find_method_by_name(p_target_cname); + + if (target_imethod) { + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(p_target_itype->proxy_name); + p_output.append("."); + p_output.append(target_imethod->proxy_name); + p_output.append("("); + bool first_key = true; + for (const ArgumentInterface &iarg : target_imethod->arguments) { + const TypeInterface *arg_type = _get_type_or_null(iarg.type); + + if (first_key) { + first_key = false; + } else { + p_output.append(", "); + } + if (!arg_type) { + ERR_PRINT("Cannot resolve argument type in documentation: '" + p_link_target + "'."); + p_output.append(iarg.type.cname); + continue; + } + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { + p_output.append("Nullable<"); + } + String arg_cs_type = arg_type->cs_type + _get_generic_type_parameters(*arg_type, iarg.type.generic_type_parameters); + p_output.append(arg_cs_type.replacen("params ", "")); + if (iarg.def_param_mode == ArgumentInterface::NULLABLE_VAL) { + p_output.append(">"); + } + } + p_output.append(")'"); + } else { + if (!p_target_itype->is_intentionally_ignored(p_link_target)) { + ERR_PRINT("Cannot resolve method reference in documentation: '" + p_link_target + "'."); + } + + _append_text_undeclared(p_output, p_link_target); + } + } + } +} + +void BindingsGenerator::_append_text_member(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { + if (p_link_target.find("/") >= 0) { + // Properties with '/' (slash) in the name are not declared in C#, so there is nothing to reference. + _append_text_undeclared(p_output, p_link_target); + } else if (!p_target_itype || !p_target_itype->is_object_type) { + if (OS::get_singleton()->is_stdout_verbose()) { + if (p_target_itype) { + OS::get_singleton()->print("Cannot resolve member reference for non-GodotObject type in documentation: %s\n", p_link_target.utf8().get_data()); + } else { + OS::get_singleton()->print("Cannot resolve type from member reference in documentation: %s\n", p_link_target.utf8().get_data()); + } + } + + // TODO Map what we can + _append_text_undeclared(p_output, p_link_target); + } else { + const TypeInterface *current_itype = p_target_itype; + const PropertyInterface *target_iprop = nullptr; + + while (target_iprop == nullptr && current_itype != nullptr) { + target_iprop = current_itype->find_property_by_name(p_target_cname); + if (target_iprop == nullptr) { + current_itype = _get_type_or_null(TypeReference(current_itype->base_name)); + } + } + + if (target_iprop) { + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(current_itype->proxy_name); + p_output.append("."); + p_output.append(target_iprop->proxy_name); + p_output.append("'"); + } else { + if (!p_target_itype->is_intentionally_ignored(p_link_target)) { + ERR_PRINT("Cannot resolve member reference in documentation: '" + p_link_target + "'."); + } + + _append_text_undeclared(p_output, p_link_target); + } + } +} + +void BindingsGenerator::_append_text_signal(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { + if (!p_target_itype || !p_target_itype->is_object_type) { + if (OS::get_singleton()->is_stdout_verbose()) { + if (p_target_itype) { + OS::get_singleton()->print("Cannot resolve signal reference for non-GodotObject type in documentation: %s\n", p_link_target.utf8().get_data()); + } else { + OS::get_singleton()->print("Cannot resolve type from signal reference in documentation: %s\n", p_link_target.utf8().get_data()); + } + } + + // TODO Map what we can + _append_text_undeclared(p_output, p_link_target); + } else { + const SignalInterface *target_isignal = p_target_itype->find_signal_by_name(p_target_cname); + + if (target_isignal) { + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(p_target_itype->proxy_name); + p_output.append("."); + p_output.append(target_isignal->proxy_name); + p_output.append("'"); + } else { + if (!p_target_itype->is_intentionally_ignored(p_link_target)) { + ERR_PRINT("Cannot resolve signal reference in documentation: '" + p_link_target + "'."); + } + + _append_text_undeclared(p_output, p_link_target); + } + } +} + +void BindingsGenerator::_append_text_enum(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { + const StringName search_cname = !p_target_itype ? p_target_cname : StringName(p_target_itype->name + "." + (String)p_target_cname); + + HashMap<StringName, TypeInterface>::ConstIterator enum_match = enum_types.find(search_cname); + + if (!enum_match && search_cname != p_target_cname) { + enum_match = enum_types.find(p_target_cname); + } + + if (enum_match) { + const TypeInterface &target_enum_itype = enum_match->value; + + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(target_enum_itype.proxy_name); // Includes nesting class if any + p_output.append("'"); + } else { + if (!p_target_itype->is_intentionally_ignored(p_link_target)) { + ERR_PRINT("Cannot resolve enum reference in documentation: '" + p_link_target + "'."); + } + + _append_text_undeclared(p_output, p_link_target); + } +} + +void BindingsGenerator::_append_text_constant(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { + if (p_link_target_parts[0] == name_cache.type_at_GlobalScope) { + _append_text_constant_in_global_scope(p_output, p_target_cname, p_link_target); + } else if (!p_target_itype || !p_target_itype->is_object_type) { + // Search in @GlobalScope as a last resort if no class was specified + if (p_link_target_parts.size() == 1) { + _append_text_constant_in_global_scope(p_output, p_target_cname, p_link_target); + return; + } + + if (OS::get_singleton()->is_stdout_verbose()) { + if (p_target_itype) { + OS::get_singleton()->print("Cannot resolve constant reference for non-GodotObject type in documentation: %s\n", p_link_target.utf8().get_data()); + } else { + OS::get_singleton()->print("Cannot resolve type from constant reference in documentation: %s\n", p_link_target.utf8().get_data()); + } + } + + // TODO Map what we can + _append_text_undeclared(p_output, p_link_target); + } else { + // Try to find the constant in the current class + if (p_target_itype->is_singleton_instance) { + // Constants and enums are declared in the static singleton class. + p_target_itype = &obj_types[p_target_itype->cname]; + } + + const ConstantInterface *target_iconst = find_constant_by_name(p_target_cname, p_target_itype->constants); + + if (target_iconst) { + // Found constant in current class + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(p_target_itype->proxy_name); + p_output.append("."); + p_output.append(target_iconst->proxy_name); + p_output.append("'"); + } else { + // Try to find as enum constant in the current class + const EnumInterface *target_ienum = nullptr; + + for (const EnumInterface &ienum : p_target_itype->enums) { + target_ienum = &ienum; + target_iconst = find_constant_by_name(p_target_cname, target_ienum->constants); + if (target_iconst) { + break; + } + } + + if (target_iconst) { + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(p_target_itype->proxy_name); + p_output.append("."); + p_output.append(target_ienum->proxy_name); + p_output.append("."); + p_output.append(target_iconst->proxy_name); + p_output.append("'"); + } else if (p_link_target_parts.size() == 1) { + // Also search in @GlobalScope as a last resort if no class was specified + _append_text_constant_in_global_scope(p_output, p_target_cname, p_link_target); + } else { + if (!p_target_itype->is_intentionally_ignored(p_link_target)) { + ERR_PRINT("Cannot resolve constant reference in documentation: '" + p_link_target + "'."); + } + + _append_xml_undeclared(p_output, p_link_target); + } + } + } +} + +void BindingsGenerator::_append_text_constant_in_global_scope(StringBuilder &p_output, const String &p_target_cname, const String &p_link_target) { + // Try to find as a global constant + const ConstantInterface *target_iconst = find_constant_by_name(p_target_cname, global_constants); + + if (target_iconst) { + // Found global constant + p_output.append("'" BINDINGS_NAMESPACE "." BINDINGS_GLOBAL_SCOPE_CLASS "."); + p_output.append(target_iconst->proxy_name); + p_output.append("'"); + } else { + // Try to find as global enum constant + const EnumInterface *target_ienum = nullptr; + + for (const EnumInterface &ienum : global_enums) { + target_ienum = &ienum; + target_iconst = find_constant_by_name(p_target_cname, target_ienum->constants); + if (target_iconst) { + break; + } + } + + if (target_iconst) { + p_output.append("'" BINDINGS_NAMESPACE "."); + p_output.append(target_ienum->proxy_name); + p_output.append("."); + p_output.append(target_iconst->proxy_name); + p_output.append("'"); + } else { + ERR_PRINT("Cannot resolve global constant reference in documentation: '" + p_link_target + "'."); + _append_text_undeclared(p_output, p_link_target); + } + } +} + +void BindingsGenerator::_append_text_param(StringBuilder &p_output, const String &p_link_target) { + const String link_target = snake_to_camel_case(p_link_target); + p_output.append("'" + link_target + "'"); +} + +void BindingsGenerator::_append_text_undeclared(StringBuilder &p_output, const String &p_link_target) { + p_output.append("'" + p_link_target + "'"); +} + void BindingsGenerator::_append_xml_method(StringBuilder &p_xml_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts) { if (p_link_target_parts[0] == name_cache.type_at_GlobalScope) { if (OS::get_singleton()->is_stdout_verbose()) { @@ -1013,7 +1555,7 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { p_output.append("namespace " BINDINGS_NAMESPACE ";\n\n"); - p_output.append("public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n{"); + p_output.append("public static partial class " BINDINGS_GLOBAL_SCOPE_CLASS "\n" OPEN_BLOCK); for (const ConstantInterface &iconstant : global_constants) { if (iconstant.const_doc && iconstant.const_doc->description.size()) { @@ -1064,50 +1606,48 @@ void BindingsGenerator::_generate_global_constants(StringBuilder &p_output) { _log("Declaring global enum '%s' inside struct '%s'\n", enum_proxy_name.utf8().get_data(), enum_class_name.utf8().get_data()); - p_output.append("\npublic partial struct "); - p_output.append(enum_class_name); - p_output.append("\n" OPEN_BLOCK); + p_output << "\npublic partial struct " << enum_class_name << "\n" OPEN_BLOCK; } + const String maybe_indent = !enum_in_static_class ? "" : INDENT1; + if (ienum.is_flags) { - p_output.append("\n[System.Flags]"); + p_output << "\n" + << maybe_indent << "[System.Flags]"; } - p_output.append("\npublic enum "); - p_output.append(enum_proxy_name); - p_output.append(" : long"); - p_output.append("\n" OPEN_BLOCK); + p_output << "\n" + << maybe_indent << "public enum " << enum_proxy_name << " : long" + << "\n" + << maybe_indent << OPEN_BLOCK; - const ConstantInterface &last = ienum.constants.back()->get(); for (const ConstantInterface &iconstant : ienum.constants) { if (iconstant.const_doc && iconstant.const_doc->description.size()) { String xml_summary = bbcode_to_xml(fix_doc_description(iconstant.const_doc->description), nullptr); Vector<String> summary_lines = xml_summary.length() ? xml_summary.split("\n") : Vector<String>(); if (summary_lines.size()) { - p_output.append(INDENT1 "/// <summary>\n"); + p_output << maybe_indent << INDENT1 "/// <summary>\n"; for (int i = 0; i < summary_lines.size(); i++) { - p_output.append(INDENT1 "/// "); - p_output.append(summary_lines[i]); - p_output.append("\n"); + p_output << maybe_indent << INDENT1 "/// " << summary_lines[i] << "\n"; } - p_output.append(INDENT1 "/// </summary>\n"); + p_output << maybe_indent << INDENT1 "/// </summary>\n"; } } - p_output.append(INDENT1); - p_output.append(iconstant.proxy_name); - p_output.append(" = "); - p_output.append(itos(iconstant.value)); - p_output.append(&iconstant != &last ? ",\n" : "\n"); + p_output << maybe_indent << INDENT1 + << iconstant.proxy_name + << " = " + << itos(iconstant.value) + << ",\n"; } - p_output.append(CLOSE_BLOCK); + p_output << maybe_indent << CLOSE_BLOCK; if (enum_in_static_class) { - p_output.append(CLOSE_BLOCK); + p_output << CLOSE_BLOCK; } } } @@ -1445,10 +1985,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append("/// </summary>\n"); } + } - if (class_doc->is_deprecated) { - output.append("[Obsolete(\"This class is deprecated.\")]\n"); - } + if (itype.is_deprecated) { + output.append("[Obsolete(\""); + output.append(bbcode_to_text(itype.deprecation_message, &itype)); + output.append("\")]\n"); } // We generate a `GodotClassName` attribute if the engine class name is not the same as the @@ -1505,10 +2047,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append(INDENT1 "/// </summary>"); } + } - if (iconstant.const_doc->is_deprecated) { - output.append(MEMBER_BEGIN "[Obsolete(\"This constant is deprecated.\")]"); - } + if (iconstant.is_deprecated) { + output.append(MEMBER_BEGIN "[Obsolete(\""); + output.append(bbcode_to_text(iconstant.deprecation_message, &itype)); + output.append("\")]"); } output.append(MEMBER_BEGIN "public const long "); @@ -1553,10 +2097,12 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output.append(INDENT2 "/// </summary>\n"); } + } - if (iconstant.const_doc->is_deprecated) { - output.append(INDENT2 "[Obsolete(\"This enum member is deprecated.\")]\n"); - } + if (iconstant.is_deprecated) { + output.append(INDENT2 "[Obsolete(\""); + output.append(bbcode_to_text(iconstant.deprecation_message, &itype)); + output.append("\")]\n"); } output.append(INDENT2); @@ -1631,21 +2177,25 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output << MEMBER_BEGIN "public " << itype.proxy_name << "() : this(" << (itype.memory_own ? "true" : "false") << ")\n" OPEN_BLOCK_L1 << INDENT2 "unsafe\n" INDENT2 OPEN_BLOCK - << INDENT3 "_ConstructAndInitialize(" CS_STATIC_FIELD_NATIVE_CTOR ", " + << INDENT3 "ConstructAndInitialize(" CS_STATIC_FIELD_NATIVE_CTOR ", " << BINDINGS_NATIVE_NAME_FIELD ", CachedType, refCounted: " << (itype.is_ref_counted ? "true" : "false") << ");\n" << CLOSE_BLOCK_L2 CLOSE_BLOCK_L1; } else { // Hide the constructor - output.append(MEMBER_BEGIN "internal "); - output.append(itype.proxy_name); - output.append("() {}\n"); + output << MEMBER_BEGIN "internal " << itype.proxy_name << "() : this(" + << (itype.memory_own ? "true" : "false") << ")\n" OPEN_BLOCK_L1 + << INDENT2 "unsafe\n" INDENT2 OPEN_BLOCK + << INDENT3 "ConstructAndInitialize(null, " + << BINDINGS_NATIVE_NAME_FIELD ", CachedType, refCounted: " + << (itype.is_ref_counted ? "true" : "false") << ");\n" + << CLOSE_BLOCK_L2 CLOSE_BLOCK_L1; } // Add.. em.. trick constructor. Sort of. output.append(MEMBER_BEGIN "internal "); output.append(itype.proxy_name); - output.append("(bool " CS_PARAM_MEMORYOWN ") : base(" CS_PARAM_MEMORYOWN ") {}\n"); + output.append("(bool " CS_PARAM_MEMORYOWN ") : base(" CS_PARAM_MEMORYOWN ") { }\n"); } } @@ -1706,6 +2256,9 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str << INDENT1 "/// <param name=\"args\">Arguments to use with the invoked method.</param>\n" << INDENT1 "/// <param name=\"ret\">Value returned by the invoked method.</param>\n"; + // Avoid raising diagnostics because of calls to obsolete methods. + output << "#pragma warning disable CS0618 // Member is obsolete\n"; + output << INDENT1 "protected internal " << (is_derived_type ? "override" : "virtual") << " bool " CS_METHOD_INVOKE_GODOT_CLASS_METHOD "(in godot_string_name method, " << "NativeVariantPtrArgs args, out godot_variant ret)\n" @@ -1784,6 +2337,8 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str output << INDENT1 "}\n"; + output << "#pragma warning restore CS0618\n"; + // Generate HasGodotClassMethod output << MEMBER_BEGIN "/// <summary>\n" @@ -2004,10 +2559,12 @@ Error BindingsGenerator::_generate_cs_property(const BindingsGenerator::TypeInte p_output.append(INDENT1 "/// </summary>"); } + } - if (p_iprop.prop_doc->is_deprecated) { - p_output.append(MEMBER_BEGIN "[Obsolete(\"This property is deprecated.\")]"); - } + if (p_iprop.is_deprecated) { + p_output.append(MEMBER_BEGIN "[Obsolete(\""); + p_output.append(bbcode_to_text(p_iprop.deprecation_message, &p_itype)); + p_output.append("\")]"); } p_output.append(MEMBER_BEGIN "public "); @@ -2253,10 +2810,6 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf p_output.append(INDENT1 "/// </summary>"); } - - if (p_imethod.method_doc->is_deprecated) { - p_output.append(MEMBER_BEGIN "[Obsolete(\"This method is deprecated.\")]"); - } } if (default_args_doc.get_string_length()) { @@ -2264,12 +2817,8 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf } if (p_imethod.is_deprecated) { - if (p_imethod.deprecation_message.is_empty()) { - WARN_PRINT("An empty deprecation message is discouraged. Method: '" + p_imethod.proxy_name + "'."); - } - p_output.append(MEMBER_BEGIN "[Obsolete(\""); - p_output.append(p_imethod.deprecation_message); + p_output.append(bbcode_to_text(p_imethod.deprecation_message, &p_itype)); p_output.append("\")]"); } @@ -2415,12 +2964,8 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(INDENT1 "/// </summary>"); if (p_isignal.is_deprecated) { - if (p_isignal.deprecation_message.is_empty()) { - WARN_PRINT("An empty deprecation message is discouraged. Signal: '" + p_isignal.proxy_name + "'."); - } - p_output.append(MEMBER_BEGIN "[Obsolete(\""); - p_output.append(p_isignal.deprecation_message); + p_output.append(bbcode_to_text(p_isignal.deprecation_message, &p_itype)); p_output.append("\")]"); } @@ -2444,7 +2989,7 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf ERR_FAIL_NULL_V(arg_type, ERR_BUG); // Argument type not found if (idx != 0) { - p_output << ","; + p_output << ", "; } p_output << sformat(arg_type->cs_variant_to_managed, @@ -2473,15 +3018,11 @@ Error BindingsGenerator::_generate_cs_signal(const BindingsGenerator::TypeInterf p_output.append(INDENT1 "/// </summary>"); } - - if (p_isignal.method_doc->is_deprecated) { - p_output.append(MEMBER_BEGIN "[Obsolete(\"This signal is deprecated.\")]"); - } } if (p_isignal.is_deprecated) { p_output.append(MEMBER_BEGIN "[Obsolete(\""); - p_output.append(p_isignal.deprecation_message); + p_output.append(bbcode_to_text(p_isignal.deprecation_message, &p_itype)); p_output.append("\")]"); } @@ -2608,7 +3149,11 @@ Error BindingsGenerator::_generate_cs_native_calls(const InternalCall &p_icall, // Generate icall function r_output << MEMBER_BEGIN "internal static unsafe " << (ret_void ? "void" : return_type->c_type_out) << " " - << icall_method << "(" << c_func_sig.as_string() << ") " OPEN_BLOCK; + << icall_method << "(" << c_func_sig.as_string() << ")\n" OPEN_BLOCK_L1; + + if (!p_icall.is_static) { + r_output << INDENT2 "ExceptionUtils.ThrowIfNullPtr(" CS_PARAM_INSTANCE ");\n"; + } if (!ret_void && (!p_icall.is_vararg || return_type->cname != name_cache.type_Variant)) { String ptrcall_return_type; @@ -2636,11 +3181,6 @@ Error BindingsGenerator::_generate_cs_native_calls(const InternalCall &p_icall, r_output << ptrcall_return_type << " " C_LOCAL_RET << initialization << ";\n"; } - if (!p_icall.is_static) { - r_output << INDENT2 "if (" CS_PARAM_INSTANCE " == IntPtr.Zero)\n" - << INDENT3 "throw new ArgumentNullException(nameof(" CS_PARAM_INSTANCE "));\n"; - } - String argc_str = itos(p_icall.get_arguments_count()); auto generate_call_and_return_stmts = [&](const char *base_indent) { @@ -2731,7 +3271,7 @@ Error BindingsGenerator::_generate_cs_native_calls(const InternalCall &p_icall, r_output << c_in_statements.as_string(); - r_output << INDENT3 "for (int i = 0; i < vararg_length; i++) " OPEN_BLOCK + r_output << INDENT3 "for (int i = 0; i < vararg_length; i++)\n" OPEN_BLOCK_L3 << INDENT4 "varargs[i] = " << vararg_arg << "[i].NativeVar;\n" << INDENT4 C_LOCAL_PTRCALL_ARGS "[" << real_argc_str << " + i] = new IntPtr(&varargs[i]);\n" << CLOSE_BLOCK_L3; @@ -3044,6 +3584,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { itype.is_ref_counted = ClassDB::is_parent_class(type_cname, name_cache.type_RefCounted); itype.memory_own = itype.is_ref_counted; + if (itype.class_doc) { + itype.is_deprecated = itype.class_doc->is_deprecated; + itype.deprecation_message = itype.class_doc->deprecated_message; + + if (itype.is_deprecated && itype.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Type: '" + itype.proxy_name + "'."); + itype.deprecation_message = "This class is deprecated."; + } + } + if (itype.is_singleton && compat_singletons.has(itype.cname)) { itype.is_singleton = false; itype.is_compat_singleton = true; @@ -3118,6 +3668,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + if (iprop.prop_doc) { + iprop.is_deprecated = iprop.prop_doc->is_deprecated; + iprop.deprecation_message = iprop.prop_doc->deprecated_message; + + if (iprop.is_deprecated && iprop.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Property: '" + itype.proxy_name + "." + iprop.proxy_name + "'."); + iprop.deprecation_message = "This property is deprecated."; + } + } + itype.properties.push_back(iprop); } @@ -3297,6 +3857,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + if (imethod.method_doc) { + imethod.is_deprecated = imethod.method_doc->is_deprecated; + imethod.deprecation_message = imethod.method_doc->deprecated_message; + + if (imethod.is_deprecated && imethod.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Method: '" + itype.proxy_name + "." + imethod.proxy_name + "'."); + imethod.deprecation_message = "This method is deprecated."; + } + } + ERR_FAIL_COND_V_MSG(itype.find_property_by_name(imethod.cname), false, "Method name conflicts with property: '" + itype.name + "." + imethod.name + "'."); @@ -3403,6 +3973,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + if (isignal.method_doc) { + isignal.is_deprecated = isignal.method_doc->is_deprecated; + isignal.deprecation_message = isignal.method_doc->deprecated_message; + + if (isignal.is_deprecated && isignal.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Signal: '" + itype.proxy_name + "." + isignal.proxy_name + "'."); + isignal.deprecation_message = "This signal is deprecated."; + } + } + itype.signals_.push_back(isignal); } @@ -3443,6 +4023,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + if (iconstant.const_doc) { + iconstant.is_deprecated = iconstant.const_doc->is_deprecated; + iconstant.deprecation_message = iconstant.const_doc->deprecated_message; + + if (iconstant.is_deprecated && iconstant.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Enum member: '" + itype.proxy_name + "." + ienum.proxy_name + "." + iconstant.proxy_name + "'."); + iconstant.deprecation_message = "This enum member is deprecated."; + } + } + ienum.constants.push_back(iconstant); } @@ -3485,6 +4075,16 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } } + if (iconstant.const_doc) { + iconstant.is_deprecated = iconstant.const_doc->is_deprecated; + iconstant.deprecation_message = iconstant.const_doc->deprecated_message; + + if (iconstant.is_deprecated && iconstant.deprecation_message.is_empty()) { + WARN_PRINT("An empty deprecation message is discouraged. Constant: '" + itype.proxy_name + "." + iconstant.proxy_name + "'."); + iconstant.deprecation_message = "This constant is deprecated."; + } + } + itype.constants.push_back(iconstant); } diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h index aa4e5ea093..bb0ba0cb00 100644 --- a/modules/mono/editor/bindings_generator.h +++ b/modules/mono/editor/bindings_generator.h @@ -47,7 +47,10 @@ class BindingsGenerator { String name; String proxy_name; int64_t value = 0; - const DocData::ConstantDoc *const_doc; + const DocData::ConstantDoc *const_doc = nullptr; + + bool is_deprecated = false; + String deprecation_message; ConstantInterface() {} @@ -86,6 +89,9 @@ class BindingsGenerator { StringName getter; const DocData::PropertyDoc *prop_doc; + + bool is_deprecated = false; + String deprecation_message; }; struct TypeReference { @@ -427,6 +433,9 @@ class BindingsGenerator { const DocData::ClassDoc *class_doc = nullptr; + bool is_deprecated = false; + String deprecation_message; + List<ConstantInterface> constants; List<EnumInterface> enums; List<PropertyInterface> properties; @@ -765,8 +774,18 @@ class BindingsGenerator { return p_type->name; } + String bbcode_to_text(const String &p_bbcode, const TypeInterface *p_itype); String bbcode_to_xml(const String &p_bbcode, const TypeInterface *p_itype, bool p_is_signal = false); + void _append_text_method(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); + void _append_text_member(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); + void _append_text_signal(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); + void _append_text_enum(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); + void _append_text_constant(StringBuilder &p_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); + void _append_text_constant_in_global_scope(StringBuilder &p_output, const String &p_target_cname, const String &p_link_target); + void _append_text_param(StringBuilder &p_output, const String &p_link_target); + void _append_text_undeclared(StringBuilder &p_output, const String &p_link_target); + void _append_xml_method(StringBuilder &p_xml_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); void _append_xml_member(StringBuilder &p_xml_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); void _append_xml_signal(StringBuilder &p_xml_output, const TypeInterface *p_target_itype, const StringName &p_target_cname, const String &p_link_target, const Vector<String> &p_link_target_parts); diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp index fc99f3ceda..05dacd28fb 100644 --- a/modules/mono/editor/editor_internal_calls.cpp +++ b/modules/mono/editor/editor_internal_calls.cpp @@ -43,10 +43,10 @@ #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/gui/editor_run_bar.h" #include "editor/plugins/script_editor_plugin.h" +#include "editor/themes/editor_scale.h" #include "main/main.h" #ifdef UNIX_ENABLED @@ -148,7 +148,7 @@ void godot_icall_Internal_ReloadAssemblies(bool p_soft_reload) { } void godot_icall_Internal_EditorDebuggerNodeReloadScripts() { - EditorDebuggerNode::get_singleton()->reload_scripts(); + EditorDebuggerNode::get_singleton()->reload_all_scripts(); } bool godot_icall_Internal_ScriptEditorEdit(Resource *p_resource, int32_t p_line, int32_t p_col, bool p_grab_focus) { @@ -175,7 +175,7 @@ void godot_icall_Internal_EditorPlugin_AddControlToEditorRunBar(Control *p_contr void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() { EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); if (ed) { - ed->reload_scripts(); + ed->reload_all_scripts(); } } diff --git a/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs b/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs index 87468fb433..698157c6b4 100644 --- a/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs +++ b/modules/mono/editor/script_templates/CharacterBody2D/basic_movement.cs @@ -8,20 +8,21 @@ public partial class _CLASS_ : _BASE_ public const float Speed = 300.0f; public const float JumpVelocity = -400.0f; - // Get the gravity from the project settings to be synced with RigidBody nodes. - public float gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle(); - public override void _PhysicsProcess(double delta) { Vector2 velocity = Velocity; // Add the gravity. if (!IsOnFloor()) - velocity.Y += gravity * (float)delta; + { + velocity += GetGravity() * (float)delta; + } // Handle Jump. if (Input.IsActionJustPressed("ui_accept") && IsOnFloor()) + { velocity.Y = JumpVelocity; + } // Get the input direction and handle the movement/deceleration. // As good practice, you should replace UI actions with custom gameplay actions. diff --git a/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs b/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs index ddeb9d7e00..30dabd31d9 100644 --- a/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs +++ b/modules/mono/editor/script_templates/CharacterBody3D/basic_movement.cs @@ -8,20 +8,21 @@ public partial class _CLASS_ : _BASE_ public const float Speed = 5.0f; public const float JumpVelocity = 4.5f; - // Get the gravity from the project settings to be synced with RigidBody nodes. - public float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle(); - public override void _PhysicsProcess(double delta) { Vector3 velocity = Velocity; // Add the gravity. if (!IsOnFloor()) - velocity.Y -= gravity * (float)delta; + { + velocity += GetGravity() * (float)delta; + } // Handle Jump. if (Input.IsActionJustPressed("ui_accept") && IsOnFloor()) + { velocity.Y = JumpVelocity; + } // Get the input direction and handle the movement/deceleration. // As good practice, you should replace UI actions with custom gameplay actions. diff --git a/modules/mono/glue/GodotSharp/.editorconfig b/modules/mono/glue/GodotSharp/.editorconfig index df4a6c2d0d..0d9a88ecb8 100644 --- a/modules/mono/glue/GodotSharp/.editorconfig +++ b/modules/mono/glue/GodotSharp/.editorconfig @@ -1,15 +1,37 @@ +# This file should only contain severity override to diagnostics, in order to make generated and +# interop code compilation readable. We want to limit the scope of suppression as much as possible. + [**/Generated/**.cs] +# IDE1006: Naming rule violation +dotnet_diagnostic.IDE1006.severity = none # CA1062: Validate parameter is non-null before using it # Useful for generated code, as it disables nullable dotnet_diagnostic.CA1062.severity = error # CA1069: Enums should not have duplicate values dotnet_diagnostic.CA1069.severity = none +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = none # CA1708: Identifiers should differ by more than case dotnet_diagnostic.CA1708.severity = none +# CA1711: Identifiers should not have incorrect suffix +# Disable warning for suffixes like EventHandler, Flags, Enum, etc. +dotnet_diagnostic.CA1711.severity = none +# CA1716: Identifiers should not match keywords +# This is suppressed, because it will report `@event` as well as `event` +dotnet_diagnostic.CA1716.severity = none +# CA1720: Identifiers should not contain type names +dotnet_diagnostic.CA1720.severity = none # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = none # CS1573: Parameter has no matching param tag in the XML comment dotnet_diagnostic.CS1573.severity = none +# TODO: Temporary change to not pollute the warnings, but this denotes with ou doc generation +# CS1734: XML comment on '' has a paramref tag for '', but there is no parameter by that name +dotnet_diagnostic.CS1734.severity = none + +[GodotSharp/Core/NativeInterop/**.cs] +# CA1720: Identifiers should not contain type names +dotnet_diagnostic.CA1720.severity = none [GodotSharp/Core/**.cs] # CS1591: Missing XML comment for publicly visible type or member diff --git a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs index 6b000cc89b..f3f6759e1d 100644 --- a/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs +++ b/modules/mono/glue/GodotSharp/Godot.SourceGenerators.Internal/UnmanagedCallbacksGenerator.cs @@ -128,16 +128,20 @@ using Godot.NativeInterop; if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -303,16 +307,20 @@ using Godot.NativeInterop; if (isInnerClass) { var containingType = symbol.ContainingType; + AppendPartialContainingTypeDeclarations(containingType); - while (containingType != null) + void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType) { + if (containingType == null) + return; + + AppendPartialContainingTypeDeclarations(containingType.ContainingType); + source.Append("partial "); source.Append(containingType.GetDeclarationKeyword()); source.Append(" "); source.Append(containingType.NameWithTypeParameters()); source.Append("\n{\n"); - - containingType = containingType.ContainingType; } } @@ -379,7 +387,7 @@ using Godot.NativeInterop; } private static bool IsGodotInteropStruct(ITypeSymbol type) => - GodotInteropStructs.Contains(type.FullQualifiedNameOmitGlobal()); + _godotInteropStructs.Contains(type.FullQualifiedNameOmitGlobal()); private static bool IsByRefParameter(IParameterSymbol parameter) => parameter.RefKind is RefKind.In or RefKind.Out or RefKind.Ref; @@ -440,7 +448,7 @@ using Godot.NativeInterop; source.Append(";\n"); } - private static readonly string[] GodotInteropStructs = + private static readonly string[] _godotInteropStructs = { "Godot.NativeInterop.godot_ref", "Godot.NativeInterop.godot_variant_call_error", diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Compat.cs b/modules/mono/glue/GodotSharp/GodotSharp/Compat.cs index 9126495a27..4e80afc4a5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Compat.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Compat.cs @@ -8,6 +8,8 @@ using System.ComponentModel; namespace Godot; #pragma warning disable CS1734 // XML comment on 'X' has a paramref tag for 'Y', but there is no parameter by that name. +// TODO: This is currently disabled because of https://github.com/dotnet/roslyn/issues/52904 +#pragma warning disable IDE0040 // Add accessibility modifiers. partial class AnimationNode { @@ -66,7 +68,7 @@ partial class AnimationTree partial class CodeEdit { - /// <inheritdoc cref="AddCodeCompletionOption(CodeCompletionKind, string, string, Nullable{Color}, Resource, Nullable{Variant}, int)"/> + /// <inheritdoc cref="AddCodeCompletionOption(CodeCompletionKind, string, string, Nullable{Color}, Resource, Variant, int)"/> [EditorBrowsable(EditorBrowsableState.Never)] public void AddCodeCompletionOption(CodeCompletionKind type, string displayText, string insertText, Nullable<Color> textColor, Resource icon, Nullable<Variant> value) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs index cc99225a33..63af6ee6e8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -95,11 +98,11 @@ namespace Godot Vector3 dstMax = with._position + with._size; return srcMin.X <= dstMin.X && - srcMax.X > dstMax.X && + srcMax.X >= dstMax.X && srcMin.Y <= dstMin.Y && - srcMax.Y > dstMax.Y && + srcMax.Y >= dstMax.Y && srcMin.Z <= dstMin.Z && - srcMax.Z > dstMax.Z; + srcMax.Z >= dstMax.Z; } /// <summary> @@ -689,7 +692,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the AABB and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Aabb other && Equals(other); } @@ -739,7 +742,7 @@ namespace Godot /// Converts this <see cref="Aabb"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this AABB.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"{_position.ToString(format)}, {_size.ToString(format)}"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index 13c0cde1ef..9b5aec7031 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Runtime.CompilerServices; using Godot.NativeInterop; +#nullable enable + namespace Godot.Collections { /// <summary> @@ -14,7 +16,9 @@ namespace Godot.Collections /// interfacing with the engine. Otherwise prefer .NET collections /// such as <see cref="System.Array"/> or <see cref="List{T}"/>. /// </summary> +#pragma warning disable CA1710 // Identifiers should have correct suffix public sealed class Array : +#pragma warning restore CA1710 IList<Variant>, IReadOnlyList<Variant>, ICollection, @@ -22,7 +26,7 @@ namespace Godot.Collections { internal godot_array.movable NativeValue; - private WeakReference<IDisposable> _weakReferenceToSelf; + private WeakReference<IDisposable>? _weakReferenceToSelf; /// <summary> /// Constructs a new empty <see cref="Array"/>. @@ -147,7 +151,6 @@ namespace Godot.Collections // from derived types (e.g.: Node[]). Implicit conversion from Derived[] to Base[] are // fine as long as the array is not mutated. However, Span does this type checking at // instantiation, so it's not possible to use it even when not mutating anything. - // ReSharper disable once RedundantNameQualifier /// <summary> /// Constructs a new <see cref="Array"/> from the given ReadOnlySpan's elements. /// </summary> @@ -1140,7 +1143,8 @@ namespace Godot.Collections /// </summary> /// <param name="from">The typed array to convert.</param> /// <returns>A new Godot Array, or <see langword="null"/> if <see paramref="from"/> was null.</returns> - public static explicit operator Array(Array<T> from) + [return: NotNullIfNotNull("from")] + public static explicit operator Array?(Array<T>? from) { return from?._underlyingArray; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/RpcAttribute.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/RpcAttribute.cs index 6a73d6f70c..c53e964156 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/RpcAttribute.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Attributes/RpcAttribute.cs @@ -19,7 +19,7 @@ namespace Godot /// <summary> /// If the method will also be called locally; otherwise, it is only called remotely. /// </summary> - public bool CallLocal { get; init; } = false; + public bool CallLocal { get; init; } /// <summary> /// Transfer mode for the annotated method. @@ -29,7 +29,7 @@ namespace Godot /// <summary> /// Transfer channel for the annotated mode. /// </summary> - public int TransferChannel { get; init; } = 0; + public int TransferChannel { get; init; } /// <summary> /// Constructs a <see cref="RpcAttribute"/> instance. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs index a7712db737..589d6596f0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Basis.cs @@ -1,7 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.ComponentModel; +#nullable enable + namespace Godot { /// <summary> @@ -239,7 +242,7 @@ namespace Godot /// <summary> /// Returns the basis's rotation in the form of Euler angles. - /// The Euler order depends on the [param order] parameter, + /// The Euler order depends on the <paramref name="order"/> parameter, /// by default it uses the YXZ convention: when decomposing, /// first Z, then X, and Y last. The returned vector contains /// the rotation angles in the format (X angle, Y angle, Z angle). @@ -1037,10 +1040,11 @@ namespace Godot } /// <summary> - /// Returns a Vector3 transformed (multiplied) by the transposed basis matrix. - /// - /// Note: This results in a multiplication by the inverse of the - /// basis matrix only if it represents a rotation-reflection. + /// Returns a Vector3 transformed (multiplied) by the inverse basis matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>vector * basis</c> is equivalent to <c>basis.Transposed() * vector</c>. See <see cref="Transposed"/>. + /// For transforming by inverse of a non-orthonormal basis (e.g. with scaling) <c>basis.Inverse() * vector</c> can be used instead. See <see cref="Inverse"/>. /// </summary> /// <param name="vector">A Vector3 to inversely transform.</param> /// <param name="basis">The basis matrix transformation to apply.</param> @@ -1089,7 +1093,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the basis matrix and the object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Basis other && Equals(other); } @@ -1139,7 +1143,7 @@ namespace Godot /// Converts this <see cref="Basis"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this basis.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"[X: {X.ToString(format)}, Y: {Y.ToString(format)}, Z: {Z.ToString(format)}]"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs index ac2e2fae3c..af1f4860cd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/AlcReloadCfg.cs @@ -2,7 +2,7 @@ namespace Godot.Bridge; public static class AlcReloadCfg { - private static bool _configured = false; + private static bool _configured; public static void Configure(bool alcReloadEnabled) { @@ -14,5 +14,5 @@ public static class AlcReloadCfg IsAlcReloadingEnabled = alcReloadEnabled; } - internal static bool IsAlcReloadingEnabled = false; + internal static bool IsAlcReloadingEnabled; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs index 16be494d99..7b6b35b68f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/CSharpInstanceBridge.cs @@ -94,7 +94,7 @@ namespace Godot.Bridge // Signals if (godotObject.HasGodotClassSignal(CustomUnsafe.AsRef(name))) { - godot_signal signal = new godot_signal(*name, godotObject.GetInstanceId()); + godot_signal signal = new godot_signal(NativeFuncs.godotsharp_string_name_new_copy(*name), godotObject.GetInstanceId()); *outRet = VariantUtils.CreateFromSignalTakingOwnershipOfDisposableValue(signal); return godot_bool.True; } @@ -102,7 +102,7 @@ namespace Godot.Bridge // Methods if (godotObject.HasGodotClassMethod(CustomUnsafe.AsRef(name))) { - godot_callable method = new godot_callable(*name, godotObject.GetInstanceId()); + godot_callable method = new godot_callable(NativeFuncs.godotsharp_string_name_new_copy(*name), godotObject.GetInstanceId()); *outRet = VariantUtils.CreateFromCallableTakingOwnershipOfDisposableValue(method); return godot_bool.True; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs index a78cb0bba9..6c34d7c29d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ManagedCallbacks.cs @@ -18,6 +18,7 @@ namespace Godot.Bridge public delegate* unmanaged<godot_string_name*, IntPtr, IntPtr> ScriptManagerBridge_CreateManagedForGodotObjectBinding; public delegate* unmanaged<IntPtr, IntPtr, godot_variant**, int, godot_bool> ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; public delegate* unmanaged<IntPtr, godot_string_name*, void> ScriptManagerBridge_GetScriptNativeName; + public delegate* unmanaged<godot_string*, godot_string*, godot_string*, godot_string*, void> ScriptManagerBridge_GetGlobalClassName; public delegate* unmanaged<IntPtr, IntPtr, void> ScriptManagerBridge_SetGodotObjectPtr; public delegate* unmanaged<IntPtr, godot_string_name*, godot_variant**, int, godot_bool*, void> ScriptManagerBridge_RaiseEventSignal; public delegate* unmanaged<IntPtr, IntPtr, godot_bool> ScriptManagerBridge_ScriptIsOrInherits; @@ -25,7 +26,7 @@ namespace Godot.Bridge public delegate* unmanaged<godot_string*, godot_ref*, void> ScriptManagerBridge_GetOrCreateScriptBridgeForPath; public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_RemoveScriptBridge; public delegate* unmanaged<IntPtr, godot_bool> ScriptManagerBridge_TryReloadRegisteredScriptWithClass; - public delegate* unmanaged<IntPtr, godot_string*, godot_bool*, godot_bool*, godot_bool*, godot_string*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; + public delegate* unmanaged<IntPtr, godot_csharp_type_info*, godot_array*, godot_dictionary*, godot_dictionary*, godot_ref*, void> ScriptManagerBridge_UpdateScriptClassInfo; public delegate* unmanaged<IntPtr, IntPtr*, godot_bool, godot_bool> ScriptManagerBridge_SwapGCHandleForType; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, godot_string*, void*, int, void>, void> ScriptManagerBridge_GetPropertyInfoList; public delegate* unmanaged<IntPtr, delegate* unmanaged<IntPtr, void*, int, void>, void> ScriptManagerBridge_GetPropertyDefaultValues; @@ -60,6 +61,7 @@ namespace Godot.Bridge ScriptManagerBridge_CreateManagedForGodotObjectBinding = &ScriptManagerBridge.CreateManagedForGodotObjectBinding, ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = &ScriptManagerBridge.CreateManagedForGodotObjectScriptInstance, ScriptManagerBridge_GetScriptNativeName = &ScriptManagerBridge.GetScriptNativeName, + ScriptManagerBridge_GetGlobalClassName = &ScriptManagerBridge.GetGlobalClassName, ScriptManagerBridge_SetGodotObjectPtr = &ScriptManagerBridge.SetGodotObjectPtr, ScriptManagerBridge_RaiseEventSignal = &ScriptManagerBridge.RaiseEventSignal, ScriptManagerBridge_ScriptIsOrInherits = &ScriptManagerBridge.ScriptIsOrInherits, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 80c26e5708..1b23276bbd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; using System.Runtime.Serialization; +using System.Text; using Godot.NativeInterop; namespace Godot.Bridge @@ -29,7 +30,8 @@ namespace Godot.Bridge foreach (var type in typesInAlc.Keys) { if (_scriptTypeBiMap.RemoveByScriptType(type, out IntPtr scriptPtr) && - !_pathTypeBiMap.TryGetScriptPath(type, out _)) + (!_pathTypeBiMap.TryGetScriptPath(type, out string? scriptPath) || + scriptPath.StartsWith("csharp://", StringComparison.Ordinal))) { // For scripts without a path, we need to keep the class qualified name for reloading _scriptDataForReload.TryAdd(scriptPtr, @@ -194,7 +196,7 @@ namespace Godot.Bridge var native = GodotObject.InternalGetClassNativeBase(scriptType); - var field = native?.GetField("NativeName", BindingFlags.DeclaredOnly | BindingFlags.Static | + var field = native.GetField("NativeName", BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field == null) @@ -221,6 +223,71 @@ namespace Godot.Bridge } [UnmanagedCallersOnly] + internal static unsafe void GetGlobalClassName(godot_string* scriptPath, godot_string* outBaseType, godot_string* outIconPath, godot_string* outClassName) + { + // This method must always return the outBaseType for every script, even if the script is + // not a global class. But if the script is not a global class it must return an empty + // outClassName string since it should not have a name. + string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); + Debug.Assert(!string.IsNullOrEmpty(scriptPathStr), "Script path can't be empty."); + + if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) + { + // Script at the given path does not exist, or it's not a C# type. + // This is fine, it may be a path to a generic script and those can't be global classes. + *outClassName = default; + return; + } + + if (outIconPath != null) + { + var iconAttr = scriptType.GetCustomAttributes(inherit: false) + .OfType<IconAttribute>() + .FirstOrDefault(); + + *outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); + } + + if (outBaseType != null) + { + bool foundGlobalBaseScript = false; + + Type native = GodotObject.InternalGetClassNativeBase(scriptType); + Type? top = scriptType.BaseType; + + while (top != null && top != native) + { + if (IsGlobalClass(top)) + { + *outBaseType = Marshaling.ConvertStringToNative(top.Name); + foundGlobalBaseScript = true; + break; + } + + top = top.BaseType; + } + if (!foundGlobalBaseScript) + { + *outBaseType = Marshaling.ConvertStringToNative(native.Name); + } + } + + if (!IsGlobalClass(scriptType)) + { + // Scripts that are not global classes should not have a name. + // Return an empty string to prevent the class from being registered + // as a global class in the editor. + *outClassName = default; + return; + } + + *outClassName = Marshaling.ConvertStringToNative(scriptType.Name); + + static bool IsGlobalClass(Type scriptType) => + scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false); + } + + [UnmanagedCallersOnly] internal static void SetGodotObjectPtr(IntPtr gcHandlePtr, IntPtr newPtr) { try @@ -253,11 +320,15 @@ namespace Godot.Bridge { var editorAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor"); - wrapperType = editorAssembly?.GetType("Godot." + nativeTypeNameStr); - if (wrapperType == null) + if (editorAssembly != null) { - wrapperType = GetTypeByGodotClassAttr(editorAssembly, nativeTypeNameStr); + wrapperType = editorAssembly.GetType("Godot." + nativeTypeNameStr); + + if (wrapperType == null) + { + wrapperType = GetTypeByGodotClassAttr(editorAssembly, nativeTypeNameStr); + } } } @@ -306,13 +377,6 @@ namespace Godot.Bridge _pathTypeBiMap.Add(scriptPathAttr.Path, type); - // This method may be called before initialization. - if (NativeFuncs.godotsharp_dotnet_module_is_initialized().ToBool() && Engine.IsEditorHint()) - { - using godot_string scriptPath = Marshaling.ConvertStringToNative(scriptPathAttr.Path); - NativeFuncs.godotsharp_internal_editor_file_system_update_file(scriptPath); - } - if (AlcReloadCfg.IsAlcReloadingEnabled) { AddTypeForAlcReloading(type); @@ -336,7 +400,7 @@ namespace Godot.Bridge foreach (var type in assembly.GetTypes()) { - if (type.IsNested || type.IsGenericType) + if (type.IsNested) continue; if (!typeOfGodotObject.IsAssignableFrom(type)) @@ -355,13 +419,20 @@ namespace Godot.Bridge { foreach (var type in scriptTypes) { - if (type.IsGenericType) - continue; - LookupScriptForClass(type); } } } + + // This method may be called before initialization. + if (NativeFuncs.godotsharp_dotnet_module_is_initialized().ToBool() && Engine.IsEditorHint()) + { + foreach (var scriptPath in _pathTypeBiMap.Paths) + { + using godot_string nativeScriptPath = Marshaling.ConvertStringToNative(scriptPath); + NativeFuncs.godotsharp_internal_editor_file_system_update_file(nativeScriptPath); + } + } } [UnmanagedCallersOnly] @@ -415,20 +486,8 @@ namespace Godot.Bridge { try { - lock (_scriptTypeBiMap.ReadWriteLock) - { - if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) - { - string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); - - if (!_pathTypeBiMap.TryGetScriptType(scriptPathStr, out Type? scriptType)) - return godot_bool.False; - - _scriptTypeBiMap.Add(scriptPtr, scriptType); - } - } - - return godot_bool.True; + string scriptPathStr = Marshaling.ConvertStringToManaged(*scriptPath); + return AddScriptBridgeCore(scriptPtr, scriptPathStr).ToGodotBool(); } catch (Exception e) { @@ -437,6 +496,22 @@ namespace Godot.Bridge } } + private static unsafe bool AddScriptBridgeCore(IntPtr scriptPtr, string scriptPath) + { + lock (_scriptTypeBiMap.ReadWriteLock) + { + if (!_scriptTypeBiMap.IsScriptRegistered(scriptPtr)) + { + if (!_pathTypeBiMap.TryGetScriptType(scriptPath, out Type? scriptType)) + return false; + + _scriptTypeBiMap.Add(scriptPtr, scriptType); + } + } + + return true; + } + [UnmanagedCallersOnly] internal static unsafe void GetOrCreateScriptBridgeForPath(godot_string* scriptPath, godot_ref* outScript) { @@ -448,6 +523,8 @@ namespace Godot.Bridge return; } + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Cannot get or create script for a generic type definition '{scriptType.FullName}'. Path: '{scriptPathStr}'."); + GetOrCreateScriptBridgeForType(scriptType, outScript); } @@ -487,16 +564,51 @@ namespace Godot.Bridge if (_pathTypeBiMap.TryGetScriptPath(scriptType, out scriptPath)) return true; + if (scriptType.IsConstructedGenericType) + { + // If the script type is generic, also try looking for the path of the generic type definition + // since we can use it to create the script. + Type genericTypeDefinition = scriptType.GetGenericTypeDefinition(); + if (_pathTypeBiMap.TryGetGenericTypeDefinitionPath(genericTypeDefinition, out scriptPath)) + return true; + } + CreateScriptBridgeForType(scriptType, outScript); scriptPath = null; return false; } } + static string GetVirtualConstructedGenericTypeScriptPath(Type scriptType, string scriptPath) + { + // Constructed generic types all have the same path which is not allowed by Godot + // (every Resource must have a unique path). So we create a unique "virtual" path + // for each type. + + if (!scriptPath.StartsWith("res://", StringComparison.Ordinal)) + { + throw new ArgumentException("Script path must start with 'res://'.", nameof(scriptPath)); + } + + scriptPath = scriptPath.Substring("res://".Length); + return $"csharp://{scriptPath}:{scriptType}.cs"; + } + if (GetPathOtherwiseGetOrCreateScript(scriptType, outScript, out string? scriptPath)) { // This path is slower, but it's only executed for the first instantiation of the type + if (scriptType.IsConstructedGenericType && !scriptPath.StartsWith("csharp://", StringComparison.Ordinal)) + { + // If the script type is generic it can't be loaded using the real script path. + // Construct a virtual path unique to this constructed generic type and add it + // to the path bimap so they can be found later by their virtual path. + // IMPORTANT: The virtual path must be added to _pathTypeBiMap before the first + // load of the script, otherwise the loaded script won't be added to _scriptTypeBiMap. + scriptPath = GetVirtualConstructedGenericTypeScriptPath(scriptType, scriptPath); + _pathTypeBiMap.Add(scriptPath, scriptType); + } + // This must be done outside the read-write lock, as the script resource loading can lock it using godot_string scriptPathIn = Marshaling.ConvertStringToNative(scriptPath); if (!NativeFuncs.godotsharp_internal_script_load(scriptPathIn, outScript).ToBool()) @@ -507,11 +619,23 @@ namespace Godot.Bridge // with no path, as we do for types without an associated script file. GetOrCreateScriptBridgeForType(scriptType, outScript); } + + if (scriptType.IsConstructedGenericType) + { + // When reloading generic scripts they won't be added to the script bimap because their + // virtual path won't be in the path bimap yet. The current method executes when a derived type + // is trying to get or create the script for their base type. The code above has now added + // the virtual path to the path bimap and loading the script with that path should retrieve + // any existing script, so now we have a chance to make sure it's added to the script bimap. + AddScriptBridgeCore(outScript->Reference, scriptPath); + } } } private static unsafe void CreateScriptBridgeForType(Type scriptType, godot_ref* outScript) { + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}."); + NativeFuncs.godotsharp_internal_new_csharp_script(outScript); IntPtr scriptPtr = outScript->Reference; @@ -577,7 +701,6 @@ namespace Godot.Bridge return godot_bool.False; } - // ReSharper disable once RedundantNameQualifier if (!typeof(GodotObject).IsAssignableFrom(scriptType)) { // The class no longer inherits GodotObject, can't reload @@ -598,45 +721,82 @@ namespace Godot.Bridge } } - [UnmanagedCallersOnly] - internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_string* outClassName, - godot_bool* outTool, godot_bool* outGlobal, godot_bool* outAbstract, godot_string* outIconPath, - godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, - godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) + private static unsafe void GetScriptTypeInfo(Type scriptType, godot_csharp_type_info* outTypeInfo) { - try + Type native = GodotObject.InternalGetClassNativeBase(scriptType); + + string typeName = scriptType.Name; + if (scriptType.IsGenericType) { - // Performance is not critical here as this will be replaced with source generators. - var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); + var sb = new StringBuilder(); + AppendTypeName(sb, scriptType); + typeName = sb.ToString(); + } - *outClassName = Marshaling.ConvertStringToNative(scriptType.Name); + godot_string className = Marshaling.ConvertStringToNative(typeName); - *outTool = scriptType.GetCustomAttributes(inherit: false) - .OfType<ToolAttribute>() - .Any().ToGodotBool(); + bool isTool = scriptType.IsDefined(typeof(ToolAttribute), inherit: false); - if (!(*outTool).ToBool() && scriptType.IsNested) - { - *outTool = (scriptType.DeclaringType?.GetCustomAttributes(inherit: false) - .OfType<ToolAttribute>() - .Any() ?? false).ToGodotBool(); - } + // If the type is nested and the parent type is a tool script, + // consider the nested type a tool script as well. + if (!isTool && scriptType.IsNested) + { + isTool = scriptType.DeclaringType?.IsDefined(typeof(ToolAttribute), inherit: false) ?? false; + } - if (!(*outTool).ToBool() && scriptType.Assembly.GetName().Name == "GodotTools") - *outTool = godot_bool.True; + // Every script in the GodotTools assembly is a tool script. + if (!isTool && scriptType.Assembly.GetName().Name == "GodotTools") + { + isTool = true; + } - var globalAttr = scriptType.GetCustomAttributes(inherit: false) - .OfType<GlobalClassAttribute>() - .FirstOrDefault(); + bool isGlobalClass = scriptType.IsDefined(typeof(GlobalClassAttribute), inherit: false); - *outGlobal = (globalAttr != null).ToGodotBool(); + var iconAttr = scriptType.GetCustomAttributes(inherit: false) + .OfType<IconAttribute>() + .FirstOrDefault(); - var iconAttr = scriptType.GetCustomAttributes(inherit: false) - .OfType<IconAttribute>() - .FirstOrDefault(); - *outIconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); + godot_string iconPath = Marshaling.ConvertStringToNative(iconAttr?.Path); - *outAbstract = scriptType.IsAbstract.ToGodotBool(); + outTypeInfo->ClassName = className; + outTypeInfo->IconPath = iconPath; + outTypeInfo->IsTool = isTool.ToGodotBool(); + outTypeInfo->IsGlobalClass = isGlobalClass.ToGodotBool(); + outTypeInfo->IsAbstract = scriptType.IsAbstract.ToGodotBool(); + outTypeInfo->IsGenericTypeDefinition = scriptType.IsGenericTypeDefinition.ToGodotBool(); + outTypeInfo->IsConstructedGenericType = scriptType.IsConstructedGenericType.ToGodotBool(); + + static void AppendTypeName(StringBuilder sb, Type type) + { + sb.Append(type.Name); + if (type.IsGenericType) + { + sb.Append('<'); + for (int i = 0; i < type.GenericTypeArguments.Length; i++) + { + Type typeArg = type.GenericTypeArguments[i]; + AppendTypeName(sb, typeArg); + if (i != type.GenericTypeArguments.Length - 1) + { + sb.Append(", "); + } + } + sb.Append('>'); + } + } + } + + [UnmanagedCallersOnly] + internal static unsafe void UpdateScriptClassInfo(IntPtr scriptPtr, godot_csharp_type_info* outTypeInfo, + godot_array* outMethodsDest, godot_dictionary* outRpcFunctionsDest, godot_dictionary* outEventSignalsDest, godot_ref* outBaseScript) + { + try + { + // Performance is not critical here as this will be replaced with source generators. + var scriptType = _scriptTypeBiMap.GetScriptType(scriptPtr); + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"Script type must be a constructed generic type or not generic at all. Type: {scriptType}."); + + GetScriptTypeInfo(scriptType, outTypeInfo); // Methods @@ -658,6 +818,19 @@ namespace Godot.Bridge methodInfo.Add("name", method.Name); + var returnVal = new Collections.Dictionary() + { + { "name", method.ReturnVal.Name }, + { "type", (int)method.ReturnVal.Type }, + { "usage", (int)method.ReturnVal.Usage } + }; + if (method.ReturnVal.ClassName != null) + { + returnVal["class_name"] = method.ReturnVal.ClassName; + } + + methodInfo.Add("return_val", returnVal); + var methodParams = new Collections.Array(); if (method.Arguments != null) @@ -800,11 +973,7 @@ namespace Godot.Bridge catch (Exception e) { ExceptionUtils.LogException(e); - *outClassName = default; - *outTool = godot_bool.False; - *outGlobal = godot_bool.False; - *outAbstract = godot_bool.False; - *outIconPath = default; + *outTypeInfo = default; *outMethodsDest = NativeFuncs.godotsharp_array_new(); *outRpcFunctionsDest = NativeFuncs.godotsharp_dictionary_new(); *outEventSignalsDest = NativeFuncs.godotsharp_dictionary_new(); @@ -838,8 +1007,9 @@ namespace Godot.Bridge return (List<MethodInfo>?)getGodotMethodListMethod.Invoke(null, null); } +#pragma warning disable IDE1006 // Naming rule violation // ReSharper disable once InconsistentNaming - [SuppressMessage("ReSharper", "NotAccessedField.Local")] + // ReSharper disable once NotAccessedField.Local [StructLayout(LayoutKind.Sequential)] private ref struct godotsharp_property_info { @@ -856,6 +1026,7 @@ namespace Godot.Bridge HintString.Dispose(); } } +#pragma warning restore IDE1006 [UnmanagedCallersOnly] internal static unsafe void GetPropertyInfoList(IntPtr scriptPtr, @@ -894,9 +1065,9 @@ namespace Godot.Bridge int length = properties.Count; // There's no recursion here, so it's ok to go with a big enough number for most cases - // stackMaxSize = stackMaxLength * sizeof(godotsharp_property_info) - const int stackMaxLength = 32; - bool useStack = length < stackMaxLength; + // StackMaxSize = StackMaxLength * sizeof(godotsharp_property_info) + const int StackMaxLength = 32; + bool useStack = length < StackMaxLength; godotsharp_property_info* interopProperties; @@ -904,7 +1075,7 @@ namespace Godot.Bridge { // Weird limitation, hence the need for aux: // "In the case of pointer types, you can use a stackalloc expression only in a local variable declaration to initialize the variable." - var aux = stackalloc godotsharp_property_info[stackMaxLength]; + var aux = stackalloc godotsharp_property_info[StackMaxLength]; interopProperties = aux; } else @@ -955,8 +1126,9 @@ namespace Godot.Bridge } } +#pragma warning disable IDE1006 // Naming rule violation // ReSharper disable once InconsistentNaming - [SuppressMessage("ReSharper", "NotAccessedField.Local")] + // ReSharper disable once NotAccessedField.Local [StructLayout(LayoutKind.Sequential)] private ref struct godotsharp_property_def_val_pair { @@ -964,6 +1136,7 @@ namespace Godot.Bridge public godot_string_name Name; // Not owned public godot_variant Value; // Not owned } +#pragma warning restore IDE1006 private delegate bool InvokeGodotClassStaticMethodDelegate(in godot_string_name method, NativeVariantPtrArgs args, out godot_variant ret); @@ -1083,9 +1256,9 @@ namespace Godot.Bridge int length = defaultValues.Count; // There's no recursion here, so it's ok to go with a big enough number for most cases - // stackMaxSize = stackMaxLength * sizeof(godotsharp_property_def_val_pair) - const int stackMaxLength = 32; - bool useStack = length < stackMaxLength; + // StackMaxSize = StackMaxLength * sizeof(godotsharp_property_def_val_pair) + const int StackMaxLength = 32; + bool useStack = length < StackMaxLength; godotsharp_property_def_val_pair* interopDefaultValues; @@ -1093,7 +1266,7 @@ namespace Godot.Bridge { // Weird limitation, hence the need for aux: // "In the case of pointer types, you can use a stackalloc expression only in a local variable declaration to initialize the variable." - var aux = stackalloc godotsharp_property_def_val_pair[stackMaxLength]; + var aux = stackalloc godotsharp_property_def_val_pair[StackMaxLength]; interopDefaultValues = aux; } else diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs index a58f6849ad..1ec1a75516 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.types.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; @@ -19,6 +20,8 @@ public static partial class ScriptManagerBridge { // TODO: What if this is called while unloading a load context, but after we already did cleanup in preparation for unloading? + Debug.Assert(!scriptType.IsGenericTypeDefinition, $"A generic type definition must never be added to the script type map. Type: {scriptType}."); + _scriptTypeMap.Add(scriptPtr, scriptType); _typeScriptMap.Add(scriptType, scriptPtr); @@ -61,6 +64,8 @@ public static partial class ScriptManagerBridge private System.Collections.Generic.Dictionary<string, Type> _pathTypeMap = new(); private System.Collections.Generic.Dictionary<Type, string> _typePathMap = new(); + public System.Collections.Generic.IEnumerable<string> Paths => _pathTypeMap.Keys; + public void Add(string scriptPath, Type scriptType) { _pathTypeMap.Add(scriptPath, scriptType); @@ -83,10 +88,29 @@ public static partial class ScriptManagerBridge [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetScriptType(string scriptPath, [MaybeNullWhen(false)] out Type scriptType) => - _pathTypeMap.TryGetValue(scriptPath, out scriptType); + // This must never return true for a generic type definition, we only consider script types + // the types that can be attached to a Node/Resource (non-generic or constructed generic types). + _pathTypeMap.TryGetValue(scriptPath, out scriptType) && !scriptType.IsGenericTypeDefinition; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) => - _typePathMap.TryGetValue(scriptType, out scriptPath); + public bool TryGetScriptPath(Type scriptType, [MaybeNullWhen(false)] out string scriptPath) + { + if (scriptType.IsGenericTypeDefinition) + { + // This must never return true for a generic type definition, we only consider script types + // the types that can be attached to a Node/Resource (non-generic or constructed generic types). + scriptPath = null; + return false; + } + + return _typePathMap.TryGetValue(scriptType, out scriptPath); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetGenericTypeDefinitionPath(Type genericTypeDefinition, [MaybeNullWhen(false)] out string scriptPath) + { + Debug.Assert(genericTypeDefinition.IsGenericTypeDefinition); + return _typePathMap.TryGetValue(genericTypeDefinition, out scriptPath); + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs index 293e680067..72a3fe3ed0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Color.cs @@ -1,7 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.InteropServices; using Godot.NativeInterop; +#nullable enable + namespace Godot { /// <summary> @@ -34,7 +38,9 @@ namespace Godot public float B; /// <summary> - /// The color's alpha (transparency) component, typically on the range of 0 to 1. + /// The color's alpha component, typically on the range of 0 to 1. + /// A value of 0 means that the color is fully transparent. + /// A value of 1 means that the color is fully opaque. /// </summary> public float A; @@ -530,7 +536,11 @@ namespace Godot /// <param name="r">The color's red component, typically on the range of 0 to 1.</param> /// <param name="g">The color's green component, typically on the range of 0 to 1.</param> /// <param name="b">The color's blue component, typically on the range of 0 to 1.</param> - /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> + /// <param name="a"> + /// The color's alpha value, typically on the range of 0 to 1. + /// A value of 0 means that the color is fully transparent. + /// A value of 1 means that the color is fully opaque. + /// </param> public Color(float r, float g, float b, float a = 1.0f) { R = r; @@ -543,7 +553,11 @@ namespace Godot /// Constructs a <see cref="Color"/> from an existing color and an alpha value. /// </summary> /// <param name="c">The color to construct from. Only its RGB values are used.</param> - /// <param name="a">The color's alpha (transparency) value, typically on the range of 0 to 1. Default: 1.</param> + /// <param name="a"> + /// The color's alpha value, typically on the range of 0 to 1. + /// A value of 0 means that the color is fully transparent. + /// A value of 1 means that the color is fully opaque. + /// </param> public Color(Color c, float a = 1.0f) { R = c.R; @@ -773,14 +787,14 @@ namespace Godot private static bool FindNamedColor(string name, out Color color) { - name = name.Replace(" ", string.Empty); - name = name.Replace("-", string.Empty); - name = name.Replace("_", string.Empty); - name = name.Replace("'", string.Empty); - name = name.Replace(".", string.Empty); + name = name.Replace(" ", string.Empty, StringComparison.Ordinal); + name = name.Replace("-", string.Empty, StringComparison.Ordinal); + name = name.Replace("_", string.Empty, StringComparison.Ordinal); + name = name.Replace("'", string.Empty, StringComparison.Ordinal); + name = name.Replace(".", string.Empty, StringComparison.Ordinal); name = name.ToUpperInvariant(); - return Colors.namedColors.TryGetValue(name, out color); + return Colors.NamedColors.TryGetValue(name, out color); } /// <summary> @@ -1274,7 +1288,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the color and the other object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Color other && Equals(other); } @@ -1324,9 +1338,11 @@ namespace Godot /// Converts this <see cref="Color"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this color.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({R.ToString(format)}, {G.ToString(format)}, {B.ToString(format)}, {A.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs index 44b1c2554c..5cb16fc385 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Colors.cs @@ -9,7 +9,7 @@ namespace Godot public static class Colors { // Color names and values are derived from core/math/color_names.inc - internal static readonly Dictionary<string, Color> namedColors = new Dictionary<string, Color> { + internal static readonly Dictionary<string, Color> NamedColors = new Dictionary<string, Color> { { "ALICEBLUE", Colors.AliceBlue }, { "ANTIQUEWHITE", Colors.AntiqueWhite }, { "AQUA", Colors.Aqua }, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs index 42f19ace1a..28a4e1bc76 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/CustomGCHandle.cs @@ -26,7 +26,6 @@ public static class CustomGCHandle [MethodImpl(MethodImplOptions.NoInlining)] public static bool IsAlcBeingUnloaded(AssemblyLoadContext alc) => _alcsBeingUnloaded.TryGetValue(alc, out _); - // ReSharper disable once RedundantNameQualifier private static ConcurrentDictionary< AssemblyLoadContext, ConcurrentDictionary<GCHandle, object> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DebuggingUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DebuggingUtils.cs index 57b292793a..0d96a9a5c1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DebuggingUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DebuggingUtils.cs @@ -62,8 +62,9 @@ namespace Godot Trace.Listeners.Add(new GodotTraceListener()); } - [StructLayout(LayoutKind.Sequential)] +#pragma warning disable IDE1006 // Naming rule violation // ReSharper disable once InconsistentNaming + [StructLayout(LayoutKind.Sequential)] internal ref struct godot_stack_info { public godot_string File; @@ -71,8 +72,8 @@ namespace Godot public int Line; } - [StructLayout(LayoutKind.Sequential)] // ReSharper disable once InconsistentNaming + [StructLayout(LayoutKind.Sequential)] internal ref struct godot_stack_info_vector { private IntPtr _writeProxy; @@ -101,6 +102,7 @@ namespace Godot _ptr = null; } } +#pragma warning restore IDE1006 internal static unsafe StackFrame? GetCurrentStackFrame(int skipFrames = 0) { diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs index 6c2fb7374c..c680142638 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; @@ -140,7 +141,6 @@ namespace Godot return true; } } - // ReSharper disable once RedundantNameQualifier case GodotObject godotObject: { using (var stream = new MemoryStream()) @@ -413,8 +413,7 @@ namespace Godot case TargetKind.GodotObject: { ulong objectId = reader.ReadUInt64(); - // ReSharper disable once RedundantNameQualifier - GodotObject godotObject = GodotObject.InstanceFromId(objectId); + GodotObject? godotObject = GodotObject.InstanceFromId(objectId); if (godotObject == null) return false; @@ -494,7 +493,7 @@ namespace Godot string methodName = reader.ReadString(); - int flags = reader.ReadInt32(); + BindingFlags flags = (BindingFlags)reader.ReadInt32(); bool hasReturn = reader.ReadBoolean(); Type? returnType = hasReturn ? DeserializeType(reader) : typeof(void); @@ -510,7 +509,11 @@ namespace Godot parameterTypes[i] = parameterType; } - methodInfo = declaringType.GetMethod(methodName, (BindingFlags)flags, null, parameterTypes, null); +#pragma warning disable REFL045 // These flags are insufficient to match any members + // TODO: Suppressing invalid warning, remove when issue is fixed + // https://github.com/DotNetAnalyzers/ReflectionAnalyzers/issues/209 + methodInfo = declaringType.GetMethod(methodName, flags, null, parameterTypes, null); +#pragma warning restore REFL045 return methodInfo != null && methodInfo.ReturnType == returnType; } @@ -587,7 +590,6 @@ namespace Godot internal static class RuntimeTypeConversionHelper { - [SuppressMessage("ReSharper", "RedundantNameQualifier")] public static godot_variant ConvertToVariant(object? obj) { if (obj == null) @@ -698,7 +700,7 @@ namespace Godot case GodotObject godotObject: return VariantUtils.CreateFrom(godotObject); case Enum @enum: - return VariantUtils.CreateFrom(Convert.ToInt64(@enum)); + return VariantUtils.CreateFrom(Convert.ToInt64(@enum, CultureInfo.InvariantCulture)); case Collections.IGenericGodotDictionary godotDictionary: return VariantUtils.CreateFrom(godotDictionary.UnderlyingDictionary); case Collections.IGenericGodotArray godotArray: @@ -712,10 +714,8 @@ namespace Godot private delegate object? ConvertToSystemObjectFunc(in godot_variant managed); - [SuppressMessage("ReSharper", "RedundantNameQualifier")] - // ReSharper disable once RedundantNameQualifier private static readonly System.Collections.Generic.Dictionary<Type, ConvertToSystemObjectFunc> - ToSystemObjectFuncByType = new() + _toSystemObjectFuncByType = new() { [typeof(bool)] = (in godot_variant variant) => VariantUtils.ConvertTo<bool>(variant), [typeof(char)] = (in godot_variant variant) => VariantUtils.ConvertTo<char>(variant), @@ -770,14 +770,13 @@ namespace Godot [typeof(Variant)] = (in godot_variant variant) => VariantUtils.ConvertTo<Variant>(variant), }; - [SuppressMessage("ReSharper", "RedundantNameQualifier")] public static object? ConvertToObjectOfType(in godot_variant variant, Type type) { - if (ToSystemObjectFuncByType.TryGetValue(type, out var func)) + if (_toSystemObjectFuncByType.TryGetValue(type, out var func)) return func(variant); if (typeof(GodotObject).IsAssignableFrom(type)) - return Convert.ChangeType(VariantUtils.ConvertTo<GodotObject>(variant), type); + return Convert.ChangeType(VariantUtils.ConvertTo<GodotObject>(variant), type, CultureInfo.InvariantCulture); if (typeof(GodotObject[]).IsAssignableFrom(type)) { @@ -796,7 +795,7 @@ namespace Godot } using var godotArray = NativeFuncs.godotsharp_variant_as_array(variant); - return Convert.ChangeType(ConvertToSystemArrayOfGodotObject(godotArray, type), type); + return Convert.ChangeType(ConvertToSystemArrayOfGodotObject(godotArray, type), type, CultureInfo.InvariantCulture); } if (type.IsEnum) @@ -837,8 +836,7 @@ namespace Godot if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>)) { - var ctor = type.GetConstructor(BindingFlags.Default, - new[] { typeof(Godot.Collections.Dictionary) }); + var ctor = type.GetConstructor(new[] { typeof(Godot.Collections.Dictionary) }); if (ctor == null) throw new InvalidOperationException("Dictionary constructor not found"); @@ -851,8 +849,7 @@ namespace Godot if (genericTypeDef == typeof(Godot.Collections.Array<>)) { - var ctor = type.GetConstructor(BindingFlags.Default, - new[] { typeof(Godot.Collections.Array) }); + var ctor = type.GetConstructor(new[] { typeof(Godot.Collections.Array) }); if (ctor == null) throw new InvalidOperationException("Array constructor not found"); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index 2a72ebc32b..d08fad90db 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -5,6 +5,8 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Godot.NativeInterop; +#nullable enable + namespace Godot.Collections { /// <summary> @@ -19,7 +21,7 @@ namespace Godot.Collections { internal godot_dictionary.movable NativeValue; - private WeakReference<IDisposable> _weakReferenceToSelf; + private WeakReference<IDisposable>? _weakReferenceToSelf; /// <summary> /// Constructs a new empty <see cref="Dictionary"/>. @@ -559,7 +561,8 @@ namespace Godot.Collections /// </summary> /// <param name="from">The typed dictionary to convert.</param> /// <returns>A new Godot Dictionary, or <see langword="null"/> if <see paramref="from"/> was null.</returns> - public static explicit operator Dictionary(Dictionary<TKey, TValue> from) + [return: NotNullIfNotNull("from")] + public static explicit operator Dictionary?(Dictionary<TKey, TValue>? from) { return from?._underlyingDict; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs index 53292e10cf..0ae3e1e130 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DisposablesTracker.cs @@ -59,7 +59,6 @@ namespace Godot GD.Print("Unloading: Finished disposing tracked instances."); } - // ReSharper disable once RedundantNameQualifier private static ConcurrentDictionary<WeakReference<GodotObject>, byte> GodotObjectInstances { get; } = new(); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/GodotObjectExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/GodotObjectExtensions.cs index 6c90c17078..563a6abe9b 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/GodotObjectExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Extensions/GodotObjectExtensions.cs @@ -1,6 +1,8 @@ using System; using Godot.NativeInterop; +#nullable enable + namespace Godot { public partial class GodotObject @@ -26,7 +28,7 @@ namespace Godot /// </example> /// <param name="instanceId">Instance ID of the Object to retrieve.</param> /// <returns>The <see cref="GodotObject"/> instance.</returns> - public static GodotObject InstanceFromId(ulong instanceId) + public static GodotObject? InstanceFromId(ulong instanceId) { return InteropUtils.UnmanagedGetManaged(NativeFuncs.godotsharp_instance_from_id(instanceId)); } @@ -49,7 +51,7 @@ namespace Godot /// </summary> /// <param name="instance">The instance to check.</param> /// <returns>If the instance is a valid object.</returns> - public static bool IsInstanceValid(GodotObject instance) + public static bool IsInstanceValid(GodotObject? instance) { return instance != null && instance.NativeInstance != IntPtr.Zero; } @@ -66,9 +68,9 @@ namespace Godot /// </summary> /// <param name="obj">The object.</param> /// <returns> - /// The <see cref="WeakRef"/> reference to the object or <see langword="null"/>. + /// The <see cref="Godot.WeakRef"/> reference to the object or <see langword="null"/>. /// </returns> - public static WeakRef WeakRef(GodotObject obj) + public static WeakRef? WeakRef(GodotObject? obj) { if (!IsInstanceValid(obj)) return null; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs index 43598ca84d..0be9cdc953 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs @@ -1,19 +1,22 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; using Godot.Bridge; using Godot.NativeInterop; +#nullable enable + namespace Godot { public partial class GodotObject : IDisposable { - private bool _disposed = false; - private static readonly Type CachedType = typeof(GodotObject); + private bool _disposed; + private static readonly Type _cachedType = typeof(GodotObject); internal IntPtr NativePtr; private bool _memoryOwn; - private WeakReference<GodotObject> _weakReferenceToSelf; + private WeakReference<GodotObject>? _weakReferenceToSelf; /// <summary> /// Constructs a new <see cref="GodotObject"/>. @@ -22,11 +25,11 @@ namespace Godot { unsafe { - _ConstructAndInitialize(NativeCtor, NativeName, CachedType, refCounted: false); + ConstructAndInitialize(NativeCtor, NativeName, _cachedType, refCounted: false); } } - internal unsafe void _ConstructAndInitialize( + internal unsafe void ConstructAndInitialize( delegate* unmanaged<IntPtr> nativeCtor, StringName nativeName, Type cachedType, @@ -35,6 +38,8 @@ namespace Godot { if (NativePtr == IntPtr.Zero) { + Debug.Assert(nativeCtor != null); + NativePtr = nativeCtor(); InteropUtils.TieManagedToUnmanaged(this, NativePtr, @@ -59,7 +64,7 @@ namespace Godot /// </summary> public IntPtr NativeInstance => NativePtr; - internal static IntPtr GetPtr(GodotObject instance) + internal static IntPtr GetPtr(GodotObject? instance) { if (instance == null) return IntPtr.Zero; @@ -105,7 +110,7 @@ namespace Godot if (gcHandleToFree != IntPtr.Zero) { - object target = GCHandle.FromIntPtr(gcHandleToFree).Target; + object? target = GCHandle.FromIntPtr(gcHandleToFree).Target; // The GC handle may have been replaced in another thread. Release it only if // it's associated to this managed instance, or if the target is no longer alive. if (target != this && target != null) @@ -176,18 +181,14 @@ namespace Godot internal static Type InternalGetClassNativeBase(Type t) { - do - { - var assemblyName = t.Assembly.GetName(); + var name = t.Assembly.GetName().Name; - if (assemblyName.Name == "GodotSharp") - return t; + if (name == "GodotSharp" || name == "GodotSharpEditor") + return t; - if (assemblyName.Name == "GodotSharpEditor") - return t; - } while ((t = t.BaseType) != null); + Debug.Assert(t.BaseType is not null, "Script types must derive from a native Godot type."); - return null; + return InternalGetClassNativeBase(t.BaseType); } // ReSharper disable once VirtualMemberNeverOverridden.Global diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.exceptions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.exceptions.cs index a7640043ce..8195828de9 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.exceptions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.exceptions.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text; #nullable enable @@ -73,7 +74,7 @@ namespace Godot if (!string.IsNullOrEmpty(_nativeClassName)) { - sb.Append($" (Method '{_nativeClassName}')"); + sb.Append(CultureInfo.InvariantCulture, $" (Method '{_nativeClassName}')"); } return sb.ToString(); @@ -131,7 +132,7 @@ namespace Godot if (!string.IsNullOrEmpty(_nativeMethodName)) { - sb.Append($" (Method '{_nativeMethodName}')"); + sb.Append(CultureInfo.InvariantCulture, $" (Method '{_nativeMethodName}')"); } return sb.ToString(); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs index 09269508b7..16d4616fcd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Mathf.cs @@ -36,11 +36,11 @@ namespace Godot public const real_t NaN = real_t.NaN; // 0.0174532924f and 0.0174532925199433 - private const float _degToRadConstF = (float)0.0174532925199432957692369077M; - private const double _degToRadConstD = (double)0.0174532925199432957692369077M; + private const float DegToRadConstF = (float)0.0174532925199432957692369077M; + private const double DegToRadConstD = (double)0.0174532925199432957692369077M; // 57.29578f and 57.2957795130823 - private const float _radToDegConstF = (float)57.295779513082320876798154814M; - private const double _radToDegConstD = (double)57.295779513082320876798154814M; + private const float RadToDegConstF = (float)57.295779513082320876798154814M; + private const double RadToDegConstD = (double)57.295779513082320876798154814M; /// <summary> /// Returns the absolute value of <paramref name="s"/> (i.e. positive value). @@ -760,7 +760,7 @@ namespace Godot /// <returns>The same angle expressed in radians.</returns> public static float DegToRad(float deg) { - return deg * _degToRadConstF; + return deg * DegToRadConstF; } /// <summary> @@ -770,7 +770,7 @@ namespace Godot /// <returns>The same angle expressed in radians.</returns> public static double DegToRad(double deg) { - return deg * _degToRadConstD; + return deg * DegToRadConstD; } /// <summary> @@ -957,10 +957,10 @@ namespace Godot return true; } // Then check for approximate equality. - float tolerance = _epsilonF * Math.Abs(a); - if (tolerance < _epsilonF) + float tolerance = EpsilonF * Math.Abs(a); + if (tolerance < EpsilonF) { - tolerance = _epsilonF; + tolerance = EpsilonF; } return Math.Abs(a - b) < tolerance; } @@ -981,10 +981,10 @@ namespace Godot return true; } // Then check for approximate equality. - double tolerance = _epsilonD * Math.Abs(a); - if (tolerance < _epsilonD) + double tolerance = EpsilonD * Math.Abs(a); + if (tolerance < EpsilonD) { - tolerance = _epsilonD; + tolerance = EpsilonD; } return Math.Abs(a - b) < tolerance; } @@ -1069,7 +1069,7 @@ namespace Godot [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsZeroApprox(float s) { - return Math.Abs(s) < _epsilonF; + return Math.Abs(s) < EpsilonF; } /// <summary> @@ -1084,7 +1084,7 @@ namespace Godot [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsZeroApprox(double s) { - return Math.Abs(s) < _epsilonD; + return Math.Abs(s) < EpsilonD; } /// <summary> @@ -1412,7 +1412,7 @@ namespace Godot [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float RadToDeg(float rad) { - return rad * _radToDegConstF; + return rad * RadToDegConstF; } /// <summary> @@ -1423,7 +1423,7 @@ namespace Godot [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double RadToDeg(double rad) { - return rad * _radToDegConstD; + return rad * RadToDegConstD; } /// <summary> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs index cc2d61f58d..f8ff78790e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/MathfEx.cs @@ -21,17 +21,17 @@ namespace Godot public const real_t Sqrt2 = (real_t)1.4142135623730950488016887242M; // 1.4142136f and 1.414213562373095 // Epsilon size should depend on the precision used. - private const float _epsilonF = 1e-06f; - private const double _epsilonD = 1e-14; + private const float EpsilonF = 1e-06f; + private const double EpsilonD = 1e-14; /// <summary> /// A very small number used for float comparison with error tolerance. /// 1e-06 with single-precision floats, but 1e-14 if <c>REAL_T_IS_DOUBLE</c>. /// </summary> #if REAL_T_IS_DOUBLE - public const real_t Epsilon = _epsilonD; + public const real_t Epsilon = EpsilonD; #else - public const real_t Epsilon = _epsilonF; + public const real_t Epsilon = EpsilonF; #endif /// <summary> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs index dc53e48bd0..04b6c2e743 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/ExceptionUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; #nullable enable @@ -239,5 +240,13 @@ namespace Godot.NativeInterop return variant->Type.ToString(); } + + internal static void ThrowIfNullPtr(IntPtr ptr, [CallerArgumentExpression("ptr")] string? paramName = null) + { + if (ptr == IntPtr.Zero) + { + throw new ArgumentNullException(paramName); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs index d5d9404ed1..a019dd3513 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs @@ -1,3 +1,7 @@ +#pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable IDE1006 // Naming rule violation +// ReSharper disable InconsistentNaming + using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -24,7 +28,6 @@ namespace Godot.NativeInterop } // Apparently a struct with a byte is not blittable? It crashes when calling a UnmanagedCallersOnly function ptr. - // ReSharper disable once InconsistentNaming public enum godot_bool : byte { True = 1, @@ -32,7 +35,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_ref { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -62,7 +64,6 @@ namespace Godot.NativeInterop } } - [SuppressMessage("ReSharper", "InconsistentNaming")] public enum godot_variant_call_error_error { GODOT_CALL_ERROR_CALL_OK = 0, @@ -75,7 +76,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_variant_call_error { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -105,8 +105,61 @@ namespace Godot.NativeInterop } } + [StructLayout(LayoutKind.Sequential)] + public ref struct godot_csharp_type_info + { + private godot_string _className; + private godot_string _iconPath; + private godot_bool _isTool; + private godot_bool _isGlobalClass; + private godot_bool _isAbstract; + private godot_bool _isConstructedGenericType; + private godot_bool _isGenericTypeDefinition; + + public godot_string ClassName + { + readonly get => _className; + set => _className = value; + } + + public godot_string IconPath + { + readonly get => _iconPath; + set => _iconPath = value; + } + + public godot_bool IsTool + { + readonly get => _isTool; + set => _isTool = value; + } + + public godot_bool IsGlobalClass + { + readonly get => _isGlobalClass; + set => _isGlobalClass = value; + } + + public godot_bool IsAbstract + { + readonly get => _isAbstract; + set => _isAbstract = value; + } + + public godot_bool IsConstructedGenericType + { + readonly get => _isConstructedGenericType; + set => _isConstructedGenericType = value; + } + + public godot_bool IsGenericTypeDefinition + { + readonly get => _isGenericTypeDefinition; + set => _isGenericTypeDefinition = value; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 8)] - // ReSharper disable once InconsistentNaming public ref struct godot_variant { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -121,7 +174,6 @@ namespace Godot.NativeInterop private godot_variant_data _data; [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming private unsafe ref struct godot_variant_data { [FieldOffset(0)] public godot_bool _bool; @@ -157,7 +209,6 @@ namespace Godot.NativeInterop [FieldOffset(0)] public godot_array _m_array; [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public struct godot_variant_obj_data { public ulong id; @@ -165,7 +216,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public struct godot_variant_data_mem { #pragma warning disable 169 @@ -425,7 +475,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming internal struct movable { // Variant.Type is generated as an enum of type long, so we can't use for the field as it must only take 32-bits. @@ -447,7 +496,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_string { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -474,12 +522,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != IntPtr.Zero ? *((int*)_ptr - 1) : 0; + get => _ptr != IntPtr.Zero ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_string_name { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -535,7 +582,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming internal struct movable { private IntPtr _data; @@ -552,7 +598,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_node_path { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -583,7 +628,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming internal struct movable { private IntPtr _data; @@ -600,7 +644,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming public ref struct godot_signal { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -643,7 +686,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming public ref struct godot_callable { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -681,7 +723,6 @@ namespace Godot.NativeInterop // Don't pass a C# default constructed `godot_array` to native code, unless it's going to // be re-assigned a new value (the copy constructor checks if `_p` is null so that's fine). [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming public ref struct godot_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -725,7 +766,7 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } @@ -762,7 +803,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming internal struct movable { private unsafe ArrayPrivate* _p; @@ -783,7 +823,6 @@ namespace Godot.NativeInterop // Don't pass a C# default constructed `godot_dictionary` to native code, unless it's going to // be re-assigned a new value (the copy constructor checks if `_p` is null so that's fine). [StructLayout(LayoutKind.Explicit)] - // ReSharper disable once InconsistentNaming public ref struct godot_dictionary { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -831,7 +870,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming internal struct movable { private unsafe DictionaryPrivate* _p; @@ -848,7 +886,6 @@ namespace Godot.NativeInterop } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_byte_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -875,12 +912,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_int32_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -907,12 +943,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *(_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_int64_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -939,12 +974,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_float32_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -971,12 +1005,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_float64_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1003,12 +1036,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_string_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1035,12 +1067,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_vector2_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1067,12 +1098,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_vector3_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1099,12 +1129,11 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming public ref struct godot_packed_color_array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1131,11 +1160,10 @@ namespace Godot.NativeInterop public readonly unsafe int Size { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _ptr != null ? *((int*)_ptr - 1) : 0; + get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0; } } - [SuppressMessage("ReSharper", "InconsistentNaming")] public enum godot_error_handler_type { ERR_HANDLER_ERROR = 0, diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs index 93a83b701b..9f7fa53e24 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/Marshaling.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1707 // Identifiers should not contain underscores + using System; using System.Runtime.InteropServices; using Godot.Collections; @@ -213,13 +215,13 @@ namespace Godot.NativeInterop if (p_string.Buffer == IntPtr.Zero) return string.Empty; - const int sizeOfChar32 = 4; + const int SizeOfChar32 = 4; byte* bytes = (byte*)p_string.Buffer; int size = p_string.Size; if (size == 0) return string.Empty; size -= 1; // zero at the end - int sizeInBytes = size * sizeOfChar32; + int sizeInBytes = size * SizeOfChar32; return System.Text.Encoding.UTF32.GetString(bytes, sizeInBytes); } @@ -259,7 +261,7 @@ namespace Godot.NativeInterop } return new godot_callable(method /* Takes ownership of disposable */, - p_managed_callable.Target.GetInstanceId()); + p_managed_callable.Target?.GetInstanceId() ?? 0); } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index d181bf2c0f..fef21fae46 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -1,9 +1,11 @@ +#pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable IDE1006 // Naming rule violation +// ReSharper disable InconsistentNaming + using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Godot.SourceGenerators.Internal; -// ReSharper disable InconsistentNaming namespace Godot.NativeInterop { @@ -16,7 +18,7 @@ namespace Godot.NativeInterop [GenerateUnmanagedCallbacks(typeof(UnmanagedCallbacks))] public static unsafe partial class NativeFuncs { - private static bool initialized = false; + private static bool initialized; // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global public static void Initialize(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.extended.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.extended.cs index 44ec16dca9..9f237e4d00 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.extended.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.extended.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable IDE1006 // Naming rule violation // ReSharper disable InconsistentNaming namespace Godot.NativeInterop diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs index e6bcd9393d..464b517428 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.cs @@ -1,8 +1,11 @@ +#pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable IDE1006 // Naming rule violation +// ReSharper disable InconsistentNaming + using System; using System.Runtime.CompilerServices; using Godot.Collections; -// ReSharper disable InconsistentNaming #nullable enable @@ -240,7 +243,6 @@ namespace Godot.NativeInterop public static godot_variant CreateFromSystemArrayOfRid(Span<Rid> from) => CreateFromArray(new Collections.Array(from)); - // ReSharper disable once RedundantNameQualifier public static godot_variant CreateFromSystemArrayOfGodotObject(GodotObject[]? from) { if (from == null) @@ -306,7 +308,6 @@ namespace Godot.NativeInterop } [MethodImpl(MethodImplOptions.AggressiveInlining)] - // ReSharper disable once RedundantNameQualifier public static godot_variant CreateFromGodotObject(GodotObject? from) => from != null ? CreateFromGodotObjectPtr(GodotObject.GetPtr(from)) : default; @@ -459,7 +460,6 @@ namespace Godot.NativeInterop => p_var.Type == Variant.Type.Object ? p_var.Object : IntPtr.Zero; [MethodImpl(MethodImplOptions.AggressiveInlining)] - // ReSharper disable once RedundantNameQualifier public static GodotObject ConvertToGodotObject(in godot_variant p_var) => InteropUtils.UnmanagedGetManaged(ConvertToGodotObjectPtr(p_var)); @@ -615,7 +615,6 @@ namespace Godot.NativeInterop } public static T[] ConvertToSystemArrayOfGodotObject<T>(in godot_variant p_var) - // ReSharper disable once RedundantNameQualifier where T : GodotObject { using var godotArray = NativeFuncs.godotsharp_variant_as_array(p_var); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs index 12b0a47079..d8f7214c2f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/VariantUtils.generic.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Godot.NativeInterop; @@ -25,7 +24,6 @@ public partial class VariantUtils // ReSharper disable once StaticMemberInGenericType internal static unsafe delegate*<in godot_variant, T> FromVariantCb; - [SuppressMessage("ReSharper", "RedundantNameQualifier")] static GenericConversion() { RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle); @@ -33,7 +31,6 @@ public partial class VariantUtils } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - [SuppressMessage("ReSharper", "RedundantNameQualifier")] public static godot_variant CreateFrom<[MustBeVariant] T>(in T from) { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -224,7 +221,6 @@ public partial class VariantUtils } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - [SuppressMessage("ReSharper", "RedundantNameQualifier")] public static T ConvertTo<[MustBeVariant] T>(in godot_variant variant) { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs index f216fb7ea3..0af640533d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using Godot.NativeInterop; +#nullable enable + namespace Godot { /// <summary> @@ -39,11 +42,11 @@ namespace Godot /// new NodePath("/root/MyAutoload"); // If you have an autoloaded node or scene. /// </code> /// </example> - public sealed class NodePath : IDisposable, IEquatable<NodePath> + public sealed class NodePath : IDisposable, IEquatable<NodePath?> { internal godot_node_path.movable NativeValue; - private WeakReference<IDisposable> _weakReferenceToSelf; + private WeakReference<IDisposable>? _weakReferenceToSelf; ~NodePath() { @@ -135,7 +138,8 @@ namespace Godot /// Converts this <see cref="NodePath"/> to a string. /// </summary> /// <param name="from">The <see cref="NodePath"/> to convert.</param> - public static implicit operator string(NodePath from) => from?.ToString(); + [return: NotNullIfNotNull("from")] + public static implicit operator string?(NodePath? from) => from?.ToString(); /// <summary> /// Converts this <see cref="NodePath"/> to a string. @@ -289,19 +293,19 @@ namespace Godot /// <returns>If the <see cref="NodePath"/> is empty.</returns> public bool IsEmpty => NativeValue.DangerousSelfRef.IsEmpty; - public static bool operator ==(NodePath left, NodePath right) + public static bool operator ==(NodePath? left, NodePath? right) { if (left is null) return right is null; return left.Equals(right); } - public static bool operator !=(NodePath left, NodePath right) + public static bool operator !=(NodePath? left, NodePath? right) { return !(left == right); } - public bool Equals(NodePath other) + public bool Equals([NotNullWhen(true)] NodePath? other) { if (other is null) return false; @@ -310,7 +314,7 @@ namespace Godot return NativeFuncs.godotsharp_node_path_equals(self, otherNative).ToBool(); } - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { return ReferenceEquals(this, obj) || (obj is NodePath other && Equals(other)); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs index 85b2b02c45..c5998eca5c 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Plane.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -382,7 +385,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the plane and the other object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Plane other && Equals(other); } @@ -430,9 +433,11 @@ namespace Godot /// Converts this <see cref="Plane"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this plane.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"{_normal.ToString(format)}, {_d.ToString(format)}"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs index a80d202ef2..c0889fb0e8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Projection.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -248,10 +251,10 @@ namespace Godot /// <param name="right">The right clipping distance.</param> /// <param name="bottom">The bottom clipping distance.</param> /// <param name="top">The top clipping distance.</param> - /// <param name="near">The near clipping distance.</param> - /// <param name="far">The far clipping distance.</param> + /// <param name="depthNear">The near clipping distance.</param> + /// <param name="depthFar">The far clipping distance.</param> /// <returns>The created projection.</returns> - public static Projection CreateFrustum(real_t left, real_t right, real_t bottom, real_t top, real_t near, real_t far) + public static Projection CreateFrustum(real_t left, real_t right, real_t bottom, real_t top, real_t depthNear, real_t depthFar) { if (right <= left) { @@ -261,18 +264,18 @@ namespace Godot { throw new ArgumentException("top is less or equal to bottom."); } - if (far <= near) + if (depthFar <= depthNear) { throw new ArgumentException("far is less or equal to near."); } - real_t x = 2 * near / (right - left); - real_t y = 2 * near / (top - bottom); + real_t x = 2 * depthNear / (right - left); + real_t y = 2 * depthNear / (top - bottom); real_t a = (right + left) / (right - left); real_t b = (top + bottom) / (top - bottom); - real_t c = -(far + near) / (far - near); - real_t d = -2 * far * near / (far - near); + real_t c = -(depthFar + depthNear) / (depthFar - depthNear); + real_t d = -2 * depthFar * depthNear / (depthFar - depthNear); return new Projection( new Vector4(x, 0, 0, 0), @@ -290,17 +293,17 @@ namespace Godot /// <param name="size">The frustum size.</param> /// <param name="aspect">The aspect ratio.</param> /// <param name="offset">The offset to apply.</param> - /// <param name="near">The near clipping distance.</param> - /// <param name="far">The far clipping distance.</param> + /// <param name="depthNear">The near clipping distance.</param> + /// <param name="depthFar">The far clipping distance.</param> /// <param name="flipFov">If the field of view is flipped over the projection's diagonal.</param> /// <returns>The created projection.</returns> - public static Projection CreateFrustumAspect(real_t size, real_t aspect, Vector2 offset, real_t near, real_t far, bool flipFov) + public static Projection CreateFrustumAspect(real_t size, real_t aspect, Vector2 offset, real_t depthNear, real_t depthFar, bool flipFov) { if (!flipFov) { size *= aspect; } - return CreateFrustum(-size / 2 + offset.X, +size / 2 + offset.X, -size / aspect / 2 + offset.Y, +size / aspect / 2 + offset.Y, near, far); + return CreateFrustum(-size / 2 + offset.X, +size / 2 + offset.X, -size / aspect / 2 + offset.Y, +size / aspect / 2 + offset.Y, depthNear, depthFar); } /// <summary> @@ -586,7 +589,7 @@ namespace Godot public readonly Vector2 GetFarPlaneHalfExtents() { var res = GetProjectionPlane(Planes.Far).Intersect3(GetProjectionPlane(Planes.Right), GetProjectionPlane(Planes.Top)); - return new Vector2(res.Value.X, res.Value.Y); + return res is null ? default : new Vector2(res.Value.X, res.Value.Y); } /// <summary> @@ -597,7 +600,7 @@ namespace Godot public readonly Vector2 GetViewportHalfExtents() { var res = GetProjectionPlane(Planes.Near).Intersect3(GetProjectionPlane(Planes.Right), GetProjectionPlane(Planes.Top)); - return new Vector2(res.Value.X, res.Value.Y); + return res is null ? default : new Vector2(res.Value.X, res.Value.Y); } /// <summary> @@ -905,7 +908,8 @@ namespace Godot } /// <summary> - /// Returns a Vector4 transformed (multiplied) by the inverse projection. + /// Returns a Vector4 transformed (multiplied) by the transpose of the projection. + /// For transforming by inverse of a projection <c>projection.Inverse() * vector</c> can be used instead. See <see cref="Inverse"/>. /// </summary> /// <param name="proj">The projection to apply.</param> /// <param name="vector">A Vector4 to transform.</param> @@ -980,7 +984,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Projection other && Equals(other); } @@ -1017,12 +1021,14 @@ namespace Godot /// Converts this <see cref="Projection"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this projection.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"{X.X.ToString(format)}, {X.Y.ToString(format)}, {X.Z.ToString(format)}, {X.W.ToString(format)}\n" + $"{Y.X.ToString(format)}, {Y.Y.ToString(format)}, {Y.Z.ToString(format)}, {Y.W.ToString(format)}\n" + $"{Z.X.ToString(format)}, {Z.Y.ToString(format)}, {Z.Z.ToString(format)}, {Z.W.ToString(format)}\n" + $"{W.X.ToString(format)}, {W.Y.ToString(format)}, {W.Z.ToString(format)}, {W.W.ToString(format)}\n"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs index 39e1b7e4b8..6a8cb1ba04 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Quaternion.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -644,6 +647,7 @@ namespace Godot /// <summary> /// Returns a Vector3 rotated (multiplied) by the inverse quaternion. + /// <c>vector * quaternion</c> is equivalent to <c>quaternion.Inverse() * vector</c>. See <see cref="Inverse"/>. /// </summary> /// <param name="vector">A Vector3 to inversely rotate.</param> /// <param name="quaternion">The quaternion to rotate by.</param> @@ -768,7 +772,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the quaternion and the other object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Quaternion other && Equals(other); } @@ -816,9 +820,11 @@ namespace Godot /// Converts this <see cref="Quaternion"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this quaternion.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index babb26960b..cf4ac45a9f 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -120,8 +123,8 @@ namespace Godot public readonly bool Encloses(Rect2 b) { return b._position.X >= _position.X && b._position.Y >= _position.Y && - b._position.X + b._size.X < _position.X + _size.X && - b._position.Y + b._size.Y < _position.Y + _size.Y; + b._position.X + b._size.X <= _position.X + _size.X && + b._position.Y + b._size.Y <= _position.Y + _size.Y; } /// <summary> @@ -427,7 +430,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the rect and the other object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Rect2 other && Equals(other); } @@ -475,7 +478,7 @@ namespace Godot /// Converts this <see cref="Rect2"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this rect.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"{_position.ToString(format)}, {_size.ToString(format)}"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs index 49fba02b54..58560df0c5 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -110,8 +113,8 @@ namespace Godot public readonly bool Encloses(Rect2I b) { return b._position.X >= _position.X && b._position.Y >= _position.Y && - b._position.X + b._size.X < _position.X + _size.X && - b._position.Y + b._size.Y < _position.Y + _size.Y; + b._position.X + b._size.X <= _position.X + _size.X && + b._position.Y + b._size.Y <= _position.Y + _size.Y; } /// <summary> @@ -398,7 +401,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the rect and the other object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Rect2I other && Equals(other); } @@ -435,7 +438,7 @@ namespace Godot /// Converts this <see cref="Rect2I"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this rect.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"{_position.ToString(format)}, {_size.ToString(format)}"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs index 350626389b..fccae94eac 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rid.cs @@ -1,8 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Godot.NativeInterop; +#nullable enable + namespace Godot { /// <summary> @@ -71,7 +74,7 @@ namespace Godot /// </summary> /// <param name="obj">The other object to compare.</param> /// <returns>Whether or not the color and the other object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Rid other && Equals(other); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index d53bb9f536..9cd5498fa8 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Security; using System.Security.Cryptography; using System.Text; @@ -107,7 +106,7 @@ namespace Godot instance = instance.Substring(1); } - if (instance.StartsWith("0b")) + if (instance.StartsWith("0b", StringComparison.OrdinalIgnoreCase)) { instance = instance.Substring(2); } @@ -220,7 +219,7 @@ namespace Godot { if (hasText) { - sb.Append(instance.Substring(indentStop, i - indentStop)); + sb.Append(instance.AsSpan(indentStop, i - indentStop)); } sb.Append('\n'); hasText = false; @@ -252,7 +251,7 @@ namespace Godot if (hasText) { - sb.Append(instance.Substring(indentStop, instance.Length - indentStop)); + sb.Append(instance.AsSpan(indentStop, instance.Length - indentStop)); } return sb.ToString(); @@ -315,7 +314,7 @@ namespace Godot /// <returns>The capitalized string.</returns> public static string Capitalize(this string instance) { - string aux = instance.CamelcaseToUnderscore(true).Replace("_", " ").Trim(); + string aux = instance.CamelcaseToUnderscore(true).Replace("_", " ", StringComparison.Ordinal).Trim(); string cap = string.Empty; for (int i = 0; i < aux.GetSliceCount(" "); i++) @@ -323,7 +322,7 @@ namespace Godot string slice = aux.GetSliceCharacter(' ', i); if (slice.Length > 0) { - slice = char.ToUpper(slice[0]) + slice.Substring(1); + slice = char.ToUpperInvariant(slice[0]) + slice.Substring(1); if (i > 0) cap += " "; cap += slice; @@ -408,13 +407,13 @@ namespace Godot bool shouldSplit = condA || condB || condC || canBreakNumberLetter || canBreakLetterNumber; if (shouldSplit) { - newString += instance.Substring(startIndex, i - startIndex) + "_"; + newString += string.Concat(instance.AsSpan(startIndex, i - startIndex), "_"); startIndex = i; } } newString += instance.Substring(startIndex, instance.Length - startIndex); - return lowerCase ? newString.ToLower() : newString; + return lowerCase ? newString.ToLowerInvariant() : newString; } /// <summary> @@ -479,9 +478,9 @@ namespace Godot return -1; // If this is empty, and the other one is not, then we're less... I think? if (to[toIndex] == 0) return 1; // Otherwise the other one is smaller.. - if (char.ToUpper(instance[instanceIndex]) < char.ToUpper(to[toIndex])) // More than + if (char.ToUpperInvariant(instance[instanceIndex]) < char.ToUpperInvariant(to[toIndex])) // More than return -1; - if (char.ToUpper(instance[instanceIndex]) > char.ToUpper(to[toIndex])) // Less than + if (char.ToUpperInvariant(instance[instanceIndex]) > char.ToUpperInvariant(to[toIndex])) // Less than return 1; instanceIndex++; @@ -743,7 +742,7 @@ namespace Godot byte[] ret = new byte[len]; for (int i = 0; i < len; i++) { - ret[i] = (byte)int.Parse(instance.AsSpan(i * 2, 2), NumberStyles.AllowHexSpecifier); + ret[i] = (byte)int.Parse(instance.AsSpan(i * 2, 2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); } return ret; } @@ -817,12 +816,12 @@ namespace Godot instance = instance.Substring(1); } - if (instance.StartsWith("0x")) + if (instance.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { instance = instance.Substring(2); } - return sign * int.Parse(instance, NumberStyles.HexNumber); + return sign * int.Parse(instance, NumberStyles.HexNumber, CultureInfo.InvariantCulture); } /// <summary> @@ -853,7 +852,7 @@ namespace Godot else { sb.Append(prefix); - sb.Append(instance.Substring(lineStart, i - lineStart + 1)); + sb.Append(instance.AsSpan(lineStart, i - lineStart + 1)); } lineStart = i + 1; } @@ -861,7 +860,7 @@ namespace Godot if (lineStart != instance.Length) { sb.Append(prefix); - sb.Append(instance.Substring(lineStart)); + sb.Append(instance.AsSpan(lineStart)); } return sb.ToString(); } @@ -879,7 +878,7 @@ namespace Godot if (string.IsNullOrEmpty(instance)) return false; else if (instance.Length > 1) - return instance[0] == '/' || instance[0] == '\\' || instance.Contains(":/") || instance.Contains(":\\"); + return instance[0] == '/' || instance[0] == '\\' || instance.Contains(":/", StringComparison.Ordinal) || instance.Contains(":\\", StringComparison.Ordinal); else return instance[0] == '/' || instance[0] == '\\'; } @@ -925,8 +924,8 @@ namespace Godot if (!caseSensitive) { - char sourcec = char.ToLower(instance[source]); - char targetc = char.ToLower(text[target]); + char sourcec = char.ToLowerInvariant(instance[source]); + char targetc = char.ToLowerInvariant(text[target]); match = sourcec == targetc; } else @@ -1121,7 +1120,7 @@ namespace Godot /// <returns>If the string contains a valid IP address.</returns> public static bool IsValidIPAddress(this string instance) { - if (instance.Contains(':')) + if (instance.Contains(':', StringComparison.Ordinal)) { string[] ip = instance.Split(':'); @@ -1234,7 +1233,7 @@ namespace Godot return false; if (caseSensitive) return instance[0] == expr[0]; - return (char.ToUpper(instance[0]) == char.ToUpper(expr[0])) && + return (char.ToUpperInvariant(instance[0]) == char.ToUpperInvariant(expr[0])) && ExprMatch(instance.Substring(1), expr.Substring(1), caseSensitive); } } @@ -1406,22 +1405,9 @@ namespace Godot } /// <summary> - /// Replace occurrences of a substring for different ones inside the string. - /// </summary> - /// <seealso cref="ReplaceN(string, string, string)"/> - /// <param name="instance">The string to modify.</param> - /// <param name="what">The substring to be replaced in the string.</param> - /// <param name="forwhat">The substring that replaces <paramref name="what"/>.</param> - /// <returns>The string with the substring occurrences replaced.</returns> - public static string Replace(this string instance, string what, string forwhat) - { - return instance.Replace(what, forwhat); - } - - /// <summary> /// Replace occurrences of a substring for different ones inside the string, but search case-insensitive. /// </summary> - /// <seealso cref="Replace(string, string, string)"/> + /// <seealso cref="string.Replace(string, string, StringComparison)"/> /// <param name="instance">The string to modify.</param> /// <param name="what">The substring to be replaced in the string.</param> /// <param name="forwhat">The substring that replaces <paramref name="what"/>.</param> @@ -1635,7 +1621,7 @@ namespace Godot if (end < 0) end = len; if (allowEmpty || end > from) - ret.Add(float.Parse(instance.Substring(from))); + ret.Add(float.Parse(instance.Substring(from), CultureInfo.InvariantCulture)); if (end == len) break; @@ -1739,7 +1725,7 @@ namespace Godot /// <returns>The number representation of the string.</returns> public static float ToFloat(this string instance) { - return float.Parse(instance); + return float.Parse(instance, CultureInfo.InvariantCulture); } /// <summary> @@ -1750,7 +1736,7 @@ namespace Godot /// <returns>The number representation of the string.</returns> public static int ToInt(this string instance) { - return int.Parse(instance); + return int.Parse(instance, CultureInfo.InvariantCulture); } /// <summary> @@ -1803,7 +1789,7 @@ namespace Godot /// <returns>A copy of the string with the prefix string removed from the start.</returns> public static string TrimPrefix(this string instance, string prefix) { - if (instance.StartsWith(prefix)) + if (instance.StartsWith(prefix, StringComparison.Ordinal)) return instance.Substring(prefix.Length); return instance; @@ -1817,7 +1803,7 @@ namespace Godot /// <returns>A copy of the string with the suffix string removed from the end.</returns> public static string TrimSuffix(this string instance, string suffix) { - if (instance.EndsWith(suffix)) + if (instance.EndsWith(suffix, StringComparison.Ordinal)) return instance.Substring(0, instance.Length - suffix.Length); return instance; @@ -1834,7 +1820,7 @@ namespace Godot /// <returns>The unescaped string.</returns> public static string URIDecode(this string instance) { - return Uri.UnescapeDataString(instance.Replace("+", "%20")); + return Uri.UnescapeDataString(instance.Replace("+", "%20", StringComparison.Ordinal)); } /// <summary> @@ -1850,8 +1836,8 @@ namespace Godot return Uri.EscapeDataString(instance); } - private const string _uniqueNodePrefix = "%"; - private static readonly string[] _invalidNodeNameCharacters = { ".", ":", "@", "/", "\"", _uniqueNodePrefix }; + private const string UniqueNodePrefix = "%"; + private static readonly string[] _invalidNodeNameCharacters = { ".", ":", "@", "/", "\"", UniqueNodePrefix }; /// <summary> /// Removes any characters from the string that are prohibited in @@ -1861,10 +1847,10 @@ namespace Godot /// <returns>The string sanitized as a valid node name.</returns> public static string ValidateNodeName(this string instance) { - string name = instance.Replace(_invalidNodeNameCharacters[0], ""); + string name = instance.Replace(_invalidNodeNameCharacters[0], "", StringComparison.Ordinal); for (int i = 1; i < _invalidNodeNameCharacters.Length; i++) { - name = name.Replace(_invalidNodeNameCharacters[i], ""); + name = name.Replace(_invalidNodeNameCharacters[i], "", StringComparison.Ordinal); } return name; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs index 97d28f9ee9..21d9ada127 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringName.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using Godot.NativeInterop; +#nullable enable + namespace Godot { /// <summary> @@ -10,11 +13,11 @@ namespace Godot /// Comparing them is much faster than with regular strings, because only the pointers are compared, /// not the whole strings. /// </summary> - public sealed class StringName : IDisposable, IEquatable<StringName> + public sealed class StringName : IDisposable, IEquatable<StringName?> { internal godot_string_name.movable NativeValue; - private WeakReference<IDisposable> _weakReferenceToSelf; + private WeakReference<IDisposable>? _weakReferenceToSelf; ~StringName() { @@ -81,7 +84,8 @@ namespace Godot /// Converts a <see cref="StringName"/> to a string. /// </summary> /// <param name="from">The <see cref="StringName"/> to convert.</param> - public static implicit operator string(StringName from) => from?.ToString(); + [return: NotNullIfNotNull("from")] + public static implicit operator string?(StringName? from) => from?.ToString(); /// <summary> /// Converts this <see cref="StringName"/> to a string. @@ -104,43 +108,43 @@ namespace Godot /// <returns>If the <see cref="StringName"/> is empty.</returns> public bool IsEmpty => NativeValue.DangerousSelfRef.IsEmpty; - public static bool operator ==(StringName left, StringName right) + public static bool operator ==(StringName? left, StringName? right) { if (left is null) return right is null; return left.Equals(right); } - public static bool operator !=(StringName left, StringName right) + public static bool operator !=(StringName? left, StringName? right) { return !(left == right); } - public bool Equals(StringName other) + public bool Equals([NotNullWhen(true)] StringName? other) { if (other is null) return false; return NativeValue.DangerousSelfRef == other.NativeValue.DangerousSelfRef; } - public static bool operator ==(StringName left, in godot_string_name right) + public static bool operator ==(StringName? left, in godot_string_name right) { if (left is null) return right.IsEmpty; return left.Equals(right); } - public static bool operator !=(StringName left, in godot_string_name right) + public static bool operator !=(StringName? left, in godot_string_name right) { return !(left == right); } - public static bool operator ==(in godot_string_name left, StringName right) + public static bool operator ==(in godot_string_name left, StringName? right) { return right == left; } - public static bool operator !=(in godot_string_name left, StringName right) + public static bool operator !=(in godot_string_name left, StringName? right) { return !(right == left); } @@ -150,7 +154,7 @@ namespace Godot return NativeValue.DangerousSelfRef == other; } - public override bool Equals(object obj) + public override bool Equals([NotNullWhen(true)] object? obj) { return ReferenceEquals(this, obj) || (obj is StringName other && Equals(other)); } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs index 0e3e54a0c2..3443277fee 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform2D.cs @@ -1,7 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -126,7 +129,7 @@ namespace Godot /// <summary> /// Returns the inverse of the transform, under the assumption that - /// the transformation is composed of rotation, scaling, and translation. + /// the basis is invertible (must have non-zero determinant). /// </summary> /// <seealso cref="Inverse"/> /// <returns>The inverse transformation matrix.</returns> @@ -180,11 +183,12 @@ namespace Godot } /// <summary> - /// Returns a vector transformed (multiplied) by the inverse basis matrix. + /// Returns a vector transformed (multiplied) by the inverse basis matrix, + /// under the assumption that the basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). /// This method does not account for translation (the <see cref="Origin"/> vector). - /// - /// Note: This results in a multiplication by the inverse of the - /// basis matrix only if it represents a rotation-reflection. + /// <c>transform.BasisXformInv(vector)</c> is equivalent to <c>transform.Inverse().BasisXform(vector)</c>. See <see cref="Inverse"/>. + /// For non-orthonormal transforms (e.g. with scaling) <c>transform.AffineInverse().BasisXform(vector)</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <seealso cref="BasisXform(Vector2)"/> /// <param name="v">A vector to inversely transform.</param> @@ -213,8 +217,9 @@ namespace Godot /// <summary> /// Returns the inverse of the transform, under the assumption that - /// the transformation is composed of rotation and translation - /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). Use <see cref="AffineInverse"/> for + /// non-orthonormal transforms (e.g. with scaling). /// </summary> /// <returns>The inverse matrix.</returns> public readonly Transform2D Inverse() @@ -480,7 +485,11 @@ namespace Godot } /// <summary> - /// Returns a Vector2 transformed (multiplied) by the inverse transformation matrix. + /// Returns a Vector2 transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>vector * transform</c> is equivalent to <c>transform.Inverse() * vector</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * vector</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="vector">A Vector2 to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -507,7 +516,11 @@ namespace Godot } /// <summary> - /// Returns a Rect2 transformed (multiplied) by the inverse transformation matrix. + /// Returns a Rect2 transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>rect * transform</c> is equivalent to <c>transform.Inverse() * rect</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * rect</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="rect">A Rect2 to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -541,7 +554,11 @@ namespace Godot } /// <summary> - /// Returns a copy of the given Vector2[] transformed (multiplied) by the inverse transformation matrix. + /// Returns a copy of the given Vector2[] transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>array * transform</c> is equivalent to <c>transform.Inverse() * array</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * array</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="array">A Vector2[] to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -592,7 +609,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the transform and the object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Transform2D other && Equals(other); } @@ -642,7 +659,7 @@ namespace Godot /// Converts this <see cref="Transform2D"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this transform.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"[X: {X.ToString(format)}, Y: {Y.ToString(format)}, O: {Origin.ToString(format)}]"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs index 7b27071df1..f80c0bd8dd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Transform3D.cs @@ -1,7 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.ComponentModel; +#nullable enable + namespace Godot { /// <summary> @@ -105,7 +108,7 @@ namespace Godot /// <summary> /// Returns the inverse of the transform, under the assumption that - /// the transformation is composed of rotation, scaling, and translation. + /// the basis is invertible (must have non-zero determinant). /// </summary> /// <seealso cref="Inverse"/> /// <returns>The inverse transformation matrix.</returns> @@ -144,8 +147,9 @@ namespace Godot /// <summary> /// Returns the inverse of the transform, under the assumption that - /// the transformation is composed of rotation and translation - /// (no scaling, use <see cref="AffineInverse"/> for transforms with scaling). + /// the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). Use <see cref="AffineInverse"/> for + /// non-orthonormal transforms (e.g. with scaling). /// </summary> /// <returns>The inverse matrix.</returns> public readonly Transform3D Inverse() @@ -426,10 +430,11 @@ namespace Godot } /// <summary> - /// Returns a Vector3 transformed (multiplied) by the transposed transformation matrix. - /// - /// Note: This results in a multiplication by the inverse of the - /// transformation matrix only if it represents a rotation-reflection. + /// Returns a Vector3 transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>vector * transform</c> is equivalent to <c>transform.Inverse() * vector</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * vector</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="vector">A Vector3 to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -482,7 +487,11 @@ namespace Godot } /// <summary> - /// Returns an AABB transformed (multiplied) by the inverse transformation matrix. + /// Returns an AABB transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>aabb * transform</c> is equivalent to <c>transform.Inverse() * aabb</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * aabb</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="aabb">An AABB to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -523,6 +532,7 @@ namespace Godot /// <summary> /// Returns a Plane transformed (multiplied) by the inverse transformation matrix. + /// <c>plane * transform</c> is equivalent to <c>transform.AffineInverse() * plane</c>. See <see cref="AffineInverse"/>. /// </summary> /// <param name="plane">A Plane to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -568,7 +578,11 @@ namespace Godot } /// <summary> - /// Returns a copy of the given Vector3[] transformed (multiplied) by the inverse transformation matrix. + /// Returns a copy of the given Vector3[] transformed (multiplied) by the inverse transformation matrix, + /// under the assumption that the transformation basis is orthonormal (i.e. rotation/reflection + /// is fine, scaling/skew is not). + /// <c>array * transform</c> is equivalent to <c>transform.Inverse() * array</c>. See <see cref="Inverse"/>. + /// For transforming by inverse of an affine transformation (e.g. with scaling) <c>transform.AffineInverse() * array</c> can be used instead. See <see cref="AffineInverse"/>. /// </summary> /// <param name="array">A Vector3[] to inversely transform.</param> /// <param name="transform">The transformation to apply.</param> @@ -619,7 +633,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the transform and the object are exactly equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Transform3D other && Equals(other); } @@ -669,7 +683,7 @@ namespace Godot /// Converts this <see cref="Transform3D"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this transform.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { return $"[X: {Basis.X.ToString(format)}, Y: {Basis.Y.ToString(format)}, Z: {Basis.Z.ToString(format)}, O: {Origin.ToString(format)}]"; } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs index 9aad965ad0..036a26328a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Variant.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Godot.NativeInterop; @@ -7,7 +6,6 @@ namespace Godot; #nullable enable -[SuppressMessage("ReSharper", "RedundantNameQualifier")] public partial struct Variant : IDisposable { internal godot_variant.movable NativeVar; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs index 4842dbc9af..8f1bc109c0 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -489,7 +492,10 @@ namespace Godot } /// <summary> - /// Returns this vector projected onto another vector <paramref name="onNormal"/>. + /// Returns a new vector resulting from projecting this vector onto the given vector <paramref name="onNormal"/>. + /// The resulting new vector is parallel to <paramref name="onNormal"/>. + /// See also <see cref="Slide(Vector2)"/>. + /// Note: If the vector <paramref name="onNormal"/> is a zero vector, the components of the resulting new vector will be <see cref="real_t.NaN"/>. /// </summary> /// <param name="onNormal">The vector to project onto.</param> /// <returns>The projected vector.</returns> @@ -580,9 +586,12 @@ namespace Godot } /// <summary> - /// Returns this vector slid along a plane defined by the given <paramref name="normal"/>. + /// Returns a new vector resulting from sliding this vector along a line with normal <paramref name="normal"/>. + /// The resulting new vector is perpendicular to <paramref name="normal"/>, and is equivalent to this vector minus its projection on <paramref name="normal"/>. + /// See also <see cref="Project(Vector2)"/>. + /// Note: The vector <paramref name="normal"/> must be normalized. See also <see cref="Normalized()"/>. /// </summary> - /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <param name="normal">The normal vector of the plane to slide on.</param> /// <returns>The slid vector.</returns> public readonly Vector2 Slide(Vector2 normal) { @@ -954,7 +963,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector2 other && Equals(other); } @@ -1016,9 +1025,11 @@ namespace Godot /// Converts this <see cref="Vector2"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs index 4ee452455e..1a386d9da1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector2I.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -121,6 +124,29 @@ namespace Godot } /// <summary> + /// Returns the squared distance between this vector and <paramref name="to"/>. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> + public readonly int DistanceSquaredTo(Vector2I to) + { + return (to - this).LengthSquared(); + } + + /// <summary> + /// Returns the distance between this vector and <paramref name="to"/>. + /// </summary> + /// <seealso cref="DistanceSquaredTo(Vector2I)"/> + /// <param name="to">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> + public readonly real_t DistanceTo(Vector2I to) + { + return (to - this).Length(); + } + + /// <summary> /// Returns the length (magnitude) of this vector. /// </summary> /// <seealso cref="LengthSquared"/> @@ -182,8 +208,8 @@ namespace Godot } // Constants - private static readonly Vector2I _min = new Vector2I(int.MinValue, int.MinValue); - private static readonly Vector2I _max = new Vector2I(int.MaxValue, int.MaxValue); + private static readonly Vector2I _minValue = new Vector2I(int.MinValue, int.MinValue); + private static readonly Vector2I _maxValue = new Vector2I(int.MaxValue, int.MaxValue); private static readonly Vector2I _zero = new Vector2I(0, 0); private static readonly Vector2I _one = new Vector2I(1, 1); @@ -197,12 +223,12 @@ namespace Godot /// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector2.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector2I(int.MinValue, int.MinValue)</c>.</value> - public static Vector2I Min { get { return _min; } } + public static Vector2I MinValue { get { return _minValue; } } /// <summary> /// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector2.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector2I(int.MaxValue, int.MaxValue)</c>.</value> - public static Vector2I Max { get { return _max; } } + public static Vector2I MaxValue { get { return _maxValue; } } /// <summary> /// Zero vector, a vector with all components set to <c>0</c>. @@ -535,7 +561,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector2I other && Equals(other); } @@ -572,9 +598,11 @@ namespace Godot /// Converts this <see cref="Vector2I"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs index d26d4662a0..74c1616e3d 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -511,7 +514,10 @@ namespace Godot } /// <summary> - /// Returns this vector projected onto another vector <paramref name="onNormal"/>. + /// Returns a new vector resulting from projecting this vector onto the given vector <paramref name="onNormal"/>. + /// The resulting new vector is parallel to <paramref name="onNormal"/>. + /// See also <see cref="Slide(Vector3)"/>. + /// Note: If the vector <paramref name="onNormal"/> is a zero vector, the components of the resulting new vector will be <see cref="real_t.NaN"/>. /// </summary> /// <param name="onNormal">The vector to project onto.</param> /// <returns>The projected vector.</returns> @@ -623,9 +629,12 @@ namespace Godot } /// <summary> - /// Returns this vector slid along a plane defined by the given <paramref name="normal"/>. + /// Returns a new vector resulting from sliding this vector along a plane with normal <paramref name="normal"/>. + /// The resulting new vector is perpendicular to <paramref name="normal"/>, and is equivalent to this vector minus its projection on <paramref name="normal"/>. + /// See also <see cref="Project(Vector3)"/>. + /// Note: The vector <paramref name="normal"/> must be normalized. See also <see cref="Normalized()"/>. /// </summary> - /// <param name="normal">The normal vector defining the plane to slide on.</param> + /// <param name="normal">The normal vector of the plane to slide on.</param> /// <returns>The slid vector.</returns> public readonly Vector3 Slide(Vector3 normal) { @@ -1056,7 +1065,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector3 other && Equals(other); } @@ -1118,9 +1127,11 @@ namespace Godot /// Converts this <see cref="Vector3"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs index db8ceb30e9..d0ad1922f6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector3I.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -129,6 +132,29 @@ namespace Godot } /// <summary> + /// Returns the squared distance between this vector and <paramref name="to"/>. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> + public readonly int DistanceSquaredTo(Vector3I to) + { + return (to - this).LengthSquared(); + } + + /// <summary> + /// Returns the distance between this vector and <paramref name="to"/>. + /// </summary> + /// <seealso cref="DistanceSquaredTo(Vector3I)"/> + /// <param name="to">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> + public readonly real_t DistanceTo(Vector3I to) + { + return (to - this).Length(); + } + + /// <summary> /// Returns the length (magnitude) of this vector. /// </summary> /// <seealso cref="LengthSquared"/> @@ -193,8 +219,8 @@ namespace Godot } // Constants - private static readonly Vector3I _min = new Vector3I(int.MinValue, int.MinValue, int.MinValue); - private static readonly Vector3I _max = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue); + private static readonly Vector3I _minValue = new Vector3I(int.MinValue, int.MinValue, int.MinValue); + private static readonly Vector3I _maxValue = new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue); private static readonly Vector3I _zero = new Vector3I(0, 0, 0); private static readonly Vector3I _one = new Vector3I(1, 1, 1); @@ -210,12 +236,12 @@ namespace Godot /// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector3.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector3I(int.MinValue, int.MinValue, int.MinValue)</c>.</value> - public static Vector3I Min { get { return _min; } } + public static Vector3I MinValue { get { return _minValue; } } /// <summary> /// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector3.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector3I(int.MaxValue, int.MaxValue, int.MaxValue)</c>.</value> - public static Vector3I Max { get { return _max; } } + public static Vector3I MaxValue { get { return _maxValue; } } /// <summary> /// Zero vector, a vector with all components set to <c>0</c>. @@ -590,7 +616,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector3I other && Equals(other); } @@ -627,9 +653,11 @@ namespace Godot /// Converts this <see cref="Vector3I"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs index eeaef5e46e..115e65bbdb 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -838,7 +841,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector4 other && Equals(other); } @@ -900,9 +903,11 @@ namespace Godot /// Converts this <see cref="Vector4"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs index e75e996b04..527c8e5022 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Vector4I.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +#nullable enable + namespace Godot { /// <summary> @@ -146,6 +149,29 @@ namespace Godot } /// <summary> + /// Returns the squared distance between this vector and <paramref name="to"/>. + /// This method runs faster than <see cref="DistanceTo"/>, so prefer it if + /// you need to compare vectors or need the squared distance for some formula. + /// </summary> + /// <param name="to">The other vector to use.</param> + /// <returns>The squared distance between the two vectors.</returns> + public readonly int DistanceSquaredTo(Vector4I to) + { + return (to - this).LengthSquared(); + } + + /// <summary> + /// Returns the distance between this vector and <paramref name="to"/>. + /// </summary> + /// <seealso cref="DistanceSquaredTo(Vector4I)"/> + /// <param name="to">The other vector to use.</param> + /// <returns>The distance between the two vectors.</returns> + public readonly real_t DistanceTo(Vector4I to) + { + return (to - this).Length(); + } + + /// <summary> /// Returns the length (magnitude) of this vector. /// </summary> /// <seealso cref="LengthSquared"/> @@ -228,8 +254,8 @@ namespace Godot } // Constants - private static readonly Vector4I _min = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue); - private static readonly Vector4I _max = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue); + private static readonly Vector4I _minValue = new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue); + private static readonly Vector4I _maxValue = new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue); private static readonly Vector4I _zero = new Vector4I(0, 0, 0, 0); private static readonly Vector4I _one = new Vector4I(1, 1, 1, 1); @@ -238,12 +264,12 @@ namespace Godot /// Min vector, a vector with all components equal to <see cref="int.MinValue"/>. Can be used as a negative integer equivalent of <see cref="Vector4.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector4I(int.MinValue, int.MinValue, int.MinValue, int.MinValue)</c>.</value> - public static Vector4I Min { get { return _min; } } + public static Vector4I MinValue { get { return _minValue; } } /// <summary> /// Max vector, a vector with all components equal to <see cref="int.MaxValue"/>. Can be used as an integer equivalent of <see cref="Vector4.Inf"/>. /// </summary> /// <value>Equivalent to <c>new Vector4I(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue)</c>.</value> - public static Vector4I Max { get { return _max; } } + public static Vector4I MaxValue { get { return _maxValue; } } /// <summary> /// Zero vector, a vector with all components set to <c>0</c>. @@ -611,7 +637,7 @@ namespace Godot /// </summary> /// <param name="obj">The object to compare with.</param> /// <returns>Whether or not the vector and the object are equal.</returns> - public override readonly bool Equals(object obj) + public override readonly bool Equals([NotNullWhen(true)] object? obj) { return obj is Vector4I other && Equals(other); } @@ -648,9 +674,11 @@ namespace Godot /// Converts this <see cref="Vector4I"/> to a string with the given <paramref name="format"/>. /// </summary> /// <returns>A string representation of this vector.</returns> - public readonly string ToString(string format) + public readonly string ToString(string? format) { - return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}), {W.ToString(format)})"; +#pragma warning disable CA1305 // Disable warning: "Specify IFormatProvider" + return $"({X.ToString(format)}, {Y.ToString(format)}, {Z.ToString(format)}, {W.ToString(format)})"; +#pragma warning restore CA1305 } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index a55b8d693b..d54942e654 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -17,7 +17,7 @@ <Authors>Godot Engine contributors</Authors> <PackageId>GodotSharp</PackageId> - <Version>4.2.0</Version> + <Version>4.3.0</Version> <PackageVersion>$(PackageVersion_GodotSharp)</PackageVersion> <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/glue/GodotSharp/GodotSharp</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> @@ -142,5 +142,5 @@ We can't use wildcards as there may be undesired old files still hanging around. Fortunately code completion, go to definition and such still work. --> - <Import Project="Generated\GeneratedIncludes.props" /> + <Import Condition=" '$(GodotSkipGenerated)' == '' " Project="Generated\GeneratedIncludes.props" /> </Project> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/Compat.cs b/modules/mono/glue/GodotSharp/GodotSharpEditor/Compat.cs index 7a3bb0df7e..d1289ee6ba 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/Compat.cs +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/Compat.cs @@ -7,6 +7,9 @@ using System.ComponentModel; namespace Godot; +// TODO: This is currently disabled because of https://github.com/dotnet/roslyn/issues/52904 +#pragma warning disable IDE0040 // Add accessibility modifiers. + partial class EditorUndoRedoManager { /// <inheritdoc cref="CreateAction(string, UndoRedo.MergeMode, GodotObject, bool)"/> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index db9337d4eb..c32cbcd3d1 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -15,7 +15,7 @@ <Authors>Godot Engine contributors</Authors> <PackageId>GodotSharpEditor</PackageId> - <Version>4.2.0</Version> + <Version>4.3.0</Version> <PackageVersion>$(PackageVersion_GodotSharp)</PackageVersion> <RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/glue/GodotSharp/GodotSharpEditor</RepositoryUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> @@ -44,5 +44,5 @@ We can't use wildcards as there may be undesired old files still hanging around. Fortunately code completion, go to definition and such still work. --> - <Import Project="Generated\GeneratedIncludes.props" /> + <Import Condition=" '$(GodotSkipGenerated)' == '' " Project="Generated\GeneratedIncludes.props" /> </Project> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 3518507f8c..0089e9c2a2 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -316,7 +316,7 @@ void godotsharp_internal_new_csharp_script(Ref<CSharpScript> *r_dest) { } void godotsharp_internal_editor_file_system_update_file(const String *p_script_path) { -#if TOOLS_ENABLED +#ifdef TOOLS_ENABLED // If the EditorFileSystem singleton is available, update the file; // otherwise, the file will be updated when the singleton becomes available. EditorFileSystem *efs = EditorFileSystem::get_singleton(); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index ca2ad315a7..0e34616951 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -53,13 +53,6 @@ #include <dlfcn.h> #endif -// TODO mobile -#if 0 -#ifdef IOS_ENABLED -#include "support/ios_support.h" -#endif -#endif - GDMono *GDMono::singleton = nullptr; namespace { @@ -395,6 +388,7 @@ void GDMono::initialize() { if (godot_plugins_initialize != nullptr) { is_native_aot = true; + runtime_initialized = true; } else { ERR_FAIL_MSG(".NET: Failed to load hostfxr"); } @@ -556,12 +550,6 @@ GDMono::GDMono() { GDMono::~GDMono() { finalizing_scripts_domain = true; - if (is_runtime_initialized()) { - if (GDMonoCache::godot_api_cache_updated) { - GDMonoCache::managed_callbacks.DisposablesTracker_OnGodotShuttingDown(); - } - } - if (hostfxr_dll_handle) { OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle); } diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 530936cfb4..1b46a619ca 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -68,7 +68,9 @@ class GDMono { String project_assembly_path; uint64_t project_assembly_modified_time = 0; +#ifdef GD_MONO_HOT_RELOAD int project_load_failure_count = 0; +#endif #ifdef TOOLS_ENABLED bool _load_project_assembly(); diff --git a/modules/mono/mono_gd/gd_mono_cache.cpp b/modules/mono/mono_gd/gd_mono_cache.cpp index ef4e32e4a7..145f4cee90 100644 --- a/modules/mono/mono_gd/gd_mono_cache.cpp +++ b/modules/mono/mono_gd/gd_mono_cache.cpp @@ -59,6 +59,7 @@ void update_godot_api_cache(const ManagedCallbacks &p_managed_callbacks) { CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectBinding); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, CreateManagedForGodotObjectScriptInstance); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetScriptNativeName); + CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, GetGlobalClassName); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, SetGodotObjectPtr); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, RaiseEventSignal); CHECK_CALLBACK_NOT_NULL(ScriptManagerBridge, ScriptIsOrInherits); diff --git a/modules/mono/mono_gd/gd_mono_cache.h b/modules/mono/mono_gd/gd_mono_cache.h index dac8cdcaef..46e9ab10cb 100644 --- a/modules/mono/mono_gd/gd_mono_cache.h +++ b/modules/mono/mono_gd/gd_mono_cache.h @@ -84,6 +84,7 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_CreateManagedForGodotObjectBinding = GCHandleIntPtr(GD_CLR_STDCALL *)(const StringName *, Object *); using FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance = bool(GD_CLR_STDCALL *)(const CSharpScript *, Object *, const Variant **, int32_t); using FuncScriptManagerBridge_GetScriptNativeName = void(GD_CLR_STDCALL *)(const CSharpScript *, StringName *); + using FuncScriptManagerBridge_GetGlobalClassName = void(GD_CLR_STDCALL *)(const String *, String *, String *, String *); using FuncScriptManagerBridge_SetGodotObjectPtr = void(GD_CLR_STDCALL *)(GCHandleIntPtr, Object *); using FuncScriptManagerBridge_RaiseEventSignal = void(GD_CLR_STDCALL *)(GCHandleIntPtr, const StringName *, const Variant **, int32_t, bool *); using FuncScriptManagerBridge_ScriptIsOrInherits = bool(GD_CLR_STDCALL *)(const CSharpScript *, const CSharpScript *); @@ -91,7 +92,7 @@ struct ManagedCallbacks { using FuncScriptManagerBridge_GetOrCreateScriptBridgeForPath = void(GD_CLR_STDCALL *)(const String *, Ref<CSharpScript> *); using FuncScriptManagerBridge_RemoveScriptBridge = void(GD_CLR_STDCALL *)(const CSharpScript *); using FuncScriptManagerBridge_TryReloadRegisteredScriptWithClass = bool(GD_CLR_STDCALL *)(const CSharpScript *); - using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, String *, bool *, bool *, bool *, String *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *); + using FuncScriptManagerBridge_UpdateScriptClassInfo = void(GD_CLR_STDCALL *)(const CSharpScript *, CSharpScript::TypeInfo *, Array *, Dictionary *, Dictionary *, Ref<CSharpScript> *); using FuncScriptManagerBridge_SwapGCHandleForType = bool(GD_CLR_STDCALL *)(GCHandleIntPtr, GCHandleIntPtr *, bool); using FuncScriptManagerBridge_GetPropertyInfoList = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyInfoList_Add); using FuncScriptManagerBridge_GetPropertyDefaultValues = void(GD_CLR_STDCALL *)(CSharpScript *, Callback_ScriptManagerBridge_GetPropertyDefaultValues_Add); @@ -120,6 +121,7 @@ struct ManagedCallbacks { FuncScriptManagerBridge_CreateManagedForGodotObjectBinding ScriptManagerBridge_CreateManagedForGodotObjectBinding; FuncScriptManagerBridge_CreateManagedForGodotObjectScriptInstance ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance; FuncScriptManagerBridge_GetScriptNativeName ScriptManagerBridge_GetScriptNativeName; + FuncScriptManagerBridge_GetGlobalClassName ScriptManagerBridge_GetGlobalClassName; FuncScriptManagerBridge_SetGodotObjectPtr ScriptManagerBridge_SetGodotObjectPtr; FuncScriptManagerBridge_RaiseEventSignal ScriptManagerBridge_RaiseEventSignal; FuncScriptManagerBridge_ScriptIsOrInherits ScriptManagerBridge_ScriptIsOrInherits; diff --git a/modules/mono/utils/path_utils.cpp b/modules/mono/utils/path_utils.cpp index aa97534675..ee17a668d7 100644 --- a/modules/mono/utils/path_utils.cpp +++ b/modules/mono/utils/path_utils.cpp @@ -152,7 +152,7 @@ String realpath(const String &p_path) { } return result.simplify_path(); -#elif UNIX_ENABLED +#elif defined(UNIX_ENABLED) char *resolved_path = ::realpath(p_path.utf8().get_data(), nullptr); if (!resolved_path) { diff --git a/modules/multiplayer/doc_classes/MultiplayerSpawner.xml b/modules/multiplayer/doc_classes/MultiplayerSpawner.xml index 482db3e8b5..3da245f806 100644 --- a/modules/multiplayer/doc_classes/MultiplayerSpawner.xml +++ b/modules/multiplayer/doc_classes/MultiplayerSpawner.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<class name="MultiplayerSpawner" inherits="Node" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> +<class name="MultiplayerSpawner" inherits="Node" keywords="network" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> Automatically replicates spawnable nodes from the authority to other multiplayer peers. </brief_description> diff --git a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml index df2644767d..c2d879962c 100644 --- a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml +++ b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<class name="MultiplayerSynchronizer" inherits="Node" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> +<class name="MultiplayerSynchronizer" inherits="Node" keywords="network" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> Synchronizes properties from the multiplayer authority to the remote peers. </brief_description> diff --git a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml index 1a51e4b6e9..a4f7fad2f0 100644 --- a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml +++ b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml @@ -27,7 +27,7 @@ <return type="bool" /> <param index="0" name="path" type="NodePath" /> <description> - Returns whether the given [param path] is configured for synchronization. + Returns [code]true[/code] if the given [param path] is configured for synchronization. </description> </method> <method name="property_get_index" qualifiers="const"> @@ -48,23 +48,21 @@ <return type="bool" /> <param index="0" name="path" type="NodePath" /> <description> - Returns whether the property identified by the given [param path] is configured to be synchronized on spawn. + Returns [code]true[/code] if the property identified by the given [param path] is configured to be synchronized on spawn. </description> </method> - <method name="property_get_sync" is_deprecated="true"> + <method name="property_get_sync" deprecated="Use [method property_get_replication_mode] instead."> <return type="bool" /> <param index="0" name="path" type="NodePath" /> <description> - Returns whether the property identified by the given [param path] is configured to be synchronized on process. - [i]Deprecated.[/i] Use [method property_get_replication_mode] instead. + Returns [code]true[/code] if the property identified by the given [param path] is configured to be synchronized on process. </description> </method> - <method name="property_get_watch" is_deprecated="true"> + <method name="property_get_watch" deprecated="Use [method property_get_replication_mode] instead."> <return type="bool" /> <param index="0" name="path" type="NodePath" /> <description> - Returns whether the property identified by the given [param path] is configured to be reliably synchronized when changes are detected on process. - [i]Deprecated.[/i] Use [method property_get_replication_mode] instead. + Returns [code]true[/code] if the property identified by the given [param path] is configured to be reliably synchronized when changes are detected on process. </description> </method> <method name="property_set_replication_mode"> @@ -83,22 +81,20 @@ Sets whether the property identified by the given [param path] is configured to be synchronized on spawn. </description> </method> - <method name="property_set_sync" is_deprecated="true"> + <method name="property_set_sync" deprecated="Use [method property_set_replication_mode] with [constant REPLICATION_MODE_ALWAYS] instead."> <return type="void" /> <param index="0" name="path" type="NodePath" /> <param index="1" name="enabled" type="bool" /> <description> Sets whether the property identified by the given [param path] is configured to be synchronized on process. - [i]Deprecated.[/i] Use [method property_set_replication_mode] with [constant REPLICATION_MODE_ALWAYS] instead. </description> </method> - <method name="property_set_watch" is_deprecated="true"> + <method name="property_set_watch" deprecated="Use [method property_set_replication_mode] with [constant REPLICATION_MODE_ON_CHANGE] instead."> <return type="void" /> <param index="0" name="path" type="NodePath" /> <param index="1" name="enabled" type="bool" /> <description> Sets whether the property identified by the given [param path] is configured to be reliably synchronized when changes are detected on process. - [i]Deprecated.[/i] Use [method property_set_replication_mode] with [constant REPLICATION_MODE_ON_CHANGE] instead. </description> </method> <method name="remove_property"> diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp index a53eefc452..a45e5ffdc0 100644 --- a/modules/multiplayer/editor/editor_network_profiler.cpp +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -31,9 +31,9 @@ #include "editor_network_profiler.h" #include "core/os/os.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" void EditorNetworkProfiler::_bind_methods() { ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.cpp b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp index af2db543c0..e8dfc3379c 100644 --- a/modules/multiplayer/editor/multiplayer_editor_plugin.cpp +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp @@ -36,6 +36,7 @@ #include "editor/editor_interface.h" #include "editor/editor_node.h" +#include "editor/gui/editor_bottom_panel.h" void MultiplayerEditorDebugger::_bind_methods() { ADD_SIGNAL(MethodInfo("open_request", PropertyInfo(Variant::STRING, "path"))); @@ -112,7 +113,7 @@ void MultiplayerEditorDebugger::setup_session(int p_session_id) { MultiplayerEditorPlugin::MultiplayerEditorPlugin() { repl_editor = memnew(ReplicationEditor); - button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor); + button = EditorNode::get_bottom_panel()->add_item(TTR("Replication"), repl_editor); button->hide(); repl_editor->get_pin()->connect("pressed", callable_mp(this, &MultiplayerEditorPlugin::_pinned)); debugger.instantiate(); @@ -139,7 +140,7 @@ void MultiplayerEditorPlugin::_node_removed(Node *p_node) { if (p_node && p_node == repl_editor->get_current()) { repl_editor->edit(nullptr); if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); + EditorNode::get_bottom_panel()->hide_bottom_panel(); } button->hide(); repl_editor->get_pin()->set_pressed(false); @@ -149,7 +150,7 @@ void MultiplayerEditorPlugin::_node_removed(Node *p_node) { void MultiplayerEditorPlugin::_pinned() { if (!repl_editor->get_pin()->is_pressed()) { if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); + EditorNode::get_bottom_panel()->hide_bottom_panel(); } button->hide(); } @@ -166,10 +167,10 @@ bool MultiplayerEditorPlugin::handles(Object *p_object) const { void MultiplayerEditorPlugin::make_visible(bool p_visible) { if (p_visible) { button->show(); - EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor); + EditorNode::get_bottom_panel()->make_item_visible(repl_editor); } else if (!repl_editor->get_pin()->is_pressed()) { if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); + EditorNode::get_bottom_panel()->hide_bottom_panel(); } button->hide(); } diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp index eab1f5d51d..f6df212d35 100644 --- a/modules/multiplayer/editor/replication_editor.cpp +++ b/modules/multiplayer/editor/replication_editor.cpp @@ -33,13 +33,14 @@ #include "../multiplayer_synchronizer.h" #include "editor/editor_node.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/scene_tree_editor.h" #include "editor/inspector_dock.h" #include "editor/property_selector.h" +#include "editor/themes/editor_scale.h" +#include "editor/themes/editor_theme_manager.h" #include "scene/gui/dialogs.h" #include "scene/gui/separator.h" #include "scene/gui/tree.h" @@ -291,7 +292,7 @@ ReplicationEditor::ReplicationEditor() { vb->add_child(tree); drop_label = memnew(Label); - drop_label->set_text(TTR("Add properties using the options above, or\ndrag them them from the inspector and drop them here.")); + drop_label->set_text(TTR("Add properties using the options above, or\ndrag them from the inspector and drop them here.")); drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); tree->add_child(drop_label); @@ -362,8 +363,13 @@ void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_da void ReplicationEditor::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_ENTER_TREE: case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!EditorThemeManager::is_generated_theme_outdated()) { + break; + } + [[fallthrough]]; + } + case NOTIFICATION_ENTER_TREE: { add_theme_style_override("panel", EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("panel"), SNAME("Panel"))); add_pick_button->set_icon(get_theme_icon(SNAME("Add"), EditorStringName(EditorIcons))); pin->set_icon(get_theme_icon(SNAME("Pin"), EditorStringName(EditorIcons))); diff --git a/modules/multiplayer/multiplayer_debugger.cpp b/modules/multiplayer/multiplayer_debugger.cpp index a4d2aed2d6..c816bd3b6b 100644 --- a/modules/multiplayer/multiplayer_debugger.cpp +++ b/modules/multiplayer/multiplayer_debugger.cpp @@ -89,7 +89,7 @@ Error MultiplayerDebugger::_capture(void *p_user, const String &p_msg, const Arr // BandwidthProfiler int MultiplayerDebugger::BandwidthProfiler::bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer) { - ERR_FAIL_COND_V(p_buffer.size() == 0, 0); + ERR_FAIL_COND_V(p_buffer.is_empty(), 0); int total_bandwidth = 0; uint64_t timestamp = OS::get_singleton()->get_ticks_msec(); @@ -174,7 +174,7 @@ Array MultiplayerDebugger::RPCFrame::serialize() { } bool MultiplayerDebugger::RPCFrame::deserialize(const Array &p_arr) { - ERR_FAIL_COND_V(p_arr.size() < 1, false); + ERR_FAIL_COND_V(p_arr.is_empty(), false); uint32_t size = p_arr[0]; ERR_FAIL_COND_V(size % 6, false); ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false); @@ -279,7 +279,7 @@ Array MultiplayerDebugger::ReplicationFrame::serialize() { } bool MultiplayerDebugger::ReplicationFrame::deserialize(const Array &p_arr) { - ERR_FAIL_COND_V(p_arr.size() < 1, false); + ERR_FAIL_COND_V(p_arr.is_empty(), false); uint32_t size = p_arr[0]; ERR_FAIL_COND_V(size % 7, false); ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false); diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp index 12b4ac540d..02e3a11964 100644 --- a/modules/multiplayer/multiplayer_synchronizer.cpp +++ b/modules/multiplayer/multiplayer_synchronizer.cpp @@ -49,11 +49,11 @@ void MultiplayerSynchronizer::_stop() { } #endif root_node_cache = ObjectID(); - reset(); Node *node = is_inside_tree() ? get_node_or_null(root_path) : nullptr; if (node) { get_multiplayer()->object_configuration_remove(node, this); } + reset(); } void MultiplayerSynchronizer::_start() { diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp index 56cd0bec18..33b05d4cc2 100644 --- a/modules/multiplayer/scene_cache_interface.cpp +++ b/modules/multiplayer/scene_cache_interface.cpp @@ -35,25 +35,61 @@ #include "core/io/marshalls.h" #include "scene/main/node.h" #include "scene/main/window.h" +#include "scene/scene_string_names.h" + +SceneCacheInterface::NodeCache &SceneCacheInterface::_track(Node *p_node) { + const ObjectID oid = p_node->get_instance_id(); + NodeCache *nc = nodes_cache.getptr(oid); + if (!nc) { + nodes_cache[oid] = NodeCache(); + p_node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache).bind(oid), Object::CONNECT_ONE_SHOT); + } + return nodes_cache[oid]; +} + +void SceneCacheInterface::_remove_node_cache(ObjectID p_oid) { + NodeCache *nc = nodes_cache.getptr(p_oid); + if (!nc) { + return; + } + for (KeyValue<int, int> &E : nc->recv_ids) { + PeerInfo *pinfo = peers_info.getptr(E.key); + ERR_CONTINUE(!pinfo); + pinfo->recv_nodes.erase(E.value); + } + for (KeyValue<int, bool> &E : nc->confirmed_peers) { + PeerInfo *pinfo = peers_info.getptr(E.key); + ERR_CONTINUE(!pinfo); + pinfo->sent_nodes.erase(p_oid); + } + nodes_cache.erase(p_oid); +} void SceneCacheInterface::on_peer_change(int p_id, bool p_connected) { if (p_connected) { - path_get_cache.insert(p_id, PathGetCache()); + peers_info.insert(p_id, PeerInfo()); } else { - // Cleanup get cache. - path_get_cache.erase(p_id); - // Cleanup sent cache. - // Some refactoring is needed to make this faster and do paths GC. - for (KeyValue<ObjectID, PathSentCache> &E : path_send_cache) { - E.value.confirmed_peers.erase(p_id); + PeerInfo *pinfo = peers_info.getptr(p_id); + ERR_FAIL_NULL(pinfo); // Bug. + for (KeyValue<int, ObjectID> E : pinfo->recv_nodes) { + NodeCache *nc = nodes_cache.getptr(E.value); + ERR_CONTINUE(!nc); + nc->recv_ids.erase(E.key); + } + for (const ObjectID &oid : pinfo->sent_nodes) { + NodeCache *nc = nodes_cache.getptr(oid); + ERR_CONTINUE(!nc); + nc->confirmed_peers.erase(p_id); } + peers_info.erase(p_id); } } void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len) { + ERR_FAIL_COND(!peers_info.has(p_from)); // Bug. + ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small."); Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path()); ERR_FAIL_NULL(root_node); - ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small."); int ofs = 1; String methods_md5; @@ -63,15 +99,13 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac int id = decode_uint32(&p_packet[ofs]); ofs += 4; + ERR_FAIL_COND_MSG(peers_info[p_from].recv_nodes.has(id), vformat("Duplicate remote cache ID %d for peer %d", id, p_from)); + String paths; paths.parse_utf8((const char *)(p_packet + ofs), p_packet_len - ofs); const NodePath path = paths; - if (!path_get_cache.has(p_from)) { - path_get_cache[p_from] = PathGetCache(); - } - Node *node = root_node->get_node(path); ERR_FAIL_NULL(node); const bool valid_rpc_checksum = multiplayer->get_rpc_md5(node) == methods_md5; @@ -79,10 +113,9 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac ERR_PRINT("The rpc node checksum failed. Make sure to have the same methods on both nodes. Node path: " + path); } - PathGetCache::NodeInfo ni; - ni.path = node->get_path(); - - path_get_cache[p_from].nodes[id] = ni; + peers_info[p_from].recv_nodes.insert(id, node->get_instance_id()); + NodeCache &cache = _track(node); + cache.recv_ids.insert(p_from, id); // Encode path to send ack. CharString pname = String(path).utf8(); @@ -122,15 +155,15 @@ void SceneCacheInterface::process_confirm_path(int p_from, const uint8_t *p_pack Node *node = root_node->get_node(path); ERR_FAIL_NULL(node); - PathSentCache *psc = path_send_cache.getptr(node->get_instance_id()); - ERR_FAIL_NULL_MSG(psc, "Invalid packet received. Tries to confirm a path which was not found in cache."); + NodeCache *cache = nodes_cache.getptr(node->get_instance_id()); + ERR_FAIL_NULL_MSG(cache, "Invalid packet received. Tries to confirm a node which was not requested."); - HashMap<int, bool>::Iterator E = psc->confirmed_peers.find(p_from); - ERR_FAIL_COND_MSG(!E, "Invalid packet received. Source peer was not found in cache for the given path."); - E->value = true; + bool *confirmed = cache->confirmed_peers.getptr(p_from); + ERR_FAIL_NULL_MSG(confirmed, "Invalid packet received. Tries to confirm a node which was not requested."); + *confirmed = true; } -Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers) { +Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers) { // Encode function name. const CharString path = String(multiplayer->get_root_path().rel_path_to(p_node->get_path())).utf8(); const int path_len = encode_cstring(path.get_data(), nullptr); @@ -148,7 +181,7 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc, ofs += encode_cstring(methods_md5.utf8().get_data(), &packet.write[ofs]); - ofs += encode_uint32(psc->id, &packet.write[ofs]); + ofs += encode_uint32(p_cache.cache_id, &packet.write[ofs]); ofs += encode_cstring(path.get_data(), &packet.write[ofs]); @@ -162,80 +195,74 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc, err = multiplayer->send_command(peer_id, packet.ptr(), packet.size()); ERR_FAIL_COND_V(err != OK, err); // Insert into confirmed, but as false since it was not confirmed. - psc->confirmed_peers.insert(peer_id, false); + p_cache.confirmed_peers.insert(peer_id, false); + ERR_CONTINUE(!peers_info.has(peer_id)); + peers_info[peer_id].sent_nodes.insert(p_node->get_instance_id()); } return err; } bool SceneCacheInterface::is_cache_confirmed(Node *p_node, int p_peer) { ERR_FAIL_NULL_V(p_node, false); - const PathSentCache *psc = path_send_cache.getptr(p_node->get_instance_id()); - ERR_FAIL_NULL_V(psc, false); - HashMap<int, bool>::ConstIterator F = psc->confirmed_peers.find(p_peer); - ERR_FAIL_COND_V(!F, false); // Should never happen. - return F->value; + const ObjectID oid = p_node->get_instance_id(); + NodeCache *cache = nodes_cache.getptr(oid); + bool *confirmed = cache ? cache->confirmed_peers.getptr(p_peer) : nullptr; + return confirmed && *confirmed; } int SceneCacheInterface::make_object_cache(Object *p_obj) { Node *node = Object::cast_to<Node>(p_obj); ERR_FAIL_NULL_V(node, -1); - const ObjectID oid = node->get_instance_id(); - // See if the path is cached. - PathSentCache *psc = path_send_cache.getptr(oid); - if (!psc) { - // Path is not cached, create. - path_send_cache[oid] = PathSentCache(); - psc = path_send_cache.getptr(oid); - psc->id = last_send_cache_id++; + NodeCache &cache = _track(node); + if (cache.cache_id == 0) { + cache.cache_id = last_send_cache_id++; } - return psc->id; + return cache.cache_id; } bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r_id) { Node *node = Object::cast_to<Node>(p_obj); ERR_FAIL_NULL_V(node, false); - const ObjectID oid = node->get_instance_id(); // See if the path is cached. - PathSentCache *psc = path_send_cache.getptr(oid); - if (!psc) { - // Path is not cached, create. - path_send_cache[oid] = PathSentCache(); - psc = path_send_cache.getptr(oid); - psc->id = last_send_cache_id++; + NodeCache &cache = _track(node); + if (cache.cache_id == 0) { + cache.cache_id = last_send_cache_id++; } - r_id = psc->id; + r_id = cache.cache_id; bool has_all_peers = true; List<int> peers_to_add; // If one is missing, take note to add it. if (p_peer_id > 0) { // Fast single peer check. - HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(p_peer_id); - if (!F) { + ERR_FAIL_COND_V_MSG(!peers_info.has(p_peer_id), false, "Peer doesn't exist: " + itos(p_peer_id)); + + bool *confirmed = cache.confirmed_peers.getptr(p_peer_id); + if (!confirmed) { peers_to_add.push_back(p_peer_id); // Need to also be notified. has_all_peers = false; - } else if (!F->value) { + } else if (!(*confirmed)) { has_all_peers = false; } } else { // Long and painful. - for (const int &E : multiplayer->get_connected_peers()) { - if (p_peer_id < 0 && E == -p_peer_id) { + for (KeyValue<int, PeerInfo> &E : peers_info) { + if (p_peer_id < 0 && E.key == -p_peer_id) { continue; // Continue, excluded. } - HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(E); - if (!F) { - peers_to_add.push_back(E); // Need to also be notified. + bool *confirmed = cache.confirmed_peers.getptr(E.key); + if (!confirmed) { + peers_to_add.push_back(E.key); // Need to also be notified. has_all_peers = false; - } else if (!F->value) { + } else if (!(*confirmed)) { has_all_peers = false; } } } if (peers_to_add.size()) { - _send_confirm_path(node, psc, peers_to_add); + _send_confirm_path(node, cache, peers_to_add); } return has_all_peers; @@ -244,22 +271,23 @@ bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r Object *SceneCacheInterface::get_cached_object(int p_from, uint32_t p_cache_id) { Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path()); ERR_FAIL_NULL_V(root_node, nullptr); - HashMap<int, PathGetCache>::Iterator E = path_get_cache.find(p_from); - ERR_FAIL_COND_V_MSG(!E, nullptr, vformat("No cache found for peer %d.", p_from)); - - HashMap<int, PathGetCache::NodeInfo>::Iterator F = E->value.nodes.find(p_cache_id); - ERR_FAIL_COND_V_MSG(!F, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from)); + PeerInfo *pinfo = peers_info.getptr(p_from); + ERR_FAIL_NULL_V(pinfo, nullptr); - PathGetCache::NodeInfo *ni = &F->value; - Node *node = root_node->get_node(ni->path); - if (!node) { - ERR_PRINT("Failed to get cached path: " + String(ni->path) + "."); - } + const ObjectID *oid = pinfo->recv_nodes.getptr(p_cache_id); + ERR_FAIL_NULL_V_MSG(oid, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from)); + Node *node = Object::cast_to<Node>(ObjectDB::get_instance(*oid)); + ERR_FAIL_NULL_V_MSG(node, nullptr, vformat("Failed to get cached node from peer %d with cache ID %d.", p_from, p_cache_id)); return node; } void SceneCacheInterface::clear() { - path_get_cache.clear(); - path_send_cache.clear(); + for (KeyValue<ObjectID, NodeCache> &E : nodes_cache) { + Object *obj = ObjectDB::get_instance(E.key); + ERR_CONTINUE(!obj); + obj->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache)); + } + peers_info.clear(); + nodes_cache.clear(); last_send_cache_id = 1; } diff --git a/modules/multiplayer/scene_cache_interface.h b/modules/multiplayer/scene_cache_interface.h index e63beb5f84..ab4a20c078 100644 --- a/modules/multiplayer/scene_cache_interface.h +++ b/modules/multiplayer/scene_cache_interface.h @@ -43,27 +43,26 @@ private: SceneMultiplayer *multiplayer = nullptr; //path sent caches - struct PathSentCache { - HashMap<int, bool> confirmed_peers; - int id; + struct NodeCache { + int cache_id; + HashMap<int, int> recv_ids; // peer id, remote cache id + HashMap<int, bool> confirmed_peers; // peer id, confirmed }; - //path get caches - struct PathGetCache { - struct NodeInfo { - NodePath path; - ObjectID instance; - }; - - HashMap<int, NodeInfo> nodes; + struct PeerInfo { + HashMap<int, ObjectID> recv_nodes; // remote cache id, ObjectID + HashSet<ObjectID> sent_nodes; }; - HashMap<ObjectID, PathSentCache> path_send_cache; - HashMap<int, PathGetCache> path_get_cache; + HashMap<ObjectID, NodeCache> nodes_cache; + HashMap<int, PeerInfo> peers_info; int last_send_cache_id = 1; + void _remove_node_cache(ObjectID p_oid); + NodeCache &_track(Node *p_node); + protected: - Error _send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers); + Error _send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers); public: void clear(); diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp index 04de3dfb7f..99aba680cc 100644 --- a/modules/multiplayer/scene_multiplayer.cpp +++ b/modules/multiplayer/scene_multiplayer.cpp @@ -96,35 +96,29 @@ Error SceneMultiplayer::poll() { #endif if (pending_peers.has(sender)) { - if (pending_peers[sender].local) { - // If the auth is over, admit the peer at the first packet. - pending_peers.erase(sender); - _admit_peer(sender); + ERR_CONTINUE(len < 2 || (packet[0] & CMD_MASK) != NETWORK_COMMAND_SYS || packet[1] != SYS_COMMAND_AUTH); + // Auth message. + PackedByteArray pba; + pba.resize(len - 2); + if (pba.size()) { + memcpy(pba.ptrw(), &packet[2], len - 2); + // User callback + const Variant sv = sender; + const Variant pbav = pba; + const Variant *argv[2] = { &sv, &pbav }; + Variant ret; + Callable::CallError ce; + auth_callback.callp(argv, 2, ret, ce); + ERR_CONTINUE_MSG(ce.error != Callable::CallError::CALL_OK, "Failed to call authentication callback"); } else { - ERR_CONTINUE(len < 2 || (packet[0] & CMD_MASK) != NETWORK_COMMAND_SYS || packet[1] != SYS_COMMAND_AUTH); - // Auth message. - PackedByteArray pba; - pba.resize(len - 2); - if (pba.size()) { - memcpy(pba.ptrw(), &packet[2], len - 2); - // User callback - const Variant sv = sender; - const Variant pbav = pba; - const Variant *argv[2] = { &sv, &pbav }; - Variant ret; - Callable::CallError ce; - auth_callback.callp(argv, 2, ret, ce); - ERR_CONTINUE_MSG(ce.error != Callable::CallError::CALL_OK, "Failed to call authentication callback"); - } else { - // Remote complete notification. - pending_peers[sender].remote = true; - if (pending_peers[sender].local) { - pending_peers.erase(sender); - _admit_peer(sender); - } + // Remote complete notification. + pending_peers[sender].remote = true; + if (pending_peers[sender].local) { + pending_peers.erase(sender); + _admit_peer(sender); } - continue; // Auth in progress. } + continue; // Auth in progress. } ERR_CONTINUE(!connected_peers.has(sender)); @@ -434,13 +428,13 @@ void SceneMultiplayer::disconnect_peer(int p_id) { if (pending_peers.has(p_id)) { pending_peers.erase(p_id); } else if (connected_peers.has(p_id)) { - connected_peers.has(p_id); + connected_peers.erase(p_id); } multiplayer_peer->disconnect_peer(p_id); } Error SceneMultiplayer::send_bytes(Vector<uint8_t> p_data, int p_to, MultiplayerPeer::TransferMode p_mode, int p_channel) { - ERR_FAIL_COND_V_MSG(p_data.size() < 1, ERR_INVALID_DATA, "Trying to send an empty raw packet."); + ERR_FAIL_COND_V_MSG(p_data.is_empty(), ERR_INVALID_DATA, "Trying to send an empty raw packet."); ERR_FAIL_COND_V_MSG(!multiplayer_peer.is_valid(), ERR_UNCONFIGURED, "Trying to send a raw packet while no multiplayer peer is active."); ERR_FAIL_COND_V_MSG(multiplayer_peer->get_connection_status() != MultiplayerPeer::CONNECTION_CONNECTED, ERR_UNCONFIGURED, "Trying to send a raw packet via a multiplayer peer which is not connected."); @@ -460,7 +454,7 @@ Error SceneMultiplayer::send_bytes(Vector<uint8_t> p_data, int p_to, Multiplayer Error SceneMultiplayer::send_auth(int p_to, Vector<uint8_t> p_data) { ERR_FAIL_COND_V(multiplayer_peer.is_null() || multiplayer_peer->get_connection_status() != MultiplayerPeer::CONNECTION_CONNECTED, ERR_UNCONFIGURED); ERR_FAIL_COND_V(!pending_peers.has(p_to), ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V(p_data.size() < 1, ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_data.is_empty(), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V_MSG(pending_peers[p_to].local, ERR_FILE_CANT_WRITE, "The authentication session was previously marked as completed, no more authentication data can be sent."); ERR_FAIL_COND_V_MSG(pending_peers[p_to].remote, ERR_FILE_CANT_WRITE, "The remote peer notified that the authentication session was completed, no more authentication data can be sent."); @@ -483,9 +477,14 @@ Error SceneMultiplayer::complete_auth(int p_peer) { ERR_FAIL_COND_V(!pending_peers.has(p_peer), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V_MSG(pending_peers[p_peer].local, ERR_FILE_CANT_WRITE, "The authentication session was already marked as completed."); pending_peers[p_peer].local = true; + // Notify the remote peer that the authentication has completed. uint8_t buf[2] = { NETWORK_COMMAND_SYS, SYS_COMMAND_AUTH }; + multiplayer_peer->set_target_peer(p_peer); + multiplayer_peer->set_transfer_channel(0); + multiplayer_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE); Error err = _send(buf, 2); + // The remote peer already reported the authentication as completed, so admit the peer. // May generate new packets, so it must happen after sending confirmation. if (pending_peers[p_peer].remote) { diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h index e799abeb48..725cb9dbb6 100644 --- a/modules/multiplayer/scene_multiplayer.h +++ b/modules/multiplayer/scene_multiplayer.h @@ -98,7 +98,7 @@ public: // This is the mask that will be used to extract the command. enum { - CMD_MASK = 7, // 0x7 -> 0b00001111 + CMD_MASK = 7, // 0x7 -> 0b00000111 }; private: diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp index c95e4ff9c9..bb32eed1a9 100644 --- a/modules/multiplayer/scene_replication_interface.cpp +++ b/modules/multiplayer/scene_replication_interface.cpp @@ -189,9 +189,6 @@ void SceneReplicationInterface::_node_ready(const ObjectID &p_oid) { spawned_nodes.insert(oid); if (_has_authority(spawner)) { - if (tobj.net_id == 0) { - tobj.net_id = ++last_net_id; - } _update_spawn_visibility(0, oid); } } @@ -249,6 +246,7 @@ Error SceneReplicationInterface::on_replication_start(Object *p_obj, Variant p_c uint32_t net_id = pending_sync_net_ids[0]; pending_sync_net_ids.pop_front(); peers_info[pending_spawn_remote].recv_sync_ids[net_id] = sync->get_instance_id(); + sync->set_net_id(net_id); // Try to apply spawn state (before ready). if (pending_buffer_size > 0) { @@ -423,7 +421,7 @@ Error SceneReplicationInterface::_update_spawn_visibility(int p_peer, const Obje // Check visibility for each peers. for (const KeyValue<int, PeerInfo> &E : peers_info) { if (is_visible) { - // This is fast, since the the object is visible to everyone, we don't need to check each peer. + // This is fast, since the object is visible to everyone, we don't need to check each peer. if (E.value.spawn_nodes.has(p_oid)) { // Already spawned. continue; @@ -472,9 +470,13 @@ Error SceneReplicationInterface::_make_spawn_packet(Node *p_node, MultiplayerSpa ERR_FAIL_COND_V(!multiplayer || !p_node || !p_spawner, ERR_BUG); const ObjectID oid = p_node->get_instance_id(); - const TrackedNode *tnode = tracked_nodes.getptr(oid); + TrackedNode *tnode = tracked_nodes.getptr(oid); ERR_FAIL_NULL_V(tnode, ERR_INVALID_PARAMETER); + if (tnode->net_id == 0) { + // Ensure the node has an ID. + tnode->net_id = ++last_net_id; + } uint32_t nid = tnode->net_id; ERR_FAIL_COND_V(!nid, ERR_UNCONFIGURED); @@ -707,7 +709,7 @@ MultiplayerSynchronizer *SceneReplicationInterface::_find_synchronizer(int p_pee return sync; } -void SceneReplicationInterface::_send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs) { +void SceneReplicationInterface::_send_delta(int p_peer, const HashSet<ObjectID> &p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> &p_last_watch_usecs) { MAKE_ROOM(/* header */ 1 + /* element */ 4 + 8 + 4 + delta_mtu); uint8_t *ptr = packet_cache.ptrw(); ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC | (1 << SceneMultiplayer::CMD_FLAG_0_SHIFT); @@ -781,7 +783,7 @@ Error SceneReplicationInterface::on_delta_receive(int p_from, const uint8_t *p_b ERR_CONTINUE_MSG(true, "Ignoring delta for non-authority or invalid synchronizer."); } List<NodePath> props = sync->get_delta_properties(indexes); - ERR_FAIL_COND_V(props.size() == 0, ERR_INVALID_DATA); + ERR_FAIL_COND_V(props.is_empty(), ERR_INVALID_DATA); Vector<Variant> vars; vars.resize(props.size()); int consumed = 0; @@ -799,7 +801,7 @@ Error SceneReplicationInterface::on_delta_receive(int p_from, const uint8_t *p_b return OK; } -void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec) { +void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> &p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec) { MAKE_ROOM(/* header */ 3 + /* element */ 4 + 4 + sync_mtu); uint8_t *ptr = packet_cache.ptrw(); ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC; diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h index 3b3ec6a9ef..31211bb108 100644 --- a/modules/multiplayer/scene_replication_interface.h +++ b/modules/multiplayer/scene_replication_interface.h @@ -101,8 +101,8 @@ private: bool _verify_synchronizer(int p_peer, MultiplayerSynchronizer *p_sync, uint32_t &r_net_id); MultiplayerSynchronizer *_find_synchronizer(int p_peer, uint32_t p_net_ida); - void _send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec); - void _send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs); + void _send_sync(int p_peer, const HashSet<ObjectID> &p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec); + void _send_delta(int p_peer, const HashSet<ObjectID> &p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> &p_last_watch_usecs); Error _make_spawn_packet(Node *p_node, MultiplayerSpawner *p_spawner, int &r_len); Error _make_despawn_packet(Node *p_node, int &r_len); Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable); diff --git a/modules/navigation/godot_navigation_server_2d.cpp b/modules/navigation/2d/godot_navigation_server_2d.cpp index b54729e06f..28bcd16310 100644 --- a/modules/navigation/godot_navigation_server_2d.cpp +++ b/modules/navigation/2d/godot_navigation_server_2d.cpp @@ -141,6 +141,13 @@ static Transform3D trf2_to_trf3(const Transform2D &d) { return Transform3D(b, o); } +static Transform2D trf3_to_trf2(const Transform3D &d) { + Vector3 o = d.get_origin(); + Vector3 nx = d.xform(Vector3(1, 0, 0)) - o; + Vector3 nz = d.xform(Vector3(0, 0, 1)) - o; + return Transform2D(nx.x, nx.z, nz.x, nz.z, o.x, o.z); +} + static ObjectID id_to_id(const ObjectID &id) { return id; } @@ -214,6 +221,14 @@ void GodotNavigationServer2D::bake_from_source_geometry_data_async(const Ref<Nav #endif // CLIPPER2_ENABLED } +bool GodotNavigationServer2D::is_baking_navigation_polygon(Ref<NavigationPolygon> p_navigation_polygon) const { +#ifdef CLIPPER2_ENABLED + return NavMeshGenerator2D::get_singleton()->is_baking(p_navigation_polygon); +#else + return false; +#endif +} + GodotNavigationServer2D::GodotNavigationServer2D() {} GodotNavigationServer2D::~GodotNavigationServer2D() {} @@ -242,6 +257,10 @@ void GodotNavigationServer2D::map_force_update(RID p_map) { NavigationServer3D::get_singleton()->map_force_update(p_map); } +uint32_t GodotNavigationServer2D::map_get_iteration_id(RID p_map) const { + return NavigationServer3D::get_singleton()->map_get_iteration_id(p_map); +} + void FORWARD_2(map_set_cell_size, RID, p_map, real_t, p_cell_size, rid_to_rid, real_to_real); real_t FORWARD_1_C(map_get_cell_size, RID, p_map, rid_to_rid); @@ -259,8 +278,12 @@ Vector<Vector2> FORWARD_5_R_C(vector_v3_to_v2, map_get_path, RID, p_map, Vector2 Vector2 FORWARD_2_R_C(v3_to_v2, map_get_closest_point, RID, p_map, const Vector2 &, p_point, rid_to_rid, v2_to_v3); RID FORWARD_2_C(map_get_closest_point_owner, RID, p_map, const Vector2 &, p_point, rid_to_rid, v2_to_v3); -RID FORWARD_0(region_create); +Vector2 GodotNavigationServer2D::map_get_random_point(RID p_map, uint32_t p_naviation_layers, bool p_uniformly) const { + Vector3 result = NavigationServer3D::get_singleton()->map_get_random_point(p_map, p_naviation_layers, p_uniformly); + return v3_to_v2(result); +} +RID FORWARD_0(region_create); void FORWARD_2(region_set_enabled, RID, p_region, bool, p_enabled, rid_to_rid, bool_to_bool); bool FORWARD_1_C(region_get_enabled, RID, p_region, rid_to_rid); void FORWARD_2(region_set_use_edge_connections, RID, p_region, bool, p_enabled, rid_to_rid, bool_to_bool); @@ -279,6 +302,10 @@ void FORWARD_2(region_set_navigation_layers, RID, p_region, uint32_t, p_navigati uint32_t FORWARD_1_C(region_get_navigation_layers, RID, p_region, rid_to_rid); void FORWARD_2(region_set_transform, RID, p_region, Transform2D, p_transform, rid_to_rid, trf2_to_trf3); +Transform2D GodotNavigationServer2D::region_get_transform(RID p_region) const { + return trf3_to_trf2(NavigationServer3D::get_singleton()->region_get_transform(p_region)); +} + void GodotNavigationServer2D::region_set_navigation_polygon(RID p_region, Ref<NavigationPolygon> p_navigation_polygon) { NavigationServer3D::get_singleton()->region_set_navigation_mesh(p_region, poly_to_mesh(p_navigation_polygon)); } @@ -287,6 +314,11 @@ int FORWARD_1_C(region_get_connections_count, RID, p_region, rid_to_rid); Vector2 FORWARD_2_R_C(v3_to_v2, region_get_connection_pathway_start, RID, p_region, int, p_connection_id, rid_to_rid, int_to_int); Vector2 FORWARD_2_R_C(v3_to_v2, region_get_connection_pathway_end, RID, p_region, int, p_connection_id, rid_to_rid, int_to_int); +Vector2 GodotNavigationServer2D::region_get_random_point(RID p_region, uint32_t p_navigation_layers, bool p_uniformly) const { + Vector3 result = NavigationServer3D::get_singleton()->region_get_random_point(p_region, p_navigation_layers, p_uniformly); + return v3_to_v2(result); +} + RID FORWARD_0(link_create); void FORWARD_2(link_set_map, RID, p_link, RID, p_map, rid_to_rid, rid_to_rid); @@ -317,25 +349,60 @@ void FORWARD_2(agent_set_avoidance_enabled, RID, p_agent, bool, p_enabled, rid_t bool FORWARD_1_C(agent_get_avoidance_enabled, RID, p_agent, rid_to_rid); void FORWARD_2(agent_set_map, RID, p_agent, RID, p_map, rid_to_rid, rid_to_rid); void FORWARD_2(agent_set_neighbor_distance, RID, p_agent, real_t, p_dist, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_neighbor_distance(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_neighbor_distance(p_agent); +} void FORWARD_2(agent_set_max_neighbors, RID, p_agent, int, p_count, rid_to_rid, int_to_int); +int GodotNavigationServer2D::agent_get_max_neighbors(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_max_neighbors(p_agent); +} void FORWARD_2(agent_set_time_horizon_agents, RID, p_agent, real_t, p_time_horizon, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_time_horizon_agents(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_time_horizon_agents(p_agent); +} void FORWARD_2(agent_set_time_horizon_obstacles, RID, p_agent, real_t, p_time_horizon, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_time_horizon_obstacles(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_time_horizon_obstacles(p_agent); +} void FORWARD_2(agent_set_radius, RID, p_agent, real_t, p_radius, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_radius(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_radius(p_agent); +} void FORWARD_2(agent_set_max_speed, RID, p_agent, real_t, p_max_speed, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_max_speed(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_max_speed(p_agent); +} void FORWARD_2(agent_set_velocity_forced, RID, p_agent, Vector2, p_velocity, rid_to_rid, v2_to_v3); void FORWARD_2(agent_set_velocity, RID, p_agent, Vector2, p_velocity, rid_to_rid, v2_to_v3); +Vector2 GodotNavigationServer2D::agent_get_velocity(RID p_agent) const { + return v3_to_v2(NavigationServer3D::get_singleton()->agent_get_velocity(p_agent)); +} void FORWARD_2(agent_set_position, RID, p_agent, Vector2, p_position, rid_to_rid, v2_to_v3); - +Vector2 GodotNavigationServer2D::agent_get_position(RID p_agent) const { + return v3_to_v2(NavigationServer3D::get_singleton()->agent_get_position(p_agent)); +} bool FORWARD_1_C(agent_is_map_changed, RID, p_agent, rid_to_rid); void FORWARD_2(agent_set_paused, RID, p_agent, bool, p_paused, rid_to_rid, bool_to_bool); bool FORWARD_1_C(agent_get_paused, RID, p_agent, rid_to_rid); void FORWARD_1(free, RID, p_object, rid_to_rid); void FORWARD_2(agent_set_avoidance_callback, RID, p_agent, Callable, p_callback, rid_to_rid, callable_to_callable); +bool GodotNavigationServer2D::agent_has_avoidance_callback(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_has_avoidance_callback(p_agent); +} void FORWARD_2(agent_set_avoidance_layers, RID, p_agent, uint32_t, p_layers, rid_to_rid, uint32_to_uint32); +uint32_t GodotNavigationServer2D::agent_get_avoidance_layers(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_avoidance_layers(p_agent); +} void FORWARD_2(agent_set_avoidance_mask, RID, p_agent, uint32_t, p_mask, rid_to_rid, uint32_to_uint32); +uint32_t GodotNavigationServer2D::agent_get_avoidance_mask(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_avoidance_mask(p_agent); +} void FORWARD_2(agent_set_avoidance_priority, RID, p_agent, real_t, p_priority, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::agent_get_avoidance_priority(RID p_agent) const { + return NavigationServer3D::get_singleton()->agent_get_avoidance_priority(p_agent); +} RID GodotNavigationServer2D::obstacle_create() { RID obstacle = NavigationServer3D::get_singleton()->obstacle_create(); @@ -348,13 +415,28 @@ RID FORWARD_1_C(obstacle_get_map, RID, p_obstacle, rid_to_rid); void FORWARD_2(obstacle_set_paused, RID, p_obstacle, bool, p_paused, rid_to_rid, bool_to_bool); bool FORWARD_1_C(obstacle_get_paused, RID, p_obstacle, rid_to_rid); void FORWARD_2(obstacle_set_radius, RID, p_obstacle, real_t, p_radius, rid_to_rid, real_to_real); +real_t GodotNavigationServer2D::obstacle_get_radius(RID p_obstacle) const { + return NavigationServer3D::get_singleton()->obstacle_get_radius(p_obstacle); +} void FORWARD_2(obstacle_set_velocity, RID, p_obstacle, Vector2, p_velocity, rid_to_rid, v2_to_v3); +Vector2 GodotNavigationServer2D::obstacle_get_velocity(RID p_obstacle) const { + return v3_to_v2(NavigationServer3D::get_singleton()->obstacle_get_velocity(p_obstacle)); +} void FORWARD_2(obstacle_set_position, RID, p_obstacle, Vector2, p_position, rid_to_rid, v2_to_v3); +Vector2 GodotNavigationServer2D::obstacle_get_position(RID p_obstacle) const { + return v3_to_v2(NavigationServer3D::get_singleton()->obstacle_get_position(p_obstacle)); +} void FORWARD_2(obstacle_set_avoidance_layers, RID, p_obstacle, uint32_t, p_layers, rid_to_rid, uint32_to_uint32); +uint32_t GodotNavigationServer2D::obstacle_get_avoidance_layers(RID p_obstacle) const { + return NavigationServer3D::get_singleton()->obstacle_get_avoidance_layers(p_obstacle); +} void GodotNavigationServer2D::obstacle_set_vertices(RID p_obstacle, const Vector<Vector2> &p_vertices) { NavigationServer3D::get_singleton()->obstacle_set_vertices(p_obstacle, vector_v2_to_v3(p_vertices)); } +Vector<Vector2> GodotNavigationServer2D::obstacle_get_vertices(RID p_obstacle) const { + return vector_v3_to_v2(NavigationServer3D::get_singleton()->obstacle_get_vertices(p_obstacle)); +} void GodotNavigationServer2D::query_path(const Ref<NavigationPathQueryParameters2D> &p_query_parameters, Ref<NavigationPathQueryResult2D> p_query_result) const { ERR_FAIL_COND(!p_query_parameters.is_valid()); diff --git a/modules/navigation/godot_navigation_server_2d.h b/modules/navigation/2d/godot_navigation_server_2d.h index 337f5f40d8..a148887a65 100644 --- a/modules/navigation/godot_navigation_server_2d.h +++ b/modules/navigation/2d/godot_navigation_server_2d.h @@ -31,11 +31,11 @@ #ifndef GODOT_NAVIGATION_SERVER_2D_H #define GODOT_NAVIGATION_SERVER_2D_H -#include "nav_agent.h" -#include "nav_link.h" -#include "nav_map.h" -#include "nav_obstacle.h" -#include "nav_region.h" +#include "../nav_agent.h" +#include "../nav_link.h" +#include "../nav_map.h" +#include "../nav_obstacle.h" +#include "../nav_region.h" #include "servers/navigation_server_2d.h" @@ -76,6 +76,8 @@ public: virtual TypedArray<RID> map_get_agents(RID p_map) const override; virtual TypedArray<RID> map_get_obstacles(RID p_map) const override; virtual void map_force_update(RID p_map) override; + virtual Vector2 map_get_random_point(RID p_map, uint32_t p_navigation_layers, bool p_uniformly) const override; + virtual uint32_t map_get_iteration_id(RID p_map) const override; virtual RID region_create() override; virtual void region_set_enabled(RID p_region, bool p_enabled) override; @@ -94,10 +96,12 @@ public: virtual void region_set_navigation_layers(RID p_region, uint32_t p_navigation_layers) override; virtual uint32_t region_get_navigation_layers(RID p_region) const override; virtual void region_set_transform(RID p_region, Transform2D p_transform) override; + virtual Transform2D region_get_transform(RID p_region) const override; virtual void region_set_navigation_polygon(RID p_region, Ref<NavigationPolygon> p_navigation_polygon) override; virtual int region_get_connections_count(RID p_region) const override; virtual Vector2 region_get_connection_pathway_start(RID p_region, int p_connection_id) const override; virtual Vector2 region_get_connection_pathway_end(RID p_region, int p_connection_id) const override; + virtual Vector2 region_get_random_point(RID p_region, uint32_t p_navigation_layers, bool p_uniformly) const override; virtual RID link_create() override; @@ -157,6 +161,7 @@ public: /// low, the simulation will not be safe. /// Must be non-negative. virtual void agent_set_neighbor_distance(RID p_agent, real_t p_distance) override; + virtual real_t agent_get_neighbor_distance(RID p_agent) const override; /// The maximum number of other agents this /// agent takes into account in the navigation. @@ -165,6 +170,7 @@ public: /// number is too low, the simulation will not /// be safe. virtual void agent_set_max_neighbors(RID p_agent, int p_count) override; + virtual int agent_get_max_neighbors(RID p_agent) const override; /// The minimal amount of time for which this /// agent's velocities that are computed by the @@ -174,17 +180,20 @@ public: /// other agents, but the less freedom this /// agent has in choosing its velocities. /// Must be positive. - virtual void agent_set_time_horizon_agents(RID p_agent, real_t p_time_horizon) override; + virtual real_t agent_get_time_horizon_agents(RID p_agent) const override; virtual void agent_set_time_horizon_obstacles(RID p_agent, real_t p_time_horizon) override; + virtual real_t agent_get_time_horizon_obstacles(RID p_agent) const override; /// The radius of this agent. /// Must be non-negative. virtual void agent_set_radius(RID p_agent, real_t p_radius) override; + virtual real_t agent_get_radius(RID p_agent) const override; /// The maximum speed of this agent. /// Must be non-negative. virtual void agent_set_max_speed(RID p_agent, real_t p_max_speed) override; + virtual real_t agent_get_max_speed(RID p_agent) const override; /// forces and agent velocity change in the avoidance simulation, adds simulation instability if done recklessly virtual void agent_set_velocity_forced(RID p_agent, Vector2 p_velocity) override; @@ -192,19 +201,27 @@ public: /// The wanted velocity for the agent as a "suggestion" to the avoidance simulation. /// The simulation will try to fulfill this velocity wish if possible but may change the velocity depending on other agent's and obstacles'. virtual void agent_set_velocity(RID p_agent, Vector2 p_velocity) override; + virtual Vector2 agent_get_velocity(RID p_agent) const override; /// Position of the agent in world space. virtual void agent_set_position(RID p_agent, Vector2 p_position) override; + virtual Vector2 agent_get_position(RID p_agent) const override; /// Returns true if the map got changed the previous frame. virtual bool agent_is_map_changed(RID p_agent) const override; /// Callback called at the end of the RVO process virtual void agent_set_avoidance_callback(RID p_agent, Callable p_callback) override; + virtual bool agent_has_avoidance_callback(RID p_agent) const override; virtual void agent_set_avoidance_layers(RID p_agent, uint32_t p_layers) override; + virtual uint32_t agent_get_avoidance_layers(RID p_agent) const override; + virtual void agent_set_avoidance_mask(RID p_agent, uint32_t p_mask) override; + virtual uint32_t agent_get_avoidance_mask(RID p_agent) const override; + virtual void agent_set_avoidance_priority(RID p_agent, real_t p_priority) override; + virtual real_t agent_get_avoidance_priority(RID p_agent) const override; virtual RID obstacle_create() override; virtual void obstacle_set_avoidance_enabled(RID p_obstacle, bool p_enabled) override; @@ -214,10 +231,15 @@ public: virtual void obstacle_set_paused(RID p_obstacle, bool p_paused) override; virtual bool obstacle_get_paused(RID p_obstacle) const override; virtual void obstacle_set_radius(RID p_obstacle, real_t p_radius) override; + virtual real_t obstacle_get_radius(RID p_obstacle) const override; virtual void obstacle_set_velocity(RID p_obstacle, Vector2 p_velocity) override; + virtual Vector2 obstacle_get_velocity(RID p_obstacle) const override; virtual void obstacle_set_position(RID p_obstacle, Vector2 p_position) override; + virtual Vector2 obstacle_get_position(RID p_obstacle) const override; virtual void obstacle_set_vertices(RID p_obstacle, const Vector<Vector2> &p_vertices) override; + virtual Vector<Vector2> obstacle_get_vertices(RID p_obstacle) const override; virtual void obstacle_set_avoidance_layers(RID p_obstacle, uint32_t p_layers) override; + virtual uint32_t obstacle_get_avoidance_layers(RID p_obstacle) const override; virtual void query_path(const Ref<NavigationPathQueryParameters2D> &p_query_parameters, Ref<NavigationPathQueryResult2D> p_query_result) const override; @@ -229,6 +251,7 @@ public: virtual void parse_source_geometry_data(const Ref<NavigationPolygon> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData2D> &p_source_geometry_data, Node *p_root_node, const Callable &p_callback = Callable()) override; virtual void bake_from_source_geometry_data(const Ref<NavigationPolygon> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData2D> &p_source_geometry_data, const Callable &p_callback = Callable()) override; virtual void bake_from_source_geometry_data_async(const Ref<NavigationPolygon> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData2D> &p_source_geometry_data, const Callable &p_callback = Callable()) override; + virtual bool is_baking_navigation_polygon(Ref<NavigationPolygon> p_navigation_polygon) const override; }; #endif // GODOT_NAVIGATION_SERVER_2D_H diff --git a/modules/navigation/nav_mesh_generator_2d.cpp b/modules/navigation/2d/nav_mesh_generator_2d.cpp index 089744c8bd..d1ac784103 100644 --- a/modules/navigation/nav_mesh_generator_2d.cpp +++ b/modules/navigation/2d/nav_mesh_generator_2d.cpp @@ -28,21 +28,23 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ +#ifdef CLIPPER2_ENABLED + #include "nav_mesh_generator_2d.h" #include "core/config/project_settings.h" #include "scene/2d/mesh_instance_2d.h" #include "scene/2d/multimesh_instance_2d.h" -#include "scene/2d/physics_body_2d.h" +#include "scene/2d/physics/static_body_2d.h" #include "scene/2d/polygon_2d.h" #include "scene/2d/tile_map.h" -#include "scene/resources/capsule_shape_2d.h" -#include "scene/resources/circle_shape_2d.h" -#include "scene/resources/concave_polygon_shape_2d.h" -#include "scene/resources/convex_polygon_shape_2d.h" +#include "scene/resources/2d/capsule_shape_2d.h" +#include "scene/resources/2d/circle_shape_2d.h" +#include "scene/resources/2d/concave_polygon_shape_2d.h" +#include "scene/resources/2d/convex_polygon_shape_2d.h" +#include "scene/resources/2d/rectangle_shape_2d.h" #include "scene/resources/navigation_mesh_source_geometry_data_2d.h" #include "scene/resources/navigation_polygon.h" -#include "scene/resources/rectangle_shape_2d.h" #include "thirdparty/clipper2/include/clipper2/clipper.h" #include "thirdparty/misc/polypartition.h" @@ -157,11 +159,10 @@ void NavMeshGenerator2D::bake_from_source_geometry_data(Ref<NavigationPolygon> p return; } - baking_navmesh_mutex.lock(); - if (baking_navmeshes.has(p_navigation_mesh)) { - baking_navmesh_mutex.unlock(); + if (is_baking(p_navigation_mesh)) { ERR_FAIL_MSG("NavigationPolygon is already baking. Wait for current bake to finish."); } + baking_navmesh_mutex.lock(); baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); @@ -193,11 +194,10 @@ void NavMeshGenerator2D::bake_from_source_geometry_data_async(Ref<NavigationPoly return; } - baking_navmesh_mutex.lock(); - if (baking_navmeshes.has(p_navigation_mesh)) { - baking_navmesh_mutex.unlock(); + if (is_baking(p_navigation_mesh)) { ERR_FAIL_MSG("NavigationPolygon is already baking. Wait for current bake to finish."); } + baking_navmesh_mutex.lock(); baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); @@ -212,6 +212,13 @@ void NavMeshGenerator2D::bake_from_source_geometry_data_async(Ref<NavigationPoly generator_task_mutex.unlock(); } +bool NavMeshGenerator2D::is_baking(Ref<NavigationPolygon> p_navigation_polygon) { + baking_navmesh_mutex.lock(); + bool baking = baking_navmeshes.has(p_navigation_polygon); + baking_navmesh_mutex.unlock(); + return baking; +} + void NavMeshGenerator2D::generator_thread_bake(void *p_arg) { NavMeshGeneratorTask2D *generator_task = static_cast<NavMeshGeneratorTask2D *>(p_arg); @@ -587,6 +594,15 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo const Vector2i &cell = used_cells[used_cell_index]; const TileData *tile_data = tilemap->get_cell_tile_data(tilemap_layer, cell, false); + if (tile_data == nullptr) { + continue; + } + + // Transform flags. + const int alternative_id = tilemap->get_cell_alternative_tile(tilemap_layer, cell, false); + bool flip_h = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H); + bool flip_v = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V); + bool transpose = (alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE); Transform2D tile_transform; tile_transform.set_origin(tilemap->map_to_local(cell)); @@ -594,13 +610,22 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo const Transform2D tile_transform_offset = tilemap_xform * tile_transform; if (navigation_layers_count > 0) { - Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(tilemap_layer); + Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(tilemap_layer, flip_h, flip_v, transpose); if (navigation_polygon.is_valid()) { for (int outline_index = 0; outline_index < navigation_polygon->get_outline_count(); outline_index++) { - Vector<Vector2> traversable_outline = navigation_polygon->get_outline(outline_index); + const Vector<Vector2> &navigation_polygon_outline = navigation_polygon->get_outline(outline_index); + if (navigation_polygon_outline.size() == 0) { + continue; + } + + Vector<Vector2> traversable_outline; + traversable_outline.resize(navigation_polygon_outline.size()); + + const Vector2 *navigation_polygon_outline_ptr = navigation_polygon_outline.ptr(); + Vector2 *traversable_outline_ptrw = traversable_outline.ptrw(); for (int traversable_outline_index = 0; traversable_outline_index < traversable_outline.size(); traversable_outline_index++) { - traversable_outline.write[traversable_outline_index] = tile_transform_offset.xform(traversable_outline[traversable_outline_index]); + traversable_outline_ptrw[traversable_outline_index] = tile_transform_offset.xform(navigation_polygon_outline_ptr[traversable_outline_index]); } p_source_geometry_data->_add_traversable_outline(traversable_outline); @@ -610,10 +635,23 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo if (physics_layers_count > 0 && (parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_STATIC_COLLIDERS || parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_BOTH) && (tile_set->get_physics_layer_collision_layer(tilemap_layer) & parsed_collision_mask)) { for (int collision_polygon_index = 0; collision_polygon_index < tile_data->get_collision_polygons_count(tilemap_layer); collision_polygon_index++) { - Vector<Vector2> obstruction_outline = tile_data->get_collision_polygon_points(tilemap_layer, collision_polygon_index); + PackedVector2Array collision_polygon_points = tile_data->get_collision_polygon_points(tilemap_layer, collision_polygon_index); + if (collision_polygon_points.size() == 0) { + continue; + } + + if (flip_h || flip_v || transpose) { + collision_polygon_points = TileData::get_transformed_vertices(collision_polygon_points, flip_h, flip_v, transpose); + } + + Vector<Vector2> obstruction_outline; + obstruction_outline.resize(collision_polygon_points.size()); + + const Vector2 *collision_polygon_points_ptr = collision_polygon_points.ptr(); + Vector2 *obstruction_outline_ptrw = obstruction_outline.ptrw(); for (int obstruction_outline_index = 0; obstruction_outline_index < obstruction_outline.size(); obstruction_outline_index++) { - obstruction_outline.write[obstruction_outline_index] = tile_transform_offset.xform(obstruction_outline[obstruction_outline_index]); + obstruction_outline_ptrw[obstruction_outline_index] = tile_transform_offset.xform(collision_polygon_points_ptr[obstruction_outline_index]); } p_source_geometry_data->_add_obstruction_outline(obstruction_outline); @@ -707,9 +745,13 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation Paths64 traversable_polygon_paths; Paths64 obstruction_polygon_paths; + traversable_polygon_paths.reserve(outline_count + traversable_outlines.size()); + obstruction_polygon_paths.reserve(obstruction_outlines.size()); + for (int i = 0; i < outline_count; i++) { const Vector<Vector2> &traversable_outline = p_navigation_mesh->get_outline(i); Path64 subject_path; + subject_path.reserve(traversable_outline.size()); for (const Vector2 &traversable_point : traversable_outline) { const Point64 &point = Point64(traversable_point.x, traversable_point.y); subject_path.push_back(point); @@ -719,6 +761,7 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation for (const Vector<Vector2> &traversable_outline : traversable_outlines) { Path64 subject_path; + subject_path.reserve(traversable_outline.size()); for (const Vector2 &traversable_point : traversable_outline) { const Point64 &point = Point64(traversable_point.x, traversable_point.y); subject_path.push_back(point); @@ -728,6 +771,7 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation for (const Vector<Vector2> &obstruction_outline : obstruction_outlines) { Path64 clip_path; + clip_path.reserve(obstruction_outline.size()); for (const Vector2 &obstruction_point : obstruction_outline) { const Point64 &point = Point64(obstruction_point.x, obstruction_point.y); clip_path.push_back(point); @@ -735,6 +779,22 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation obstruction_polygon_paths.push_back(clip_path); } + Rect2 baking_rect = p_navigation_mesh->get_baking_rect(); + if (baking_rect.has_area()) { + Vector2 baking_rect_offset = p_navigation_mesh->get_baking_rect_offset(); + + const int rect_begin_x = baking_rect.position[0] + baking_rect_offset.x; + const int rect_begin_y = baking_rect.position[1] + baking_rect_offset.y; + const int rect_end_x = baking_rect.position[0] + baking_rect.size[0] + baking_rect_offset.x; + const int rect_end_y = baking_rect.position[1] + baking_rect.size[1] + baking_rect_offset.y; + + Rect64 clipper_rect = Rect64(rect_begin_x, rect_begin_y, rect_end_x, rect_end_y); + RectClip rect_clip = RectClip(clipper_rect); + + traversable_polygon_paths = rect_clip.Execute(traversable_polygon_paths); + obstruction_polygon_paths = rect_clip.Execute(obstruction_polygon_paths); + } + Paths64 path_solution; // first merge all traversable polygons according to user specified fill rule @@ -751,6 +811,21 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation } //path_solution = RamerDouglasPeucker(path_solution, 0.025); // + real_t border_size = p_navigation_mesh->get_border_size(); + if (baking_rect.has_area() && border_size > 0.0) { + Vector2 baking_rect_offset = p_navigation_mesh->get_baking_rect_offset(); + + const int rect_begin_x = baking_rect.position[0] + baking_rect_offset.x + border_size; + const int rect_begin_y = baking_rect.position[1] + baking_rect_offset.y + border_size; + const int rect_end_x = baking_rect.position[0] + baking_rect.size[0] + baking_rect_offset.x - border_size; + const int rect_end_y = baking_rect.position[1] + baking_rect.size[1] + baking_rect_offset.y - border_size; + + Rect64 clipper_rect = Rect64(rect_begin_x, rect_begin_y, rect_end_x, rect_end_y); + RectClip rect_clip = RectClip(clipper_rect); + + path_solution = rect_clip.Execute(path_solution); + } + Vector<Vector<Vector2>> new_baked_outlines; for (const Path64 &scaled_path : path_solution) { @@ -768,6 +843,7 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation } Paths64 polygon_paths; + polygon_paths.reserve(new_baked_outlines.size()); for (const Vector<Vector2> &baked_outline : new_baked_outlines) { Path64 polygon_path; @@ -828,3 +904,5 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation p_navigation_mesh->add_polygon(new_polygons[i]); } } + +#endif // CLIPPER2_ENABLED diff --git a/modules/navigation/nav_mesh_generator_2d.h b/modules/navigation/2d/nav_mesh_generator_2d.h index 763ad24636..b606f3f6fc 100644 --- a/modules/navigation/nav_mesh_generator_2d.h +++ b/modules/navigation/2d/nav_mesh_generator_2d.h @@ -31,6 +31,8 @@ #ifndef NAV_MESH_GENERATOR_2D_H #define NAV_MESH_GENERATOR_2D_H +#ifdef CLIPPER2_ENABLED + #include "core/object/class_db.h" #include "core/object/worker_thread_pool.h" @@ -92,9 +94,12 @@ public: static void parse_source_geometry_data(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, Node *p_root_node, const Callable &p_callback = Callable()); static void bake_from_source_geometry_data(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, const Callable &p_callback = Callable()); static void bake_from_source_geometry_data_async(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, const Callable &p_callback = Callable()); + static bool is_baking(Ref<NavigationPolygon> p_navigation_polygon); NavMeshGenerator2D(); ~NavMeshGenerator2D(); }; +#endif // CLIPPER2_ENABLED + #endif // NAV_MESH_GENERATOR_2D_H diff --git a/modules/navigation/godot_navigation_server.cpp b/modules/navigation/3d/godot_navigation_server_3d.cpp index 6a3bf6793e..d293b9edbe 100644 --- a/modules/navigation/godot_navigation_server.cpp +++ b/modules/navigation/3d/godot_navigation_server_3d.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* godot_navigation_server.cpp */ +/* godot_navigation_server_3d.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,7 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#include "godot_navigation_server.h" +#include "godot_navigation_server_3d.h" #ifndef _3D_DISABLED #include "nav_mesh_generator_3d.h" @@ -42,58 +42,58 @@ using namespace NavigationUtilities; /// an instance of that struct with the submitted parameters. /// Then, that struct is stored in an array; the `sync` function consume that array. -#define COMMAND_1(F_NAME, T_0, D_0) \ - struct MERGE(F_NAME, _command) : public SetCommand { \ - T_0 d_0; \ - MERGE(F_NAME, _command) \ - (T_0 p_d_0) : \ - d_0(p_d_0) {} \ - virtual void exec(GodotNavigationServer *server) override { \ - server->MERGE(_cmd_, F_NAME)(d_0); \ - } \ - }; \ - void GodotNavigationServer::F_NAME(T_0 D_0) { \ - auto cmd = memnew(MERGE(F_NAME, _command)( \ - D_0)); \ - add_command(cmd); \ - } \ - void GodotNavigationServer::MERGE(_cmd_, F_NAME)(T_0 D_0) - -#define COMMAND_2(F_NAME, T_0, D_0, T_1, D_1) \ - struct MERGE(F_NAME, _command) : public SetCommand { \ - T_0 d_0; \ - T_1 d_1; \ - MERGE(F_NAME, _command) \ - ( \ - T_0 p_d_0, \ - T_1 p_d_1) : \ - d_0(p_d_0), \ - d_1(p_d_1) {} \ - virtual void exec(GodotNavigationServer *server) override { \ - server->MERGE(_cmd_, F_NAME)(d_0, d_1); \ - } \ - }; \ - void GodotNavigationServer::F_NAME(T_0 D_0, T_1 D_1) { \ - auto cmd = memnew(MERGE(F_NAME, _command)( \ - D_0, \ - D_1)); \ - add_command(cmd); \ - } \ - void GodotNavigationServer::MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1) - -GodotNavigationServer::GodotNavigationServer() {} - -GodotNavigationServer::~GodotNavigationServer() { +#define COMMAND_1(F_NAME, T_0, D_0) \ + struct MERGE(F_NAME, _command) : public SetCommand { \ + T_0 d_0; \ + MERGE(F_NAME, _command) \ + (T_0 p_d_0) : \ + d_0(p_d_0) {} \ + virtual void exec(GodotNavigationServer3D *server) override { \ + server->MERGE(_cmd_, F_NAME)(d_0); \ + } \ + }; \ + void GodotNavigationServer3D::F_NAME(T_0 D_0) { \ + auto cmd = memnew(MERGE(F_NAME, _command)( \ + D_0)); \ + add_command(cmd); \ + } \ + void GodotNavigationServer3D::MERGE(_cmd_, F_NAME)(T_0 D_0) + +#define COMMAND_2(F_NAME, T_0, D_0, T_1, D_1) \ + struct MERGE(F_NAME, _command) : public SetCommand { \ + T_0 d_0; \ + T_1 d_1; \ + MERGE(F_NAME, _command) \ + ( \ + T_0 p_d_0, \ + T_1 p_d_1) : \ + d_0(p_d_0), \ + d_1(p_d_1) {} \ + virtual void exec(GodotNavigationServer3D *server) override { \ + server->MERGE(_cmd_, F_NAME)(d_0, d_1); \ + } \ + }; \ + void GodotNavigationServer3D::F_NAME(T_0 D_0, T_1 D_1) { \ + auto cmd = memnew(MERGE(F_NAME, _command)( \ + D_0, \ + D_1)); \ + add_command(cmd); \ + } \ + void GodotNavigationServer3D::MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1) + +GodotNavigationServer3D::GodotNavigationServer3D() {} + +GodotNavigationServer3D::~GodotNavigationServer3D() { flush_queries(); } -void GodotNavigationServer::add_command(SetCommand *command) { +void GodotNavigationServer3D::add_command(SetCommand *command) { MutexLock lock(commands_mutex); commands.push_back(command); } -TypedArray<RID> GodotNavigationServer::get_maps() const { +TypedArray<RID> GodotNavigationServer3D::get_maps() const { TypedArray<RID> all_map_rids; List<RID> maps_owned; map_owner.get_owned_list(&maps_owned); @@ -105,7 +105,7 @@ TypedArray<RID> GodotNavigationServer::get_maps() const { return all_map_rids; } -RID GodotNavigationServer::map_create() { +RID GodotNavigationServer3D::map_create() { MutexLock lock(operations_mutex); RID rid = map_owner.make_rid(); @@ -121,17 +121,17 @@ COMMAND_2(map_set_active, RID, p_map, bool, p_active) { if (p_active) { if (!map_is_active(p_map)) { active_maps.push_back(map); - active_maps_update_id.push_back(map->get_map_update_id()); + active_maps_iteration_id.push_back(map->get_iteration_id()); } } else { int map_index = active_maps.find(map); ERR_FAIL_COND(map_index < 0); active_maps.remove_at(map_index); - active_maps_update_id.remove_at(map_index); + active_maps_iteration_id.remove_at(map_index); } } -bool GodotNavigationServer::map_is_active(RID p_map) const { +bool GodotNavigationServer3D::map_is_active(RID p_map) const { NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, false); @@ -145,7 +145,7 @@ COMMAND_2(map_set_up, RID, p_map, Vector3, p_up) { map->set_up(p_up); } -Vector3 GodotNavigationServer::map_get_up(RID p_map) const { +Vector3 GodotNavigationServer3D::map_get_up(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, Vector3()); @@ -159,7 +159,7 @@ COMMAND_2(map_set_cell_size, RID, p_map, real_t, p_cell_size) { map->set_cell_size(p_cell_size); } -real_t GodotNavigationServer::map_get_cell_size(RID p_map) const { +real_t GodotNavigationServer3D::map_get_cell_size(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, 0); @@ -173,13 +173,27 @@ COMMAND_2(map_set_cell_height, RID, p_map, real_t, p_cell_height) { map->set_cell_height(p_cell_height); } -real_t GodotNavigationServer::map_get_cell_height(RID p_map) const { +real_t GodotNavigationServer3D::map_get_cell_height(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, 0); return map->get_cell_height(); } +COMMAND_2(map_set_merge_rasterizer_cell_scale, RID, p_map, float, p_value) { + NavMap *map = map_owner.get_or_null(p_map); + ERR_FAIL_NULL(map); + + map->set_merge_rasterizer_cell_scale(p_value); +} + +float GodotNavigationServer3D::map_get_merge_rasterizer_cell_scale(RID p_map) const { + NavMap *map = map_owner.get_or_null(p_map); + ERR_FAIL_NULL_V(map, false); + + return map->get_merge_rasterizer_cell_scale(); +} + COMMAND_2(map_set_use_edge_connections, RID, p_map, bool, p_enabled) { NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL(map); @@ -187,7 +201,7 @@ COMMAND_2(map_set_use_edge_connections, RID, p_map, bool, p_enabled) { map->set_use_edge_connections(p_enabled); } -bool GodotNavigationServer::map_get_use_edge_connections(RID p_map) const { +bool GodotNavigationServer3D::map_get_use_edge_connections(RID p_map) const { NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, false); @@ -201,7 +215,7 @@ COMMAND_2(map_set_edge_connection_margin, RID, p_map, real_t, p_connection_margi map->set_edge_connection_margin(p_connection_margin); } -real_t GodotNavigationServer::map_get_edge_connection_margin(RID p_map) const { +real_t GodotNavigationServer3D::map_get_edge_connection_margin(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, 0); @@ -215,49 +229,49 @@ COMMAND_2(map_set_link_connection_radius, RID, p_map, real_t, p_connection_radiu map->set_link_connection_radius(p_connection_radius); } -real_t GodotNavigationServer::map_get_link_connection_radius(RID p_map) const { +real_t GodotNavigationServer3D::map_get_link_connection_radius(RID p_map) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, 0); return map->get_link_connection_radius(); } -Vector<Vector3> GodotNavigationServer::map_get_path(RID p_map, Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers) const { +Vector<Vector3> GodotNavigationServer3D::map_get_path(RID p_map, Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, Vector<Vector3>()); return map->get_path(p_origin, p_destination, p_optimize, p_navigation_layers, nullptr, nullptr, nullptr); } -Vector3 GodotNavigationServer::map_get_closest_point_to_segment(RID p_map, const Vector3 &p_from, const Vector3 &p_to, const bool p_use_collision) const { +Vector3 GodotNavigationServer3D::map_get_closest_point_to_segment(RID p_map, const Vector3 &p_from, const Vector3 &p_to, const bool p_use_collision) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, Vector3()); return map->get_closest_point_to_segment(p_from, p_to, p_use_collision); } -Vector3 GodotNavigationServer::map_get_closest_point(RID p_map, const Vector3 &p_point) const { +Vector3 GodotNavigationServer3D::map_get_closest_point(RID p_map, const Vector3 &p_point) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, Vector3()); return map->get_closest_point(p_point); } -Vector3 GodotNavigationServer::map_get_closest_point_normal(RID p_map, const Vector3 &p_point) const { +Vector3 GodotNavigationServer3D::map_get_closest_point_normal(RID p_map, const Vector3 &p_point) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, Vector3()); return map->get_closest_point_normal(p_point); } -RID GodotNavigationServer::map_get_closest_point_owner(RID p_map, const Vector3 &p_point) const { +RID GodotNavigationServer3D::map_get_closest_point_owner(RID p_map, const Vector3 &p_point) const { const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, RID()); return map->get_closest_point_owner(p_point); } -TypedArray<RID> GodotNavigationServer::map_get_links(RID p_map) const { +TypedArray<RID> GodotNavigationServer3D::map_get_links(RID p_map) const { TypedArray<RID> link_rids; const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, link_rids); @@ -271,7 +285,7 @@ TypedArray<RID> GodotNavigationServer::map_get_links(RID p_map) const { return link_rids; } -TypedArray<RID> GodotNavigationServer::map_get_regions(RID p_map) const { +TypedArray<RID> GodotNavigationServer3D::map_get_regions(RID p_map) const { TypedArray<RID> regions_rids; const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, regions_rids); @@ -285,7 +299,7 @@ TypedArray<RID> GodotNavigationServer::map_get_regions(RID p_map) const { return regions_rids; } -TypedArray<RID> GodotNavigationServer::map_get_agents(RID p_map) const { +TypedArray<RID> GodotNavigationServer3D::map_get_agents(RID p_map) const { TypedArray<RID> agents_rids; const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, agents_rids); @@ -299,7 +313,7 @@ TypedArray<RID> GodotNavigationServer::map_get_agents(RID p_map) const { return agents_rids; } -TypedArray<RID> GodotNavigationServer::map_get_obstacles(RID p_map) const { +TypedArray<RID> GodotNavigationServer3D::map_get_obstacles(RID p_map) const { TypedArray<RID> obstacles_rids; const NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL_V(map, obstacles_rids); @@ -311,7 +325,7 @@ TypedArray<RID> GodotNavigationServer::map_get_obstacles(RID p_map) const { return obstacles_rids; } -RID GodotNavigationServer::region_get_map(RID p_region) const { +RID GodotNavigationServer3D::region_get_map(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, RID()); @@ -321,7 +335,7 @@ RID GodotNavigationServer::region_get_map(RID p_region) const { return RID(); } -RID GodotNavigationServer::agent_get_map(RID p_agent) const { +RID GodotNavigationServer3D::agent_get_map(RID p_agent) const { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL_V(agent, RID()); @@ -331,7 +345,14 @@ RID GodotNavigationServer::agent_get_map(RID p_agent) const { return RID(); } -RID GodotNavigationServer::region_create() { +Vector3 GodotNavigationServer3D::map_get_random_point(RID p_map, uint32_t p_navigation_layers, bool p_uniformly) const { + const NavMap *map = map_owner.get_or_null(p_map); + ERR_FAIL_NULL_V(map, Vector3()); + + return map->get_random_point(p_navigation_layers, p_uniformly); +} + +RID GodotNavigationServer3D::region_create() { MutexLock lock(operations_mutex); RID rid = region_owner.make_rid(); @@ -347,7 +368,7 @@ COMMAND_2(region_set_enabled, RID, p_region, bool, p_enabled) { region->set_enabled(p_enabled); } -bool GodotNavigationServer::region_get_enabled(RID p_region) const { +bool GodotNavigationServer3D::region_get_enabled(RID p_region) const { const NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, false); @@ -361,7 +382,7 @@ COMMAND_2(region_set_use_edge_connections, RID, p_region, bool, p_enabled) { region->set_use_edge_connections(p_enabled); } -bool GodotNavigationServer::region_get_use_edge_connections(RID p_region) const { +bool GodotNavigationServer3D::region_get_use_edge_connections(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, false); @@ -384,6 +405,13 @@ COMMAND_2(region_set_transform, RID, p_region, Transform3D, p_transform) { region->set_transform(p_transform); } +Transform3D GodotNavigationServer3D::region_get_transform(RID p_region) const { + NavRegion *region = region_owner.get_or_null(p_region); + ERR_FAIL_NULL_V(region, Transform3D()); + + return region->get_transform(); +} + COMMAND_2(region_set_enter_cost, RID, p_region, real_t, p_enter_cost) { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL(region); @@ -392,7 +420,7 @@ COMMAND_2(region_set_enter_cost, RID, p_region, real_t, p_enter_cost) { region->set_enter_cost(p_enter_cost); } -real_t GodotNavigationServer::region_get_enter_cost(RID p_region) const { +real_t GodotNavigationServer3D::region_get_enter_cost(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, 0); @@ -407,7 +435,7 @@ COMMAND_2(region_set_travel_cost, RID, p_region, real_t, p_travel_cost) { region->set_travel_cost(p_travel_cost); } -real_t GodotNavigationServer::region_get_travel_cost(RID p_region) const { +real_t GodotNavigationServer3D::region_get_travel_cost(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, 0); @@ -421,14 +449,14 @@ COMMAND_2(region_set_owner_id, RID, p_region, ObjectID, p_owner_id) { region->set_owner_id(p_owner_id); } -ObjectID GodotNavigationServer::region_get_owner_id(RID p_region) const { +ObjectID GodotNavigationServer3D::region_get_owner_id(RID p_region) const { const NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, ObjectID()); return region->get_owner_id(); } -bool GodotNavigationServer::region_owns_point(RID p_region, const Vector3 &p_point) const { +bool GodotNavigationServer3D::region_owns_point(RID p_region, const Vector3 &p_point) const { const NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, false); @@ -446,7 +474,7 @@ COMMAND_2(region_set_navigation_layers, RID, p_region, uint32_t, p_navigation_la region->set_navigation_layers(p_navigation_layers); } -uint32_t GodotNavigationServer::region_get_navigation_layers(RID p_region) const { +uint32_t GodotNavigationServer3D::region_get_navigation_layers(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, 0); @@ -461,7 +489,7 @@ COMMAND_2(region_set_navigation_mesh, RID, p_region, Ref<NavigationMesh>, p_navi } #ifndef DISABLE_DEPRECATED -void GodotNavigationServer::region_bake_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh, Node *p_root_node) { +void GodotNavigationServer3D::region_bake_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh, Node *p_root_node) { ERR_FAIL_COND(p_navigation_mesh.is_null()); ERR_FAIL_NULL(p_root_node); @@ -477,28 +505,35 @@ void GodotNavigationServer::region_bake_navigation_mesh(Ref<NavigationMesh> p_na } #endif // DISABLE_DEPRECATED -int GodotNavigationServer::region_get_connections_count(RID p_region) const { +int GodotNavigationServer3D::region_get_connections_count(RID p_region) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, 0); return region->get_connections_count(); } -Vector3 GodotNavigationServer::region_get_connection_pathway_start(RID p_region, int p_connection_id) const { +Vector3 GodotNavigationServer3D::region_get_connection_pathway_start(RID p_region, int p_connection_id) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, Vector3()); return region->get_connection_pathway_start(p_connection_id); } -Vector3 GodotNavigationServer::region_get_connection_pathway_end(RID p_region, int p_connection_id) const { +Vector3 GodotNavigationServer3D::region_get_connection_pathway_end(RID p_region, int p_connection_id) const { NavRegion *region = region_owner.get_or_null(p_region); ERR_FAIL_NULL_V(region, Vector3()); return region->get_connection_pathway_end(p_connection_id); } -RID GodotNavigationServer::link_create() { +Vector3 GodotNavigationServer3D::region_get_random_point(RID p_region, uint32_t p_navigation_layers, bool p_uniformly) const { + const NavRegion *region = region_owner.get_or_null(p_region); + ERR_FAIL_NULL_V(region, Vector3()); + + return region->get_random_point(p_navigation_layers, p_uniformly); +} + +RID GodotNavigationServer3D::link_create() { MutexLock lock(operations_mutex); RID rid = link_owner.make_rid(); @@ -516,7 +551,7 @@ COMMAND_2(link_set_map, RID, p_link, RID, p_map) { link->set_map(map); } -RID GodotNavigationServer::link_get_map(const RID p_link) const { +RID GodotNavigationServer3D::link_get_map(const RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, RID()); @@ -533,7 +568,7 @@ COMMAND_2(link_set_enabled, RID, p_link, bool, p_enabled) { link->set_enabled(p_enabled); } -bool GodotNavigationServer::link_get_enabled(RID p_link) const { +bool GodotNavigationServer3D::link_get_enabled(RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, false); @@ -547,7 +582,7 @@ COMMAND_2(link_set_bidirectional, RID, p_link, bool, p_bidirectional) { link->set_bidirectional(p_bidirectional); } -bool GodotNavigationServer::link_is_bidirectional(RID p_link) const { +bool GodotNavigationServer3D::link_is_bidirectional(RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, false); @@ -561,7 +596,7 @@ COMMAND_2(link_set_navigation_layers, RID, p_link, uint32_t, p_navigation_layers link->set_navigation_layers(p_navigation_layers); } -uint32_t GodotNavigationServer::link_get_navigation_layers(const RID p_link) const { +uint32_t GodotNavigationServer3D::link_get_navigation_layers(const RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, 0); @@ -575,7 +610,7 @@ COMMAND_2(link_set_start_position, RID, p_link, Vector3, p_position) { link->set_start_position(p_position); } -Vector3 GodotNavigationServer::link_get_start_position(RID p_link) const { +Vector3 GodotNavigationServer3D::link_get_start_position(RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, Vector3()); @@ -589,7 +624,7 @@ COMMAND_2(link_set_end_position, RID, p_link, Vector3, p_position) { link->set_end_position(p_position); } -Vector3 GodotNavigationServer::link_get_end_position(RID p_link) const { +Vector3 GodotNavigationServer3D::link_get_end_position(RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, Vector3()); @@ -603,7 +638,7 @@ COMMAND_2(link_set_enter_cost, RID, p_link, real_t, p_enter_cost) { link->set_enter_cost(p_enter_cost); } -real_t GodotNavigationServer::link_get_enter_cost(const RID p_link) const { +real_t GodotNavigationServer3D::link_get_enter_cost(const RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, 0); @@ -617,7 +652,7 @@ COMMAND_2(link_set_travel_cost, RID, p_link, real_t, p_travel_cost) { link->set_travel_cost(p_travel_cost); } -real_t GodotNavigationServer::link_get_travel_cost(const RID p_link) const { +real_t GodotNavigationServer3D::link_get_travel_cost(const RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, 0); @@ -631,14 +666,14 @@ COMMAND_2(link_set_owner_id, RID, p_link, ObjectID, p_owner_id) { link->set_owner_id(p_owner_id); } -ObjectID GodotNavigationServer::link_get_owner_id(RID p_link) const { +ObjectID GodotNavigationServer3D::link_get_owner_id(RID p_link) const { const NavLink *link = link_owner.get_or_null(p_link); ERR_FAIL_NULL_V(link, ObjectID()); return link->get_owner_id(); } -RID GodotNavigationServer::agent_create() { +RID GodotNavigationServer3D::agent_create() { MutexLock lock(operations_mutex); RID rid = agent_owner.make_rid(); @@ -654,7 +689,7 @@ COMMAND_2(agent_set_avoidance_enabled, RID, p_agent, bool, p_enabled) { agent->set_avoidance_enabled(p_enabled); } -bool GodotNavigationServer::agent_get_avoidance_enabled(RID p_agent) const { +bool GodotNavigationServer3D::agent_get_avoidance_enabled(RID p_agent) const { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL_V(agent, false); @@ -668,7 +703,7 @@ COMMAND_2(agent_set_use_3d_avoidance, RID, p_agent, bool, p_enabled) { agent->set_use_3d_avoidance(p_enabled); } -bool GodotNavigationServer::agent_get_use_3d_avoidance(RID p_agent) const { +bool GodotNavigationServer3D::agent_get_use_3d_avoidance(RID p_agent) const { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL_V(agent, false); @@ -691,7 +726,7 @@ COMMAND_2(agent_set_paused, RID, p_agent, bool, p_paused) { agent->set_paused(p_paused); } -bool GodotNavigationServer::agent_get_paused(RID p_agent) const { +bool GodotNavigationServer3D::agent_get_paused(RID p_agent) const { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL_V(agent, false); @@ -705,6 +740,13 @@ COMMAND_2(agent_set_neighbor_distance, RID, p_agent, real_t, p_distance) { agent->set_neighbor_distance(p_distance); } +real_t GodotNavigationServer3D::agent_get_neighbor_distance(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_neighbor_distance(); +} + COMMAND_2(agent_set_max_neighbors, RID, p_agent, int, p_count) { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); @@ -712,22 +754,43 @@ COMMAND_2(agent_set_max_neighbors, RID, p_agent, int, p_count) { agent->set_max_neighbors(p_count); } +int GodotNavigationServer3D::agent_get_max_neighbors(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_max_neighbors(); +} + COMMAND_2(agent_set_time_horizon_agents, RID, p_agent, real_t, p_time_horizon) { - ERR_FAIL_COND_MSG(p_time_horizon < 0.0, "Time horizion must be positive."); + ERR_FAIL_COND_MSG(p_time_horizon < 0.0, "Time horizon must be positive."); NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); agent->set_time_horizon_agents(p_time_horizon); } +real_t GodotNavigationServer3D::agent_get_time_horizon_agents(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_time_horizon_agents(); +} + COMMAND_2(agent_set_time_horizon_obstacles, RID, p_agent, real_t, p_time_horizon) { - ERR_FAIL_COND_MSG(p_time_horizon < 0.0, "Time horizion must be positive."); + ERR_FAIL_COND_MSG(p_time_horizon < 0.0, "Time horizon must be positive."); NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); agent->set_time_horizon_obstacles(p_time_horizon); } +real_t GodotNavigationServer3D::agent_get_time_horizon_obstacles(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_time_horizon_obstacles(); +} + COMMAND_2(agent_set_radius, RID, p_agent, real_t, p_radius) { ERR_FAIL_COND_MSG(p_radius < 0.0, "Radius must be positive."); NavAgent *agent = agent_owner.get_or_null(p_agent); @@ -736,6 +799,13 @@ COMMAND_2(agent_set_radius, RID, p_agent, real_t, p_radius) { agent->set_radius(p_radius); } +real_t GodotNavigationServer3D::agent_get_radius(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_radius(); +} + COMMAND_2(agent_set_height, RID, p_agent, real_t, p_height) { ERR_FAIL_COND_MSG(p_height < 0.0, "Height must be positive."); NavAgent *agent = agent_owner.get_or_null(p_agent); @@ -744,6 +814,13 @@ COMMAND_2(agent_set_height, RID, p_agent, real_t, p_height) { agent->set_height(p_height); } +real_t GodotNavigationServer3D::agent_get_height(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_height(); +} + COMMAND_2(agent_set_max_speed, RID, p_agent, real_t, p_max_speed) { ERR_FAIL_COND_MSG(p_max_speed < 0.0, "Max speed must be positive."); NavAgent *agent = agent_owner.get_or_null(p_agent); @@ -752,6 +829,13 @@ COMMAND_2(agent_set_max_speed, RID, p_agent, real_t, p_max_speed) { agent->set_max_speed(p_max_speed); } +real_t GodotNavigationServer3D::agent_get_max_speed(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_max_speed(); +} + COMMAND_2(agent_set_velocity, RID, p_agent, Vector3, p_velocity) { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); @@ -759,6 +843,13 @@ COMMAND_2(agent_set_velocity, RID, p_agent, Vector3, p_velocity) { agent->set_velocity(p_velocity); } +Vector3 GodotNavigationServer3D::agent_get_velocity(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, Vector3()); + + return agent->get_velocity(); +} + COMMAND_2(agent_set_velocity_forced, RID, p_agent, Vector3, p_velocity) { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); @@ -773,7 +864,14 @@ COMMAND_2(agent_set_position, RID, p_agent, Vector3, p_position) { agent->set_position(p_position); } -bool GodotNavigationServer::agent_is_map_changed(RID p_agent) const { +Vector3 GodotNavigationServer3D::agent_get_position(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, Vector3()); + + return agent->get_position(); +} + +bool GodotNavigationServer3D::agent_is_map_changed(RID p_agent) const { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL_V(agent, false); @@ -795,18 +893,39 @@ COMMAND_2(agent_set_avoidance_callback, RID, p_agent, Callable, p_callback) { } } +bool GodotNavigationServer3D::agent_has_avoidance_callback(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, false); + + return agent->has_avoidance_callback(); +} + COMMAND_2(agent_set_avoidance_layers, RID, p_agent, uint32_t, p_layers) { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); agent->set_avoidance_layers(p_layers); } +uint32_t GodotNavigationServer3D::agent_get_avoidance_layers(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_avoidance_layers(); +} + COMMAND_2(agent_set_avoidance_mask, RID, p_agent, uint32_t, p_mask) { NavAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_NULL(agent); agent->set_avoidance_mask(p_mask); } +uint32_t GodotNavigationServer3D::agent_get_avoidance_mask(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_avoidance_mask(); +} + COMMAND_2(agent_set_avoidance_priority, RID, p_agent, real_t, p_priority) { ERR_FAIL_COND_MSG(p_priority < 0.0, "Avoidance priority must be between 0.0 and 1.0 inclusive."); ERR_FAIL_COND_MSG(p_priority > 1.0, "Avoidance priority must be between 0.0 and 1.0 inclusive."); @@ -815,7 +934,14 @@ COMMAND_2(agent_set_avoidance_priority, RID, p_agent, real_t, p_priority) { agent->set_avoidance_priority(p_priority); } -RID GodotNavigationServer::obstacle_create() { +real_t GodotNavigationServer3D::agent_get_avoidance_priority(RID p_agent) const { + NavAgent *agent = agent_owner.get_or_null(p_agent); + ERR_FAIL_NULL_V(agent, 0); + + return agent->get_avoidance_priority(); +} + +RID GodotNavigationServer3D::obstacle_create() { MutexLock lock(operations_mutex); RID rid = obstacle_owner.make_rid(); @@ -838,7 +964,7 @@ COMMAND_2(obstacle_set_avoidance_enabled, RID, p_obstacle, bool, p_enabled) { obstacle->set_avoidance_enabled(p_enabled); } -bool GodotNavigationServer::obstacle_get_avoidance_enabled(RID p_obstacle) const { +bool GodotNavigationServer3D::obstacle_get_avoidance_enabled(RID p_obstacle) const { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL_V(obstacle, false); @@ -852,7 +978,7 @@ COMMAND_2(obstacle_set_use_3d_avoidance, RID, p_obstacle, bool, p_enabled) { obstacle->set_use_3d_avoidance(p_enabled); } -bool GodotNavigationServer::obstacle_get_use_3d_avoidance(RID p_obstacle) const { +bool GodotNavigationServer3D::obstacle_get_use_3d_avoidance(RID p_obstacle) const { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL_V(obstacle, false); @@ -868,7 +994,7 @@ COMMAND_2(obstacle_set_map, RID, p_obstacle, RID, p_map) { obstacle->set_map(map); } -RID GodotNavigationServer::obstacle_get_map(RID p_obstacle) const { +RID GodotNavigationServer3D::obstacle_get_map(RID p_obstacle) const { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL_V(obstacle, RID()); if (obstacle->get_map()) { @@ -884,7 +1010,7 @@ COMMAND_2(obstacle_set_paused, RID, p_obstacle, bool, p_paused) { obstacle->set_paused(p_paused); } -bool GodotNavigationServer::obstacle_get_paused(RID p_obstacle) const { +bool GodotNavigationServer3D::obstacle_get_paused(RID p_obstacle) const { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL_V(obstacle, false); @@ -899,12 +1025,26 @@ COMMAND_2(obstacle_set_radius, RID, p_obstacle, real_t, p_radius) { obstacle->set_radius(p_radius); } +real_t GodotNavigationServer3D::obstacle_get_radius(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, 0); + + return obstacle->get_radius(); +} + COMMAND_2(obstacle_set_height, RID, p_obstacle, real_t, p_height) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL(obstacle); obstacle->set_height(p_height); } +real_t GodotNavigationServer3D::obstacle_get_height(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, 0); + + return obstacle->get_height(); +} + COMMAND_2(obstacle_set_velocity, RID, p_obstacle, Vector3, p_velocity) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL(obstacle); @@ -912,25 +1052,53 @@ COMMAND_2(obstacle_set_velocity, RID, p_obstacle, Vector3, p_velocity) { obstacle->set_velocity(p_velocity); } +Vector3 GodotNavigationServer3D::obstacle_get_velocity(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, Vector3()); + + return obstacle->get_velocity(); +} + COMMAND_2(obstacle_set_position, RID, p_obstacle, Vector3, p_position) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL(obstacle); obstacle->set_position(p_position); } -void GodotNavigationServer::obstacle_set_vertices(RID p_obstacle, const Vector<Vector3> &p_vertices) { +Vector3 GodotNavigationServer3D::obstacle_get_position(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, Vector3()); + + return obstacle->get_position(); +} + +void GodotNavigationServer3D::obstacle_set_vertices(RID p_obstacle, const Vector<Vector3> &p_vertices) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL(obstacle); obstacle->set_vertices(p_vertices); } +Vector<Vector3> GodotNavigationServer3D::obstacle_get_vertices(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, Vector<Vector3>()); + + return obstacle->get_vertices(); +} + COMMAND_2(obstacle_set_avoidance_layers, RID, p_obstacle, uint32_t, p_layers) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); ERR_FAIL_NULL(obstacle); obstacle->set_avoidance_layers(p_layers); } -void GodotNavigationServer::parse_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, Node *p_root_node, const Callable &p_callback) { +uint32_t GodotNavigationServer3D::obstacle_get_avoidance_layers(RID p_obstacle) const { + NavObstacle *obstacle = obstacle_owner.get_or_null(p_obstacle); + ERR_FAIL_NULL_V(obstacle, 0); + + return obstacle->get_avoidance_layers(); +} + +void GodotNavigationServer3D::parse_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, Node *p_root_node, const Callable &p_callback) { #ifndef _3D_DISABLED ERR_FAIL_COND_MSG(!Thread::is_main_thread(), "The SceneTree can only be parsed on the main thread. Call this function from the main thread or use call_deferred()."); ERR_FAIL_COND_MSG(!p_navigation_mesh.is_valid(), "Invalid navigation mesh."); @@ -942,7 +1110,7 @@ void GodotNavigationServer::parse_source_geometry_data(const Ref<NavigationMesh> #endif // _3D_DISABLED } -void GodotNavigationServer::bake_from_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback) { +void GodotNavigationServer3D::bake_from_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback) { #ifndef _3D_DISABLED ERR_FAIL_COND_MSG(!p_navigation_mesh.is_valid(), "Invalid navigation mesh."); ERR_FAIL_COND_MSG(!p_source_geometry_data.is_valid(), "Invalid NavigationMeshSourceGeometryData3D."); @@ -952,7 +1120,7 @@ void GodotNavigationServer::bake_from_source_geometry_data(const Ref<NavigationM #endif // _3D_DISABLED } -void GodotNavigationServer::bake_from_source_geometry_data_async(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback) { +void GodotNavigationServer3D::bake_from_source_geometry_data_async(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback) { #ifndef _3D_DISABLED ERR_FAIL_COND_MSG(!p_navigation_mesh.is_valid(), "Invalid navigation mesh."); ERR_FAIL_COND_MSG(!p_source_geometry_data.is_valid(), "Invalid NavigationMeshSourceGeometryData3D."); @@ -962,6 +1130,14 @@ void GodotNavigationServer::bake_from_source_geometry_data_async(const Ref<Navig #endif // _3D_DISABLED } +bool GodotNavigationServer3D::is_baking_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh) const { +#ifdef _3D_DISABLED + return false; +#else + return NavMeshGenerator3D::get_singleton()->is_baking(p_navigation_mesh); +#endif // _3D_DISABLED +} + COMMAND_1(free, RID, p_object) { if (map_owner.owns(p_object)) { NavMap *map = map_owner.get_or_null(p_object); @@ -993,7 +1169,7 @@ COMMAND_1(free, RID, p_object) { int map_index = active_maps.find(map); if (map_index >= 0) { active_maps.remove_at(map_index); - active_maps_update_id.remove_at(map_index); + active_maps_iteration_id.remove_at(map_index); } map_owner.free(p_object); @@ -1030,7 +1206,7 @@ COMMAND_1(free, RID, p_object) { } } -void GodotNavigationServer::internal_free_agent(RID p_object) { +void GodotNavigationServer3D::internal_free_agent(RID p_object) { NavAgent *agent = agent_owner.get_or_null(p_object); if (agent) { if (agent->get_map() != nullptr) { @@ -1041,7 +1217,7 @@ void GodotNavigationServer::internal_free_agent(RID p_object) { } } -void GodotNavigationServer::internal_free_obstacle(RID p_object) { +void GodotNavigationServer3D::internal_free_obstacle(RID p_object) { NavObstacle *obstacle = obstacle_owner.get_or_null(p_object); if (obstacle) { NavAgent *obstacle_agent = obstacle->get_agent(); @@ -1058,13 +1234,13 @@ void GodotNavigationServer::internal_free_obstacle(RID p_object) { } } -void GodotNavigationServer::set_active(bool p_active) { +void GodotNavigationServer3D::set_active(bool p_active) { MutexLock lock(operations_mutex); active = p_active; } -void GodotNavigationServer::flush_queries() { +void GodotNavigationServer3D::flush_queries() { // In c++ we can't be sure that this is performed in the main thread // even with mutable functions. MutexLock lock(commands_mutex); @@ -1077,7 +1253,7 @@ void GodotNavigationServer::flush_queries() { commands.clear(); } -void GodotNavigationServer::map_force_update(RID p_map) { +void GodotNavigationServer3D::map_force_update(RID p_map) { NavMap *map = map_owner.get_or_null(p_map); ERR_FAIL_NULL(map); @@ -1086,7 +1262,14 @@ void GodotNavigationServer::map_force_update(RID p_map) { map->sync(); } -void GodotNavigationServer::sync() { +uint32_t GodotNavigationServer3D::map_get_iteration_id(RID p_map) const { + NavMap *map = map_owner.get_or_null(p_map); + ERR_FAIL_NULL_V(map, 0); + + return map->get_iteration_id(); +} + +void GodotNavigationServer3D::sync() { #ifndef _3D_DISABLED if (navmesh_generator_3d) { navmesh_generator_3d->sync(); @@ -1094,7 +1277,7 @@ void GodotNavigationServer::sync() { #endif // _3D_DISABLED } -void GodotNavigationServer::process(real_t p_delta_time) { +void GodotNavigationServer3D::process(real_t p_delta_time) { flush_queries(); if (!active) { @@ -1128,10 +1311,10 @@ void GodotNavigationServer::process(real_t p_delta_time) { _new_pm_edge_free_count += active_maps[i]->get_pm_edge_free_count(); // Emit a signal if a map changed. - const uint32_t new_map_update_id = active_maps[i]->get_map_update_id(); - if (new_map_update_id != active_maps_update_id[i]) { + const uint32_t new_map_iteration_id = active_maps[i]->get_iteration_id(); + if (new_map_iteration_id != active_maps_iteration_id[i]) { emit_signal(SNAME("map_changed"), active_maps[i]->get_self()); - active_maps_update_id[i] = new_map_update_id; + active_maps_iteration_id[i] = new_map_iteration_id; } } @@ -1145,13 +1328,13 @@ void GodotNavigationServer::process(real_t p_delta_time) { pm_edge_free_count = _new_pm_edge_free_count; } -void GodotNavigationServer::init() { +void GodotNavigationServer3D::init() { #ifndef _3D_DISABLED navmesh_generator_3d = memnew(NavMeshGenerator3D); #endif // _3D_DISABLED } -void GodotNavigationServer::finish() { +void GodotNavigationServer3D::finish() { flush_queries(); #ifndef _3D_DISABLED if (navmesh_generator_3d) { @@ -1162,7 +1345,7 @@ void GodotNavigationServer::finish() { #endif // _3D_DISABLED } -PathQueryResult GodotNavigationServer::_query_path(const PathQueryParameters &p_parameters) const { +PathQueryResult GodotNavigationServer3D::_query_path(const PathQueryParameters &p_parameters) const { PathQueryResult r_query_result; const NavMap *map = map_owner.get_or_null(p_parameters.map); @@ -1202,7 +1385,7 @@ PathQueryResult GodotNavigationServer::_query_path(const PathQueryParameters &p_ return r_query_result; } -int GodotNavigationServer::get_process_info(ProcessInfo p_info) const { +int GodotNavigationServer3D::get_process_info(ProcessInfo p_info) const { switch (p_info) { case INFO_ACTIVE_MAPS: { return active_maps.size(); diff --git a/modules/navigation/godot_navigation_server.h b/modules/navigation/3d/godot_navigation_server_3d.h index 4ead4fc398..f7d991d47a 100644 --- a/modules/navigation/godot_navigation_server.h +++ b/modules/navigation/3d/godot_navigation_server_3d.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* godot_navigation_server.h */ +/* godot_navigation_server_3d.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,14 +28,14 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef GODOT_NAVIGATION_SERVER_H -#define GODOT_NAVIGATION_SERVER_H +#ifndef GODOT_NAVIGATION_SERVER_3D_H +#define GODOT_NAVIGATION_SERVER_3D_H -#include "nav_agent.h" -#include "nav_link.h" -#include "nav_map.h" -#include "nav_obstacle.h" -#include "nav_region.h" +#include "../nav_agent.h" +#include "../nav_link.h" +#include "../nav_map.h" +#include "../nav_obstacle.h" +#include "../nav_region.h" #include "core/templates/local_vector.h" #include "core/templates/rid.h" @@ -55,17 +55,17 @@ virtual void F_NAME(T_0 D_0, T_1 D_1) override; \ void MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1) -class GodotNavigationServer; +class GodotNavigationServer3D; #ifndef _3D_DISABLED class NavMeshGenerator3D; #endif // _3D_DISABLED struct SetCommand { virtual ~SetCommand() {} - virtual void exec(GodotNavigationServer *server) = 0; + virtual void exec(GodotNavigationServer3D *server) = 0; }; -class GodotNavigationServer : public NavigationServer3D { +class GodotNavigationServer3D : public NavigationServer3D { Mutex commands_mutex; /// Mutex used to make any operation threadsafe. Mutex operations_mutex; @@ -80,7 +80,7 @@ class GodotNavigationServer : public NavigationServer3D { bool active = true; LocalVector<NavMap *> active_maps; - LocalVector<uint32_t> active_maps_update_id; + LocalVector<uint32_t> active_maps_iteration_id; #ifndef _3D_DISABLED NavMeshGenerator3D *navmesh_generator_3d = nullptr; @@ -97,8 +97,8 @@ class GodotNavigationServer : public NavigationServer3D { int pm_edge_free_count = 0; public: - GodotNavigationServer(); - virtual ~GodotNavigationServer(); + GodotNavigationServer3D(); + virtual ~GodotNavigationServer3D(); void add_command(SetCommand *command); @@ -117,6 +117,9 @@ public: COMMAND_2(map_set_cell_height, RID, p_map, real_t, p_cell_height); virtual real_t map_get_cell_height(RID p_map) const override; + COMMAND_2(map_set_merge_rasterizer_cell_scale, RID, p_map, float, p_value); + virtual float map_get_merge_rasterizer_cell_scale(RID p_map) const override; + COMMAND_2(map_set_use_edge_connections, RID, p_map, bool, p_enabled); virtual bool map_get_use_edge_connections(RID p_map) const override; @@ -139,6 +142,9 @@ public: virtual TypedArray<RID> map_get_obstacles(RID p_map) const override; virtual void map_force_update(RID p_map) override; + virtual uint32_t map_get_iteration_id(RID p_map) const override; + + virtual Vector3 map_get_random_point(RID p_map, uint32_t p_navigation_layers, bool p_uniformly) const override; virtual RID region_create() override; @@ -163,6 +169,7 @@ public: COMMAND_2(region_set_navigation_layers, RID, p_region, uint32_t, p_navigation_layers); virtual uint32_t region_get_navigation_layers(RID p_region) const override; COMMAND_2(region_set_transform, RID, p_region, Transform3D, p_transform); + virtual Transform3D region_get_transform(RID p_region) const override; COMMAND_2(region_set_navigation_mesh, RID, p_region, Ref<NavigationMesh>, p_navigation_mesh); #ifndef DISABLE_DEPRECATED virtual void region_bake_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh, Node *p_root_node) override; @@ -170,6 +177,7 @@ public: virtual int region_get_connections_count(RID p_region) const override; virtual Vector3 region_get_connection_pathway_start(RID p_region, int p_connection_id) const override; virtual Vector3 region_get_connection_pathway_end(RID p_region, int p_connection_id) const override; + virtual Vector3 region_get_random_point(RID p_region, uint32_t p_navigation_layers, bool p_uniformly) const override; virtual RID link_create() override; COMMAND_2(link_set_map, RID, p_link, RID, p_map); @@ -201,20 +209,33 @@ public: COMMAND_2(agent_set_paused, RID, p_agent, bool, p_paused); virtual bool agent_get_paused(RID p_agent) const override; COMMAND_2(agent_set_neighbor_distance, RID, p_agent, real_t, p_distance); + virtual real_t agent_get_neighbor_distance(RID p_agent) const override; COMMAND_2(agent_set_max_neighbors, RID, p_agent, int, p_count); + virtual int agent_get_max_neighbors(RID p_agent) const override; COMMAND_2(agent_set_time_horizon_agents, RID, p_agent, real_t, p_time_horizon); + virtual real_t agent_get_time_horizon_agents(RID p_agent) const override; COMMAND_2(agent_set_time_horizon_obstacles, RID, p_agent, real_t, p_time_horizon); + virtual real_t agent_get_time_horizon_obstacles(RID p_agent) const override; COMMAND_2(agent_set_radius, RID, p_agent, real_t, p_radius); + virtual real_t agent_get_radius(RID p_agent) const override; COMMAND_2(agent_set_height, RID, p_agent, real_t, p_height); + virtual real_t agent_get_height(RID p_agent) const override; COMMAND_2(agent_set_max_speed, RID, p_agent, real_t, p_max_speed); + virtual real_t agent_get_max_speed(RID p_agent) const override; COMMAND_2(agent_set_velocity, RID, p_agent, Vector3, p_velocity); + virtual Vector3 agent_get_velocity(RID p_agent) const override; COMMAND_2(agent_set_velocity_forced, RID, p_agent, Vector3, p_velocity); COMMAND_2(agent_set_position, RID, p_agent, Vector3, p_position); + virtual Vector3 agent_get_position(RID p_agent) const override; virtual bool agent_is_map_changed(RID p_agent) const override; COMMAND_2(agent_set_avoidance_callback, RID, p_agent, Callable, p_callback); + virtual bool agent_has_avoidance_callback(RID p_agent) const override; COMMAND_2(agent_set_avoidance_layers, RID, p_agent, uint32_t, p_layers); + virtual uint32_t agent_get_avoidance_layers(RID p_agent) const override; COMMAND_2(agent_set_avoidance_mask, RID, p_agent, uint32_t, p_mask); + virtual uint32_t agent_get_avoidance_mask(RID p_agent) const override; COMMAND_2(agent_set_avoidance_priority, RID, p_agent, real_t, p_priority); + virtual real_t agent_get_avoidance_priority(RID p_agent) const override; virtual RID obstacle_create() override; COMMAND_2(obstacle_set_avoidance_enabled, RID, p_obstacle, bool, p_enabled); @@ -226,15 +247,22 @@ public: COMMAND_2(obstacle_set_paused, RID, p_obstacle, bool, p_paused); virtual bool obstacle_get_paused(RID p_obstacle) const override; COMMAND_2(obstacle_set_radius, RID, p_obstacle, real_t, p_radius); + virtual real_t obstacle_get_radius(RID p_obstacle) const override; COMMAND_2(obstacle_set_velocity, RID, p_obstacle, Vector3, p_velocity); + virtual Vector3 obstacle_get_velocity(RID p_obstacle) const override; COMMAND_2(obstacle_set_position, RID, p_obstacle, Vector3, p_position); + virtual Vector3 obstacle_get_position(RID p_obstacle) const override; COMMAND_2(obstacle_set_height, RID, p_obstacle, real_t, p_height); + virtual real_t obstacle_get_height(RID p_obstacle) const override; virtual void obstacle_set_vertices(RID p_obstacle, const Vector<Vector3> &p_vertices) override; + virtual Vector<Vector3> obstacle_get_vertices(RID p_obstacle) const override; COMMAND_2(obstacle_set_avoidance_layers, RID, p_obstacle, uint32_t, p_layers); + virtual uint32_t obstacle_get_avoidance_layers(RID p_obstacle) const override; virtual void parse_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, Node *p_root_node, const Callable &p_callback = Callable()) override; virtual void bake_from_source_geometry_data(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback = Callable()) override; virtual void bake_from_source_geometry_data_async(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback = Callable()) override; + virtual bool is_baking_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh) const override; COMMAND_1(free, RID, p_object); @@ -258,4 +286,4 @@ private: #undef COMMAND_1 #undef COMMAND_2 -#endif // GODOT_NAVIGATION_SERVER_H +#endif // GODOT_NAVIGATION_SERVER_3D_H diff --git a/modules/navigation/nav_mesh_generator_3d.cpp b/modules/navigation/3d/nav_mesh_generator_3d.cpp index 5de1c4cba9..e1ed9c51aa 100644 --- a/modules/navigation/nav_mesh_generator_3d.cpp +++ b/modules/navigation/3d/nav_mesh_generator_3d.cpp @@ -37,19 +37,19 @@ #include "core/os/thread.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/3d/multimesh_instance_3d.h" -#include "scene/3d/physics_body_3d.h" -#include "scene/resources/box_shape_3d.h" -#include "scene/resources/capsule_shape_3d.h" -#include "scene/resources/concave_polygon_shape_3d.h" -#include "scene/resources/convex_polygon_shape_3d.h" -#include "scene/resources/cylinder_shape_3d.h" -#include "scene/resources/height_map_shape_3d.h" +#include "scene/3d/physics/static_body_3d.h" +#include "scene/resources/3d/box_shape_3d.h" +#include "scene/resources/3d/capsule_shape_3d.h" +#include "scene/resources/3d/concave_polygon_shape_3d.h" +#include "scene/resources/3d/convex_polygon_shape_3d.h" +#include "scene/resources/3d/cylinder_shape_3d.h" +#include "scene/resources/3d/height_map_shape_3d.h" +#include "scene/resources/3d/primitive_meshes.h" +#include "scene/resources/3d/shape_3d.h" +#include "scene/resources/3d/sphere_shape_3d.h" +#include "scene/resources/3d/world_boundary_shape_3d.h" #include "scene/resources/navigation_mesh.h" #include "scene/resources/navigation_mesh_source_geometry_data_3d.h" -#include "scene/resources/primitive_meshes.h" -#include "scene/resources/shape_3d.h" -#include "scene/resources/sphere_shape_3d.h" -#include "scene/resources/world_boundary_shape_3d.h" #include "modules/modules_enabled.gen.h" // For csg, gridmap. @@ -172,11 +172,10 @@ void NavMeshGenerator3D::bake_from_source_geometry_data(Ref<NavigationMesh> p_na return; } - baking_navmesh_mutex.lock(); - if (baking_navmeshes.has(p_navigation_mesh)) { - baking_navmesh_mutex.unlock(); + if (is_baking(p_navigation_mesh)) { ERR_FAIL_MSG("NavigationMesh is already baking. Wait for current bake to finish."); } + baking_navmesh_mutex.lock(); baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); @@ -208,12 +207,11 @@ void NavMeshGenerator3D::bake_from_source_geometry_data_async(Ref<NavigationMesh return; } - baking_navmesh_mutex.lock(); - if (baking_navmeshes.has(p_navigation_mesh)) { - baking_navmesh_mutex.unlock(); + if (is_baking(p_navigation_mesh)) { ERR_FAIL_MSG("NavigationMesh is already baking. Wait for current bake to finish."); return; } + baking_navmesh_mutex.lock(); baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); @@ -228,6 +226,13 @@ void NavMeshGenerator3D::bake_from_source_geometry_data_async(Ref<NavigationMesh generator_task_mutex.unlock(); } +bool NavMeshGenerator3D::is_baking(Ref<NavigationMesh> p_navigation_mesh) { + baking_navmesh_mutex.lock(); + bool baking = baking_navmeshes.has(p_navigation_mesh); + baking_navmesh_mutex.unlock(); + return baking; +} + void NavMeshGenerator3D::generator_thread_bake(void *p_arg) { NavMeshGeneratorTask3D *generator_task = static_cast<NavMeshGeneratorTask3D *>(p_arg); @@ -384,33 +389,23 @@ void NavMeshGenerator3D::generator_parse_staticbody3d_node(const Ref<NavigationM const Vector<real_t> &map_data = heightmap_shape->get_map_data(); Vector2 heightmap_gridsize(heightmap_width - 1, heightmap_depth - 1); - Vector2 start = heightmap_gridsize * -0.5; + Vector3 start = Vector3(heightmap_gridsize.x, 0, heightmap_gridsize.y) * -0.5; Vector<Vector3> vertex_array; vertex_array.resize((heightmap_depth - 1) * (heightmap_width - 1) * 6); - int map_data_current_index = 0; - - for (int d = 0; d < heightmap_depth; d++) { - for (int w = 0; w < heightmap_width; w++) { - if (map_data_current_index + 1 + heightmap_depth < map_data.size()) { - float top_left_height = map_data[map_data_current_index]; - float top_right_height = map_data[map_data_current_index + 1]; - float bottom_left_height = map_data[map_data_current_index + heightmap_depth]; - float bottom_right_height = map_data[map_data_current_index + 1 + heightmap_depth]; - - Vector3 top_left = Vector3(start.x + w, top_left_height, start.y + d); - Vector3 top_right = Vector3(start.x + w + 1.0, top_right_height, start.y + d); - Vector3 bottom_left = Vector3(start.x + w, bottom_left_height, start.y + d + 1.0); - Vector3 bottom_right = Vector3(start.x + w + 1.0, bottom_right_height, start.y + d + 1.0); - - vertex_array.push_back(top_right); - vertex_array.push_back(bottom_left); - vertex_array.push_back(top_left); - vertex_array.push_back(top_right); - vertex_array.push_back(bottom_right); - vertex_array.push_back(bottom_left); - } - map_data_current_index += 1; + Vector3 *vertex_array_ptrw = vertex_array.ptrw(); + const real_t *map_data_ptr = map_data.ptr(); + int vertex_index = 0; + + for (int d = 0; d < heightmap_depth - 1; d++) { + for (int w = 0; w < heightmap_width - 1; w++) { + vertex_array_ptrw[vertex_index] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + w], d); + vertex_array_ptrw[vertex_index + 1] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + w + 1], d); + vertex_array_ptrw[vertex_index + 2] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + heightmap_width + w], d + 1); + vertex_array_ptrw[vertex_index + 3] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + w + 1], d); + vertex_array_ptrw[vertex_index + 4] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + heightmap_width + w + 1], d + 1); + vertex_array_ptrw[vertex_index + 5] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + heightmap_width + w], d + 1); + vertex_index += 6; } } if (vertex_array.size() > 0) { @@ -540,33 +535,23 @@ void NavMeshGenerator3D::generator_parse_gridmap_node(const Ref<NavigationMesh> const Vector<real_t> &map_data = dict["heights"]; Vector2 heightmap_gridsize(heightmap_width - 1, heightmap_depth - 1); - Vector2 start = heightmap_gridsize * -0.5; + Vector3 start = Vector3(heightmap_gridsize.x, 0, heightmap_gridsize.y) * -0.5; Vector<Vector3> vertex_array; vertex_array.resize((heightmap_depth - 1) * (heightmap_width - 1) * 6); - int map_data_current_index = 0; - - for (int d = 0; d < heightmap_depth; d++) { - for (int w = 0; w < heightmap_width; w++) { - if (map_data_current_index + 1 + heightmap_depth < map_data.size()) { - float top_left_height = map_data[map_data_current_index]; - float top_right_height = map_data[map_data_current_index + 1]; - float bottom_left_height = map_data[map_data_current_index + heightmap_depth]; - float bottom_right_height = map_data[map_data_current_index + 1 + heightmap_depth]; - - Vector3 top_left = Vector3(start.x + w, top_left_height, start.y + d); - Vector3 top_right = Vector3(start.x + w + 1.0, top_right_height, start.y + d); - Vector3 bottom_left = Vector3(start.x + w, bottom_left_height, start.y + d + 1.0); - Vector3 bottom_right = Vector3(start.x + w + 1.0, bottom_right_height, start.y + d + 1.0); - - vertex_array.push_back(top_right); - vertex_array.push_back(bottom_left); - vertex_array.push_back(top_left); - vertex_array.push_back(top_right); - vertex_array.push_back(bottom_right); - vertex_array.push_back(bottom_left); - } - map_data_current_index += 1; + Vector3 *vertex_array_ptrw = vertex_array.ptrw(); + const real_t *map_data_ptr = map_data.ptr(); + int vertex_index = 0; + + for (int d = 0; d < heightmap_depth - 1; d++) { + for (int w = 0; w < heightmap_width - 1; w++) { + vertex_array_ptrw[vertex_index] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + w], d); + vertex_array_ptrw[vertex_index + 1] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + w + 1], d); + vertex_array_ptrw[vertex_index + 2] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + heightmap_width + w], d + 1); + vertex_array_ptrw[vertex_index + 3] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + w + 1], d); + vertex_array_ptrw[vertex_index + 4] = start + Vector3(w + 1, map_data_ptr[(heightmap_width * d) + heightmap_width + w + 1], d + 1); + vertex_array_ptrw[vertex_index + 5] = start + Vector3(w, map_data_ptr[(heightmap_width * d) + heightmap_width + w], d + 1); + vertex_index += 6; } } if (vertex_array.size() > 0) { @@ -645,6 +630,9 @@ void NavMeshGenerator3D::generator_bake_from_source_geometry_data(Ref<Navigation cfg.cs = p_navigation_mesh->get_cell_size(); cfg.ch = p_navigation_mesh->get_cell_height(); + if (p_navigation_mesh->get_border_size() > 0.0) { + cfg.borderSize = (int)Math::ceil(p_navigation_mesh->get_border_size() / cfg.cs); + } cfg.walkableSlopeAngle = p_navigation_mesh->get_agent_max_slope(); cfg.walkableHeight = (int)Math::ceil(p_navigation_mesh->get_agent_height() / cfg.ch); cfg.walkableClimb = (int)Math::floor(p_navigation_mesh->get_agent_max_climb() / cfg.ch); @@ -657,6 +645,9 @@ void NavMeshGenerator3D::generator_bake_from_source_geometry_data(Ref<Navigation cfg.detailSampleDist = MAX(p_navigation_mesh->get_cell_size() * p_navigation_mesh->get_detail_sample_distance(), 0.1f); cfg.detailSampleMaxError = p_navigation_mesh->get_cell_height() * p_navigation_mesh->get_detail_sample_max_error(); + if (p_navigation_mesh->get_border_size() > 0.0 && !Math::is_equal_approx(p_navigation_mesh->get_cell_size(), p_navigation_mesh->get_border_size())) { + WARN_PRINT("Property border_size is ceiled to cell_size voxel units and loses precision."); + } if (!Math::is_equal_approx((float)cfg.walkableHeight * cfg.ch, p_navigation_mesh->get_agent_height())) { WARN_PRINT("Property agent_height is ceiled to cell_height voxel units and loses precision."); } @@ -722,7 +713,7 @@ void NavMeshGenerator3D::generator_bake_from_source_geometry_data(Ref<Navigation Vector<unsigned char> tri_areas; tri_areas.resize(ntris); - ERR_FAIL_COND(tri_areas.size() == 0); + ERR_FAIL_COND(tri_areas.is_empty()); memset(tri_areas.ptrw(), 0, ntris * sizeof(unsigned char)); rcMarkWalkableTriangles(&ctx, cfg.walkableSlopeAngle, verts, nverts, tris, ntris, tri_areas.ptrw()); @@ -758,11 +749,11 @@ void NavMeshGenerator3D::generator_bake_from_source_geometry_data(Ref<Navigation if (p_navigation_mesh->get_sample_partition_type() == NavigationMesh::SAMPLE_PARTITION_WATERSHED) { ERR_FAIL_COND(!rcBuildDistanceField(&ctx, *chf)); - ERR_FAIL_COND(!rcBuildRegions(&ctx, *chf, 0, cfg.minRegionArea, cfg.mergeRegionArea)); + ERR_FAIL_COND(!rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)); } else if (p_navigation_mesh->get_sample_partition_type() == NavigationMesh::SAMPLE_PARTITION_MONOTONE) { - ERR_FAIL_COND(!rcBuildRegionsMonotone(&ctx, *chf, 0, cfg.minRegionArea, cfg.mergeRegionArea)); + ERR_FAIL_COND(!rcBuildRegionsMonotone(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)); } else { - ERR_FAIL_COND(!rcBuildLayerRegions(&ctx, *chf, 0, cfg.minRegionArea)); + ERR_FAIL_COND(!rcBuildLayerRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea)); } bake_state = "Creating contours..."; // step #8 diff --git a/modules/navigation/nav_mesh_generator_3d.h b/modules/navigation/3d/nav_mesh_generator_3d.h index 4220927641..0251b02235 100644 --- a/modules/navigation/nav_mesh_generator_3d.h +++ b/modules/navigation/3d/nav_mesh_generator_3d.h @@ -99,6 +99,7 @@ public: static void parse_source_geometry_data(Ref<NavigationMesh> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData3D> p_source_geometry_data, Node *p_root_node, const Callable &p_callback = Callable()); static void bake_from_source_geometry_data(Ref<NavigationMesh> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData3D> p_source_geometry_data, const Callable &p_callback = Callable()); static void bake_from_source_geometry_data_async(Ref<NavigationMesh> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData3D> p_source_geometry_data, const Callable &p_callback = Callable()); + static bool is_baking(Ref<NavigationMesh> p_navigation_mesh); NavMeshGenerator3D(); ~NavMeshGenerator3D(); diff --git a/modules/navigation/navigation_mesh_generator.cpp b/modules/navigation/3d/navigation_mesh_generator.cpp index 8393896db1..8393896db1 100644 --- a/modules/navigation/navigation_mesh_generator.cpp +++ b/modules/navigation/3d/navigation_mesh_generator.cpp diff --git a/modules/navigation/navigation_mesh_generator.h b/modules/navigation/3d/navigation_mesh_generator.h index 08fe9f9142..08fe9f9142 100644 --- a/modules/navigation/navigation_mesh_generator.h +++ b/modules/navigation/3d/navigation_mesh_generator.h diff --git a/modules/navigation/SCsub b/modules/navigation/SCsub index 46bcb0fba4..02d3b7487e 100644 --- a/modules/navigation/SCsub +++ b/modules/navigation/SCsub @@ -74,6 +74,9 @@ env.modules_sources += thirdparty_obj module_obj = [] env_navigation.add_source_files(module_obj, "*.cpp") +env_navigation.add_source_files(module_obj, "2d/*.cpp") +if not env["disable_3d"]: + env_navigation.add_source_files(module_obj, "3d/*.cpp") if env.editor_build: env_navigation.add_source_files(module_obj, "editor/*.cpp") env.modules_sources += module_obj diff --git a/modules/navigation/nav_agent.cpp b/modules/navigation/nav_agent.cpp index cb219ac6c0..2dbe57eb4a 100644 --- a/modules/navigation/nav_agent.cpp +++ b/modules/navigation/nav_agent.cpp @@ -111,8 +111,8 @@ void NavAgent::set_map(NavMap *p_map) { bool NavAgent::is_map_changed() { if (map) { - bool is_changed = map->get_map_update_id() != map_update_id; - map_update_id = map->get_map_update_id(); + bool is_changed = map->get_iteration_id() != last_map_iteration_id; + last_map_iteration_id = map->get_iteration_id(); return is_changed; } else { return false; @@ -326,7 +326,7 @@ const Dictionary NavAgent::get_avoidance_data() const { _avoidance_data["new_velocity"] = Vector3(rvo_agent_3d.newVelocity_.x(), rvo_agent_3d.newVelocity_.y(), rvo_agent_3d.newVelocity_.z()); _avoidance_data["velocity"] = Vector3(rvo_agent_3d.velocity_.x(), rvo_agent_3d.velocity_.y(), rvo_agent_3d.velocity_.z()); _avoidance_data["position"] = Vector3(rvo_agent_3d.position_.x(), rvo_agent_3d.position_.y(), rvo_agent_3d.position_.z()); - _avoidance_data["prefered_velocity"] = Vector3(rvo_agent_3d.prefVelocity_.x(), rvo_agent_3d.prefVelocity_.y(), rvo_agent_3d.prefVelocity_.z()); + _avoidance_data["preferred_velocity"] = Vector3(rvo_agent_3d.prefVelocity_.x(), rvo_agent_3d.prefVelocity_.y(), rvo_agent_3d.prefVelocity_.z()); _avoidance_data["radius"] = float(rvo_agent_3d.radius_); _avoidance_data["time_horizon_agents"] = float(rvo_agent_3d.timeHorizon_); _avoidance_data["time_horizon_obstacles"] = 0.0; @@ -341,7 +341,7 @@ const Dictionary NavAgent::get_avoidance_data() const { _avoidance_data["new_velocity"] = Vector3(rvo_agent_2d.newVelocity_.x(), 0.0, rvo_agent_2d.newVelocity_.y()); _avoidance_data["velocity"] = Vector3(rvo_agent_2d.velocity_.x(), 0.0, rvo_agent_2d.velocity_.y()); _avoidance_data["position"] = Vector3(rvo_agent_2d.position_.x(), 0.0, rvo_agent_2d.position_.y()); - _avoidance_data["prefered_velocity"] = Vector3(rvo_agent_2d.prefVelocity_.x(), 0.0, rvo_agent_2d.prefVelocity_.y()); + _avoidance_data["preferred_velocity"] = Vector3(rvo_agent_2d.prefVelocity_.x(), 0.0, rvo_agent_2d.prefVelocity_.y()); _avoidance_data["radius"] = float(rvo_agent_2d.radius_); _avoidance_data["time_horizon_agents"] = float(rvo_agent_2d.timeHorizon_); _avoidance_data["time_horizon_obstacles"] = float(rvo_agent_2d.timeHorizonObst_); diff --git a/modules/navigation/nav_agent.h b/modules/navigation/nav_agent.h index 9ab6f55544..18997803f2 100644 --- a/modules/navigation/nav_agent.h +++ b/modules/navigation/nav_agent.h @@ -31,7 +31,6 @@ #ifndef NAV_AGENT_H #define NAV_AGENT_H -#include "nav_agent.h" #include "nav_rid.h" #include "core/object/class_db.h" @@ -72,7 +71,7 @@ class NavAgent : public NavRid { bool agent_dirty = true; - uint32_t map_update_id = 0; + uint32_t last_map_iteration_id = 0; bool paused = false; public: diff --git a/modules/navigation/nav_map.cpp b/modules/navigation/nav_map.cpp index 737ccaf3cd..a3f2ee2e61 100644 --- a/modules/navigation/nav_map.cpp +++ b/modules/navigation/nav_map.cpp @@ -54,6 +54,15 @@ r_path_owners->push_back(poly->owner->get_owner_id()); \ } +#ifdef DEBUG_ENABLED +#define NAVMAP_ITERATION_ZERO_ERROR_MSG() \ + ERR_PRINT_ONCE("NavigationServer navigation map query failed because it was made before first map synchronization.\n\ + NavigationServer 'map_changed' signal can be used to receive update notifications.\n\ + NavigationServer 'map_get_iteration_id()' can be used to check if a map has finished its newest iteration."); +#else +#define NAVMAP_ITERATION_ZERO_ERROR_MSG() +#endif // DEBUG_ENABLED + void NavMap::set_up(Vector3 p_up) { if (up == p_up) { return; @@ -67,6 +76,7 @@ void NavMap::set_cell_size(real_t p_cell_size) { return; } cell_size = p_cell_size; + _update_merge_rasterizer_cell_dimensions(); regenerate_polygons = true; } @@ -75,6 +85,16 @@ void NavMap::set_cell_height(real_t p_cell_height) { return; } cell_height = p_cell_height; + _update_merge_rasterizer_cell_dimensions(); + regenerate_polygons = true; +} + +void NavMap::set_merge_rasterizer_cell_scale(float p_value) { + if (merge_rasterizer_cell_scale == p_value) { + return; + } + merge_rasterizer_cell_scale = p_value; + _update_merge_rasterizer_cell_dimensions(); regenerate_polygons = true; } @@ -103,9 +123,9 @@ void NavMap::set_link_connection_radius(real_t p_link_connection_radius) { } gd::PointKey NavMap::get_point_key(const Vector3 &p_pos) const { - const int x = static_cast<int>(Math::floor(p_pos.x / cell_size)); - const int y = static_cast<int>(Math::floor(p_pos.y / cell_height)); - const int z = static_cast<int>(Math::floor(p_pos.z / cell_size)); + const int x = static_cast<int>(Math::floor(p_pos.x / merge_rasterizer_cell_size)); + const int y = static_cast<int>(Math::floor(p_pos.y / merge_rasterizer_cell_height)); + const int z = static_cast<int>(Math::floor(p_pos.z / merge_rasterizer_cell_size)); gd::PointKey p; p.key = 0; @@ -116,7 +136,12 @@ gd::PointKey NavMap::get_point_key(const Vector3 &p_pos) const { } Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p_optimize, uint32_t p_navigation_layers, Vector<int32_t> *r_path_types, TypedArray<RID> *r_path_rids, Vector<int64_t> *r_path_owners) const { - ERR_FAIL_COND_V_MSG(map_update_id == 0, Vector<Vector3>(), "NavigationServer map query failed because it was made before first map synchronization."); + RWLockRead read_lock(map_rwlock); + if (iteration_id == 0) { + NAVMAP_ITERATION_ZERO_ERROR_MSG(); + return Vector<Vector3>(); + } + // Clear metadata outputs. if (r_path_types) { r_path_types->clear(); @@ -372,7 +397,7 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p // Stores the further reachable end polygon, in case our goal is not reachable. if (is_reachable) { - real_t d = navigation_polys[least_cost_id].entry.distance_to(p_destination) * navigation_polys[least_cost_id].poly->owner->get_travel_cost(); + real_t d = navigation_polys[least_cost_id].entry.distance_to(p_destination); if (reachable_d > d) { reachable_d = d; reachable_end = navigation_polys[least_cost_id].poly; @@ -576,7 +601,12 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p } Vector3 NavMap::get_closest_point_to_segment(const Vector3 &p_from, const Vector3 &p_to, const bool p_use_collision) const { - ERR_FAIL_COND_V_MSG(map_update_id == 0, Vector3(), "NavigationServer map query failed because it was made before first map synchronization."); + RWLockRead read_lock(map_rwlock); + if (iteration_id == 0) { + NAVMAP_ITERATION_ZERO_ERROR_MSG(); + return Vector3(); + } + bool use_collision = p_use_collision; Vector3 closest_point; real_t closest_point_d = FLT_MAX; @@ -624,24 +654,38 @@ Vector3 NavMap::get_closest_point_to_segment(const Vector3 &p_from, const Vector } Vector3 NavMap::get_closest_point(const Vector3 &p_point) const { - ERR_FAIL_COND_V_MSG(map_update_id == 0, Vector3(), "NavigationServer map query failed because it was made before first map synchronization."); + RWLockRead read_lock(map_rwlock); + if (iteration_id == 0) { + NAVMAP_ITERATION_ZERO_ERROR_MSG(); + return Vector3(); + } gd::ClosestPointQueryResult cp = get_closest_point_info(p_point); return cp.point; } Vector3 NavMap::get_closest_point_normal(const Vector3 &p_point) const { - ERR_FAIL_COND_V_MSG(map_update_id == 0, Vector3(), "NavigationServer map query failed because it was made before first map synchronization."); + RWLockRead read_lock(map_rwlock); + if (iteration_id == 0) { + NAVMAP_ITERATION_ZERO_ERROR_MSG(); + return Vector3(); + } gd::ClosestPointQueryResult cp = get_closest_point_info(p_point); return cp.normal; } RID NavMap::get_closest_point_owner(const Vector3 &p_point) const { - ERR_FAIL_COND_V_MSG(map_update_id == 0, RID(), "NavigationServer map query failed because it was made before first map synchronization."); + RWLockRead read_lock(map_rwlock); + if (iteration_id == 0) { + NAVMAP_ITERATION_ZERO_ERROR_MSG(); + return RID(); + } gd::ClosestPointQueryResult cp = get_closest_point_info(p_point); return cp.owner; } gd::ClosestPointQueryResult NavMap::get_closest_point_info(const Vector3 &p_point) const { + RWLockRead read_lock(map_rwlock); + gd::ClosestPointQueryResult result; real_t closest_point_ds = FLT_MAX; @@ -769,7 +813,75 @@ void NavMap::remove_agent_as_controlled(NavAgent *agent) { } } +Vector3 NavMap::get_random_point(uint32_t p_navigation_layers, bool p_uniformly) const { + RWLockRead read_lock(map_rwlock); + + const LocalVector<NavRegion *> map_regions = get_regions(); + + if (map_regions.is_empty()) { + return Vector3(); + } + + LocalVector<const NavRegion *> accessible_regions; + + for (const NavRegion *region : map_regions) { + if (!region->get_enabled() || (p_navigation_layers & region->get_navigation_layers()) == 0) { + continue; + } + accessible_regions.push_back(region); + } + + if (accessible_regions.is_empty()) { + // All existing region polygons are disabled. + return Vector3(); + } + + if (p_uniformly) { + real_t accumulated_region_surface_area = 0; + RBMap<real_t, uint32_t> accessible_regions_area_map; + + for (uint32_t accessible_region_index = 0; accessible_region_index < accessible_regions.size(); accessible_region_index++) { + const NavRegion *region = accessible_regions[accessible_region_index]; + + real_t region_surface_area = region->get_surface_area(); + + if (region_surface_area == 0.0f) { + continue; + } + + accessible_regions_area_map[accumulated_region_surface_area] = accessible_region_index; + accumulated_region_surface_area += region_surface_area; + } + if (accessible_regions_area_map.is_empty() || accumulated_region_surface_area == 0) { + // All faces have no real surface / no area. + return Vector3(); + } + + real_t random_accessible_regions_area_map = Math::random(real_t(0), accumulated_region_surface_area); + + RBMap<real_t, uint32_t>::Iterator E = accessible_regions_area_map.find_closest(random_accessible_regions_area_map); + ERR_FAIL_COND_V(!E, Vector3()); + uint32_t random_region_index = E->value; + ERR_FAIL_UNSIGNED_INDEX_V(random_region_index, accessible_regions.size(), Vector3()); + + const NavRegion *random_region = accessible_regions[random_region_index]; + ERR_FAIL_NULL_V(random_region, Vector3()); + + return random_region->get_random_point(p_navigation_layers, p_uniformly); + + } else { + uint32_t random_region_index = Math::random(int(0), accessible_regions.size() - 1); + + const NavRegion *random_region = accessible_regions[random_region_index]; + ERR_FAIL_NULL_V(random_region, Vector3()); + + return random_region->get_random_point(p_navigation_layers, p_uniformly); + } +} + void NavMap::sync() { + RWLockWrite write_lock(map_rwlock); + // Performance Monitor int _new_pm_region_count = regions.size(); int _new_pm_agent_count = agents.size(); @@ -859,7 +971,7 @@ void NavMap::sync() { connections[ek].push_back(new_connection); } else { // The edge is already connected with another edge, skip. - ERR_PRINT_ONCE("Navigation map synchronization error. Attempted to merge a navigation mesh polygon edge with another already-merged edge. This is usually caused by crossing edges, overlapping polygons, or a mismatch of the NavigationMesh / NavigationPolygon baked 'cell_size' and navigation map 'cell_size'."); + ERR_PRINT_ONCE("Navigation map synchronization error. Attempted to merge a navigation mesh polygon edge with another already-merged edge. This is usually caused by crossing edges, overlapping polygons, or a mismatch of the NavigationMesh / NavigationPolygon baked 'cell_size' and navigation map 'cell_size'. If you're certain none of above is the case, change 'navigation/3d/merge_rasterizer_cell_scale' to 0.001."); } } } @@ -953,6 +1065,9 @@ void NavMap::sync() { // Search for polygons within range of a nav link. for (const NavLink *link : links) { + if (!link->get_enabled()) { + continue; + } const Vector3 start = link->get_start_position(); const Vector3 end = link->get_end_position(); @@ -1059,9 +1174,8 @@ void NavMap::sync() { } } - // Update the update ID. - // Some code treats 0 as a failure case, so we avoid returning 0. - map_update_id = map_update_id % 9999999 + 1; + // Some code treats 0 as a failure case, so we avoid returning 0 and modulo wrap UINT32_MAX manually. + iteration_id = iteration_id % UINT32_MAX + 1; } // Do we have modified obstacle positions? @@ -1104,8 +1218,14 @@ void NavMap::_update_rvo_obstacles_tree_2d() { obstacle_vertex_count += obstacle->get_vertices().size(); } + // Cleaning old obstacles. + for (size_t i = 0; i < rvo_simulation_2d.obstacles_.size(); ++i) { + delete rvo_simulation_2d.obstacles_[i]; + } + rvo_simulation_2d.obstacles_.clear(); + // Cannot use LocalVector here as RVO library expects std::vector to build KdTree - std::vector<RVO2D::Obstacle2D *> raw_obstacles; + std::vector<RVO2D::Obstacle2D *> &raw_obstacles = rvo_simulation_2d.obstacles_; raw_obstacles.reserve(obstacle_vertex_count); // The following block is modified copy from RVO2D::AddObstacle() @@ -1122,8 +1242,14 @@ void NavMap::_update_rvo_obstacles_tree_2d() { rvo_2d_vertices.reserve(_obstacle_vertices.size()); uint32_t _obstacle_avoidance_layers = obstacle->get_avoidance_layers(); + real_t _obstacle_height = obstacle->get_height(); for (const Vector3 &_obstacle_vertex : _obstacle_vertices) { +#ifdef TOOLS_ENABLED + if (_obstacle_vertex.y != 0) { + WARN_PRINT_ONCE("Y coordinates of static obstacle vertices are ignored. Please use obstacle position Y to change elevation of obstacle."); + } +#endif rvo_2d_vertices.push_back(RVO2D::Vector2(_obstacle_vertex.x + _obstacle_position.x, _obstacle_vertex.z + _obstacle_position.z)); } @@ -1132,6 +1258,9 @@ void NavMap::_update_rvo_obstacles_tree_2d() { for (size_t i = 0; i < rvo_2d_vertices.size(); i++) { RVO2D::Obstacle2D *rvo_2d_obstacle = new RVO2D::Obstacle2D(); rvo_2d_obstacle->point_ = rvo_2d_vertices[i]; + rvo_2d_obstacle->height_ = _obstacle_height; + rvo_2d_obstacle->elevation_ = _obstacle_position.y; + rvo_2d_obstacle->avoidance_layers_ = _obstacle_avoidance_layers; if (i != 0) { @@ -1283,6 +1412,11 @@ void NavMap::clip_path(const LocalVector<gd::NavigationPoly> &p_navigation_polys } } +void NavMap::_update_merge_rasterizer_cell_dimensions() { + merge_rasterizer_cell_size = cell_size * merge_rasterizer_cell_scale; + merge_rasterizer_cell_height = cell_height * merge_rasterizer_cell_scale; +} + NavMap::NavMap() { avoidance_use_multiple_threads = GLOBAL_GET("navigation/avoidance/thread_model/avoidance_use_multiple_threads"); avoidance_use_high_priority_threads = GLOBAL_GET("navigation/avoidance/thread_model/avoidance_use_high_priority_threads"); diff --git a/modules/navigation/nav_map.h b/modules/navigation/nav_map.h index 5d78c14627..d6215ea57f 100644 --- a/modules/navigation/nav_map.h +++ b/modules/navigation/nav_map.h @@ -48,6 +48,8 @@ class NavAgent; class NavObstacle; class NavMap : public NavRid { + RWLock map_rwlock; + /// Map Up Vector3 up = Vector3(0, 1, 0); @@ -56,6 +58,12 @@ class NavMap : public NavRid { real_t cell_size = 0.25; // Must match ProjectSettings default 3D cell_size and NavigationMesh cell_size. real_t cell_height = 0.25; // Must match ProjectSettings default 3D cell_height and NavigationMesh cell_height. + // For the inter-region merging to work, internal rasterization is performed. + float merge_rasterizer_cell_size = 0.25; + float merge_rasterizer_cell_height = 0.25; + // This value is used to control sensitivity of internal rasterizer. + float merge_rasterizer_cell_scale = 1.0; + bool use_edge_connections = true; /// This value is used to detect the near edges to connect. real_t edge_connection_margin = 0.25; @@ -100,7 +108,7 @@ class NavMap : public NavRid { real_t deltatime = 0.0; /// Change the id each time the map is updated. - uint32_t map_update_id = 0; + uint32_t iteration_id = 0; bool use_threads = true; bool avoidance_use_multiple_threads = true; @@ -120,6 +128,8 @@ public: NavMap(); ~NavMap(); + uint32_t get_iteration_id() const { return iteration_id; } + void set_up(Vector3 p_up); Vector3 get_up() const { return up; @@ -133,6 +143,11 @@ public: void set_cell_height(real_t p_cell_height); real_t get_cell_height() const { return cell_height; } + void set_merge_rasterizer_cell_scale(float p_value); + float get_merge_rasterizer_cell_scale() const { + return merge_rasterizer_cell_scale; + } + void set_use_edge_connections(bool p_enabled); bool get_use_edge_connections() const { return use_edge_connections; @@ -186,9 +201,7 @@ public: return obstacles; } - uint32_t get_map_update_id() const { - return map_update_id; - } + Vector3 get_random_point(uint32_t p_navigation_layers, bool p_uniformly) const; void sync(); void step(real_t p_deltatime); @@ -215,6 +228,8 @@ private: void _update_rvo_obstacles_tree_2d(); void _update_rvo_agents_tree_2d(); void _update_rvo_agents_tree_3d(); + + void _update_merge_rasterizer_cell_dimensions(); }; #endif // NAV_MAP_H diff --git a/modules/navigation/nav_obstacle.cpp b/modules/navigation/nav_obstacle.cpp index 34e5f7aa85..14dfd4eae3 100644 --- a/modules/navigation/nav_obstacle.cpp +++ b/modules/navigation/nav_obstacle.cpp @@ -149,8 +149,8 @@ void NavObstacle::set_vertices(const Vector<Vector3> &p_vertices) { bool NavObstacle::is_map_changed() { if (map) { - bool is_changed = map->get_map_update_id() != map_update_id; - map_update_id = map->get_map_update_id(); + bool is_changed = map->get_iteration_id() != last_map_iteration_id; + last_map_iteration_id = map->get_iteration_id(); return is_changed; } else { return false; diff --git a/modules/navigation/nav_obstacle.h b/modules/navigation/nav_obstacle.h index eb44f63d03..e231e83836 100644 --- a/modules/navigation/nav_obstacle.h +++ b/modules/navigation/nav_obstacle.h @@ -55,7 +55,7 @@ class NavObstacle : public NavRid { bool obstacle_dirty = true; - uint32_t map_update_id = 0; + uint32_t last_map_iteration_id = 0; bool paused = false; public: diff --git a/modules/navigation/nav_region.cpp b/modules/navigation/nav_region.cpp index 3675aae518..9cb235d79f 100644 --- a/modules/navigation/nav_region.cpp +++ b/modules/navigation/nav_region.cpp @@ -100,6 +100,88 @@ Vector3 NavRegion::get_connection_pathway_end(int p_connection_id) const { return connections[p_connection_id].pathway_end; } +Vector3 NavRegion::get_random_point(uint32_t p_navigation_layers, bool p_uniformly) const { + if (!get_enabled()) { + return Vector3(); + } + + const LocalVector<gd::Polygon> ®ion_polygons = get_polygons(); + + if (region_polygons.is_empty()) { + return Vector3(); + } + + if (p_uniformly) { + real_t accumulated_area = 0; + RBMap<real_t, uint32_t> region_area_map; + + for (uint32_t rp_index = 0; rp_index < region_polygons.size(); rp_index++) { + const gd::Polygon ®ion_polygon = region_polygons[rp_index]; + real_t polyon_area = region_polygon.surface_area; + + if (polyon_area == 0.0) { + continue; + } + region_area_map[accumulated_area] = rp_index; + accumulated_area += polyon_area; + } + if (region_area_map.is_empty() || accumulated_area == 0) { + // All polygons have no real surface / no area. + return Vector3(); + } + + real_t region_area_map_pos = Math::random(real_t(0), accumulated_area); + + RBMap<real_t, uint32_t>::Iterator region_E = region_area_map.find_closest(region_area_map_pos); + ERR_FAIL_COND_V(!region_E, Vector3()); + uint32_t rrp_polygon_index = region_E->value; + ERR_FAIL_UNSIGNED_INDEX_V(rrp_polygon_index, region_polygons.size(), Vector3()); + + const gd::Polygon &rr_polygon = region_polygons[rrp_polygon_index]; + + real_t accumulated_polygon_area = 0; + RBMap<real_t, uint32_t> polygon_area_map; + + for (uint32_t rpp_index = 2; rpp_index < rr_polygon.points.size(); rpp_index++) { + real_t face_area = Face3(rr_polygon.points[0].pos, rr_polygon.points[rpp_index - 1].pos, rr_polygon.points[rpp_index].pos).get_area(); + + if (face_area == 0.0) { + continue; + } + polygon_area_map[accumulated_polygon_area] = rpp_index; + accumulated_polygon_area += face_area; + } + if (polygon_area_map.is_empty() || accumulated_polygon_area == 0) { + // All faces have no real surface / no area. + return Vector3(); + } + + real_t polygon_area_map_pos = Math::random(real_t(0), accumulated_polygon_area); + + RBMap<real_t, uint32_t>::Iterator polygon_E = polygon_area_map.find_closest(polygon_area_map_pos); + ERR_FAIL_COND_V(!polygon_E, Vector3()); + uint32_t rrp_face_index = polygon_E->value; + ERR_FAIL_UNSIGNED_INDEX_V(rrp_face_index, rr_polygon.points.size(), Vector3()); + + const Face3 face(rr_polygon.points[0].pos, rr_polygon.points[rrp_face_index - 1].pos, rr_polygon.points[rrp_face_index].pos); + + Vector3 face_random_position = face.get_random_point_inside(); + return face_random_position; + + } else { + uint32_t rrp_polygon_index = Math::random(int(0), region_polygons.size() - 1); + + const gd::Polygon &rr_polygon = region_polygons[rrp_polygon_index]; + + uint32_t rrp_face_index = Math::random(int(2), rr_polygon.points.size() - 1); + + const Face3 face(rr_polygon.points[0].pos, rr_polygon.points[rrp_face_index - 1].pos, rr_polygon.points[rrp_face_index].pos); + + Vector3 face_random_position = face.get_random_point_inside(); + return face_random_position; + } +} + bool NavRegion::sync() { bool something_changed = polygons_dirty /* || something_dirty? */; @@ -113,6 +195,7 @@ void NavRegion::update_polygons() { return; } polygons.clear(); + surface_area = 0.0; polygons_dirty = false; if (map == nullptr) { @@ -125,11 +208,11 @@ void NavRegion::update_polygons() { #ifdef DEBUG_ENABLED if (!Math::is_equal_approx(double(map->get_cell_size()), double(mesh->get_cell_size()))) { - ERR_PRINT_ONCE(vformat("Navigation map synchronization error. Attempted to update a navigation region with a navigation mesh that uses a `cell_size` of %s while assigned to a navigation map set to a `cell_size` of %s. The cell size for navigation maps can be changed by using the NavigationServer map_set_cell_size() function. The cell size for default navigation maps can also be changed in the ProjectSettings.", double(map->get_cell_size()), double(mesh->get_cell_size()))); + ERR_PRINT_ONCE(vformat("Navigation map synchronization error. Attempted to update a navigation region with a navigation mesh that uses a `cell_size` of %s while assigned to a navigation map set to a `cell_size` of %s. The cell size for navigation maps can be changed by using the NavigationServer map_set_cell_size() function. The cell size for default navigation maps can also be changed in the ProjectSettings.", double(mesh->get_cell_size()), double(map->get_cell_size()))); } if (!Math::is_equal_approx(double(map->get_cell_height()), double(mesh->get_cell_height()))) { - ERR_PRINT_ONCE(vformat("Navigation map synchronization error. Attempted to update a navigation region with a navigation mesh that uses a `cell_height` of %s while assigned to a navigation map set to a `cell_height` of %s. The cell height for navigation maps can be changed by using the NavigationServer map_set_cell_height() function. The cell height for default navigation maps can also be changed in the ProjectSettings.", double(map->get_cell_height()), double(mesh->get_cell_height()))); + ERR_PRINT_ONCE(vformat("Navigation map synchronization error. Attempted to update a navigation region with a navigation mesh that uses a `cell_height` of %s while assigned to a navigation map set to a `cell_height` of %s. The cell height for navigation maps can be changed by using the NavigationServer map_set_cell_height() function. The cell height for default navigation maps can also be changed in the ProjectSettings.", double(mesh->get_cell_height()), double(map->get_cell_height()))); } if (map && Math::rad_to_deg(map->get_up().angle_to(transform.basis.get_column(1))) >= 90.0f) { @@ -147,21 +230,46 @@ void NavRegion::update_polygons() { polygons.resize(mesh->get_polygon_count()); + real_t _new_region_surface_area = 0.0; + // Build - for (size_t i(0); i < polygons.size(); i++) { - gd::Polygon &p = polygons[i]; - p.owner = this; + int navigation_mesh_polygon_index = 0; + for (gd::Polygon &polygon : polygons) { + polygon.owner = this; + polygon.surface_area = 0.0; - Vector<int> mesh_poly = mesh->get_polygon(i); - const int *indices = mesh_poly.ptr(); + Vector<int> navigation_mesh_polygon = mesh->get_polygon(navigation_mesh_polygon_index); + navigation_mesh_polygon_index += 1; + + int navigation_mesh_polygon_size = navigation_mesh_polygon.size(); + if (navigation_mesh_polygon_size < 3) { + continue; + } + + const int *indices = navigation_mesh_polygon.ptr(); bool valid(true); - p.points.resize(mesh_poly.size()); - p.edges.resize(mesh_poly.size()); - Vector3 center; + polygon.points.resize(navigation_mesh_polygon_size); + polygon.edges.resize(navigation_mesh_polygon_size); + + real_t _new_polygon_surface_area = 0.0; + + for (int j(2); j < navigation_mesh_polygon_size; j++) { + const Face3 face = Face3( + transform.xform(vertices_r[indices[0]]), + transform.xform(vertices_r[indices[j - 1]]), + transform.xform(vertices_r[indices[j]])); + + _new_polygon_surface_area += face.get_area(); + } + + polygon.surface_area = _new_polygon_surface_area; + _new_region_surface_area += _new_polygon_surface_area; + + Vector3 polygon_center; real_t sum(0); - for (int j(0); j < mesh_poly.size(); j++) { + for (int j(0); j < navigation_mesh_polygon_size; j++) { int idx = indices[j]; if (idx < 0 || idx >= len) { valid = false; @@ -169,10 +277,10 @@ void NavRegion::update_polygons() { } Vector3 point_position = transform.xform(vertices_r[idx]); - p.points[j].pos = point_position; - p.points[j].key = map->get_point_key(point_position); + polygon.points[j].pos = point_position; + polygon.points[j].key = map->get_point_key(point_position); - center += point_position; // Composing the center of the polygon + polygon_center += point_position; // Composing the center of the polygon if (j >= 2) { Vector3 epa = transform.xform(vertices_r[indices[j - 2]]); @@ -186,9 +294,11 @@ void NavRegion::update_polygons() { ERR_BREAK_MSG(!valid, "The navigation mesh set in this region is not valid!"); } - p.clockwise = sum > 0; - if (mesh_poly.size() != 0) { - p.center = center / real_t(mesh_poly.size()); + polygon.clockwise = sum > 0; + if (!navigation_mesh_polygon.is_empty()) { + polygon.center = polygon_center / real_t(navigation_mesh_polygon.size()); } } + + surface_area = _new_region_surface_area; } diff --git a/modules/navigation/nav_region.h b/modules/navigation/nav_region.h index 6a8ebe5336..a9cfc53c7e 100644 --- a/modules/navigation/nav_region.h +++ b/modules/navigation/nav_region.h @@ -50,6 +50,8 @@ class NavRegion : public NavBase { /// Cache LocalVector<gd::Polygon> polygons; + real_t surface_area = 0.0; + public: NavRegion() { type = NavigationUtilities::PathSegmentType::PATH_SEGMENT_TYPE_REGION; @@ -93,6 +95,10 @@ public: return polygons; } + Vector3 get_random_point(uint32_t p_navigation_layers, bool p_uniformly) const; + + real_t get_surface_area() const { return surface_area; }; + bool sync(); private: diff --git a/modules/navigation/nav_utils.h b/modules/navigation/nav_utils.h index 6ddd8b9078..175d08ca6d 100644 --- a/modules/navigation/nav_utils.h +++ b/modules/navigation/nav_utils.h @@ -112,6 +112,8 @@ struct Polygon { /// The center of this `Polygon` Vector3 center; + + real_t surface_area = 0.0; }; struct NavigationPoly { @@ -136,7 +138,7 @@ struct NavigationPoly { poly(p_poly) {} bool operator==(const NavigationPoly &other) const { - return this->poly == other.poly; + return poly == other.poly; } bool operator!=(const NavigationPoly &other) const { diff --git a/modules/navigation/register_types.cpp b/modules/navigation/register_types.cpp index 525fe71134..dbc9e53035 100644 --- a/modules/navigation/register_types.cpp +++ b/modules/navigation/register_types.cpp @@ -30,12 +30,12 @@ #include "register_types.h" -#include "godot_navigation_server.h" -#include "godot_navigation_server_2d.h" +#include "2d/godot_navigation_server_2d.h" +#include "3d/godot_navigation_server_3d.h" #ifndef DISABLE_DEPRECATED #ifndef _3D_DISABLED -#include "navigation_mesh_generator.h" +#include "3d/navigation_mesh_generator.h" #endif #endif // DISABLE_DEPRECATED @@ -53,8 +53,8 @@ NavigationMeshGenerator *_nav_mesh_generator = nullptr; #endif #endif // DISABLE_DEPRECATED -NavigationServer3D *new_server() { - return memnew(GodotNavigationServer); +NavigationServer3D *new_navigation_server_3d() { + return memnew(GodotNavigationServer3D); } NavigationServer2D *new_navigation_server_2d() { @@ -63,7 +63,7 @@ NavigationServer2D *new_navigation_server_2d() { void initialize_navigation_module(ModuleInitializationLevel p_level) { if (p_level == MODULE_INITIALIZATION_LEVEL_SERVERS) { - NavigationServer3DManager::set_default_server(new_server); + NavigationServer3DManager::set_default_server(new_navigation_server_3d); NavigationServer2DManager::set_default_server(new_navigation_server_2d); #ifndef DISABLE_DEPRECATED diff --git a/modules/noise/SCsub b/modules/noise/SCsub index 1430aa0c4e..f309fd2dd4 100644 --- a/modules/noise/SCsub +++ b/modules/noise/SCsub @@ -5,23 +5,9 @@ Import("env_modules") env_noise = env_modules.Clone() -# Thirdparty source files - -thirdparty_obj = [] - thirdparty_dir = "#thirdparty/noise/" -thirdparty_sources = [ - # Add C++ source files for noise modules here -] -thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources] - env_noise.Prepend(CPPPATH=[thirdparty_dir]) -env_thirdparty = env_noise.Clone() -env_thirdparty.disable_warnings() -env_thirdparty.add_source_files(thirdparty_obj, thirdparty_sources) -env.modules_sources += thirdparty_obj - # Godot source files module_obj = [] @@ -29,6 +15,3 @@ module_obj = [] env_noise.add_source_files(module_obj, "*.cpp") env_noise.add_source_files(module_obj, "editor/*.cpp") env.modules_sources += module_obj - -# Needed to force rebuilding the module files when the thirdparty library is updated. -env.Depends(module_obj, thirdparty_obj) diff --git a/modules/noise/editor/noise_editor_plugin.cpp b/modules/noise/editor/noise_editor_plugin.cpp index 91e9f7d477..917fa0cd15 100644 --- a/modules/noise/editor/noise_editor_plugin.cpp +++ b/modules/noise/editor/noise_editor_plugin.cpp @@ -36,7 +36,7 @@ #include "../noise_texture_2d.h" #include "editor/editor_inspector.h" -#include "editor/editor_scale.h" +#include "editor/themes/editor_scale.h" #include "scene/gui/button.h" #include "scene/gui/texture_rect.h" diff --git a/modules/noise/fastnoise_lite.cpp b/modules/noise/fastnoise_lite.cpp index 4aea98c4de..1b0ef6506b 100644 --- a/modules/noise/fastnoise_lite.cpp +++ b/modules/noise/fastnoise_lite.cpp @@ -416,8 +416,8 @@ void FastNoiseLite::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "noise_type", PROPERTY_HINT_ENUM, "Simplex,Simplex Smooth,Cellular,Perlin,Value Cubic,Value"), "set_noise_type", "get_noise_type"); ADD_PROPERTY(PropertyInfo(Variant::INT, "seed"), "set_seed", "get_seed"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "frequency", PROPERTY_HINT_RANGE, ".0001,1,.0001"), "set_frequency", "get_frequency"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "offset", PROPERTY_HINT_RANGE, "-999999999,999999999,0.01"), "set_offset", "get_offset"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "frequency", PROPERTY_HINT_RANGE, ".0001,1,.0001,exp"), "set_frequency", "get_frequency"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "offset", PROPERTY_HINT_RANGE, "-1000,1000,0.01,or_less,or_greater"), "set_offset", "get_offset"); ADD_GROUP("Fractal", "fractal_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "fractal_type", PROPERTY_HINT_ENUM, "None,FBM,Ridged,Ping-Pong"), "set_fractal_type", "get_fractal_type"); diff --git a/modules/noise/noise.cpp b/modules/noise/noise.cpp index 1115d92f58..9b9fd640f4 100644 --- a/modules/noise/noise.cpp +++ b/modules/noise/noise.cpp @@ -54,6 +54,9 @@ Vector<Ref<Image>> Noise::_get_seamless_image(int p_width, int p_height, int p_d Ref<Image> Noise::get_seamless_image(int p_width, int p_height, bool p_invert, bool p_in_3d_space, real_t p_blend_skirt, bool p_normalize) const { Vector<Ref<Image>> images = _get_seamless_image(p_width, p_height, 1, p_invert, p_in_3d_space, p_blend_skirt, p_normalize); + if (images.size() == 0) { + return Ref<Image>(); + } return images[0]; } @@ -163,6 +166,9 @@ Vector<Ref<Image>> Noise::_get_image(int p_width, int p_height, int p_depth, boo Ref<Image> Noise::get_image(int p_width, int p_height, bool p_invert, bool p_in_3d_space, bool p_normalize) const { Vector<Ref<Image>> images = _get_image(p_width, p_height, 1, p_invert, p_in_3d_space, p_normalize); + if (images.is_empty()) { + return Ref<Image>(); + } return images[0]; } diff --git a/modules/noise/noise_texture_2d.cpp b/modules/noise/noise_texture_2d.cpp index 1b0c5cb9e3..0443fec4a0 100644 --- a/modules/noise/noise_texture_2d.cpp +++ b/modules/noise/noise_texture_2d.cpp @@ -49,10 +49,6 @@ NoiseTexture2D::~NoiseTexture2D() { } void NoiseTexture2D::_bind_methods() { - ClassDB::bind_method(D_METHOD("_update_texture"), &NoiseTexture2D::_update_texture); - ClassDB::bind_method(D_METHOD("_generate_texture"), &NoiseTexture2D::_generate_texture); - ClassDB::bind_method(D_METHOD("_thread_done", "image"), &NoiseTexture2D::_thread_done); - ClassDB::bind_method(D_METHOD("set_width", "width"), &NoiseTexture2D::set_width); ClassDB::bind_method(D_METHOD("set_height", "height"), &NoiseTexture2D::set_height); @@ -138,7 +134,7 @@ void NoiseTexture2D::_thread_done(const Ref<Image> &p_image) { void NoiseTexture2D::_thread_function(void *p_ud) { NoiseTexture2D *tex = static_cast<NoiseTexture2D *>(p_ud); - tex->call_deferred(SNAME("_thread_done"), tex->_generate_texture()); + callable_mp(tex, &NoiseTexture2D::_thread_done).call_deferred(tex->_generate_texture()); } void NoiseTexture2D::_queue_update() { @@ -147,7 +143,7 @@ void NoiseTexture2D::_queue_update() { } update_queued = true; - call_deferred(SNAME("_update_texture")); + callable_mp(this, &NoiseTexture2D::_update_texture).call_deferred(); } Ref<Image> NoiseTexture2D::_generate_texture() { diff --git a/modules/noise/noise_texture_3d.cpp b/modules/noise/noise_texture_3d.cpp index 33e257a5c2..1e929e6f63 100644 --- a/modules/noise/noise_texture_3d.cpp +++ b/modules/noise/noise_texture_3d.cpp @@ -49,10 +49,6 @@ NoiseTexture3D::~NoiseTexture3D() { } void NoiseTexture3D::_bind_methods() { - ClassDB::bind_method(D_METHOD("_update_texture"), &NoiseTexture3D::_update_texture); - ClassDB::bind_method(D_METHOD("_generate_texture"), &NoiseTexture3D::_generate_texture); - ClassDB::bind_method(D_METHOD("_thread_done", "image"), &NoiseTexture3D::_thread_done); - ClassDB::bind_method(D_METHOD("set_width", "width"), &NoiseTexture3D::set_width); ClassDB::bind_method(D_METHOD("set_height", "height"), &NoiseTexture3D::set_height); ClassDB::bind_method(D_METHOD("set_depth", "depth"), &NoiseTexture3D::set_depth); @@ -126,7 +122,7 @@ void NoiseTexture3D::_thread_done(const TypedArray<Image> &p_data) { void NoiseTexture3D::_thread_function(void *p_ud) { NoiseTexture3D *tex = static_cast<NoiseTexture3D *>(p_ud); - tex->call_deferred(SNAME("_thread_done"), tex->_generate_texture()); + callable_mp(tex, &NoiseTexture3D::_thread_done).call_deferred(tex->_generate_texture()); } void NoiseTexture3D::_queue_update() { @@ -135,7 +131,7 @@ void NoiseTexture3D::_queue_update() { } update_queued = true; - call_deferred(SNAME("_update_texture")); + callable_mp(this, &NoiseTexture3D::_update_texture).call_deferred(); } TypedArray<Image> NoiseTexture3D::_generate_texture() { @@ -146,6 +142,8 @@ TypedArray<Image> NoiseTexture3D::_generate_texture() { return TypedArray<Image>(); } + ERR_FAIL_COND_V_MSG((int64_t)width * height * depth > Image::MAX_PIXELS, TypedArray<Image>(), "The NoiseTexture3D is too big, consider lowering its width, height, or depth."); + Vector<Ref<Image>> images; if (seamless) { diff --git a/modules/ogg/ogg_packet_sequence.cpp b/modules/ogg/ogg_packet_sequence.cpp index 1100367f03..1e6a9bbb6a 100644 --- a/modules/ogg/ogg_packet_sequence.cpp +++ b/modules/ogg/ogg_packet_sequence.cpp @@ -159,9 +159,7 @@ bool OggPacketSequencePlayback::next_ogg_packet(ogg_packet **p_packet) const { *p_packet = packet; - if (!packet->e_o_s) { // Added this so it doesn't try to go to the next packet if it's the last packet of the file. - packet_cursor++; - } + packet_cursor++; return true; } diff --git a/modules/openxr/SCsub b/modules/openxr/SCsub index 73a3723ea4..77922045eb 100644 --- a/modules/openxr/SCsub +++ b/modules/openxr/SCsub @@ -24,6 +24,9 @@ elif env["platform"] == "linuxbsd": if env["x11"]: env_openxr.AppendUnique(CPPDEFINES=["XR_USE_PLATFORM_XLIB"]) + if env["wayland"]: + env_openxr.AppendUnique(CPPDEFINES=["XR_USE_PLATFORM_WAYLAND"]) + # FIXME: Review what needs to be set for Android and macOS. env_openxr.AppendUnique(CPPDEFINES=["HAVE_SECURE_GETENV"]) elif env["platform"] == "windows": @@ -70,7 +73,7 @@ if env["builtin_openxr"]: # On Android the openxr_loader is provided by separate plugins for each device # Build the engine using object files khrloader_obj = [] - env_thirdparty.add_source_files(khrloader_obj, thirdparty_dir + "/src/xr_generated_dispatch_table.c") + env_thirdparty.add_source_files(khrloader_obj, thirdparty_dir + "/src/xr_generated_dispatch_table_core.c") env_thirdparty.add_source_files(khrloader_obj, thirdparty_dir + "/src/common/filesystem_utils.cpp") env_thirdparty.add_source_files(khrloader_obj, thirdparty_dir + "/src/common/object_info.cpp") @@ -93,35 +96,13 @@ if env["builtin_openxr"]: module_obj = [] env_openxr.add_source_files(module_obj, "*.cpp") -env_openxr.add_source_files(module_obj, "action_map/*.cpp") -env_openxr.add_source_files(module_obj, "scene/*.cpp") +env.modules_sources += module_obj -# We're a little more targeted with our extensions -if env["platform"] == "android": - env_openxr.add_source_files(module_obj, "extensions/openxr_android_extension.cpp") -if env["vulkan"]: - env_openxr.add_source_files(module_obj, "extensions/openxr_vulkan_extension.cpp") -if env["opengl3"] and env["platform"] != "macos": - env_openxr.add_source_files(module_obj, "extensions/openxr_opengl_extension.cpp") - -env_openxr.add_source_files(module_obj, "extensions/openxr_palm_pose_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_composition_layer_depth_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_eye_gaze_interaction.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_htc_controller_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_htc_vive_tracker_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_huawei_controller_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_hand_tracking_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_fb_foveation_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_fb_update_swapchain_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_fb_passthrough_extension_wrapper.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_fb_display_refresh_rate_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_pico_controller_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_wmr_controller_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_ml2_controller_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_extension_wrapper_extension.cpp") -env_openxr.add_source_files(module_obj, "extensions/openxr_api_extension.cpp") +Export("env_openxr") -env.modules_sources += module_obj +SConscript("action_map/SCsub") +SConscript("extensions/SCsub") +SConscript("scene/SCsub") if env.editor_build: SConscript("editor/SCsub") diff --git a/modules/openxr/action_map/SCsub b/modules/openxr/action_map/SCsub new file mode 100644 index 0000000000..7a493011ec --- /dev/null +++ b/modules/openxr/action_map/SCsub @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +Import("env") +Import("env_openxr") + +module_obj = [] + +env_openxr.add_source_files(module_obj, "*.cpp") + +env.modules_sources += module_obj diff --git a/modules/openxr/action_map/openxr_action_map.cpp b/modules/openxr/action_map/openxr_action_map.cpp index 72866f1cf7..bbcb63a7e6 100644 --- a/modules/openxr/action_map/openxr_action_map.cpp +++ b/modules/openxr/action_map/openxr_action_map.cpp @@ -177,7 +177,6 @@ void OpenXRActionMap::create_default_action_sets() { Ref<OpenXRAction> trigger_touch = action_set->add_new_action("trigger_touch", "Trigger touching", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> grip = action_set->add_new_action("grip", "Grip", OpenXRAction::OPENXR_ACTION_FLOAT, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> grip_click = action_set->add_new_action("grip_click", "Grip click", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); - Ref<OpenXRAction> grip_touch = action_set->add_new_action("grip_touch", "Grip touching", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> grip_force = action_set->add_new_action("grip_force", "Grip force", OpenXRAction::OPENXR_ACTION_FLOAT, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> primary = action_set->add_new_action("primary", "Primary joystick/thumbstick/trackpad", OpenXRAction::OPENXR_ACTION_VECTOR2, "/user/hand/left,/user/hand/right"); Ref<OpenXRAction> primary_click = action_set->add_new_action("primary_click", "Primary joystick/thumbstick/trackpad click", OpenXRAction::OPENXR_ACTION_BOOL, "/user/hand/left,/user/hand/right"); diff --git a/modules/openxr/action_map/openxr_interaction_profile_metadata.cpp b/modules/openxr/action_map/openxr_interaction_profile_metadata.cpp index df607b0def..75cfb095bb 100644 --- a/modules/openxr/action_map/openxr_interaction_profile_metadata.cpp +++ b/modules/openxr/action_map/openxr_interaction_profile_metadata.cpp @@ -360,6 +360,9 @@ void OpenXRInteractionProfileMetadata::_register_core_metadata() { register_io_path("/interaction_profiles/oculus/touch_controller", "Thumbstick click", "/user/hand/right", "/user/hand/right/input/thumbstick/click", "", OpenXRAction::OPENXR_ACTION_BOOL); register_io_path("/interaction_profiles/oculus/touch_controller", "Thumbstick touch", "/user/hand/right", "/user/hand/right/input/thumbstick/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + register_io_path("/interaction_profiles/oculus/touch_controller", "Thumbrest touch", "/user/hand/left", "/user/hand/left/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + register_io_path("/interaction_profiles/oculus/touch_controller", "Thumbrest touch", "/user/hand/right", "/user/hand/right/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + register_io_path("/interaction_profiles/oculus/touch_controller", "Haptic output", "/user/hand/left", "/user/hand/left/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); register_io_path("/interaction_profiles/oculus/touch_controller", "Haptic output", "/user/hand/right", "/user/hand/right/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml index e795311651..f737f3b642 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -30,6 +30,13 @@ Returns an error string for the given [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrResult.html]XrResult[/url]. </description> </method> + <method name="get_hand_tracker"> + <return type="int" /> + <param index="0" name="hand_index" type="int" /> + <description> + Returns the corresponding [code]XRHandTrackerEXT[/code] handle for the given hand index value. + </description> + </method> <method name="get_instance"> <return type="int" /> <description> @@ -75,6 +82,12 @@ Returns the id of the system, which is a [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSystemId.html]XrSystemId[/url] cast to an integer. </description> </method> + <method name="is_environment_blend_mode_alpha_supported"> + <return type="int" enum="OpenXRAPIExtension.OpenXRAlphaBlendModeSupport" /> + <description> + Returns [enum OpenXRAPIExtension.OpenXRAlphaBlendModeSupport] denoting if [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] is really supported, emulated or not supported at all. + </description> + </method> <method name="is_initialized"> <return type="bool" /> <description> @@ -94,6 +107,20 @@ Returns [code]true[/code] if OpenXR is enabled. </description> </method> + <method name="register_composition_layer_provider"> + <return type="void" /> + <param index="0" name="extension" type="OpenXRExtensionWrapperExtension" /> + <description> + Registers the given extension as a composition layer provider. + </description> + </method> + <method name="set_emulate_environment_blend_mode_alpha_blend"> + <return type="void" /> + <param index="0" name="enabled" type="bool" /> + <description> + If set to [code]true[/code], an OpenXR extension is loaded which is capable of emulating the [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] blend mode. + </description> + </method> <method name="transform_from_pose"> <return type="Transform3D" /> <param index="0" name="pose" type="const void*" /> @@ -101,6 +128,13 @@ Creates a [Transform3D] from an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrPosef.html]XrPosef[/url]. </description> </method> + <method name="unregister_composition_layer_provider"> + <return type="void" /> + <param index="0" name="extension" type="OpenXRExtensionWrapperExtension" /> + <description> + Unregisters the given extension as a composition layer provider. + </description> + </method> <method name="xr_result"> <return type="bool" /> <param index="0" name="result" type="int" /> @@ -111,4 +145,15 @@ </description> </method> </methods> + <constants> + <constant name="OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE" value="0" enum="OpenXRAlphaBlendModeSupport"> + Means that [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] isn't supported at all. + </constant> + <constant name="OPENXR_ALPHA_BLEND_MODE_SUPPORT_REAL" value="1" enum="OpenXRAlphaBlendModeSupport"> + Means that [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] is really supported. + </constant> + <constant name="OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING" value="2" enum="OpenXRAlphaBlendModeSupport"> + Means that [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] is emulated. + </constant> + </constants> </class> diff --git a/modules/openxr/doc_classes/OpenXRAction.xml b/modules/openxr/doc_classes/OpenXRAction.xml index 6a3529e43e..2a9f255f2f 100644 --- a/modules/openxr/doc_classes/OpenXRAction.xml +++ b/modules/openxr/doc_classes/OpenXRAction.xml @@ -4,7 +4,7 @@ An OpenXR action. </brief_description> <description> - This resource defines an OpenXR action. Actions can be used both for inputs (buttons/joystick/trigger/etc) and outputs (haptics). + This resource defines an OpenXR action. Actions can be used both for inputs (buttons, joysticks, triggers, etc.) and outputs (haptics). OpenXR performs automatic conversion between action type and input type whenever possible. An analog trigger bound to a boolean action will thus return [code]false[/code] if the trigger is depressed and [code]true[/code] if pressed fully. Actions are not directly bound to specific devices, instead OpenXR recognizes a limited number of top level paths that identify devices by usage. We can restrict which devices an action can be bound to by these top level paths. For instance an action that should only be used for hand held controllers can have the top level paths "/user/hand/left" and "/user/hand/right" associated with them. See the [url=https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#semantic-path-reserved]reserved path section in the OpenXR specification[/url] for more info on the top level paths. Note that the name of the resource is used to register the action with. diff --git a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml index e09b58e5b4..ee2aa33108 100644 --- a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml +++ b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml @@ -9,6 +9,12 @@ <tutorials> </tutorials> <methods> + <method name="_get_composition_layer" qualifiers="virtual"> + <return type="int" /> + <description> + Returns a pointer to a [code]XrCompositionLayerBaseHeader[/code] struct to provide a composition layer. This will only be called if the extension previously registered itself with [method OpenXRAPIExtension.register_composition_layer_provider]. + </description> + </method> <method name="_get_requested_extensions" qualifiers="virtual"> <return type="Dictionary" /> <description> @@ -17,6 +23,12 @@ - If the [code]bool *[/code] points to a boolean, the boolean will be updated to [code]true[/code] if the extension is enabled. </description> </method> + <method name="_get_suggested_tracker_names" qualifiers="virtual"> + <return type="PackedStringArray" /> + <description> + Returns a [PackedStringArray] of positional tracker names that are used within the extension wrapper. + </description> + </method> <method name="_on_before_instance_created" qualifiers="virtual"> <return type="void" /> <description> @@ -123,6 +135,14 @@ Called when the OpenXR session state is changed to visible. This means OpenXR is now ready to receive frames. </description> </method> + <method name="_set_hand_joint_locations_and_get_next_pointer" qualifiers="virtual"> + <return type="int" /> + <param index="0" name="hand_index" type="int" /> + <param index="1" name="next_pointer" type="void*" /> + <description> + Adds additional data structures when each hand tracker is created. + </description> + </method> <method name="_set_instance_create_info_and_get_next_pointer" qualifiers="virtual"> <return type="int" /> <param index="0" name="next_pointer" type="void*" /> diff --git a/modules/openxr/doc_classes/OpenXRHand.xml b/modules/openxr/doc_classes/OpenXRHand.xml index cc7766507f..23d932ddd7 100644 --- a/modules/openxr/doc_classes/OpenXRHand.xml +++ b/modules/openxr/doc_classes/OpenXRHand.xml @@ -1,14 +1,20 @@ <?xml version="1.0" encoding="UTF-8" ?> -<class name="OpenXRHand" inherits="Node3D" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> +<class name="OpenXRHand" inherits="Node3D" deprecated="Use [XRHandModifier3D] instead." xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <brief_description> - Node supporting finger tracking in OpenXR. + Node supporting hand and finger tracking in OpenXR. </brief_description> <description> - This node enables OpenXR's hand tracking functionality. The node should be a child node of an [XROrigin3D] node, tracking will update its position to where the player's actual hand is positioned. This node also updates the skeleton of a properly skinned hand model. The hand mesh should be a child node of this node. + This node enables OpenXR's hand tracking functionality. The node should be a child node of an [XROrigin3D] node, tracking will update its position to the player's tracked hand Palm joint location (the center of the middle finger's metacarpal bone). This node also updates the skeleton of a properly skinned hand or avatar model. + If the skeleton is a hand (one of the hand bones is the root node of the skeleton), then the skeleton will be placed relative to the hand palm location and the hand mesh and skeleton should be children of the OpenXRHand node. + If the hand bones are part of a full skeleton, then the root of the hand will keep its location with the assumption that IK is used to position the hand and arm. + By default the skeleton hand bones are repositioned to match the size of the tracked hand. To preserve the modeled bone sizes change [member bone_update] to apply rotation only. </description> <tutorials> </tutorials> <members> + <member name="bone_update" type="int" setter="set_bone_update" getter="get_bone_update" enum="OpenXRHand.BoneUpdate" default="0"> + Specify the type of updates to perform on the bone. + </member> <member name="hand" type="int" setter="set_hand" getter="get_hand" enum="OpenXRHand.Hands" default="0"> Specifies whether this node tracks the left or right hand of the player. </member> @@ -18,6 +24,9 @@ <member name="motion_range" type="int" setter="set_motion_range" getter="get_motion_range" enum="OpenXRHand.MotionRange" default="0"> Set the motion range (if supported) limiting the hand motion. </member> + <member name="skeleton_rig" type="int" setter="set_skeleton_rig" getter="get_skeleton_rig" enum="OpenXRHand.SkeletonRig" default="0"> + Set the type of skeleton rig the [member hand_skeleton] is compliant with. + </member> </members> <constants> <constant name="HAND_LEFT" value="0" enum="Hands"> @@ -38,5 +47,23 @@ <constant name="MOTION_RANGE_MAX" value="2" enum="MotionRange"> Maximum supported motion ranges. </constant> + <constant name="SKELETON_RIG_OPENXR" value="0" enum="SkeletonRig"> + An OpenXR compliant skeleton. + </constant> + <constant name="SKELETON_RIG_HUMANOID" value="1" enum="SkeletonRig"> + A [SkeletonProfileHumanoid] compliant skeleton. + </constant> + <constant name="SKELETON_RIG_MAX" value="2" enum="SkeletonRig"> + Maximum supported hands. + </constant> + <constant name="BONE_UPDATE_FULL" value="0" enum="BoneUpdate"> + The skeletons bones are fully updated (both position and rotation) to match the tracked bones. + </constant> + <constant name="BONE_UPDATE_ROTATION_ONLY" value="1" enum="BoneUpdate"> + The skeletons bones are only rotated to align with the tracked bones, preserving bone length. + </constant> + <constant name="BONE_UPDATE_MAX" value="2" enum="BoneUpdate"> + Maximum supported bone update mode. + </constant> </constants> </class> diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 6d1c215ffc..5d38788dcd 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -23,7 +23,7 @@ Returns display refresh rates supported by the current HMD. Only returned if this feature is supported by the OpenXR runtime and after the interface has been initialized. </description> </method> - <method name="get_hand_joint_angular_velocity" qualifiers="const"> + <method name="get_hand_joint_angular_velocity" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_angular_velocity] obtained from [method XRServer.get_hand_tracker] instead."> <return type="Vector3" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -31,7 +31,7 @@ If handtracking is enabled, returns the angular velocity of a joint ([param joint]) of a hand ([param hand]) as provided by OpenXR. This is relative to [XROrigin3D]! </description> </method> - <method name="get_hand_joint_flags" qualifiers="const"> + <method name="get_hand_joint_flags" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_flags] obtained from [method XRServer.get_hand_tracker] instead."> <return type="int" enum="OpenXRInterface.HandJointFlags" is_bitfield="true" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -39,7 +39,7 @@ If handtracking is enabled, returns flags that inform us of the validity of the tracking data. </description> </method> - <method name="get_hand_joint_linear_velocity" qualifiers="const"> + <method name="get_hand_joint_linear_velocity" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_linear_velocity] obtained from [method XRServer.get_hand_tracker] instead."> <return type="Vector3" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -47,7 +47,7 @@ If handtracking is enabled, returns the linear velocity of a joint ([param joint]) of a hand ([param hand]) as provided by OpenXR. This is relative to [XROrigin3D] without worldscale applied! </description> </method> - <method name="get_hand_joint_position" qualifiers="const"> + <method name="get_hand_joint_position" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_transform] obtained from [method XRServer.get_hand_tracker] instead."> <return type="Vector3" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -55,7 +55,7 @@ If handtracking is enabled, returns the position of a joint ([param joint]) of a hand ([param hand]) as provided by OpenXR. This is relative to [XROrigin3D] without worldscale applied! </description> </method> - <method name="get_hand_joint_radius" qualifiers="const"> + <method name="get_hand_joint_radius" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_radius] obtained from [method XRServer.get_hand_tracker] instead."> <return type="float" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -63,7 +63,7 @@ If handtracking is enabled, returns the radius of a joint ([param joint]) of a hand ([param hand]) as provided by OpenXR. This is without worldscale applied! </description> </method> - <method name="get_hand_joint_rotation" qualifiers="const"> + <method name="get_hand_joint_rotation" qualifiers="const" deprecated="Use [method XRHandTracker.get_hand_joint_transform] obtained from [method XRServer.get_hand_tracker] instead."> <return type="Quaternion" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> <param index="1" name="joint" type="int" enum="OpenXRInterface.HandJoints" /> @@ -71,6 +71,13 @@ If handtracking is enabled, returns the rotation of a joint ([param joint]) of a hand ([param hand]) as provided by OpenXR. </description> </method> + <method name="get_hand_tracking_source" qualifiers="const" deprecated="Use [member XRHandTracker.hand_tracking_source] obtained from [method XRServer.get_hand_tracker] instead."> + <return type="int" enum="OpenXRInterface.HandTrackedSource" /> + <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> + <description> + If handtracking is enabled and hand tracking source is supported, gets the source of the hand tracking data for [param hand]. + </description> + </method> <method name="get_motion_range" qualifiers="const"> <return type="int" enum="OpenXRInterface.HandMotionRange" /> <param index="0" name="hand" type="int" enum="OpenXRInterface.Hand" /> @@ -129,9 +136,11 @@ </member> <member name="foveation_dynamic" type="bool" setter="set_foveation_dynamic" getter="get_foveation_dynamic" default="false"> Enable dynamic foveation adjustment, the interface must be initialized before this is accessible. If enabled foveation will automatically adjusted between low and [member foveation_level]. + [b]Note:[/b] Only works on compatibility renderer. </member> <member name="foveation_level" type="int" setter="set_foveation_level" getter="get_foveation_level" default="0"> Set foveation level from 0 (off) to 3 (high), the interface must be initialized before this is accessible. + [b]Note:[/b] Only works on compatibility renderer. </member> <member name="render_target_size_multiplier" type="float" setter="set_render_target_size_multiplier" getter="get_render_target_size_multiplier" default="1.0"> The render size multiplier for the current HMD. Must be set before the interface has been initialized. @@ -175,10 +184,25 @@ Maximum value for the hand enum. </constant> <constant name="HAND_MOTION_RANGE_UNOBSTRUCTED" value="0" enum="HandMotionRange"> + Full hand range, if user closes their hands, we make a full fist. </constant> <constant name="HAND_MOTION_RANGE_CONFORM_TO_CONTROLLER" value="1" enum="HandMotionRange"> + Conform to controller, if user closes their hands, the tracked data conforms to the shape of the controller. </constant> <constant name="HAND_MOTION_RANGE_MAX" value="2" enum="HandMotionRange"> + Maximum value for the motion range enum. + </constant> + <constant name="HAND_TRACKED_SOURCE_UNKNOWN" value="0" enum="HandTrackedSource"> + The source of hand tracking data is unknown (the extension is likely unsupported). + </constant> + <constant name="HAND_TRACKED_SOURCE_UNOBSTRUCTED" value="1" enum="HandTrackedSource"> + The source of hand tracking is unobstructed, this means that an accurate method of hand tracking is used, e.g. optical hand tracking, data gloves, etc. + </constant> + <constant name="HAND_TRACKED_SOURCE_CONTROLLER" value="2" enum="HandTrackedSource"> + The source of hand tracking is a controller, bone positions are inferred from controller inputs. + </constant> + <constant name="HAND_TRACKED_SOURCE_MAX" value="3" enum="HandTrackedSource"> + Maximum value for the hand tracked source enum. </constant> <constant name="HAND_JOINT_PALM" value="0" enum="HandJoints"> Palm joint. diff --git a/modules/openxr/editor/openxr_action_map_editor.cpp b/modules/openxr/editor/openxr_action_map_editor.cpp index 64e07eff21..5dd737756a 100644 --- a/modules/openxr/editor/openxr_action_map_editor.cpp +++ b/modules/openxr/editor/openxr_action_map_editor.cpp @@ -32,9 +32,10 @@ #include "core/config/project_settings.h" #include "editor/editor_node.h" -#include "editor/editor_scale.h" #include "editor/editor_settings.h" +#include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/themes/editor_scale.h" // TODO implement redo/undo system @@ -43,7 +44,6 @@ void OpenXRActionMapEditor::_bind_methods() { ClassDB::bind_method("_add_interaction_profile_editor", &OpenXRActionMapEditor::_add_interaction_profile_editor); ClassDB::bind_method(D_METHOD("_add_action_set", "name"), &OpenXRActionMapEditor::_add_action_set); - ClassDB::bind_method(D_METHOD("_set_focus_on_action_set", "action_set"), &OpenXRActionMapEditor::_set_focus_on_action_set); ClassDB::bind_method(D_METHOD("_remove_action_set", "name"), &OpenXRActionMapEditor::_remove_action_set); ClassDB::bind_method(D_METHOD("_do_add_action_set_editor", "action_set_editor"), &OpenXRActionMapEditor::_do_add_action_set_editor); @@ -175,7 +175,7 @@ void OpenXRActionMapEditor::_on_add_action_set() { // Make sure our action set is the current tab tabs->set_current_tab(0); - call_deferred("_set_focus_on_action_set", new_action_set_editor); + callable_mp(this, &OpenXRActionMapEditor::_set_focus_on_action_set).call_deferred(new_action_set_editor); } void OpenXRActionMapEditor::_set_focus_on_action_set(OpenXRActionSetEditor *p_action_set_editor) { @@ -357,7 +357,7 @@ void OpenXRActionMapEditor::_do_remove_interaction_profile_editor(OpenXRInteract } void OpenXRActionMapEditor::open_action_map(String p_path) { - EditorNode::get_singleton()->make_bottom_panel_item_visible(this); + EditorNode::get_bottom_panel()->make_item_visible(this); // out with the old... _clear_action_map(); diff --git a/modules/openxr/editor/openxr_editor_plugin.cpp b/modules/openxr/editor/openxr_editor_plugin.cpp index 51ebbcf44a..f545e31073 100644 --- a/modules/openxr/editor/openxr_editor_plugin.cpp +++ b/modules/openxr/editor/openxr_editor_plugin.cpp @@ -33,6 +33,7 @@ #include "../action_map/openxr_action_map.h" #include "editor/editor_node.h" +#include "editor/gui/editor_bottom_panel.h" void OpenXREditorPlugin::edit(Object *p_node) { if (Object::cast_to<OpenXRActionMap>(p_node)) { @@ -52,7 +53,12 @@ void OpenXREditorPlugin::make_visible(bool p_visible) { OpenXREditorPlugin::OpenXREditorPlugin() { action_map_editor = memnew(OpenXRActionMapEditor); - EditorNode::get_singleton()->add_bottom_panel_item(TTR("OpenXR Action Map"), action_map_editor); + EditorNode::get_bottom_panel()->add_item(TTR("OpenXR Action Map"), action_map_editor); + +#ifndef ANDROID_ENABLED + select_runtime = memnew(OpenXRSelectRuntime); + add_control_to_container(CONTAINER_TOOLBAR, select_runtime); +#endif } OpenXREditorPlugin::~OpenXREditorPlugin() { diff --git a/modules/openxr/editor/openxr_editor_plugin.h b/modules/openxr/editor/openxr_editor_plugin.h index 9764f8fe21..b80f20d049 100644 --- a/modules/openxr/editor/openxr_editor_plugin.h +++ b/modules/openxr/editor/openxr_editor_plugin.h @@ -32,6 +32,7 @@ #define OPENXR_EDITOR_PLUGIN_H #include "openxr_action_map_editor.h" +#include "openxr_select_runtime.h" #include "editor/editor_plugin.h" @@ -39,6 +40,9 @@ class OpenXREditorPlugin : public EditorPlugin { GDCLASS(OpenXREditorPlugin, EditorPlugin); OpenXRActionMapEditor *action_map_editor = nullptr; +#ifndef ANDROID_ENABLED + OpenXRSelectRuntime *select_runtime = nullptr; +#endif public: virtual String get_name() const override { return "OpenXRPlugin"; } diff --git a/modules/openxr/editor/openxr_interaction_profile_editor.cpp b/modules/openxr/editor/openxr_interaction_profile_editor.cpp index 7bccabf936..da6a9eed70 100644 --- a/modules/openxr/editor/openxr_interaction_profile_editor.cpp +++ b/modules/openxr/editor/openxr_interaction_profile_editor.cpp @@ -45,7 +45,6 @@ void OpenXRInteractionProfileEditorBase::_bind_methods() { ClassDB::bind_method(D_METHOD("_add_binding", "action", "path"), &OpenXRInteractionProfileEditorBase::_add_binding); ClassDB::bind_method(D_METHOD("_remove_binding", "action", "path"), &OpenXRInteractionProfileEditorBase::_remove_binding); - ClassDB::bind_method(D_METHOD("_update_interaction_profile"), &OpenXRInteractionProfileEditorBase::_update_interaction_profile); } void OpenXRInteractionProfileEditorBase::_notification(int p_what) { @@ -63,7 +62,7 @@ void OpenXRInteractionProfileEditorBase::_notification(int p_what) { void OpenXRInteractionProfileEditorBase::_do_update_interaction_profile() { if (!is_dirty) { is_dirty = true; - call_deferred("_update_interaction_profile"); + callable_mp(this, &OpenXRInteractionProfileEditorBase::_update_interaction_profile).call_deferred(); } } diff --git a/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp b/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp index 08fcdaf771..51642d8503 100644 --- a/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp +++ b/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp @@ -77,7 +77,7 @@ void OpenXRSelectInteractionProfileDialog::open(PackedStringArray p_do_not_inclu // in with the new PackedStringArray interaction_profiles = OpenXRInteractionProfileMetadata::get_singleton()->get_interaction_profile_paths(); for (int i = 0; i < interaction_profiles.size(); i++) { - String path = interaction_profiles[i]; + const String &path = interaction_profiles[i]; if (!p_do_not_include.has(path)) { Button *ip_button = memnew(Button); ip_button->set_flat(true); diff --git a/modules/openxr/editor/openxr_select_runtime.cpp b/modules/openxr/editor/openxr_select_runtime.cpp new file mode 100644 index 0000000000..f6aa157907 --- /dev/null +++ b/modules/openxr/editor/openxr_select_runtime.cpp @@ -0,0 +1,132 @@ +/**************************************************************************/ +/* openxr_select_runtime.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_select_runtime.h" + +#include "core/io/dir_access.h" +#include "core/os/os.h" +#include "editor/editor_settings.h" +#include "editor/editor_string_names.h" + +void OpenXRSelectRuntime::_bind_methods() { +} + +void OpenXRSelectRuntime::_update_items() { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + OS *os = OS::get_singleton(); + Dictionary runtimes = EDITOR_GET("xr/openxr/runtime_paths"); + + int current_runtime = 0; + int index = 0; + String current_path = os->get_environment("XR_RUNTIME_JSON"); + + // Parse the user's home folder. + String home_folder = os->get_environment("HOME"); + if (home_folder.is_empty()) { + home_folder = os->get_environment("HOMEDRIVE") + os->get_environment("HOMEPATH"); + } + + clear(); + add_item("Default", -1); + set_item_metadata(index, ""); + index++; + + Array keys = runtimes.keys(); + for (int i = 0; i < keys.size(); i++) { + String key = keys[i]; + String path = runtimes[key]; + String adj_path = path.replace("~", home_folder); + + if (da->file_exists(adj_path)) { + add_item(key, index); + set_item_metadata(index, adj_path); + + if (current_path == adj_path) { + current_runtime = index; + } + index++; + } + } + + select(current_runtime); +} + +void OpenXRSelectRuntime::_item_selected(int p_which) { + OS *os = OS::get_singleton(); + + if (p_which == 0) { + // Return to default runtime + os->set_environment("XR_RUNTIME_JSON", ""); + } else { + // Select the runtime we want + String runtime_path = get_item_metadata(p_which); + os->set_environment("XR_RUNTIME_JSON", runtime_path); + } +} + +void OpenXRSelectRuntime::_notification(int p_notification) { + switch (p_notification) { + case NOTIFICATION_ENTER_TREE: { + // Update dropdown + _update_items(); + + // Connect signal + connect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected)); + } break; + case NOTIFICATION_EXIT_TREE: { + // Disconnect signal + disconnect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected)); + } break; + } +} + +OpenXRSelectRuntime::OpenXRSelectRuntime() { + Dictionary default_runtimes; + + // Add known common runtimes by default. +#ifdef WINDOWS_ENABLED + default_runtimes["Meta"] = "C:\\Program Files\\Oculus\\Support\\oculus-runtime\\oculus_openxr_64.json"; + default_runtimes["SteamVR"] = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\SteamVR\\steamxr_win64.json"; + default_runtimes["Varjo"] = "C:\\Program Files\\Varjo\\varjo-openxr\\VarjoOpenXR.json"; + default_runtimes["WMR"] = "C:\\WINDOWS\\system32\\MixedRealityRuntime.json"; +#endif +#ifdef LINUXBSD_ENABLED + default_runtimes["Monado"] = "/usr/share/openxr/1/openxr_monado.json"; + default_runtimes["SteamVR"] = "~/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json"; +#endif + + EDITOR_DEF_RST("xr/openxr/runtime_paths", default_runtimes); + + set_flat(true); + set_theme_type_variation("TopBarOptionButton"); + set_fit_to_longest_item(false); + set_focus_mode(Control::FOCUS_NONE); + set_tooltip_text(TTR("Choose an XR runtime.")); +} diff --git a/modules/openxr/editor/openxr_select_runtime.h b/modules/openxr/editor/openxr_select_runtime.h new file mode 100644 index 0000000000..60b5137f67 --- /dev/null +++ b/modules/openxr/editor/openxr_select_runtime.h @@ -0,0 +1,51 @@ +/**************************************************************************/ +/* openxr_select_runtime.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef OPENXR_SELECT_RUNTIME_H +#define OPENXR_SELECT_RUNTIME_H + +#include "scene/gui/option_button.h" + +class OpenXRSelectRuntime : public OptionButton { + GDCLASS(OpenXRSelectRuntime, OptionButton); + +public: + OpenXRSelectRuntime(); + +protected: + static void _bind_methods(); + void _notification(int p_notification); + +private: + void _update_items(); + void _item_selected(int p_which); +}; + +#endif // OPENXR_SELECT_RUNTIME_H diff --git a/modules/openxr/extensions/SCsub b/modules/openxr/extensions/SCsub new file mode 100644 index 0000000000..1bd9cfaa22 --- /dev/null +++ b/modules/openxr/extensions/SCsub @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +Import("env") +Import("env_openxr") + +module_obj = [] + +env_openxr.add_source_files(module_obj, "*.cpp") + +# These are platform dependent +if env["platform"] == "android": + env_openxr.add_source_files(module_obj, "platform/openxr_android_extension.cpp") +if env["vulkan"]: + env_openxr.add_source_files(module_obj, "platform/openxr_vulkan_extension.cpp") +if env["opengl3"] and env["platform"] != "macos": + env_openxr.add_source_files(module_obj, "platform/openxr_opengl_extension.cpp") + +env.modules_sources += module_obj diff --git a/modules/openxr/extensions/openxr_extension_wrapper.h b/modules/openxr/extensions/openxr_extension_wrapper.h index 31f8d23268..ad326472ab 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper.h +++ b/modules/openxr/extensions/openxr_extension_wrapper.h @@ -35,6 +35,7 @@ #include "core/math/projection.h" #include "core/templates/hash_map.h" #include "core/templates/rid.h" +#include "core/variant/variant.h" #include <openxr/openxr.h> @@ -60,6 +61,9 @@ public: virtual void *set_instance_create_info_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when we create our OpenXR instance. virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when we create our OpenXR session. virtual void *set_swapchain_create_info_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when creating OpenXR swap chains. + virtual void *set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) { return p_next_pointer; } + + virtual PackedStringArray get_suggested_tracker_names() { return PackedStringArray(); } // `on_register_metadata` allows extensions to register additional controller metadata. // This function is called even when OpenXRApi is not constructured as the metadata diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp index 4829f713d2..a9b62819b7 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp +++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp @@ -38,6 +38,9 @@ void OpenXRExtensionWrapperExtension::_bind_methods() { GDVIRTUAL_BIND(_set_instance_create_info_and_get_next_pointer, "next_pointer"); GDVIRTUAL_BIND(_set_session_create_and_get_next_pointer, "next_pointer"); GDVIRTUAL_BIND(_set_swapchain_create_info_and_get_next_pointer, "next_pointer"); + GDVIRTUAL_BIND(_set_hand_joint_locations_and_get_next_pointer, "hand_index", "next_pointer"); + GDVIRTUAL_BIND(_get_composition_layer); + GDVIRTUAL_BIND(_get_suggested_tracker_names); GDVIRTUAL_BIND(_on_register_metadata); GDVIRTUAL_BIND(_on_before_instance_created); GDVIRTUAL_BIND(_on_instance_created, "instance"); @@ -117,6 +120,36 @@ void *OpenXRExtensionWrapperExtension::set_swapchain_create_info_and_get_next_po return nullptr; } +void *OpenXRExtensionWrapperExtension::set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) { + uint64_t pointer; + + if (GDVIRTUAL_CALL(_set_hand_joint_locations_and_get_next_pointer, p_hand_index, GDExtensionPtr<void>(p_next_pointer), pointer)) { + return reinterpret_cast<void *>(pointer); + } + + return nullptr; +} + +PackedStringArray OpenXRExtensionWrapperExtension::get_suggested_tracker_names() { + PackedStringArray ret; + + if (GDVIRTUAL_CALL(_get_suggested_tracker_names, ret)) { + return ret; + } + + return PackedStringArray(); +} + +XrCompositionLayerBaseHeader *OpenXRExtensionWrapperExtension::get_composition_layer() { + uint64_t pointer; + + if (GDVIRTUAL_CALL(_get_composition_layer, pointer)) { + return reinterpret_cast<XrCompositionLayerBaseHeader *>(pointer); + } + + return nullptr; +} + void OpenXRExtensionWrapperExtension::on_register_metadata() { GDVIRTUAL_CALL(_on_register_metadata); } @@ -205,3 +238,6 @@ OpenXRExtensionWrapperExtension::OpenXRExtensionWrapperExtension() : Object(), OpenXRExtensionWrapper() { openxr_api.instantiate(); } + +OpenXRExtensionWrapperExtension::~OpenXRExtensionWrapperExtension() { +} diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.h b/modules/openxr/extensions/openxr_extension_wrapper_extension.h index 5c5e64f927..edcbc0139b 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper_extension.h +++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.h @@ -39,7 +39,7 @@ #include "core/os/thread_safe.h" #include "core/variant/native_ptr.h" -class OpenXRExtensionWrapperExtension : public Object, OpenXRExtensionWrapper { +class OpenXRExtensionWrapperExtension : public Object, public OpenXRExtensionWrapper, public OpenXRCompositionLayerProvider { GDCLASS(OpenXRExtensionWrapperExtension, Object); protected: @@ -58,12 +58,20 @@ public: virtual void *set_instance_create_info_and_get_next_pointer(void *p_next_pointer) override; virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer) override; virtual void *set_swapchain_create_info_and_get_next_pointer(void *p_next_pointer) override; + virtual void *set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) override; + virtual XrCompositionLayerBaseHeader *get_composition_layer() override; //TODO workaround as GDExtensionPtr<void> return type results in build error in godot-cpp GDVIRTUAL1R(uint64_t, _set_system_properties_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_instance_create_info_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_session_create_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_swapchain_create_info_and_get_next_pointer, GDExtensionPtr<void>); + GDVIRTUAL2R(uint64_t, _set_hand_joint_locations_and_get_next_pointer, int, GDExtensionPtr<void>); + GDVIRTUAL0R(uint64_t, _get_composition_layer); + + virtual PackedStringArray get_suggested_tracker_names() override; + + GDVIRTUAL0R(PackedStringArray, _get_suggested_tracker_names); virtual void on_register_metadata() override; virtual void on_before_instance_created() override; @@ -110,6 +118,7 @@ public: void register_extension_wrapper(); OpenXRExtensionWrapperExtension(); + virtual ~OpenXRExtensionWrapperExtension() override; }; #endif // OPENXR_EXTENSION_WRAPPER_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp b/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp index 59bdec5c8e..477a1c2609 100644 --- a/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp +++ b/modules/openxr/extensions/openxr_eye_gaze_interaction.cpp @@ -30,6 +30,7 @@ #include "openxr_eye_gaze_interaction.h" +#include "core/config/project_settings.h" #include "core/os/os.h" #include "../action_map/openxr_interaction_profile_metadata.h" @@ -52,7 +53,11 @@ OpenXREyeGazeInteractionExtension::~OpenXREyeGazeInteractionExtension() { HashMap<String, bool *> OpenXREyeGazeInteractionExtension::get_requested_extensions() { HashMap<String, bool *> request_extensions; - request_extensions[XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME] = &available; + // Only enable this extension when requested. + // We still register our meta data or the action map editor will fail. + if (GLOBAL_GET("xr/openxr/extensions/eye_gaze_interaction") && (!OS::get_singleton()->has_feature("mobile") || OS::get_singleton()->has_feature(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME))) { + request_extensions[XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME] = &available; + } return request_extensions; } @@ -69,6 +74,11 @@ void *OpenXREyeGazeInteractionExtension::set_system_properties_and_get_next_poin return &properties; } +PackedStringArray OpenXREyeGazeInteractionExtension::get_suggested_tracker_names() { + PackedStringArray arr = { "/user/eyes_ext" }; + return arr; +} + bool OpenXREyeGazeInteractionExtension::is_available() { return available; } diff --git a/modules/openxr/extensions/openxr_eye_gaze_interaction.h b/modules/openxr/extensions/openxr_eye_gaze_interaction.h index 704940ad26..2b99f8edff 100644 --- a/modules/openxr/extensions/openxr_eye_gaze_interaction.h +++ b/modules/openxr/extensions/openxr_eye_gaze_interaction.h @@ -43,6 +43,8 @@ public: virtual HashMap<String, bool *> get_requested_extensions() override; virtual void *set_system_properties_and_get_next_pointer(void *p_next_pointer) override; + PackedStringArray get_suggested_tracker_names() override; + bool is_available(); bool supports_eye_gaze_interaction(); diff --git a/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.cpp b/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.cpp deleted file mode 100644 index 3da0ffd9c7..0000000000 --- a/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.cpp +++ /dev/null @@ -1,246 +0,0 @@ -/**************************************************************************/ -/* openxr_fb_passthrough_extension_wrapper.cpp */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#include "openxr_fb_passthrough_extension_wrapper.h" - -#include "core/os/os.h" -#include "scene/main/viewport.h" -#include "scene/main/window.h" - -using namespace godot; - -OpenXRFbPassthroughExtensionWrapper *OpenXRFbPassthroughExtensionWrapper::singleton = nullptr; - -OpenXRFbPassthroughExtensionWrapper *OpenXRFbPassthroughExtensionWrapper::get_singleton() { - return singleton; -} - -OpenXRFbPassthroughExtensionWrapper::OpenXRFbPassthroughExtensionWrapper() { - singleton = this; -} - -OpenXRFbPassthroughExtensionWrapper::~OpenXRFbPassthroughExtensionWrapper() { - cleanup(); -} - -HashMap<String, bool *> OpenXRFbPassthroughExtensionWrapper::get_requested_extensions() { - HashMap<String, bool *> request_extensions; - - request_extensions[XR_FB_PASSTHROUGH_EXTENSION_NAME] = &fb_passthrough_ext; - request_extensions[XR_FB_TRIANGLE_MESH_EXTENSION_NAME] = &fb_triangle_mesh_ext; - - return request_extensions; -} - -void OpenXRFbPassthroughExtensionWrapper::cleanup() { - fb_passthrough_ext = false; - fb_triangle_mesh_ext = false; -} - -Viewport *OpenXRFbPassthroughExtensionWrapper::get_main_viewport() { - MainLoop *main_loop = OS::get_singleton()->get_main_loop(); - if (!main_loop) { - print_error("Unable to retrieve main loop"); - return nullptr; - } - - SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop); - if (!scene_tree) { - print_error("Unable to retrieve scene tree"); - return nullptr; - } - - Viewport *viewport = scene_tree->get_root()->get_viewport(); - return viewport; -} - -void OpenXRFbPassthroughExtensionWrapper::on_instance_created(const XrInstance instance) { - if (fb_passthrough_ext) { - bool result = initialize_fb_passthrough_extension(instance); - if (!result) { - print_error("Failed to initialize fb_passthrough extension"); - fb_passthrough_ext = false; - } - } - - if (fb_triangle_mesh_ext) { - bool result = initialize_fb_triangle_mesh_extension(instance); - if (!result) { - print_error("Failed to initialize fb_triangle_mesh extension"); - fb_triangle_mesh_ext = false; - } - } - - if (fb_passthrough_ext) { - OpenXRAPI::get_singleton()->register_composition_layer_provider(this); - } -} - -bool OpenXRFbPassthroughExtensionWrapper::is_passthrough_enabled() { - return fb_passthrough_ext && passthrough_handle != XR_NULL_HANDLE && passthrough_layer != XR_NULL_HANDLE; -} - -bool OpenXRFbPassthroughExtensionWrapper::start_passthrough() { - if (passthrough_handle == XR_NULL_HANDLE) { - return false; - } - - if (is_passthrough_enabled()) { - return true; - } - - // Start the passthrough feature - XrResult result = xrPassthroughStartFB(passthrough_handle); - if (!is_valid_passthrough_result(result, "Failed to start passthrough")) { - stop_passthrough(); - return false; - } - - // Create the passthrough layer - XrPassthroughLayerCreateInfoFB passthrough_layer_config = { - XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB, - nullptr, - passthrough_handle, - XR_PASSTHROUGH_IS_RUNNING_AT_CREATION_BIT_FB, - XR_PASSTHROUGH_LAYER_PURPOSE_RECONSTRUCTION_FB, - }; - result = xrCreatePassthroughLayerFB(OpenXRAPI::get_singleton()->get_session(), &passthrough_layer_config, &passthrough_layer); - if (!is_valid_passthrough_result(result, "Failed to create the passthrough layer")) { - stop_passthrough(); - return false; - } - - // Check if the the viewport has transparent background - Viewport *viewport = get_main_viewport(); - if (viewport && !viewport->has_transparent_background()) { - print_error("Main viewport doesn't have transparent background! Passthrough may not properly render."); - } - - return true; -} - -void OpenXRFbPassthroughExtensionWrapper::on_session_created(const XrSession session) { - if (fb_passthrough_ext) { - // Create the passthrough feature and start it. - XrPassthroughCreateInfoFB passthrough_create_info = { - XR_TYPE_PASSTHROUGH_CREATE_INFO_FB, - nullptr, - 0, - }; - - XrResult result = xrCreatePassthroughFB(OpenXRAPI::get_singleton()->get_session(), &passthrough_create_info, &passthrough_handle); - if (!OpenXRAPI::get_singleton()->xr_result(result, "Failed to create passthrough")) { - passthrough_handle = XR_NULL_HANDLE; - return; - } - } -} - -XrCompositionLayerBaseHeader *OpenXRFbPassthroughExtensionWrapper::get_composition_layer() { - if (is_passthrough_enabled()) { - composition_passthrough_layer.layerHandle = passthrough_layer; - return (XrCompositionLayerBaseHeader *)&composition_passthrough_layer; - } else { - return nullptr; - } -} - -void OpenXRFbPassthroughExtensionWrapper::stop_passthrough() { - if (!fb_passthrough_ext) { - return; - } - - XrResult result; - if (passthrough_layer != XR_NULL_HANDLE) { - // Destroy the layer - result = xrDestroyPassthroughLayerFB(passthrough_layer); - OpenXRAPI::get_singleton()->xr_result(result, "Unable to destroy passthrough layer"); - passthrough_layer = XR_NULL_HANDLE; - } - - if (passthrough_handle != XR_NULL_HANDLE) { - result = xrPassthroughPauseFB(passthrough_handle); - OpenXRAPI::get_singleton()->xr_result(result, "Unable to stop passthrough feature"); - } -} - -void OpenXRFbPassthroughExtensionWrapper::on_session_destroyed() { - if (fb_passthrough_ext) { - stop_passthrough(); - - XrResult result; - if (passthrough_handle != XR_NULL_HANDLE) { - result = xrDestroyPassthroughFB(passthrough_handle); - OpenXRAPI::get_singleton()->xr_result(result, "Unable to destroy passthrough feature"); - passthrough_handle = XR_NULL_HANDLE; - } - } -} - -void OpenXRFbPassthroughExtensionWrapper::on_instance_destroyed() { - if (fb_passthrough_ext) { - OpenXRAPI::get_singleton()->unregister_composition_layer_provider(this); - } - cleanup(); -} - -bool OpenXRFbPassthroughExtensionWrapper::initialize_fb_passthrough_extension(const XrInstance p_instance) { - ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), false); - - EXT_INIT_XR_FUNC_V(xrCreatePassthroughFB); - EXT_INIT_XR_FUNC_V(xrDestroyPassthroughFB); - EXT_INIT_XR_FUNC_V(xrPassthroughStartFB); - EXT_INIT_XR_FUNC_V(xrPassthroughPauseFB); - EXT_INIT_XR_FUNC_V(xrCreatePassthroughLayerFB); - EXT_INIT_XR_FUNC_V(xrDestroyPassthroughLayerFB); - EXT_INIT_XR_FUNC_V(xrPassthroughLayerPauseFB); - EXT_INIT_XR_FUNC_V(xrPassthroughLayerResumeFB); - EXT_INIT_XR_FUNC_V(xrPassthroughLayerSetStyleFB); - EXT_INIT_XR_FUNC_V(xrCreateGeometryInstanceFB); - EXT_INIT_XR_FUNC_V(xrDestroyGeometryInstanceFB); - EXT_INIT_XR_FUNC_V(xrGeometryInstanceSetTransformFB); - - return true; -} - -bool OpenXRFbPassthroughExtensionWrapper::initialize_fb_triangle_mesh_extension(const XrInstance p_instance) { - ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), false); - - EXT_INIT_XR_FUNC_V(xrCreateTriangleMeshFB); - EXT_INIT_XR_FUNC_V(xrDestroyTriangleMeshFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshGetVertexBufferFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshGetIndexBufferFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshBeginUpdateFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshEndUpdateFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshBeginVertexBufferUpdateFB); - EXT_INIT_XR_FUNC_V(xrTriangleMeshEndVertexBufferUpdateFB); - - return true; -} diff --git a/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.h b/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.h deleted file mode 100644 index 045e424202..0000000000 --- a/modules/openxr/extensions/openxr_fb_passthrough_extension_wrapper.h +++ /dev/null @@ -1,231 +0,0 @@ -/**************************************************************************/ -/* openxr_fb_passthrough_extension_wrapper.h */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#ifndef OPENXR_FB_PASSTHROUGH_EXTENSION_WRAPPER_H -#define OPENXR_FB_PASSTHROUGH_EXTENSION_WRAPPER_H - -#include "../openxr_api.h" -#include "../util.h" -#include "openxr_composition_layer_provider.h" -#include "openxr_extension_wrapper.h" - -#include <map> - -class Viewport; - -// Wrapper for the set of Facebook XR passthrough extensions. -class OpenXRFbPassthroughExtensionWrapper : public OpenXRExtensionWrapper, public OpenXRCompositionLayerProvider { -public: - OpenXRFbPassthroughExtensionWrapper(); - ~OpenXRFbPassthroughExtensionWrapper(); - - virtual HashMap<String, bool *> get_requested_extensions() override; - - void on_instance_created(const XrInstance instance) override; - - void on_session_created(const XrSession session) override; - - void on_session_destroyed() override; - - void on_instance_destroyed() override; - - XrCompositionLayerBaseHeader *get_composition_layer() override; - - bool is_passthrough_supported() { - return fb_passthrough_ext; - } - - bool is_passthrough_enabled(); - - bool start_passthrough(); - - void stop_passthrough(); - - static OpenXRFbPassthroughExtensionWrapper *get_singleton(); - -private: - // Create a passthrough feature - EXT_PROTO_XRRESULT_FUNC3(xrCreatePassthroughFB, - (XrSession), session, - (const XrPassthroughCreateInfoFB *), create_info, - (XrPassthroughFB *), feature_out) - - // Destroy a previously created passthrough feature - EXT_PROTO_XRRESULT_FUNC1(xrDestroyPassthroughFB, (XrPassthroughFB), feature) - - //*** Passthrough feature state management functions ********* - // Start the passthrough feature - EXT_PROTO_XRRESULT_FUNC1(xrPassthroughStartFB, (XrPassthroughFB), passthrough) - // Pause the passthrough feature - EXT_PROTO_XRRESULT_FUNC1(xrPassthroughPauseFB, (XrPassthroughFB), passthrough) - - EXT_PROTO_XRRESULT_FUNC3(xrCreatePassthroughLayerFB, (XrSession), session, - (const XrPassthroughLayerCreateInfoFB *), config, - (XrPassthroughLayerFB *), layer_out) - - EXT_PROTO_XRRESULT_FUNC1(xrDestroyPassthroughLayerFB, (XrPassthroughLayerFB), layer) - - EXT_PROTO_XRRESULT_FUNC1(xrPassthroughLayerPauseFB, (XrPassthroughLayerFB), layer) - EXT_PROTO_XRRESULT_FUNC1(xrPassthroughLayerResumeFB, (XrPassthroughLayerFB), layer) - - // Set the style of an existing passthrough layer. If the enabled feature set - // doesn’t change, this is a lightweight operation that can be called in every - // frame to animate the style. Changes that may incur a bigger cost: - // - Enabling/disabling the color mapping, or changing the type of mapping - // (monochromatic to RGBA or back). - // - Changing `textureOpacityFactor` from 0 to non-zero or vice versa - // - Changing `edgeColor[3]` from 0 to non-zero or vice versa - // NOTE: For XR_FB_passthrough, all color values are treated as linear. - EXT_PROTO_XRRESULT_FUNC2(xrPassthroughLayerSetStyleFB, - (XrPassthroughLayerFB), layer, - (const XrPassthroughStyleFB *), style) - - // Create a geometry instance to be used as a projection surface for passthrough. - // A geometry instance assigns a triangle mesh as part of the specified layer's - // projection surface. - // The operation is only valid if the passthrough layer's purpose has been set to - // `XR_PASSTHROUGH_LAYER_PURPOSE_PROJECTED_FB`. Otherwise, the call this function will - // result in an error. In the specified layer, Passthrough will be visible where the view - // is covered by the user-specified geometries. - // - // A triangle mesh object can be instantiated multiple times - in the same or different layers' - // projection surface. Each instantiation has its own transformation, which - // can be updated using `xrGeometryInstanceSetTransformFB`. - EXT_PROTO_XRRESULT_FUNC3(xrCreateGeometryInstanceFB, - (XrSession), session, - (const XrGeometryInstanceCreateInfoFB *), create_info, - (XrGeometryInstanceFB *), out_geometry_instance) - - // Destroys a previously created geometry instance from passthrough rendering. - // This removes the geometry instance from passthrough rendering. - // The operation has no effect on other instances or the underlying mesh. - EXT_PROTO_XRRESULT_FUNC1(xrDestroyGeometryInstanceFB, (XrGeometryInstanceFB), instance) - - // Update the transformation of a passthrough geometry instance. - EXT_PROTO_XRRESULT_FUNC2(xrGeometryInstanceSetTransformFB, - (XrGeometryInstanceFB), instance, - (const XrGeometryInstanceTransformFB *), transformation) - - // Create a triangle mesh geometry object. - // Depending on the behavior flags, the mesh could be created immutable (data is assigned - // at creation and cannot be changed) or mutable (the mesh is created empty and can be updated - // by calling begin/end update functions). - EXT_PROTO_XRRESULT_FUNC3(xrCreateTriangleMeshFB, - (XrSession), session, - (const XrTriangleMeshCreateInfoFB *), create_info, - (XrTriangleMeshFB *), out_triangle_mesh) - - // Destroy an `XrTriangleMeshFB` object along with its data. The mesh buffers must not be - // accessed anymore after their parent mesh object has been destroyed. - EXT_PROTO_XRRESULT_FUNC1(xrDestroyTriangleMeshFB, (XrTriangleMeshFB), mesh) - - // Retrieve a pointer to the vertex buffer. The vertex buffer is structured as an array of 3 floats - // per vertex representing x, y, and z: `[x0, y0, z0, x1, y1, z1, ...]`. The size of the buffer is - // `maxVertexCount * 3` floats. The application must call `xrTriangleMeshBeginUpdateFB` or - // `xrTriangleMeshBeginVertexBufferUpdateFB` before making modifications to the vertex - // buffer. The buffer location is guaranteed to remain constant over the lifecycle of the mesh - // object. - EXT_PROTO_XRRESULT_FUNC2(xrTriangleMeshGetVertexBufferFB, - (XrTriangleMeshFB), mesh, - (XrVector3f **), out_vertex_buffer) - - // Retrieve the index buffer that defines the topology of the triangle mesh. Each triplet of - // consecutive elements point to three vertices in the vertex buffer and thus form a triangle. The - // size of each element is `indexElementSize` bytes, and thus the size of the buffer is - // `maxTriangleCount * 3 * indexElementSize` bytes. The application must call - // `xrTriangleMeshBeginUpdateFB` before making modifications to the index buffer. The buffer - // location is guaranteed to remain constant over the lifecycle of the mesh object. - EXT_PROTO_XRRESULT_FUNC2(xrTriangleMeshGetIndexBufferFB, - (XrTriangleMeshFB), mesh, - (uint32_t **), out_index_buffer) - - // Begin updating the mesh buffer data. The application must call this function before it makes any - // modifications to the buffers retrieved by `xrTriangleMeshGetVertexBufferFB` and - // `xrTriangleMeshGetIndexBufferFB`. If only the vertex buffer needs to be updated, - // `xrTriangleMeshBeginVertexBufferUpdateFB` can be used instead. To commit the - // modifications, the application must call `xrTriangleMeshEndUpdateFB`. - EXT_PROTO_XRRESULT_FUNC1(xrTriangleMeshBeginUpdateFB, (XrTriangleMeshFB), mesh) - - // Signal the API that the application has finished updating the mesh buffers after a call to - // `xrTriangleMeshBeginUpdateFB`. `vertexCount` and `triangleCount` specify the actual - // number of primitives that make up the mesh after the update. They must be larger than zero but - // smaller or equal to the maximum counts defined at create time. Buffer data beyond these counts - // is ignored. - EXT_PROTO_XRRESULT_FUNC3(xrTriangleMeshEndUpdateFB, - (XrTriangleMeshFB), mesh, - (uint32_t), vertexCount, - (uint32_t), triangle_count) - - // Update the vertex positions of a triangle mesh. Can only be called once the mesh topology has - // been set using `xrTriangleMeshBeginUpdateFB`/`xrTriangleMeshEndUpdateFB`. The - // vertex count is defined by the last invocation to `xrTriangleMeshEndUpdateFB`. Once the - // modification is done, `xrTriangleMeshEndVertexBufferUpdateFB` must be called. - EXT_PROTO_XRRESULT_FUNC2(xrTriangleMeshBeginVertexBufferUpdateFB, - (XrTriangleMeshFB), mesh, - (uint32_t *), out_vertex_count) - - // Signal the API that the contents of the vertex buffer data has been updated - // after a call to `xrTriangleMeshBeginVertexBufferUpdateFB`. - EXT_PROTO_XRRESULT_FUNC1(xrTriangleMeshEndVertexBufferUpdateFB, (XrTriangleMeshFB), mesh) - - bool initialize_fb_passthrough_extension(const XrInstance instance); - - bool initialize_fb_triangle_mesh_extension(const XrInstance instance); - - void cleanup(); - - // TODO: Temporary workaround (https://github.com/GodotVR/godot_openxr/issues/138) - // Address a bug in the passthrough api where XR_ERROR_UNEXPECTED_STATE_PASSTHROUGH_FB is - // returned even when the operation is valid on Meta Quest devices. - // The issue should be addressed on that platform in OS release v37. - inline bool is_valid_passthrough_result(XrResult result, const char *format) { - return OpenXRAPI::get_singleton()->xr_result(result, format) || result == XR_ERROR_UNEXPECTED_STATE_PASSTHROUGH_FB; - } - - Viewport *get_main_viewport(); - - static OpenXRFbPassthroughExtensionWrapper *singleton; - - bool fb_passthrough_ext = false; // required for any passthrough functionality - bool fb_triangle_mesh_ext = false; // only use for projected passthrough - - XrPassthroughFB passthrough_handle = XR_NULL_HANDLE; - XrPassthroughLayerFB passthrough_layer = XR_NULL_HANDLE; - - XrCompositionLayerPassthroughFB composition_passthrough_layer = { - XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB, - nullptr, - XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT, - XR_NULL_HANDLE, - XR_NULL_HANDLE, - }; -}; - -#endif // OPENXR_FB_PASSTHROUGH_EXTENSION_WRAPPER_H diff --git a/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp b/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp index 1289183ea4..c3f692185b 100644 --- a/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp +++ b/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp @@ -30,7 +30,7 @@ #include "openxr_fb_update_swapchain_extension.h" -// always include this as late as possible +// Always include this as late as possible. #include "../openxr_platform_inc.h" OpenXRFBUpdateSwapchainExtension *OpenXRFBUpdateSwapchainExtension::singleton = nullptr; diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp index 0d667b56c6..b3c20ef8b9 100644 --- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp +++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp @@ -60,6 +60,7 @@ HashMap<String, bool *> OpenXRHandTrackingExtension::get_requested_extensions() request_extensions[XR_EXT_HAND_TRACKING_EXTENSION_NAME] = &hand_tracking_ext; request_extensions[XR_EXT_HAND_JOINTS_MOTION_RANGE_EXTENSION_NAME] = &hand_motion_range_ext; + request_extensions[XR_EXT_HAND_TRACKING_DATA_SOURCE_EXTENSION_NAME] = &hand_tracking_source_ext; return request_extensions; } @@ -137,20 +138,32 @@ void OpenXRHandTrackingExtension::on_process() { for (int i = 0; i < OPENXR_MAX_TRACKED_HANDS; i++) { if (hand_trackers[i].hand_tracker == XR_NULL_HANDLE) { - XrHandTrackerCreateInfoEXT createInfo = { + void *next_pointer = nullptr; + + // Originally not all XR runtimes supported hand tracking data sourced both from controllers and normal hand tracking. + // With this extension we can indicate we accept input from both sources so hand tracking data is consistently provided + // on runtimes that support this. + XrHandTrackingDataSourceEXT data_sources[2] = { XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT, XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT }; + XrHandTrackingDataSourceInfoEXT data_source_info = { XR_TYPE_HAND_TRACKING_DATA_SOURCE_INFO_EXT, next_pointer, 2, data_sources }; + if (hand_tracking_source_ext) { + // If supported include this info + next_pointer = &data_source_info; + } + + XrHandTrackerCreateInfoEXT create_info = { XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, // type - nullptr, // next + next_pointer, // next i == 0 ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT, // hand XR_HAND_JOINT_SET_DEFAULT_EXT, // handJointSet }; - result = xrCreateHandTrackerEXT(OpenXRAPI::get_singleton()->get_session(), &createInfo, &hand_trackers[i].hand_tracker); + result = xrCreateHandTrackerEXT(OpenXRAPI::get_singleton()->get_session(), &create_info, &hand_trackers[i].hand_tracker); if (XR_FAILED(result)) { // not successful? then we do nothing. print_line("OpenXR: Failed to obtain hand tracking information [", OpenXRAPI::get_singleton()->get_error_string(result), "]"); hand_trackers[i].is_initialized = false; } else { - void *next_pointer = nullptr; + next_pointer = nullptr; hand_trackers[i].velocities.type = XR_TYPE_HAND_JOINT_VELOCITIES_EXT; hand_trackers[i].velocities.next = next_pointer; @@ -158,27 +171,45 @@ void OpenXRHandTrackingExtension::on_process() { hand_trackers[i].velocities.jointVelocities = hand_trackers[i].joint_velocities; next_pointer = &hand_trackers[i].velocities; + if (hand_tracking_source_ext) { + hand_trackers[i].data_source.type = XR_TYPE_HAND_TRACKING_DATA_SOURCE_STATE_EXT; + hand_trackers[i].data_source.next = next_pointer; + hand_trackers[i].data_source.isActive = false; + hand_trackers[i].data_source.dataSource = XrHandTrackingDataSourceEXT(0); + next_pointer = &hand_trackers[i].data_source; + } + + // Needed for vendor hand tracking extensions implemented from GDExtension. + for (OpenXRExtensionWrapper *wrapper : OpenXRAPI::get_singleton()->get_registered_extension_wrappers()) { + void *np = wrapper->set_hand_joint_locations_and_get_next_pointer(i, next_pointer); + if (np != nullptr) { + next_pointer = np; + } + } + hand_trackers[i].locations.type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT; hand_trackers[i].locations.next = next_pointer; hand_trackers[i].locations.isActive = false; hand_trackers[i].locations.jointCount = XR_HAND_JOINT_COUNT_EXT; hand_trackers[i].locations.jointLocations = hand_trackers[i].joint_locations; + Ref<XRHandTracker> godot_tracker; + godot_tracker.instantiate(); + godot_tracker->set_hand(i == 0 ? XRHandTracker::HAND_LEFT : XRHandTracker::HAND_RIGHT); + XRServer::get_singleton()->add_hand_tracker(i == 0 ? "/user/left" : "/user/right", godot_tracker); + hand_trackers[i].godot_tracker = godot_tracker; + hand_trackers[i].is_initialized = true; } } if (hand_trackers[i].is_initialized) { + Ref<XRHandTracker> godot_tracker = hand_trackers[i].godot_tracker; void *next_pointer = nullptr; - XrHandJointsMotionRangeInfoEXT motionRangeInfo; - + XrHandJointsMotionRangeInfoEXT motion_range_info = { XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT, next_pointer, hand_trackers[i].motion_range }; if (hand_motion_range_ext) { - motionRangeInfo.type = XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT; - motionRangeInfo.next = next_pointer; - motionRangeInfo.handJointsMotionRange = hand_trackers[i].motion_range; - - next_pointer = &motionRangeInfo; + next_pointer = &motion_range_info; } XrHandJointsLocateInfoEXT locateInfo = { @@ -192,6 +223,7 @@ void OpenXRHandTrackingExtension::on_process() { if (XR_FAILED(result)) { // not successful? then we do nothing. print_line("OpenXR: Failed to get tracking for hand", i, "[", OpenXRAPI::get_singleton()->get_error_string(result), "]"); + godot_tracker->set_has_tracking_data(false); continue; } @@ -201,6 +233,64 @@ void OpenXRHandTrackingExtension::on_process() { !hand_trackers[i].locations.isActive || isnan(palm.position.x) || palm.position.x < -1000000.00 || palm.position.x > 1000000.00) { hand_trackers[i].locations.isActive = false; // workaround, make sure its inactive } + + if (hand_trackers[i].locations.isActive) { + godot_tracker->set_has_tracking_data(true); + + // SKELETON_RIG_HUMANOID bone adjustment. This rotation performs: + // OpenXR Z+ -> Godot Humanoid Y- (Back along the bone) + // OpenXR Y+ -> Godot Humanoid Z- (Out the back of the hand) + const Quaternion bone_adjustment(0.0, -Math_SQRT12, Math_SQRT12, 0.0); + + for (int joint = 0; joint < XR_HAND_JOINT_COUNT_EXT; joint++) { + const XrHandJointLocationEXT &location = hand_trackers[i].joint_locations[joint]; + const XrHandJointVelocityEXT &velocity = hand_trackers[i].joint_velocities[joint]; + const XrHandTrackingDataSourceStateEXT &data_source = hand_trackers[i].data_source; + const XrPosef &pose = location.pose; + + Transform3D transform; + BitField<XRHandTracker::HandJointFlags> flags; + + if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { + if (pose.orientation.x != 0 || pose.orientation.y != 0 || pose.orientation.z != 0 || pose.orientation.w != 0) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_VALID); + transform.basis = Basis(Quaternion(pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w) * bone_adjustment); + } + } + if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_POSITION_VALID); + transform.origin = Vector3(pose.position.x, pose.position.y, pose.position.z); + } + if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_TRACKED); + } + if (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_POSITION_TRACKED); + } + if (location.locationFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_LINEAR_VELOCITY_VALID); + godot_tracker->set_hand_joint_linear_velocity((XRHandTracker::HandJoint)joint, Vector3(velocity.linearVelocity.x, velocity.linearVelocity.y, velocity.linearVelocity.z)); + } + if (location.locationFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT) { + flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ANGULAR_VELOCITY_VALID); + godot_tracker->set_hand_joint_angular_velocity((XRHandTracker::HandJoint)joint, Vector3(velocity.angularVelocity.x, velocity.angularVelocity.y, velocity.angularVelocity.z)); + } + + godot_tracker->set_hand_joint_flags((XRHandTracker::HandJoint)joint, flags); + godot_tracker->set_hand_joint_transform((XRHandTracker::HandJoint)joint, transform); + godot_tracker->set_hand_joint_radius((XRHandTracker::HandJoint)joint, location.radius); + + XRHandTracker::HandTrackingSource source = XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN; + if (data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT) { + source = XRHandTracker::HAND_TRACKING_SOURCE_UNOBSTRUCTED; + } else if (data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT) { + source = XRHandTracker::HAND_TRACKING_SOURCE_CONTROLLER; + } + godot_tracker->set_hand_tracking_source(source); + } + } else { + godot_tracker->set_has_tracking_data(false); + } } } } @@ -220,6 +310,8 @@ void OpenXRHandTrackingExtension::cleanup_hand_tracking() { hand_trackers[i].is_initialized = false; hand_trackers[i].hand_tracker = XR_NULL_HANDLE; + + XRServer::get_singleton()->remove_hand_tracker(i == 0 ? "/user/left" : "/user/right"); } } } @@ -240,6 +332,25 @@ XrHandJointsMotionRangeEXT OpenXRHandTrackingExtension::get_motion_range(HandTra return hand_trackers[p_hand].motion_range; } +OpenXRHandTrackingExtension::HandTrackedSource OpenXRHandTrackingExtension::get_hand_tracking_source(HandTrackedHands p_hand) const { + ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, OPENXR_SOURCE_UNKNOWN); + + if (hand_tracking_source_ext && hand_trackers[p_hand].data_source.isActive) { + switch (hand_trackers[p_hand].data_source.dataSource) { + case XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT: + return OPENXR_SOURCE_UNOBSTRUCTED; + + case XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT: + return OPENXR_SOURCE_CONTROLLER; + + default: + return OPENXR_SOURCE_UNKNOWN; + } + } + + return OPENXR_SOURCE_UNKNOWN; +} + void OpenXRHandTrackingExtension::set_motion_range(HandTrackedHands p_hand, XrHandJointsMotionRangeEXT p_motion_range) { ERR_FAIL_UNSIGNED_INDEX(p_hand, OPENXR_MAX_TRACKED_HANDS); hand_trackers[p_hand].motion_range = p_motion_range; diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.h b/modules/openxr/extensions/openxr_hand_tracking_extension.h index f9b26fd604..f709bc05c2 100644 --- a/modules/openxr/extensions/openxr_hand_tracking_extension.h +++ b/modules/openxr/extensions/openxr_hand_tracking_extension.h @@ -34,6 +34,7 @@ #include "../util.h" #include "core/math/quaternion.h" #include "openxr_extension_wrapper.h" +#include "servers/xr/xr_hand_tracker.h" class OpenXRHandTrackingExtension : public OpenXRExtensionWrapper { public: @@ -43,9 +44,18 @@ public: OPENXR_MAX_TRACKED_HANDS }; + enum HandTrackedSource { + OPENXR_SOURCE_UNKNOWN, + OPENXR_SOURCE_UNOBSTRUCTED, + OPENXR_SOURCE_CONTROLLER, + OPENXR_SOURCE_MAX + }; + struct HandTracker { bool is_initialized = false; + Ref<XRHandTracker> godot_tracker; XrHandJointsMotionRangeEXT motion_range = XR_HAND_JOINTS_MOTION_RANGE_UNOBSTRUCTED_EXT; + HandTrackedSource source = OPENXR_SOURCE_UNKNOWN; XrHandTrackerEXT hand_tracker = XR_NULL_HANDLE; XrHandJointLocationEXT joint_locations[XR_HAND_JOINT_COUNT_EXT]; @@ -53,6 +63,7 @@ public: XrHandJointVelocitiesEXT velocities; XrHandJointLocationsEXT locations; + XrHandTrackingDataSourceStateEXT data_source; }; static OpenXRHandTrackingExtension *get_singleton(); @@ -77,6 +88,8 @@ public: XrHandJointsMotionRangeEXT get_motion_range(HandTrackedHands p_hand) const; void set_motion_range(HandTrackedHands p_hand, XrHandJointsMotionRangeEXT p_motion_range); + HandTrackedSource get_hand_tracking_source(HandTrackedHands p_hand) const; + XrSpaceLocationFlags get_hand_joint_location_flags(HandTrackedHands p_hand, XrHandJointEXT p_joint) const; Quaternion get_hand_joint_rotation(HandTrackedHands p_hand, XrHandJointEXT p_joint) const; Vector3 get_hand_joint_position(HandTrackedHands p_hand, XrHandJointEXT p_joint) const; @@ -96,6 +109,7 @@ private: // related extensions bool hand_tracking_ext = false; bool hand_motion_range_ext = false; + bool hand_tracking_source_ext = false; // functions void cleanup_hand_tracking(); diff --git a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp index 8b8c6c5353..bb60f7adef 100644 --- a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp +++ b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.cpp @@ -42,6 +42,25 @@ HashMap<String, bool *> OpenXRHTCViveTrackerExtension::get_requested_extensions( return request_extensions; } +PackedStringArray OpenXRHTCViveTrackerExtension::get_suggested_tracker_names() { + PackedStringArray arr = { + "/user/vive_tracker_htcx/role/handheld_object", + "/user/vive_tracker_htcx/role/left_foot", + "/user/vive_tracker_htcx/role/right_foot", + "/user/vive_tracker_htcx/role/left_shoulder", + "/user/vive_tracker_htcx/role/right_shoulder", + "/user/vive_tracker_htcx/role/left_elbow", + "/user/vive_tracker_htcx/role/right_elbow", + "/user/vive_tracker_htcx/role/left_knee", + "/user/vive_tracker_htcx/role/right_knee", + "/user/vive_tracker_htcx/role/waist", + "/user/vive_tracker_htcx/role/chest", + "/user/vive_tracker_htcx/role/camera", + "/user/vive_tracker_htcx/role/keyboard", + }; + return arr; +} + bool OpenXRHTCViveTrackerExtension::is_available() { return available; } diff --git a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.h b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.h index b51398fd4e..e9c3d338ab 100644 --- a/modules/openxr/extensions/openxr_htc_vive_tracker_extension.h +++ b/modules/openxr/extensions/openxr_htc_vive_tracker_extension.h @@ -37,6 +37,8 @@ class OpenXRHTCViveTrackerExtension : public OpenXRExtensionWrapper { public: virtual HashMap<String, bool *> get_requested_extensions() override; + PackedStringArray get_suggested_tracker_names() override; + bool is_available(); virtual void on_register_metadata() override; diff --git a/modules/openxr/extensions/openxr_local_floor_extension.cpp b/modules/openxr/extensions/openxr_local_floor_extension.cpp new file mode 100644 index 0000000000..8e06dd8ed5 --- /dev/null +++ b/modules/openxr/extensions/openxr_local_floor_extension.cpp @@ -0,0 +1,59 @@ +/**************************************************************************/ +/* openxr_local_floor_extension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_local_floor_extension.h" + +#include "core/string/print_string.h" + +OpenXRLocalFloorExtension *OpenXRLocalFloorExtension::singleton = nullptr; + +OpenXRLocalFloorExtension *OpenXRLocalFloorExtension::get_singleton() { + return singleton; +} + +OpenXRLocalFloorExtension::OpenXRLocalFloorExtension() { + singleton = this; +} + +OpenXRLocalFloorExtension::~OpenXRLocalFloorExtension() { + singleton = nullptr; +} + +HashMap<String, bool *> OpenXRLocalFloorExtension::get_requested_extensions() { + HashMap<String, bool *> request_extensions; + + request_extensions[XR_EXT_LOCAL_FLOOR_EXTENSION_NAME] = &available; + + return request_extensions; +} + +bool OpenXRLocalFloorExtension::is_available() { + return available; +} diff --git a/modules/openxr/extensions/openxr_local_floor_extension.h b/modules/openxr/extensions/openxr_local_floor_extension.h new file mode 100644 index 0000000000..dff97d9954 --- /dev/null +++ b/modules/openxr/extensions/openxr_local_floor_extension.h @@ -0,0 +1,53 @@ +/**************************************************************************/ +/* openxr_local_floor_extension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef OPENXR_LOCAL_FLOOR_EXTENSION_H +#define OPENXR_LOCAL_FLOOR_EXTENSION_H + +#include "openxr_extension_wrapper.h" + +class OpenXRLocalFloorExtension : public OpenXRExtensionWrapper { +public: + static OpenXRLocalFloorExtension *get_singleton(); + + OpenXRLocalFloorExtension(); + virtual ~OpenXRLocalFloorExtension() override; + + virtual HashMap<String, bool *> get_requested_extensions() override; + + bool is_available(); + +private: + static OpenXRLocalFloorExtension *singleton; + + bool available = false; +}; + +#endif // OPENXR_LOCAL_FLOOR_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_meta_controller_extension.cpp b/modules/openxr/extensions/openxr_meta_controller_extension.cpp new file mode 100644 index 0000000000..72dc74bf64 --- /dev/null +++ b/modules/openxr/extensions/openxr_meta_controller_extension.cpp @@ -0,0 +1,184 @@ +/**************************************************************************/ +/* openxr_meta_controller_extension.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "openxr_meta_controller_extension.h" + +#include "../action_map/openxr_interaction_profile_metadata.h" + +HashMap<String, bool *> OpenXRMetaControllerExtension::get_requested_extensions() { + HashMap<String, bool *> request_extensions; + + request_extensions[XR_FB_TOUCH_CONTROLLER_PROXIMITY_EXTENSION_NAME] = &available[META_TOUCH_PROXIMITY]; + request_extensions[XR_FB_TOUCH_CONTROLLER_PRO_EXTENSION_NAME] = &available[META_TOUCH_PRO]; + request_extensions[XR_META_TOUCH_CONTROLLER_PLUS_EXTENSION_NAME] = &available[META_TOUCH_PLUS]; + + return request_extensions; +} + +bool OpenXRMetaControllerExtension::is_available(MetaControllers p_type) { + return available[p_type]; +} + +void OpenXRMetaControllerExtension::on_register_metadata() { + OpenXRInteractionProfileMetadata *metadata = OpenXRInteractionProfileMetadata::get_singleton(); + ERR_FAIL_NULL(metadata); + + // Note, we register controllers regardless if they are supported on the current hardware. + + // Normal touch controller is part of the core spec, but we do have some extensions. + metadata->register_io_path("/interaction_profiles/oculus/touch_controller", "Trigger proximity", "/user/hand/left", "/user/hand/left/input/trigger/proximity_fb ", "XR_FB_touch_controller_proximity", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/oculus/touch_controller", "Trigger proximity", "/user/hand/right", "/user/hand/right/input/trigger/proximity_fb ", "XR_FB_touch_controller_proximity", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/oculus/touch_controller", "Thumb proximity", "/user/hand/left", "/user/hand/left/input/thumb_fb/proximity_fb ", "XR_FB_touch_controller_proximity", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/oculus/touch_controller", "Thumb proximity", "/user/hand/right", "/user/hand/right/input/thumb_fb/proximity_fb ", "XR_FB_touch_controller_proximity", OpenXRAction::OPENXR_ACTION_BOOL); + + // Touch controller pro (Quest Pro) + metadata->register_interaction_profile("Touch controller pro", "/interaction_profiles/facebook/touch_controller_pro", "XR_FB_touch_controller_pro"); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Grip pose", "/user/hand/left", "/user/hand/left/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Grip pose", "/user/hand/right", "/user/hand/right/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Aim pose", "/user/hand/left", "/user/hand/left/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Aim pose", "/user/hand/right", "/user/hand/right/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Palm pose", "/user/hand/left", "/user/hand/left/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Palm pose", "/user/hand/right", "/user/hand/right/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Menu click", "/user/hand/left", "/user/hand/left/input/menu/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "System click", "/user/hand/right", "/user/hand/right/input/system/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "X click", "/user/hand/left", "/user/hand/left/input/x/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "X touch", "/user/hand/left", "/user/hand/left/input/x/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Y click", "/user/hand/left", "/user/hand/left/input/y/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Y touch", "/user/hand/left", "/user/hand/left/input/y/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "A click", "/user/hand/right", "/user/hand/right/input/a/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "A touch", "/user/hand/right", "/user/hand/right/input/a/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "B click", "/user/hand/right", "/user/hand/right/input/b/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "B touch", "/user/hand/right", "/user/hand/right/input/b/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger", "/user/hand/left", "/user/hand/left/input/trigger/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger touch", "/user/hand/left", "/user/hand/left/input/trigger/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger proximity", "/user/hand/left", "/user/hand/left/input/trigger/proximity_fb", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger curl", "/user/hand/left", "/user/hand/left/input/trigger/curl_fb", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger slide", "/user/hand/left", "/user/hand/left/input/trigger/slide_fb", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger force", "/user/hand/left", "/user/hand/left/input/trigger/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger", "/user/hand/right", "/user/hand/right/input/trigger/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger touch", "/user/hand/right", "/user/hand/right/input/trigger/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger proximity", "/user/hand/right", "/user/hand/right/input/trigger/proximity_fb", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger curl", "/user/hand/right", "/user/hand/right/input/trigger/curl_fb", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger slide", "/user/hand/right", "/user/hand/right/input/trigger/slide_fb", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Trigger force", "/user/hand/right", "/user/hand/right/input/trigger/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Squeeze", "/user/hand/left", "/user/hand/left/input/squeeze/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Squeeze", "/user/hand/right", "/user/hand/right/input/squeeze/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumb proximity", "/user/hand/left", "/user/hand/left/input/thumb_fb/proximity_fb", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumb proximity", "/user/hand/right", "/user/hand/right/input/thumb_fb/proximity_fb", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick", "/user/hand/left", "/user/hand/left/input/thumbstick", "", OpenXRAction::OPENXR_ACTION_VECTOR2); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick X", "/user/hand/left", "/user/hand/left/input/thumbstick/x", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick Y", "/user/hand/left", "/user/hand/left/input/thumbstick/y", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick click", "/user/hand/left", "/user/hand/left/input/thumbstick/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick touch", "/user/hand/left", "/user/hand/left/input/thumbstick/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick", "/user/hand/right", "/user/hand/right/input/thumbstick", "", OpenXRAction::OPENXR_ACTION_VECTOR2); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick X", "/user/hand/right", "/user/hand/right/input/thumbstick/x", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick Y", "/user/hand/right", "/user/hand/right/input/thumbstick/y", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick click", "/user/hand/right", "/user/hand/right/input/thumbstick/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbstick touch", "/user/hand/right", "/user/hand/right/input/thumbstick/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbrest touch", "/user/hand/left", "/user/hand/left/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbrest force", "/user/hand/left", "/user/hand/left/input/thumbrest/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbrest touch", "/user/hand/right", "/user/hand/right/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Thumbrest force", "/user/hand/right", "/user/hand/right/input/thumbrest/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Stylus force", "/user/hand/left", "/user/hand/left/input/stylus_fb/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Stylus force", "/user/hand/right", "/user/hand/right/input/stylus_fb/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic output", "/user/hand/left", "/user/hand/left/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic trigger output", "/user/hand/left", "/user/hand/left/output/haptic_trigger_fb", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic thumb output", "/user/hand/left", "/user/hand/left/output/haptic_thumb_fb", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic output", "/user/hand/right", "/user/hand/right/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic trigger output", "/user/hand/right", "/user/hand/right/output/haptic_trigger_fb", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/facebook/touch_controller_pro", "Haptic thumb output", "/user/hand/right", "/user/hand/right/output/haptic_thumb_fb", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + + // Touch controller plus (Quest 3) + metadata->register_interaction_profile("Touch controller plus", "/interaction_profiles/meta/touch_controller_plus", "XR_META_touch_controller_plus"); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Grip pose", "/user/hand/left", "/user/hand/left/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Grip pose", "/user/hand/right", "/user/hand/right/input/grip/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Aim pose", "/user/hand/left", "/user/hand/left/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Aim pose", "/user/hand/right", "/user/hand/right/input/aim/pose", "", OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Palm pose", "/user/hand/left", "/user/hand/left/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Palm pose", "/user/hand/right", "/user/hand/right/input/palm_ext/pose", XR_EXT_PALM_POSE_EXTENSION_NAME, OpenXRAction::OPENXR_ACTION_POSE); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Menu click", "/user/hand/left", "/user/hand/left/input/menu/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "System click", "/user/hand/right", "/user/hand/right/input/system/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "X click", "/user/hand/left", "/user/hand/left/input/x/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "X touch", "/user/hand/left", "/user/hand/left/input/x/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Y click", "/user/hand/left", "/user/hand/left/input/y/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Y touch", "/user/hand/left", "/user/hand/left/input/y/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "A click", "/user/hand/right", "/user/hand/right/input/a/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "A touch", "/user/hand/right", "/user/hand/right/input/a/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "B click", "/user/hand/right", "/user/hand/right/input/b/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "B touch", "/user/hand/right", "/user/hand/right/input/b/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger", "/user/hand/left", "/user/hand/left/input/trigger/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger touch", "/user/hand/left", "/user/hand/left/input/trigger/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger proximity", "/user/hand/left", "/user/hand/left/input/trigger/proximity_meta", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger curl", "/user/hand/left", "/user/hand/left/input/trigger/curl_meta", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger slide", "/user/hand/left", "/user/hand/left/input/trigger/slide_meta", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger force", "/user/hand/left", "/user/hand/left/input/trigger/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger", "/user/hand/right", "/user/hand/right/input/trigger/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger touch", "/user/hand/right", "/user/hand/right/input/trigger/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger proximity", "/user/hand/right", "/user/hand/right/input/trigger/proximity_meta", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger curl", "/user/hand/right", "/user/hand/right/input/trigger/curl_meta", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger slide", "/user/hand/right", "/user/hand/right/input/trigger/slide_meta", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Trigger force", "/user/hand/right", "/user/hand/right/input/trigger/force", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Squeeze", "/user/hand/left", "/user/hand/left/input/squeeze/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Squeeze", "/user/hand/right", "/user/hand/right/input/squeeze/value", "", OpenXRAction::OPENXR_ACTION_FLOAT); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumb proximity", "/user/hand/left", "/user/hand/left/input/thumb_meta/proximity_meta", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumb proximity", "/user/hand/right", "/user/hand/right/input/thumb_meta/proximity_meta", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick", "/user/hand/left", "/user/hand/left/input/thumbstick", "", OpenXRAction::OPENXR_ACTION_VECTOR2); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick X", "/user/hand/left", "/user/hand/left/input/thumbstick/x", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick Y", "/user/hand/left", "/user/hand/left/input/thumbstick/y", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick click", "/user/hand/left", "/user/hand/left/input/thumbstick/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick touch", "/user/hand/left", "/user/hand/left/input/thumbstick/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick", "/user/hand/right", "/user/hand/right/input/thumbstick", "", OpenXRAction::OPENXR_ACTION_VECTOR2); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick X", "/user/hand/right", "/user/hand/right/input/thumbstick/x", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick Y", "/user/hand/right", "/user/hand/right/input/thumbstick/y", "", OpenXRAction::OPENXR_ACTION_FLOAT); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick click", "/user/hand/right", "/user/hand/right/input/thumbstick/click", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbstick touch", "/user/hand/right", "/user/hand/right/input/thumbstick/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbrest touch", "/user/hand/left", "/user/hand/left/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Thumbrest touch", "/user/hand/right", "/user/hand/right/input/thumbrest/touch", "", OpenXRAction::OPENXR_ACTION_BOOL); + + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Haptic output", "/user/hand/left", "/user/hand/left/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); + metadata->register_io_path("/interaction_profiles/meta/touch_controller_plus", "Haptic output", "/user/hand/right", "/user/hand/right/output/haptic", "", OpenXRAction::OPENXR_ACTION_HAPTIC); +} diff --git a/modules/openxr/extensions/openxr_meta_controller_extension.h b/modules/openxr/extensions/openxr_meta_controller_extension.h new file mode 100644 index 0000000000..7a60d2a0b9 --- /dev/null +++ b/modules/openxr/extensions/openxr_meta_controller_extension.h @@ -0,0 +1,55 @@ +/**************************************************************************/ +/* openxr_meta_controller_extension.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef OPENXR_META_CONTROLLER_EXTENSION_H +#define OPENXR_META_CONTROLLER_EXTENSION_H + +#include "openxr_extension_wrapper.h" + +class OpenXRMetaControllerExtension : public OpenXRExtensionWrapper { +public: + enum MetaControllers { + META_TOUCH_PROXIMITY, // Proximity extensions for normal touch controllers + META_TOUCH_PRO, // Touch controller for the Quest Pro + META_TOUCH_PLUS, // Touch controller for the Quest Plus + META_MAX_CONTROLLERS + }; + + virtual HashMap<String, bool *> get_requested_extensions() override; + + bool is_available(MetaControllers p_type); + + virtual void on_register_metadata() override; + +private: + bool available[META_MAX_CONTROLLERS] = { false, false, false }; +}; + +#endif // OPENXR_META_CONTROLLER_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_android_extension.cpp b/modules/openxr/extensions/platform/openxr_android_extension.cpp index c6082ca404..de542828c3 100644 --- a/modules/openxr/extensions/openxr_android_extension.cpp +++ b/modules/openxr/extensions/platform/openxr_android_extension.cpp @@ -30,7 +30,7 @@ #include "openxr_android_extension.h" -#include "../openxr_api.h" +#include "../../openxr_api.h" #include "java_godot_wrapper.h" #include "os_android.h" diff --git a/modules/openxr/extensions/openxr_android_extension.h b/modules/openxr/extensions/platform/openxr_android_extension.h index 0e7c44d6d5..e51b5824e8 100644 --- a/modules/openxr/extensions/openxr_android_extension.h +++ b/modules/openxr/extensions/platform/openxr_android_extension.h @@ -31,8 +31,8 @@ #ifndef OPENXR_ANDROID_EXTENSION_H #define OPENXR_ANDROID_EXTENSION_H -#include "../util.h" -#include "openxr_extension_wrapper.h" +#include "../../util.h" +#include "../openxr_extension_wrapper.h" class OpenXRAndroidExtension : public OpenXRExtensionWrapper { public: diff --git a/modules/openxr/extensions/openxr_opengl_extension.cpp b/modules/openxr/extensions/platform/openxr_opengl_extension.cpp index 9038e9f458..d92084a220 100644 --- a/modules/openxr/extensions/openxr_opengl_extension.cpp +++ b/modules/openxr/extensions/platform/openxr_opengl_extension.cpp @@ -32,7 +32,7 @@ #ifdef GLES3_ENABLED -#include "../openxr_util.h" +#include "../../openxr_util.h" #include "drivers/gles3/effects/copy_effects.h" #include "drivers/gles3/storage/texture_storage.h" @@ -131,9 +131,9 @@ bool OpenXROpenGLExtension::check_graphics_api_support(XrVersion p_desired_versi #ifdef WIN32 XrGraphicsBindingOpenGLWin32KHR OpenXROpenGLExtension::graphics_binding_gl; -#elif ANDROID_ENABLED +#elif defined(ANDROID_ENABLED) XrGraphicsBindingOpenGLESAndroidKHR OpenXROpenGLExtension::graphics_binding_gl; -#else +#elif defined(X11_ENABLED) XrGraphicsBindingOpenGLXlibKHR OpenXROpenGLExtension::graphics_binding_gl; #endif @@ -147,20 +147,24 @@ void *OpenXROpenGLExtension::set_session_create_and_get_next_pointer(void *p_nex DisplayServer *display_server = DisplayServer::get_singleton(); +#ifdef WAYLAND_ENABLED + ERR_FAIL_COND_V_MSG(display_server->get_name() == "Wayland", p_next_pointer, "OpenXR is not yet supported on OpenGL Wayland."); +#endif + #ifdef WIN32 graphics_binding_gl.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR, graphics_binding_gl.next = p_next_pointer; graphics_binding_gl.hDC = (HDC)display_server->window_get_native_handle(DisplayServer::WINDOW_VIEW); graphics_binding_gl.hGLRC = (HGLRC)display_server->window_get_native_handle(DisplayServer::OPENGL_CONTEXT); -#elif ANDROID_ENABLED +#elif defined(ANDROID_ENABLED) graphics_binding_gl.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR; graphics_binding_gl.next = p_next_pointer; graphics_binding_gl.display = (void *)display_server->window_get_native_handle(DisplayServer::DISPLAY_HANDLE); graphics_binding_gl.config = (EGLConfig)0; // https://github.com/KhronosGroup/OpenXR-SDK-Source/blob/master/src/tests/hello_xr/graphicsplugin_opengles.cpp#L122 graphics_binding_gl.context = (void *)display_server->window_get_native_handle(DisplayServer::OPENGL_CONTEXT); -#else +#elif defined(X11_ENABLED) graphics_binding_gl.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR; graphics_binding_gl.next = p_next_pointer; diff --git a/modules/openxr/extensions/openxr_opengl_extension.h b/modules/openxr/extensions/platform/openxr_opengl_extension.h index 3b0aa0bce9..a3052d3f53 100644 --- a/modules/openxr/extensions/openxr_opengl_extension.h +++ b/modules/openxr/extensions/platform/openxr_opengl_extension.h @@ -33,14 +33,14 @@ #ifdef GLES3_ENABLED -#include "../openxr_api.h" -#include "../util.h" -#include "openxr_extension_wrapper.h" +#include "../../openxr_api.h" +#include "../../util.h" +#include "../openxr_extension_wrapper.h" #include "core/templates/vector.h" -// always include this as late as possible -#include "../openxr_platform_inc.h" +// Always include this as late as possible. +#include "../../openxr_platform_inc.h" class OpenXROpenGLExtension : public OpenXRGraphicsExtensionWrapper { public: @@ -65,9 +65,9 @@ private: #ifdef WIN32 static XrGraphicsBindingOpenGLWin32KHR graphics_binding_gl; -#elif ANDROID_ENABLED +#elif defined(ANDROID_ENABLED) static XrGraphicsBindingOpenGLESAndroidKHR graphics_binding_gl; -#else +#else // Linux/X11 static XrGraphicsBindingOpenGLXlibKHR graphics_binding_gl; #endif diff --git a/modules/openxr/extensions/openxr_vulkan_extension.cpp b/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp index 9429d9e082..f5e7fc192c 100644 --- a/modules/openxr/extensions/openxr_vulkan_extension.cpp +++ b/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp @@ -30,7 +30,7 @@ #include "openxr_vulkan_extension.h" -#include "../openxr_util.h" +#include "../../openxr_util.h" #include "core/string/print_string.h" #include "servers/rendering/renderer_rd/effects/copy_effects.h" @@ -38,14 +38,6 @@ #include "servers/rendering/rendering_server_globals.h" #include "servers/rendering_server.h" -OpenXRVulkanExtension::OpenXRVulkanExtension() { - VulkanContext::set_vulkan_hooks(this); -} - -OpenXRVulkanExtension::~OpenXRVulkanExtension() { - VulkanContext::set_vulkan_hooks(nullptr); -} - HashMap<String, bool *> OpenXRVulkanExtension::get_requested_extensions() { HashMap<String, bool *> request_extensions; @@ -178,10 +170,6 @@ bool OpenXRVulkanExtension::get_physical_device(VkPhysicalDevice *r_device) { bool OpenXRVulkanExtension::create_vulkan_device(const VkDeviceCreateInfo *p_device_create_info, VkDevice *r_device) { ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), false); - // the first entry in our queue list should be the one we need to remember... - vulkan_queue_family_index = p_device_create_info->pQueueCreateInfos[0].queueFamilyIndex; - vulkan_queue_index = 0; // ?? - XrVulkanDeviceCreateInfoKHR create_info = { XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR, // type nullptr, // next @@ -209,9 +197,17 @@ bool OpenXRVulkanExtension::create_vulkan_device(const VkDeviceCreateInfo *p_dev return true; } +void OpenXRVulkanExtension::set_direct_queue_family_and_index(uint32_t p_queue_family_index, uint32_t p_queue_index) { + vulkan_queue_family_index = p_queue_family_index; + vulkan_queue_index = p_queue_index; +} + XrGraphicsBindingVulkanKHR OpenXRVulkanExtension::graphics_binding_vulkan; void *OpenXRVulkanExtension::set_session_create_and_get_next_pointer(void *p_next_pointer) { + DEV_ASSERT(vulkan_queue_family_index < UINT32_MAX && "Direct queue family index was not specified yet."); + DEV_ASSERT(vulkan_queue_index < UINT32_MAX && "Direct queue index was not specified yet."); + graphics_binding_vulkan.type = XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR; graphics_binding_vulkan.next = p_next_pointer; graphics_binding_vulkan.instance = vulkan_instance; diff --git a/modules/openxr/extensions/openxr_vulkan_extension.h b/modules/openxr/extensions/platform/openxr_vulkan_extension.h index f31621fda0..a3f86a9968 100644 --- a/modules/openxr/extensions/openxr_vulkan_extension.h +++ b/modules/openxr/extensions/platform/openxr_vulkan_extension.h @@ -31,28 +31,30 @@ #ifndef OPENXR_VULKAN_EXTENSION_H #define OPENXR_VULKAN_EXTENSION_H -#include "../openxr_api.h" -#include "../util.h" -#include "openxr_extension_wrapper.h" +#include "../../openxr_api.h" +#include "../../util.h" +#include "../openxr_extension_wrapper.h" #include "core/templates/vector.h" +#include "drivers/vulkan/vulkan_hooks.h" -// always include this as late as possible -#include "../openxr_platform_inc.h" +// Always include this as late as possible. +#include "../../openxr_platform_inc.h" class OpenXRVulkanExtension : public OpenXRGraphicsExtensionWrapper, VulkanHooks { public: - OpenXRVulkanExtension(); - virtual ~OpenXRVulkanExtension() override; + OpenXRVulkanExtension() = default; + virtual ~OpenXRVulkanExtension() override = default; virtual HashMap<String, bool *> get_requested_extensions() override; virtual void on_instance_created(const XrInstance p_instance) override; virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer) override; - virtual bool create_vulkan_instance(const VkInstanceCreateInfo *p_vulkan_create_info, VkInstance *r_instance) override; - virtual bool get_physical_device(VkPhysicalDevice *r_device) override; - virtual bool create_vulkan_device(const VkDeviceCreateInfo *p_device_create_info, VkDevice *r_device) override; + virtual bool create_vulkan_instance(const VkInstanceCreateInfo *p_vulkan_create_info, VkInstance *r_instance) override final; + virtual bool get_physical_device(VkPhysicalDevice *r_device) override final; + virtual bool create_vulkan_device(const VkDeviceCreateInfo *p_device_create_info, VkDevice *r_device) override final; + virtual void set_direct_queue_family_and_index(uint32_t p_queue_family_index, uint32_t p_queue_index) override final; virtual void get_usable_swapchain_formats(Vector<int64_t> &p_usable_swap_chains) override; virtual void get_usable_depth_formats(Vector<int64_t> &p_usable_swap_chains) override; @@ -76,8 +78,8 @@ private: VkInstance vulkan_instance = nullptr; VkPhysicalDevice vulkan_physical_device = nullptr; VkDevice vulkan_device = nullptr; - uint32_t vulkan_queue_family_index = 0; - uint32_t vulkan_queue_index = 0; + uint32_t vulkan_queue_family_index = UINT32_MAX; + uint32_t vulkan_queue_index = UINT32_MAX; EXT_PROTO_XRRESULT_FUNC3(xrGetVulkanGraphicsRequirements2KHR, (XrInstance), p_instance, (XrSystemId), p_system_id, (XrGraphicsRequirementsVulkanKHR *), p_graphics_requirements) EXT_PROTO_XRRESULT_FUNC4(xrCreateVulkanInstanceKHR, (XrInstance), p_instance, (const XrVulkanInstanceCreateInfoKHR *), p_create_info, (VkInstance *), r_vulkan_instance, (VkResult *), r_vulkan_result) diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index d0e958164d..e978c012b5 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -46,18 +46,18 @@ #include "openxr_platform_inc.h" #ifdef VULKAN_ENABLED -#include "extensions/openxr_vulkan_extension.h" +#include "extensions/platform/openxr_vulkan_extension.h" #endif #if defined(GLES3_ENABLED) && !defined(MACOS_ENABLED) -#include "extensions/openxr_opengl_extension.h" +#include "extensions/platform/openxr_opengl_extension.h" #endif #include "extensions/openxr_composition_layer_depth_extension.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" #include "extensions/openxr_fb_foveation_extension.h" -#include "extensions/openxr_fb_passthrough_extension_wrapper.h" #include "extensions/openxr_fb_update_swapchain_extension.h" +#include "extensions/openxr_hand_tracking_extension.h" #ifdef ANDROID_ENABLED #define OPENXR_LOADER_NAME "libopenxr_loader.so" @@ -87,7 +87,7 @@ String OpenXRAPI::get_default_action_map_resource_name() { return name; } -String OpenXRAPI::get_error_string(XrResult result) { +String OpenXRAPI::get_error_string(XrResult result) const { if (XR_SUCCEEDED(result)) { return String("Succeeded"); } @@ -289,7 +289,7 @@ bool OpenXRAPI::create_instance() { for (KeyValue<String, bool *> &requested_extension : requested_extensions) { if (!is_extension_supported(requested_extension.key)) { if (requested_extension.value == nullptr) { - // Null means this is a manditory extension so we fail. + // Null means this is a mandatory extension so we fail. ERR_FAIL_V_MSG(false, String("OpenXR: OpenXR Runtime does not support ") + requested_extension.key + String(" extension!")); } else { // Set this extension as not supported. @@ -665,13 +665,6 @@ bool OpenXRAPI::load_supported_reference_spaces() { print_verbose(String("OpenXR: Found supported reference space ") + OpenXRUtil::get_reference_space_name(supported_reference_spaces[i])); } - // Check value we loaded at startup... - if (!is_reference_space_supported(reference_space)) { - print_verbose(String("OpenXR: ") + OpenXRUtil::get_reference_space_name(reference_space) + String(" isn't supported, defaulting to ") + OpenXRUtil::get_reference_space_name(supported_reference_spaces[0])); - - reference_space = supported_reference_spaces[0]; - } - return true; } @@ -687,58 +680,161 @@ bool OpenXRAPI::is_reference_space_supported(XrReferenceSpaceType p_reference_sp return false; } -bool OpenXRAPI::setup_spaces() { - XrResult result; +bool OpenXRAPI::setup_play_space() { + ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false); XrPosef identityPose = { { 0.0, 0.0, 0.0, 1.0 }, { 0.0, 0.0, 0.0 } }; + XrReferenceSpaceType new_reference_space; + XrSpace new_play_space = XR_NULL_HANDLE; + bool will_emulate_local_floor = false; + + if (is_reference_space_supported(requested_reference_space)) { + new_reference_space = requested_reference_space; + } else if (requested_reference_space == XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT && is_reference_space_supported(XR_REFERENCE_SPACE_TYPE_STAGE)) { + print_verbose("OpenXR: LOCAL_FLOOR space isn't supported, emulating using STAGE and LOCAL spaces."); + + new_reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; + will_emulate_local_floor = true; + } else { + // Fallback on LOCAL, which all OpenXR runtimes are required to support. + print_verbose(String("OpenXR: ") + OpenXRUtil::get_reference_space_name(requested_reference_space) + String(" isn't supported, defaulting to LOCAL space.")); + new_reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; + } + + XrReferenceSpaceCreateInfo play_space_create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + new_reference_space, // referenceSpaceType + identityPose, // poseInReferenceSpace + }; + + XrResult result = xrCreateReferenceSpace(session, &play_space_create_info, &new_play_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create play space [", get_error_string(result), "]"); + return false; + } + + // If we've previously created a play space, clean it up first. + if (play_space != XR_NULL_HANDLE) { + xrDestroySpace(play_space); + } + play_space = new_play_space; + reference_space = new_reference_space; + + emulating_local_floor = will_emulate_local_floor; + if (emulating_local_floor) { + // We'll use the STAGE space to get the floor height, but we can't do that until + // after xrWaitFrame(), so just set this flag for now. + should_reset_emulated_floor_height = true; + } + + return true; +} + +bool OpenXRAPI::setup_view_space() { ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false); - // create play space - { - if (!is_reference_space_supported(reference_space)) { - print_line("OpenXR: reference space ", OpenXRUtil::get_reference_space_name(reference_space), " is not supported."); - return false; - } + if (!is_reference_space_supported(XR_REFERENCE_SPACE_TYPE_VIEW)) { + print_line("OpenXR: reference space XR_REFERENCE_SPACE_TYPE_VIEW is not supported."); + return false; + } - XrReferenceSpaceCreateInfo play_space_create_info = { - XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type - nullptr, // next - reference_space, // referenceSpaceType - identityPose // poseInReferenceSpace - }; + XrPosef identityPose = { + { 0.0, 0.0, 0.0, 1.0 }, + { 0.0, 0.0, 0.0 } + }; - result = xrCreateReferenceSpace(session, &play_space_create_info, &play_space); - if (XR_FAILED(result)) { - print_line("OpenXR: Failed to create play space [", get_error_string(result), "]"); - return false; - } + XrReferenceSpaceCreateInfo view_space_create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + XR_REFERENCE_SPACE_TYPE_VIEW, // referenceSpaceType + identityPose // poseInReferenceSpace + }; + + XrResult result = xrCreateReferenceSpace(session, &view_space_create_info, &view_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create view space [", get_error_string(result), "]"); + return false; } - // create view space - { - if (!is_reference_space_supported(XR_REFERENCE_SPACE_TYPE_VIEW)) { - print_line("OpenXR: reference space XR_REFERENCE_SPACE_TYPE_VIEW is not supported."); - return false; - } + return true; +} - XrReferenceSpaceCreateInfo view_space_create_info = { - XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type - nullptr, // next - XR_REFERENCE_SPACE_TYPE_VIEW, // referenceSpaceType - identityPose // poseInReferenceSpace - }; +bool OpenXRAPI::reset_emulated_floor_height() { + ERR_FAIL_COND_V(!emulating_local_floor, false); - result = xrCreateReferenceSpace(session, &view_space_create_info, &view_space); - if (XR_FAILED(result)) { - print_line("OpenXR: Failed to create view space [", get_error_string(result), "]"); - return false; - } + // This is based on the example code in the OpenXR spec which shows how to + // emulate LOCAL_FLOOR if it's not supported. + // See: https://registry.khronos.org/OpenXR/specs/1.0/html/xrspec.html#XR_EXT_local_floor + + XrResult result; + + XrPosef identityPose = { + { 0.0, 0.0, 0.0, 1.0 }, + { 0.0, 0.0, 0.0 } + }; + + XrSpace local_space = XR_NULL_HANDLE; + XrSpace stage_space = XR_NULL_HANDLE; + + XrReferenceSpaceCreateInfo create_info = { + XR_TYPE_REFERENCE_SPACE_CREATE_INFO, // type + nullptr, // next + XR_REFERENCE_SPACE_TYPE_LOCAL, // referenceSpaceType + identityPose, // poseInReferenceSpace + }; + + result = xrCreateReferenceSpace(session, &create_info, &local_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create LOCAL space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); + return false; + } + + create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + result = xrCreateReferenceSpace(session, &create_info, &stage_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to create STAGE space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); + xrDestroySpace(local_space); + return false; + } + + XrSpaceLocation stage_location = { + XR_TYPE_SPACE_LOCATION, // type + nullptr, // next + 0, // locationFlags + identityPose, // pose + }; + + result = xrLocateSpace(stage_space, local_space, get_next_frame_time(), &stage_location); + + xrDestroySpace(local_space); + xrDestroySpace(stage_space); + + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to locate STAGE space in LOCAL space, in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); + return false; + } + + XrSpace new_play_space; + create_info.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + create_info.poseInReferenceSpace.position.y = stage_location.pose.position.y; + result = xrCreateReferenceSpace(session, &create_info, &new_play_space); + if (XR_FAILED(result)) { + print_line("OpenXR: Failed to recreate emulated LOCAL_FLOOR play space with latest floor estimate [", get_error_string(result), "]"); + return false; } + xrDestroySpace(play_space); + play_space = new_play_space; + + // If we've made it this far, it means we can properly emulate LOCAL_FLOOR, so we'll + // report that as the reference space to the outside world. + reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT; + return true; } @@ -804,6 +900,7 @@ bool OpenXRAPI::create_swapchains() { */ Size2 recommended_size = get_recommended_target_size(); + uint32_t sample_count = 1; // We start with our color swapchain... { @@ -827,7 +924,7 @@ bool OpenXRAPI::create_swapchains() { print_verbose(String("Using color swap chain format:") + get_swapchain_format_name(swapchain_format_to_use)); } - if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, view_configuration_views[0].recommendedSwapchainSampleCount, view_count, swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain, &swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain_graphics_data)) { + if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, sample_count, view_count, swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain, &swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain_graphics_data)) { return false; } } @@ -863,7 +960,7 @@ bool OpenXRAPI::create_swapchains() { // Note, if VK_FORMAT_D32_SFLOAT is used here but we're using the forward+ renderer, we should probably output a warning. - if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, view_configuration_views[0].recommendedSwapchainSampleCount, view_count, swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain, &swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain_graphics_data)) { + if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, sample_count, view_count, swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain, &swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain_graphics_data)) { return false; } @@ -1179,10 +1276,14 @@ void OpenXRAPI::set_view_configuration(XrViewConfigurationType p_view_configurat view_configuration = p_view_configuration; } -void OpenXRAPI::set_reference_space(XrReferenceSpaceType p_reference_space) { - ERR_FAIL_COND(is_initialized()); +bool OpenXRAPI::set_requested_reference_space(XrReferenceSpaceType p_requested_reference_space) { + requested_reference_space = p_requested_reference_space; + + if (is_initialized()) { + return setup_play_space(); + } - reference_space = p_reference_space; + return true; } void OpenXRAPI::set_submit_depth_buffer(bool p_submit_depth_buffer) { @@ -1260,6 +1361,7 @@ bool OpenXRAPI::resolve_instance_openxr_symbols() { OPENXR_API_INIT_XR_FUNC_V(xrGetActionStateFloat); OPENXR_API_INIT_XR_FUNC_V(xrGetActionStateVector2f); OPENXR_API_INIT_XR_FUNC_V(xrGetCurrentInteractionProfile); + OPENXR_API_INIT_XR_FUNC_V(xrGetReferenceSpaceBoundsRect); OPENXR_API_INIT_XR_FUNC_V(xrGetSystem); OPENXR_API_INIT_XR_FUNC_V(xrGetSystemProperties); OPENXR_API_INIT_XR_FUNC_V(xrLocateViews); @@ -1382,7 +1484,12 @@ bool OpenXRAPI::initialize_session() { return false; } - if (!setup_spaces()) { + if (!setup_play_space()) { + destroy_session(); + return false; + } + + if (!setup_view_space()) { destroy_session(); return false; } @@ -1413,6 +1520,10 @@ void OpenXRAPI::unregister_extension_wrapper(OpenXRExtensionWrapper *p_extension registered_extension_wrappers.erase(p_extension_wrapper); } +const Vector<OpenXRExtensionWrapper *> &OpenXRAPI::get_registered_extension_wrappers() { + return registered_extension_wrappers; +} + void OpenXRAPI::register_extension_metadata() { for (OpenXRExtensionWrapper *extension_wrapper : registered_extension_wrappers) { extension_wrapper->on_register_metadata(); @@ -1421,11 +1532,23 @@ void OpenXRAPI::register_extension_metadata() { void OpenXRAPI::cleanup_extension_wrappers() { for (OpenXRExtensionWrapper *extension_wrapper : registered_extension_wrappers) { - memdelete(extension_wrapper); + // Fix crash when the extension wrapper comes from GDExtension. + OpenXRExtensionWrapperExtension *gdextension_extension_wrapper = dynamic_cast<OpenXRExtensionWrapperExtension *>(extension_wrapper); + if (gdextension_extension_wrapper) { + memdelete(gdextension_extension_wrapper); + } else { + memdelete(extension_wrapper); + } } registered_extension_wrappers.clear(); } +XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) { + ERR_FAIL_INDEX_V(p_hand_index, OpenXRHandTrackingExtension::HandTrackedHands::OPENXR_MAX_TRACKED_HANDS, XR_NULL_HANDLE); + OpenXRHandTrackingExtension::HandTrackedHands hand = static_cast<OpenXRHandTrackingExtension::HandTrackedHands>(p_hand_index); + return OpenXRHandTrackingExtension::get_singleton()->get_hand_tracker(hand)->hand_tracker; +} + Size2 OpenXRAPI::get_recommended_target_size() { ERR_FAIL_NULL_V(view_configuration_views, Size2()); @@ -1626,6 +1749,9 @@ bool OpenXRAPI::poll_events() { XrEventDataReferenceSpaceChangePending *event = (XrEventDataReferenceSpaceChangePending *)&runtimeEvent; print_verbose(String("OpenXR EVENT: reference space type ") + OpenXRUtil::get_reference_space_name(event->referenceSpaceType) + " change pending!"); + if (emulating_local_floor) { + should_reset_emulated_floor_height = true; + } if (event->poseValid && xr_interface) { xr_interface->on_pose_recentered(); } @@ -1781,6 +1907,11 @@ void OpenXRAPI::pre_render() { frame_state.predictedDisplayPeriod = 0; } + if (unlikely(should_reset_emulated_floor_height)) { + reset_emulated_floor_height(); + should_reset_emulated_floor_height = false; + } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_pre_render(); } @@ -2071,6 +2202,25 @@ void OpenXRAPI::set_foveation_dynamic(bool p_foveation_dynamic) { } } +Size2 OpenXRAPI::get_play_space_bounds() const { + Size2 ret; + + ERR_FAIL_COND_V(session == XR_NULL_HANDLE, Size2()); + + XrExtent2Df extents; + + XrResult result = xrGetReferenceSpaceBoundsRect(session, reference_space, &extents); + if (XR_FAILED(result)) { + print_line("OpenXR: failed to get play space bounds! [", get_error_string(result), "]"); + return ret; + } + + ret.width = extents.width; + ret.height = extents.height; + + return ret; +} + OpenXRAPI::OpenXRAPI() { // OpenXRAPI is only constructed if OpenXR is enabled. singleton = this; @@ -2115,10 +2265,13 @@ OpenXRAPI::OpenXRAPI() { int reference_space_setting = GLOBAL_GET("xr/openxr/reference_space"); switch (reference_space_setting) { case 0: { - reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; + requested_reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; } break; case 1: { - reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; + requested_reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; + } break; + case 2: { + requested_reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT; } break; default: break; @@ -2717,7 +2870,7 @@ bool OpenXRAPI::sync_action_sets(const Vector<RID> p_active_sets) { } } - ERR_FAIL_COND_V(active_sets.size() == 0, false); + ERR_FAIL_COND_V(active_sets.is_empty(), false); XrActionsSyncInfo sync_info = { XR_TYPE_ACTIONS_SYNC_INFO, // type @@ -2981,11 +3134,30 @@ bool OpenXRAPI::is_environment_blend_mode_supported(XrEnvironmentBlendMode p_ble } bool OpenXRAPI::set_environment_blend_mode(XrEnvironmentBlendMode p_blend_mode) { + if (emulate_environment_blend_mode_alpha_blend && p_blend_mode == XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND) { + requested_environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND; + environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + return true; + } // We allow setting this when not initialized and will check if it is supported when initializing. // After OpenXR is initialized we verify we're setting a supported blend mode. - if (!is_initialized() || is_environment_blend_mode_supported(p_blend_mode)) { + else if (!is_initialized() || is_environment_blend_mode_supported(p_blend_mode)) { + requested_environment_blend_mode = p_blend_mode; environment_blend_mode = p_blend_mode; return true; } return false; } + +void OpenXRAPI::set_emulate_environment_blend_mode_alpha_blend(bool p_enabled) { + emulate_environment_blend_mode_alpha_blend = p_enabled; +} + +OpenXRAPI::OpenXRAlphaBlendModeSupport OpenXRAPI::is_environment_blend_mode_alpha_blend_supported() { + if (is_environment_blend_mode_supported(XR_ENVIRONMENT_BLEND_MODE_ALPHA_BLEND)) { + return OPENXR_ALPHA_BLEND_MODE_SUPPORT_REAL; + } else if (emulate_environment_blend_mode_alpha_blend) { + return OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING; + } + return OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE; +} diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 64769b244c..d3e6eb01ce 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -98,13 +98,16 @@ private: // configuration XrFormFactor form_factor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; XrViewConfigurationType view_configuration = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; - XrReferenceSpaceType reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; + XrReferenceSpaceType requested_reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; + XrReferenceSpaceType reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled. // blend mode XrEnvironmentBlendMode environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + XrEnvironmentBlendMode requested_environment_blend_mode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; uint32_t num_supported_environment_blend_modes = 0; XrEnvironmentBlendMode *supported_environment_blend_modes = nullptr; + bool emulate_environment_blend_mode_alpha_blend = false; // state XrInstance instance = XR_NULL_HANDLE; @@ -149,6 +152,10 @@ private: bool view_pose_valid = false; XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; + bool emulating_local_floor = false; + bool should_reset_emulated_floor_height = false; + bool reset_emulated_floor_height(); + bool load_layer_properties(); bool load_supported_extensions(); bool is_extension_supported(const String &p_extension) const; @@ -199,6 +206,7 @@ private: EXT_PROTO_XRRESULT_FUNC3(xrGetActionStateVector2f, (XrSession), session, (const XrActionStateGetInfo *), getInfo, (XrActionStateVector2f *), state) EXT_PROTO_XRRESULT_FUNC3(xrGetCurrentInteractionProfile, (XrSession), session, (XrPath), topLevelUserPath, (XrInteractionProfileState *), interactionProfile) EXT_PROTO_XRRESULT_FUNC2(xrGetInstanceProperties, (XrInstance), instance, (XrInstanceProperties *), instanceProperties) + EXT_PROTO_XRRESULT_FUNC3(xrGetReferenceSpaceBoundsRect, (XrSession), session, (XrReferenceSpaceType), referenceSpaceType, (XrExtent2Df *), bounds) EXT_PROTO_XRRESULT_FUNC3(xrGetSystem, (XrInstance), instance, (const XrSystemGetInfo *), getInfo, (XrSystemId *), systemId) EXT_PROTO_XRRESULT_FUNC3(xrGetSystemProperties, (XrInstance), instance, (XrSystemId), systemId, (XrSystemProperties *), properties) EXT_PROTO_XRRESULT_FUNC4(xrLocateSpace, (XrSpace), space, (XrSpace), baseSpace, (XrTime), time, (XrSpaceLocation *), location) @@ -226,7 +234,8 @@ private: bool create_session(); bool load_supported_reference_spaces(); bool is_reference_space_supported(XrReferenceSpaceType p_reference_space); - bool setup_spaces(); + bool setup_play_space(); + bool setup_view_space(); bool load_supported_swapchain_formats(); bool is_swapchain_format_supported(int64_t p_swapchain_format); bool create_swapchains(); @@ -317,12 +326,13 @@ public: XrResult try_get_instance_proc_addr(const char *p_name, PFN_xrVoidFunction *p_addr); XrResult get_instance_proc_addr(const char *p_name, PFN_xrVoidFunction *p_addr); - String get_error_string(XrResult result); + String get_error_string(XrResult result) const; String get_swapchain_format_name(int64_t p_swapchain_format) const; void set_xr_interface(OpenXRInterface *p_xr_interface); static void register_extension_wrapper(OpenXRExtensionWrapper *p_extension_wrapper); static void unregister_extension_wrapper(OpenXRExtensionWrapper *p_extension_wrapper); + static const Vector<OpenXRExtensionWrapper *> &get_registered_extension_wrappers(); static void register_extension_metadata(); static void cleanup_extension_wrappers(); @@ -332,7 +342,8 @@ public: void set_view_configuration(XrViewConfigurationType p_view_configuration); XrViewConfigurationType get_view_configuration() const { return view_configuration; } - void set_reference_space(XrReferenceSpaceType p_reference_space); + bool set_requested_reference_space(XrReferenceSpaceType p_requested_reference_space); + XrReferenceSpaceType get_requested_reference_space() const { return requested_reference_space; } XrReferenceSpaceType get_reference_space() const { return reference_space; } void set_submit_depth_buffer(bool p_submit_depth_buffer); @@ -348,6 +359,8 @@ public: XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } bool can_render() { return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && view_pose_valid && frame_state.shouldRender; } + XrHandTrackerEXT get_hand_tracker(int p_hand_index); + Size2 get_recommended_target_size(); XRPose::TrackingConfidence get_head_center(Transform3D &r_transform, Vector3 &r_linear_velocity, Vector3 &r_angular_velocity); bool get_view_transform(uint32_t p_view, Transform3D &r_transform); @@ -380,6 +393,9 @@ public: bool get_foveation_dynamic() const; void set_foveation_dynamic(bool p_foveation_dynamic); + // Play space. + Size2 get_play_space_bounds() const; + // action map String get_default_action_map_resource_name(); @@ -417,7 +433,16 @@ public: const XrEnvironmentBlendMode *get_supported_environment_blend_modes(uint32_t &count); bool is_environment_blend_mode_supported(XrEnvironmentBlendMode p_blend_mode) const; bool set_environment_blend_mode(XrEnvironmentBlendMode p_blend_mode); - XrEnvironmentBlendMode get_environment_blend_mode() const { return environment_blend_mode; } + XrEnvironmentBlendMode get_environment_blend_mode() const { return requested_environment_blend_mode; } + + enum OpenXRAlphaBlendModeSupport { + OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE = 0, + OPENXR_ALPHA_BLEND_MODE_SUPPORT_REAL = 1, + OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING = 2, + }; + + void set_emulate_environment_blend_mode_alpha_blend(bool p_enabled); + OpenXRAlphaBlendModeSupport is_environment_blend_mode_alpha_blend_supported(); OpenXRAPI(); ~OpenXRAPI(); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index f0f0835f78..fae0fc13d3 100644 --- a/modules/openxr/openxr_api_extension.cpp +++ b/modules/openxr/openxr_api_extension.cpp @@ -30,6 +30,8 @@ #include "openxr_api_extension.h" +#include "extensions/openxr_extension_wrapper_extension.h" + void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("get_instance"), &OpenXRAPIExtension::get_instance); ClassDB::bind_method(D_METHOD("get_system_id"), &OpenXRAPIExtension::get_system_id); @@ -48,6 +50,18 @@ void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("get_play_space"), &OpenXRAPIExtension::get_play_space); ClassDB::bind_method(D_METHOD("get_next_frame_time"), &OpenXRAPIExtension::get_next_frame_time); ClassDB::bind_method(D_METHOD("can_render"), &OpenXRAPIExtension::can_render); + + ClassDB::bind_method(D_METHOD("get_hand_tracker", "hand_index"), &OpenXRAPIExtension::get_hand_tracker); + + ClassDB::bind_method(D_METHOD("register_composition_layer_provider", "extension"), &OpenXRAPIExtension::register_composition_layer_provider); + ClassDB::bind_method(D_METHOD("unregister_composition_layer_provider", "extension"), &OpenXRAPIExtension::unregister_composition_layer_provider); + + ClassDB::bind_method(D_METHOD("set_emulate_environment_blend_mode_alpha_blend", "enabled"), &OpenXRAPIExtension::set_emulate_environment_blend_mode_alpha_blend); + ClassDB::bind_method(D_METHOD("is_environment_blend_mode_alpha_supported"), &OpenXRAPIExtension::is_environment_blend_mode_alpha_blend_supported); + + BIND_ENUM_CONSTANT(OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE); + BIND_ENUM_CONSTANT(OPENXR_ALPHA_BLEND_MODE_SUPPORT_REAL); + BIND_ENUM_CONSTANT(OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING); } uint64_t OpenXRAPIExtension::get_instance() { @@ -126,5 +140,30 @@ bool OpenXRAPIExtension::can_render() { return OpenXRAPI::get_singleton()->can_render(); } +uint64_t OpenXRAPIExtension::get_hand_tracker(int p_hand_index) { + ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + return (uint64_t)OpenXRAPI::get_singleton()->get_hand_tracker(p_hand_index); +} + +void OpenXRAPIExtension::register_composition_layer_provider(OpenXRExtensionWrapperExtension *p_extension) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + OpenXRAPI::get_singleton()->register_composition_layer_provider(p_extension); +} + +void OpenXRAPIExtension::unregister_composition_layer_provider(OpenXRExtensionWrapperExtension *p_extension) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + OpenXRAPI::get_singleton()->unregister_composition_layer_provider(p_extension); +} + +void OpenXRAPIExtension::set_emulate_environment_blend_mode_alpha_blend(bool p_enabled) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + OpenXRAPI::get_singleton()->set_emulate_environment_blend_mode_alpha_blend(p_enabled); +} + +OpenXRAPIExtension::OpenXRAlphaBlendModeSupport OpenXRAPIExtension::is_environment_blend_mode_alpha_blend_supported() { + ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE); + return (OpenXRAPIExtension::OpenXRAlphaBlendModeSupport)OpenXRAPI::get_singleton()->is_environment_blend_mode_alpha_blend_supported(); +} + OpenXRAPIExtension::OpenXRAPIExtension() { } diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h index 98f87c7aa1..576e497798 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -38,6 +38,8 @@ #include "core/os/thread_safe.h" #include "core/variant/native_ptr.h" +class OpenXRExtensionWrapperExtension; + class OpenXRAPIExtension : public RefCounted { GDCLASS(OpenXRAPIExtension, RefCounted); @@ -70,7 +72,23 @@ public: int64_t get_next_frame_time(); bool can_render(); + uint64_t get_hand_tracker(int p_hand_index); + + void register_composition_layer_provider(OpenXRExtensionWrapperExtension *p_extension); + void unregister_composition_layer_provider(OpenXRExtensionWrapperExtension *p_extension); + + enum OpenXRAlphaBlendModeSupport { + OPENXR_ALPHA_BLEND_MODE_SUPPORT_NONE = 0, + OPENXR_ALPHA_BLEND_MODE_SUPPORT_REAL = 1, + OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING = 2, + }; + + void set_emulate_environment_blend_mode_alpha_blend(bool p_enabled); + OpenXRAlphaBlendModeSupport is_environment_blend_mode_alpha_blend_supported(); + OpenXRAPIExtension(); }; +VARIANT_ENUM_CAST(OpenXRAPIExtension::OpenXRAlphaBlendModeSupport); + #endif // OPENXR_API_EXTENSION_H diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index 8ce76a5fad..956e5ed3f3 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -77,6 +77,8 @@ void OpenXRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("set_motion_range", "hand", "motion_range"), &OpenXRInterface::set_motion_range); ClassDB::bind_method(D_METHOD("get_motion_range", "hand"), &OpenXRInterface::get_motion_range); + ClassDB::bind_method(D_METHOD("get_hand_tracking_source", "hand"), &OpenXRInterface::get_hand_tracking_source); + ClassDB::bind_method(D_METHOD("get_hand_joint_flags", "hand", "joint"), &OpenXRInterface::get_hand_joint_flags); ClassDB::bind_method(D_METHOD("get_hand_joint_rotation", "hand", "joint"), &OpenXRInterface::get_hand_joint_rotation); @@ -97,6 +99,11 @@ void OpenXRInterface::_bind_methods() { BIND_ENUM_CONSTANT(HAND_MOTION_RANGE_CONFORM_TO_CONTROLLER); BIND_ENUM_CONSTANT(HAND_MOTION_RANGE_MAX); + BIND_ENUM_CONSTANT(HAND_TRACKED_SOURCE_UNKNOWN); + BIND_ENUM_CONSTANT(HAND_TRACKED_SOURCE_UNOBSTRUCTED); + BIND_ENUM_CONSTANT(HAND_TRACKED_SOURCE_CONTROLLER); + BIND_ENUM_CONSTANT(HAND_TRACKED_SOURCE_MAX); + BIND_ENUM_CONSTANT(HAND_JOINT_PALM); BIND_ENUM_CONSTANT(HAND_JOINT_WRIST); BIND_ENUM_CONSTANT(HAND_JOINT_THUMB_METACARPAL); @@ -149,27 +156,12 @@ PackedStringArray OpenXRInterface::get_suggested_tracker_names() const { "left_hand", // /user/hand/left is mapped to our defaults "right_hand", // /user/hand/right is mapped to our defaults "/user/treadmill", - - // Even though these are only available if you have the tracker extension, - // we add these as we may be deploying on a different platform than our - // editor is running on. - "/user/vive_tracker_htcx/role/handheld_object", - "/user/vive_tracker_htcx/role/left_foot", - "/user/vive_tracker_htcx/role/right_foot", - "/user/vive_tracker_htcx/role/left_shoulder", - "/user/vive_tracker_htcx/role/right_shoulder", - "/user/vive_tracker_htcx/role/left_elbow", - "/user/vive_tracker_htcx/role/right_elbow", - "/user/vive_tracker_htcx/role/left_knee", - "/user/vive_tracker_htcx/role/right_knee", - "/user/vive_tracker_htcx/role/waist", - "/user/vive_tracker_htcx/role/chest", - "/user/vive_tracker_htcx/role/camera", - "/user/vive_tracker_htcx/role/keyboard", - - "/user/eyes_ext", }; + for (OpenXRExtensionWrapper *wrapper : OpenXRAPI::get_singleton()->get_registered_extension_wrappers()) { + arr.append_array(wrapper->get_suggested_tracker_names()); + } + return arr; } @@ -679,17 +671,92 @@ Dictionary OpenXRInterface::get_system_info() { } bool OpenXRInterface::supports_play_area_mode(XRInterface::PlayAreaMode p_mode) { - return false; + if (p_mode == XRInterface::XR_PLAY_AREA_3DOF) { + return false; + } + return true; } XRInterface::PlayAreaMode OpenXRInterface::get_play_area_mode() const { + if (!openxr_api || !initialized) { + return XRInterface::XR_PLAY_AREA_UNKNOWN; + } + + XrReferenceSpaceType reference_space = openxr_api->get_reference_space(); + + if (reference_space == XR_REFERENCE_SPACE_TYPE_LOCAL) { + return XRInterface::XR_PLAY_AREA_SITTING; + } else if (reference_space == XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT) { + return XRInterface::XR_PLAY_AREA_ROOMSCALE; + } else if (reference_space == XR_REFERENCE_SPACE_TYPE_STAGE) { + return XRInterface::XR_PLAY_AREA_STAGE; + } + return XRInterface::XR_PLAY_AREA_UNKNOWN; } bool OpenXRInterface::set_play_area_mode(XRInterface::PlayAreaMode p_mode) { + ERR_FAIL_NULL_V(openxr_api, false); + + XrReferenceSpaceType reference_space; + + if (p_mode == XRInterface::XR_PLAY_AREA_SITTING) { + reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL; + } else if (p_mode == XRInterface::XR_PLAY_AREA_ROOMSCALE) { + reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT; + } else if (p_mode == XRInterface::XR_PLAY_AREA_STAGE) { + reference_space = XR_REFERENCE_SPACE_TYPE_STAGE; + } else { + return false; + } + + if (openxr_api->set_requested_reference_space(reference_space)) { + XRServer *xr_server = XRServer::get_singleton(); + if (xr_server) { + xr_server->clear_reference_frame(); + } + return true; + } + return false; } +PackedVector3Array OpenXRInterface::get_play_area() const { + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL_V(xr_server, PackedVector3Array()); + PackedVector3Array arr; + + Vector3 sides[4] = { + Vector3(-0.5f, 0.0f, -0.5f), + Vector3(0.5f, 0.0f, -0.5f), + Vector3(0.5f, 0.0f, 0.5f), + Vector3(-0.5f, 0.0f, 0.5f), + }; + + if (openxr_api != nullptr && openxr_api->is_initialized()) { + Size2 extents = openxr_api->get_play_space_bounds(); + if (extents.width != 0.0 && extents.height != 0.0) { + Transform3D reference_frame = xr_server->get_reference_frame(); + + for (int i = 0; i < 4; i++) { + Vector3 coord = sides[i]; + + // Scale it up. + coord.x *= extents.width; + coord.z *= extents.height; + + // Now apply our reference. + Vector3 out = reference_frame.xform(coord); + arr.push_back(out); + } + } else { + WARN_PRINT_ONCE("OpenXR: No extents available."); + } + } + + return arr; +} + float OpenXRInterface::get_display_refresh_rate() const { if (openxr_api == nullptr) { return 0.0; @@ -1076,21 +1143,19 @@ void OpenXRInterface::end_frame() { } bool OpenXRInterface::is_passthrough_supported() { - return passthrough_wrapper != nullptr && passthrough_wrapper->is_passthrough_supported(); + return get_supported_environment_blend_modes().find(XR_ENV_BLEND_MODE_ALPHA_BLEND); } bool OpenXRInterface::is_passthrough_enabled() { - return passthrough_wrapper != nullptr && passthrough_wrapper->is_passthrough_enabled(); + return get_environment_blend_mode() == XR_ENV_BLEND_MODE_ALPHA_BLEND; } bool OpenXRInterface::start_passthrough() { - return passthrough_wrapper != nullptr && passthrough_wrapper->start_passthrough(); + return set_environment_blend_mode(XR_ENV_BLEND_MODE_ALPHA_BLEND); } void OpenXRInterface::stop_passthrough() { - if (passthrough_wrapper) { - passthrough_wrapper->stop_passthrough(); - } + set_environment_blend_mode(XR_ENV_BLEND_MODE_OPAQUE); } Array OpenXRInterface::get_supported_environment_blend_modes() { @@ -1122,6 +1187,11 @@ Array OpenXRInterface::get_supported_environment_blend_modes() { WARN_PRINT("Unsupported blend mode found: " + String::num_int64(int64_t(env_blend_modes[i]))); } } + + if (openxr_api->is_environment_blend_mode_alpha_blend_supported() == OpenXRAPI::OPENXR_ALPHA_BLEND_MODE_SUPPORT_EMULATING) { + modes.push_back(XR_ENV_BLEND_MODE_ALPHA_BLEND); + } + return modes; } @@ -1233,6 +1303,27 @@ OpenXRInterface::HandMotionRange OpenXRInterface::get_motion_range(const Hand p_ return HAND_MOTION_RANGE_MAX; } +OpenXRInterface::HandTrackedSource OpenXRInterface::get_hand_tracking_source(const Hand p_hand) const { + ERR_FAIL_INDEX_V(p_hand, HAND_MAX, HAND_TRACKED_SOURCE_UNKNOWN); + + OpenXRHandTrackingExtension *hand_tracking_ext = OpenXRHandTrackingExtension::get_singleton(); + if (hand_tracking_ext && hand_tracking_ext->get_active()) { + OpenXRHandTrackingExtension::HandTrackedSource source = hand_tracking_ext->get_hand_tracking_source(OpenXRHandTrackingExtension::HandTrackedHands(p_hand)); + switch (source) { + case OpenXRHandTrackingExtension::OPENXR_SOURCE_UNOBSTRUCTED: + return HAND_TRACKED_SOURCE_UNOBSTRUCTED; + case OpenXRHandTrackingExtension::OPENXR_SOURCE_CONTROLLER: + return HAND_TRACKED_SOURCE_CONTROLLER; + case OpenXRHandTrackingExtension::OPENXR_SOURCE_UNKNOWN: + return HAND_TRACKED_SOURCE_UNKNOWN; + default: + ERR_FAIL_V_MSG(HAND_TRACKED_SOURCE_UNKNOWN, "Unknown hand tracking source returned by OpenXR"); + } + } + + return HAND_TRACKED_SOURCE_UNKNOWN; +} + BitField<OpenXRInterface::HandJointFlags> OpenXRInterface::get_hand_joint_flags(Hand p_hand, HandJoints p_joint) const { BitField<OpenXRInterface::HandJointFlags> bits; @@ -1319,8 +1410,6 @@ OpenXRInterface::OpenXRInterface() { _set_default_pos(head_transform, 1.0, 0); _set_default_pos(transform_for_view[0], 1.0, 1); _set_default_pos(transform_for_view[1], 1.0, 2); - - passthrough_wrapper = OpenXRFbPassthroughExtensionWrapper::get_singleton(); } OpenXRInterface::~OpenXRInterface() { diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index 51ef4ea228..aee9751d6b 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -32,7 +32,6 @@ #define OPENXR_INTERFACE_H #include "action_map/openxr_action_map.h" -#include "extensions/openxr_fb_passthrough_extension_wrapper.h" #include "extensions/openxr_hand_tracking_extension.h" #include "openxr_api.h" @@ -49,7 +48,6 @@ private: OpenXRAPI *openxr_api = nullptr; bool initialized = false; XRInterface::TrackingStatus tracking_state; - OpenXRFbPassthroughExtensionWrapper *passthrough_wrapper = nullptr; // At a minimum we need a tracker for our head Ref<XRPositionalTracker> head; @@ -125,6 +123,7 @@ public: virtual bool supports_play_area_mode(XRInterface::PlayAreaMode p_mode) override; virtual XRInterface::PlayAreaMode get_play_area_mode() const override; virtual bool set_play_area_mode(XRInterface::PlayAreaMode p_mode) override; + virtual PackedVector3Array get_play_area() const override; float get_display_refresh_rate() const; void set_display_refresh_rate(float p_refresh_rate); @@ -193,6 +192,15 @@ public: void set_motion_range(const Hand p_hand, const HandMotionRange p_motion_range); HandMotionRange get_motion_range(const Hand p_hand) const; + enum HandTrackedSource { + HAND_TRACKED_SOURCE_UNKNOWN, + HAND_TRACKED_SOURCE_UNOBSTRUCTED, + HAND_TRACKED_SOURCE_CONTROLLER, + HAND_TRACKED_SOURCE_MAX + }; + + HandTrackedSource get_hand_tracking_source(const Hand p_hand) const; + enum HandJoints { HAND_JOINT_PALM = 0, HAND_JOINT_WRIST = 1, @@ -247,6 +255,7 @@ public: VARIANT_ENUM_CAST(OpenXRInterface::Hand) VARIANT_ENUM_CAST(OpenXRInterface::HandMotionRange) +VARIANT_ENUM_CAST(OpenXRInterface::HandTrackedSource) VARIANT_ENUM_CAST(OpenXRInterface::HandJoints) VARIANT_BITFIELD_CAST(OpenXRInterface::HandJointFlags) diff --git a/modules/openxr/openxr_platform_inc.h b/modules/openxr/openxr_platform_inc.h index 6288d1e380..957a87cbb2 100644 --- a/modules/openxr/openxr_platform_inc.h +++ b/modules/openxr/openxr_platform_inc.h @@ -36,7 +36,7 @@ #ifdef VULKAN_ENABLED #define XR_USE_GRAPHICS_API_VULKAN -#include "drivers/vulkan/vulkan_context.h" +#include "drivers/vulkan/rendering_context_driver_vulkan.h" #endif // VULKAN_ENABLED #if defined(GLES3_ENABLED) && !defined(MACOS_ENABLED) diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index 544932bdeb..3d34b27407 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -44,11 +44,12 @@ #include "extensions/openxr_composition_layer_depth_extension.h" #include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" -#include "extensions/openxr_fb_passthrough_extension_wrapper.h" #include "extensions/openxr_hand_tracking_extension.h" #include "extensions/openxr_htc_controller_extension.h" #include "extensions/openxr_htc_vive_tracker_extension.h" #include "extensions/openxr_huawei_controller_extension.h" +#include "extensions/openxr_local_floor_extension.h" +#include "extensions/openxr_meta_controller_extension.h" #include "extensions/openxr_ml2_controller_extension.h" #include "extensions/openxr_palm_pose_extension.h" #include "extensions/openxr_pico_controller_extension.h" @@ -59,7 +60,7 @@ #endif #ifdef ANDROID_ENABLED -#include "extensions/openxr_android_extension.h" +#include "extensions/platform/openxr_android_extension.h" #endif #include "core/config/project_settings.h" @@ -106,20 +107,19 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { // register our other extensions OpenXRAPI::register_extension_wrapper(memnew(OpenXRPalmPoseExtension)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXRLocalFloorExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRPicoControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRCompositionLayerDepthExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRHTCControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRHTCViveTrackerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRHuaweiControllerExtension)); - OpenXRAPI::register_extension_wrapper(memnew(OpenXRFbPassthroughExtensionWrapper)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRDisplayRefreshRateExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRWMRControllerExtension)); OpenXRAPI::register_extension_wrapper(memnew(OpenXRML2ControllerExtension)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXRMetaControllerExtension)); + OpenXRAPI::register_extension_wrapper(memnew(OpenXREyeGazeInteractionExtension)); // register gated extensions - if (GLOBAL_GET("xr/openxr/extensions/eye_gaze_interaction") && (!OS::get_singleton()->has_feature("mobile") || OS::get_singleton()->has_feature(XR_EXT_EYE_GAZE_INTERACTION_EXTENSION_NAME))) { - OpenXRAPI::register_extension_wrapper(memnew(OpenXREyeGazeInteractionExtension)); - } if (GLOBAL_GET("xr/openxr/extensions/hand_tracking")) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRHandTrackingExtension)); } diff --git a/modules/openxr/scene/SCsub b/modules/openxr/scene/SCsub new file mode 100644 index 0000000000..7a493011ec --- /dev/null +++ b/modules/openxr/scene/SCsub @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +Import("env") +Import("env_openxr") + +module_obj = [] + +env_openxr.add_source_files(module_obj, "*.cpp") + +env.modules_sources += module_obj diff --git a/modules/openxr/scene/openxr_hand.cpp b/modules/openxr/scene/openxr_hand.cpp index c48fac8055..2a4104f6ee 100644 --- a/modules/openxr/scene/openxr_hand.cpp +++ b/modules/openxr/scene/openxr_hand.cpp @@ -46,9 +46,17 @@ void OpenXRHand::_bind_methods() { ClassDB::bind_method(D_METHOD("set_motion_range", "motion_range"), &OpenXRHand::set_motion_range); ClassDB::bind_method(D_METHOD("get_motion_range"), &OpenXRHand::get_motion_range); + ClassDB::bind_method(D_METHOD("set_skeleton_rig", "skeleton_rig"), &OpenXRHand::set_skeleton_rig); + ClassDB::bind_method(D_METHOD("get_skeleton_rig"), &OpenXRHand::get_skeleton_rig); + + ClassDB::bind_method(D_METHOD("set_bone_update", "bone_update"), &OpenXRHand::set_bone_update); + ClassDB::bind_method(D_METHOD("get_bone_update"), &OpenXRHand::get_bone_update); + ADD_PROPERTY(PropertyInfo(Variant::INT, "hand", PROPERTY_HINT_ENUM, "Left,Right"), "set_hand", "get_hand"); ADD_PROPERTY(PropertyInfo(Variant::INT, "motion_range", PROPERTY_HINT_ENUM, "Unobstructed,Conform to controller"), "set_motion_range", "get_motion_range"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "hand_skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton3D"), "set_hand_skeleton", "get_hand_skeleton"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "skeleton_rig", PROPERTY_HINT_ENUM, "OpenXR,Humanoid"), "set_skeleton_rig", "get_skeleton_rig"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "bone_update", PROPERTY_HINT_ENUM, "Full,Rotation Only"), "set_bone_update", "get_bone_update"); BIND_ENUM_CONSTANT(HAND_LEFT); BIND_ENUM_CONSTANT(HAND_RIGHT); @@ -57,6 +65,14 @@ void OpenXRHand::_bind_methods() { BIND_ENUM_CONSTANT(MOTION_RANGE_UNOBSTRUCTED); BIND_ENUM_CONSTANT(MOTION_RANGE_CONFORM_TO_CONTROLLER); BIND_ENUM_CONSTANT(MOTION_RANGE_MAX); + + BIND_ENUM_CONSTANT(SKELETON_RIG_OPENXR); + BIND_ENUM_CONSTANT(SKELETON_RIG_HUMANOID); + BIND_ENUM_CONSTANT(SKELETON_RIG_MAX); + + BIND_ENUM_CONSTANT(BONE_UPDATE_FULL); + BIND_ENUM_CONSTANT(BONE_UPDATE_ROTATION_ONLY); + BIND_ENUM_CONSTANT(BONE_UPDATE_MAX); } OpenXRHand::OpenXRHand() { @@ -64,7 +80,7 @@ OpenXRHand::OpenXRHand() { hand_tracking_ext = OpenXRHandTrackingExtension::get_singleton(); } -void OpenXRHand::set_hand(const Hands p_hand) { +void OpenXRHand::set_hand(Hands p_hand) { ERR_FAIL_INDEX(p_hand, HAND_MAX); hand = p_hand; @@ -80,7 +96,7 @@ void OpenXRHand::set_hand_skeleton(const NodePath &p_hand_skeleton) { // TODO if inside tree call _get_bones() } -void OpenXRHand::set_motion_range(const MotionRange p_motion_range) { +void OpenXRHand::set_motion_range(MotionRange p_motion_range) { ERR_FAIL_INDEX(p_motion_range, MOTION_RANGE_MAX); motion_range = p_motion_range; @@ -116,6 +132,26 @@ void OpenXRHand::_set_motion_range() { hand_tracking_ext->set_motion_range(OpenXRHandTrackingExtension::HandTrackedHands(hand), xr_motion_range); } +void OpenXRHand::set_skeleton_rig(SkeletonRig p_skeleton_rig) { + ERR_FAIL_INDEX(p_skeleton_rig, SKELETON_RIG_MAX); + + skeleton_rig = p_skeleton_rig; +} + +OpenXRHand::SkeletonRig OpenXRHand::get_skeleton_rig() const { + return skeleton_rig; +} + +void OpenXRHand::set_bone_update(BoneUpdate p_bone_update) { + ERR_FAIL_INDEX(p_bone_update, BONE_UPDATE_MAX); + + bone_update = p_bone_update; +} + +OpenXRHand::BoneUpdate OpenXRHand::get_bone_update() const { + return bone_update; +} + Skeleton3D *OpenXRHand::get_skeleton() { if (!has_node(hand_skeleton)) { return nullptr; @@ -130,39 +166,81 @@ Skeleton3D *OpenXRHand::get_skeleton() { return skeleton; } -void OpenXRHand::_get_bones() { - const char *bone_names[XR_HAND_JOINT_COUNT_EXT] = { - "Palm", - "Wrist", - "Thumb_Metacarpal", - "Thumb_Proximal", - "Thumb_Distal", - "Thumb_Tip", - "Index_Metacarpal", - "Index_Proximal", - "Index_Intermediate", - "Index_Distal", - "Index_Tip", - "Middle_Metacarpal", - "Middle_Proximal", - "Middle_Intermediate", - "Middle_Distal", - "Middle_Tip", - "Ring_Metacarpal", - "Ring_Proximal", - "Ring_Intermediate", - "Ring_Distal", - "Ring_Tip", - "Little_Metacarpal", - "Little_Proximal", - "Little_Intermediate", - "Little_Distal", - "Little_Tip", +void OpenXRHand::_get_joint_data() { + // Table of bone names for different rig types. + static const String bone_names[SKELETON_RIG_MAX][XR_HAND_JOINT_COUNT_EXT] = { + // SKELETON_RIG_OPENXR bone names. + { + "Palm", + "Wrist", + "Thumb_Metacarpal", + "Thumb_Proximal", + "Thumb_Distal", + "Thumb_Tip", + "Index_Metacarpal", + "Index_Proximal", + "Index_Intermediate", + "Index_Distal", + "Index_Tip", + "Middle_Metacarpal", + "Middle_Proximal", + "Middle_Intermediate", + "Middle_Distal", + "Middle_Tip", + "Ring_Metacarpal", + "Ring_Proximal", + "Ring_Intermediate", + "Ring_Distal", + "Ring_Tip", + "Little_Metacarpal", + "Little_Proximal", + "Little_Intermediate", + "Little_Distal", + "Little_Tip" }, + + // SKELETON_RIG_HUMANOID bone names. + { + "Palm", + "Hand", + "ThumbMetacarpal", + "ThumbProximal", + "ThumbDistal", + "ThumbTip", + "IndexMetacarpal", + "IndexProximal", + "IndexIntermediate", + "IndexDistal", + "IndexTip", + "MiddleMetacarpal", + "MiddleProximal", + "MiddleIntermediate", + "MiddleDistal", + "MiddleTip", + "RingMetacarpal", + "RingProximal", + "RingIntermediate", + "RingDistal", + "RingTip", + "LittleMetacarpal", + "LittleProximal", + "LittleIntermediate", + "LittleDistal", + "LittleTip" } + }; + + // Table of bone name formats for different rig types and left/right hands. + static const String bone_name_formats[SKELETON_RIG_MAX][2] = { + // SKELETON_RIG_OPENXR bone name format. + { "<bone>_L", "<bone>_R" }, + + // SKELETON_RIG_HUMANOID bone name format. + { "Left<bone>", "Right<bone>" } }; // reset JIC for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) { - bones[i] = -1; + joints[i].bone = -1; + joints[i].parent_joint = -1; } Skeleton3D *skeleton = get_skeleton(); @@ -170,20 +248,46 @@ void OpenXRHand::_get_bones() { return; } - // We cast to spatials which should allow us to use any subclass of that. + // Find the skeleton-bones associated with each OpenXR joint. + int bones[XR_HAND_JOINT_COUNT_EXT]; for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) { - String bone_name = bone_names[i]; - if (hand == 0) { - bone_name += String("_L"); - } else { - bone_name += String("_R"); - } + // Construct the expected bone name. + String bone_name = bone_name_formats[skeleton_rig][hand].replace("<bone>", bone_names[skeleton_rig][i]); + // Find the skeleton bone. bones[i] = skeleton->find_bone(bone_name); if (bones[i] == -1) { print_line("Couldn't obtain bone for", bone_name); } } + + // Assemble the OpenXR joint relationship to the available skeleton bones. + for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) { + // Get the skeleton bone (skip if not found). + const int bone = bones[i]; + if (bone == -1) { + continue; + } + + // Find the parent skeleton-bone. + const int parent_bone = skeleton->get_bone_parent(bone); + if (parent_bone == -1) { + // If no parent skeleton-bone exists then drive this relative to palm joint. + joints[i].bone = bone; + joints[i].parent_joint = XR_HAND_JOINT_PALM_EXT; + continue; + } + + // Find the OpenXR joint associated with the parent skeleton-bone. + for (int j = 0; j < XR_HAND_JOINT_COUNT_EXT; ++j) { + if (bones[j] == parent_bone) { + // If a parent joint is found then drive this bone relative to it. + joints[i].bone = bone; + joints[i].parent_joint = j; + break; + } + } + } } void OpenXRHand::_update_skeleton() { @@ -198,12 +302,25 @@ void OpenXRHand::_update_skeleton() { return; } + // Table of bone adjustments for different rig types + static const Quaternion bone_adjustments[SKELETON_RIG_MAX] = { + // SKELETON_RIG_OPENXR bone adjustment. This is an identity quaternion + // because the incoming quaternions are already in OpenXR format. + Quaternion(), + + // SKELETON_RIG_HUMANOID bone adjustment. This rotation performs: + // OpenXR Z+ -> Godot Humanoid Y- (Back along the bone) + // OpenXR Y+ -> Godot Humanoid Z- (Out the back of the hand) + Quaternion(0.0, -Math_SQRT12, Math_SQRT12, 0.0), + }; + // we cache our transforms so we can quickly calculate local transforms XRPose::TrackingConfidence confidences[XR_HAND_JOINT_COUNT_EXT]; Quaternion quaternions[XR_HAND_JOINT_COUNT_EXT]; Quaternion inv_quaternions[XR_HAND_JOINT_COUNT_EXT]; Vector3 positions[XR_HAND_JOINT_COUNT_EXT]; + const Quaternion &rig_adjustment = bone_adjustments[skeleton_rig]; const OpenXRHandTrackingExtension::HandTracker *hand_tracker = hand_tracking_ext->get_hand_tracker(OpenXRHandTrackingExtension::HandTrackedHands(hand)); const float ws = XRServer::get_singleton()->get_world_scale(); @@ -218,7 +335,7 @@ void OpenXRHand::_update_skeleton() { if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) { if (pose.orientation.x != 0 || pose.orientation.y != 0 || pose.orientation.z != 0 || pose.orientation.w != 0) { - quaternions[i] = Quaternion(pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w); + quaternions[i] = Quaternion(pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w) * rig_adjustment; inv_quaternions[i] = quaternions[i].inverse(); if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { @@ -234,40 +351,29 @@ void OpenXRHand::_update_skeleton() { } if (confidences[XR_HAND_JOINT_PALM_EXT] != XRPose::XR_TRACKING_CONFIDENCE_NONE) { - // now update our skeleton - for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) { - if (bones[i] != -1) { - int bone = bones[i]; - int parent = skeleton->get_bone_parent(bone); - - // Get our target quaternion - Quaternion q = quaternions[i]; - - // Get our target position - Vector3 p = positions[i]; + // Iterate over all the OpenXR joints. + for (int joint = 0; joint < XR_HAND_JOINT_COUNT_EXT; joint++) { + // Get the skeleton bone (skip if none). + const int bone = joints[joint].bone; + if (bone == -1) { + continue; + } - // get local translation, parent should already be processed - if (parent == -1) { - // use our palm location here, that is what we are tracking - q = inv_quaternions[XR_HAND_JOINT_PALM_EXT] * q; - p = inv_quaternions[XR_HAND_JOINT_PALM_EXT].xform(p - positions[XR_HAND_JOINT_PALM_EXT]); - } else { - int found = false; - for (int b = 0; b < XR_HAND_JOINT_COUNT_EXT && !found; b++) { - if (bones[b] == parent) { - q = inv_quaternions[b] * q; - p = inv_quaternions[b].xform(p - positions[b]); - found = true; - } - } - } + // Calculate the relative relationship to the parent bone joint. + const int parent_joint = joints[joint].parent_joint; + const Quaternion q = inv_quaternions[parent_joint] * quaternions[joint]; + const Vector3 p = inv_quaternions[parent_joint].xform(positions[joint] - positions[parent_joint]); - // and set our pose - skeleton->set_bone_pose_position(bones[i], p); - skeleton->set_bone_pose_rotation(bones[i], q); + // Update the bone position if enabled by update mode. + if (bone_update == BONE_UPDATE_FULL) { + skeleton->set_bone_pose_position(joints[joint].bone, p); } + + // Always update the bone rotation. + skeleton->set_bone_pose_rotation(joints[joint].bone, q); } + // Transform the OpenXRHand to the skeleton pose. Transform3D t; t.basis = Basis(quaternions[XR_HAND_JOINT_PALM_EXT]); t.origin = positions[XR_HAND_JOINT_PALM_EXT]; @@ -288,7 +394,7 @@ void OpenXRHand::_update_skeleton() { void OpenXRHand::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { - _get_bones(); + _get_joint_data(); set_process_internal(true); } break; @@ -297,7 +403,8 @@ void OpenXRHand::_notification(int p_what) { // reset for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++) { - bones[i] = -1; + joints[i].bone = -1; + joints[i].parent_joint = -1; } } break; case NOTIFICATION_INTERNAL_PROCESS: { diff --git a/modules/openxr/scene/openxr_hand.h b/modules/openxr/scene/openxr_hand.h index edfb474ac7..4c77e7277c 100644 --- a/modules/openxr/scene/openxr_hand.h +++ b/modules/openxr/scene/openxr_hand.h @@ -55,20 +55,39 @@ public: MOTION_RANGE_MAX }; + enum SkeletonRig { + SKELETON_RIG_OPENXR, + SKELETON_RIG_HUMANOID, + SKELETON_RIG_MAX + }; + + enum BoneUpdate { + BONE_UPDATE_FULL, + BONE_UPDATE_ROTATION_ONLY, + BONE_UPDATE_MAX + }; + private: + struct JointData { + int bone = -1; + int parent_joint = -1; + }; + OpenXRAPI *openxr_api = nullptr; OpenXRHandTrackingExtension *hand_tracking_ext = nullptr; Hands hand = HAND_LEFT; MotionRange motion_range = MOTION_RANGE_UNOBSTRUCTED; NodePath hand_skeleton; + SkeletonRig skeleton_rig = SKELETON_RIG_OPENXR; + BoneUpdate bone_update = BONE_UPDATE_FULL; - int64_t bones[XR_HAND_JOINT_COUNT_EXT]; + JointData joints[XR_HAND_JOINT_COUNT_EXT]; void _set_motion_range(); Skeleton3D *get_skeleton(); - void _get_bones(); + void _get_joint_data(); void _update_skeleton(); protected: @@ -77,19 +96,27 @@ protected: public: OpenXRHand(); - void set_hand(const Hands p_hand); + void set_hand(Hands p_hand); Hands get_hand() const; - void set_motion_range(const MotionRange p_motion_range); + void set_motion_range(MotionRange p_motion_range); MotionRange get_motion_range() const; void set_hand_skeleton(const NodePath &p_hand_skeleton); NodePath get_hand_skeleton() const; + void set_skeleton_rig(SkeletonRig p_skeleton_rig); + SkeletonRig get_skeleton_rig() const; + + void set_bone_update(BoneUpdate p_bone_update); + BoneUpdate get_bone_update() const; + void _notification(int p_what); }; VARIANT_ENUM_CAST(OpenXRHand::Hands) VARIANT_ENUM_CAST(OpenXRHand::MotionRange) +VARIANT_ENUM_CAST(OpenXRHand::SkeletonRig) +VARIANT_ENUM_CAST(OpenXRHand::BoneUpdate) #endif // OPENXR_HAND_H diff --git a/modules/regex/regex.cpp b/modules/regex/regex.cpp index 704c107f20..4a1037431a 100644 --- a/modules/regex/regex.cpp +++ b/modules/regex/regex.cpp @@ -270,16 +270,18 @@ Ref<RegExMatch> RegEx::search(const String &p_subject, int p_offset, int p_end) TypedArray<RegExMatch> RegEx::search_all(const String &p_subject, int p_offset, int p_end) const { ERR_FAIL_COND_V_MSG(p_offset < 0, Array(), "RegEx search offset must be >= 0"); - int last_end = -1; + int last_end = 0; TypedArray<RegExMatch> result; Ref<RegExMatch> match = search(p_subject, p_offset, p_end); + while (match.is_valid()) { - if (last_end == match->get_end(0)) { - break; + last_end = match->get_end(0); + if (match->get_start(0) == last_end) { + last_end++; } + result.push_back(match); - last_end = match->get_end(0); - match = search(p_subject, match->get_end(0), p_end); + match = search(p_subject, last_end, p_end); } return result; } @@ -332,7 +334,7 @@ String RegEx::sub(const String &p_subject, const String &p_replacement, bool p_a return String(); } - return String(output.ptr(), olength); + return String(output.ptr(), olength) + p_subject.substr(length); } bool RegEx::is_valid() const { diff --git a/modules/regex/tests/test_regex.h b/modules/regex/tests/test_regex.h index 3e4d769377..1c305f70f7 100644 --- a/modules/regex/tests/test_regex.h +++ b/modules/regex/tests/test_regex.h @@ -133,6 +133,18 @@ TEST_CASE("[RegEx] Substitution") { RegEx re4("(a)(b){0}(c)"); REQUIRE(re4.is_valid()); CHECK(re4.sub(s4, "${1}.${3}.", true) == "a.c.a.c.a.c."); + + const String s5 = "aaaa"; + + RegEx re5("a"); + REQUIRE(re5.is_valid()); + CHECK(re5.sub(s5, "b", true, 0, 2) == "bbaa"); + CHECK(re5.sub(s5, "b", true, 1, 3) == "abba"); + CHECK(re5.sub(s5, "b", true, 0, 0) == "aaaa"); + CHECK(re5.sub(s5, "b", true, 1, 1) == "aaaa"); + CHECK(re5.sub(s5, "cc", true, 0, 2) == "ccccaa"); + CHECK(re5.sub(s5, "cc", true, 1, 3) == "acccca"); + CHECK(re5.sub(s5, "", true, 0, 2) == "aa"); } TEST_CASE("[RegEx] Substitution with empty input and/or replacement") { @@ -164,7 +176,7 @@ TEST_CASE("[RegEx] Uninitialized use") { ERR_PRINT_ON } -TEST_CASE("[RegEx] Empty Pattern") { +TEST_CASE("[RegEx] Empty pattern") { const String s = "Godot"; RegEx re; @@ -222,6 +234,143 @@ TEST_CASE("[RegEx] Match start and end positions") { CHECK(match->get_start("vowel") == 2); CHECK(match->get_end("vowel") == 3); } + +TEST_CASE("[RegEx] Asterisk search all") { + const String s = "Godot Engine"; + + RegEx re("o*"); + REQUIRE(re.is_valid()); + Ref<RegExMatch> match; + const Array all_results = re.search_all(s); + CHECK(all_results.size() == 13); + + match = all_results[0]; + CHECK(match->get_string(0) == ""); + match = all_results[1]; + CHECK(match->get_string(0) == "o"); + match = all_results[2]; + CHECK(match->get_string(0) == ""); + match = all_results[3]; + CHECK(match->get_string(0) == "o"); + + for (int i = 4; i < 13; i++) { + match = all_results[i]; + CHECK(match->get_string(0) == ""); + } +} + +TEST_CASE("[RegEx] Simple lookahead") { + const String s = "Godot Engine"; + + RegEx re("o(?=t)"); + REQUIRE(re.is_valid()); + Ref<RegExMatch> match = re.search(s); + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 3); + CHECK(match->get_end(0) == 4); +} + +TEST_CASE("[RegEx] Lookahead groups empty matches") { + const String s = "12"; + + RegEx re("(?=(\\d+))"); + REQUIRE(re.is_valid()); + Ref<RegExMatch> match = re.search(s); + CHECK(match->get_string(0) == ""); + CHECK(match->get_string(1) == "12"); + + const Array all_results = re.search_all(s); + CHECK(all_results.size() == 2); + + match = all_results[0]; + REQUIRE(match != nullptr); + CHECK(match->get_string(0) == String("")); + CHECK(match->get_string(1) == String("12")); + + match = all_results[1]; + REQUIRE(match != nullptr); + CHECK(match->get_string(0) == String("")); + CHECK(match->get_string(1) == String("2")); +} + +TEST_CASE("[RegEx] Simple lookbehind") { + const String s = "Godot Engine"; + + RegEx re("(?<=d)o"); + REQUIRE(re.is_valid()); + Ref<RegExMatch> match = re.search(s); + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 3); + CHECK(match->get_end(0) == 4); +} + +TEST_CASE("[RegEx] Simple lookbehind search all") { + const String s = "ababbaabab"; + + RegEx re("(?<=a)b"); + REQUIRE(re.is_valid()); + const Array all_results = re.search_all(s); + CHECK(all_results.size() == 4); + + Ref<RegExMatch> match = all_results[0]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 1); + CHECK(match->get_end(0) == 2); + + match = all_results[1]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 3); + CHECK(match->get_end(0) == 4); + + match = all_results[2]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 7); + CHECK(match->get_end(0) == 8); + + match = all_results[3]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 9); + CHECK(match->get_end(0) == 10); +} + +TEST_CASE("[RegEx] Lookbehind groups empty matches") { + const String s = "abaaabab"; + + RegEx re("(?<=(b))"); + REQUIRE(re.is_valid()); + Ref<RegExMatch> match; + + const Array all_results = re.search_all(s); + CHECK(all_results.size() == 3); + + match = all_results[0]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 2); + CHECK(match->get_end(0) == 2); + CHECK(match->get_start(1) == 1); + CHECK(match->get_end(1) == 2); + CHECK(match->get_string(0) == String("")); + CHECK(match->get_string(1) == String("b")); + + match = all_results[1]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 6); + CHECK(match->get_end(0) == 6); + CHECK(match->get_start(1) == 5); + CHECK(match->get_end(1) == 6); + CHECK(match->get_string(0) == String("")); + CHECK(match->get_string(1) == String("b")); + + match = all_results[2]; + REQUIRE(match != nullptr); + CHECK(match->get_start(0) == 8); + CHECK(match->get_end(0) == 8); + CHECK(match->get_start(1) == 7); + CHECK(match->get_end(1) == 8); + CHECK(match->get_string(0) == String("")); + CHECK(match->get_string(1) == String("b")); +} + } // namespace TestRegEx #endif // TEST_REGEX_H diff --git a/modules/squish/image_decompress_squish.cpp b/modules/squish/image_decompress_squish.cpp index 5b35a2643a..fba76621d6 100644 --- a/modules/squish/image_decompress_squish.cpp +++ b/modules/squish/image_decompress_squish.cpp @@ -36,7 +36,9 @@ void image_decompress_squish(Image *p_image) { int w = p_image->get_width(); int h = p_image->get_height(); + Image::Format source_format = p_image->get_format(); Image::Format target_format = Image::FORMAT_RGBA8; + Vector<uint8_t> data; int target_size = Image::get_image_data_size(w, h, target_format, p_image->has_mipmaps()); int mm_count = p_image->get_mipmap_count(); @@ -45,33 +47,49 @@ void image_decompress_squish(Image *p_image) { const uint8_t *rb = p_image->get_data().ptr(); uint8_t *wb = data.ptrw(); - int squish_flags = Image::FORMAT_MAX; - if (p_image->get_format() == Image::FORMAT_DXT1) { - squish_flags = squish::kDxt1; - } else if (p_image->get_format() == Image::FORMAT_DXT3) { - squish_flags = squish::kDxt3; - } else if (p_image->get_format() == Image::FORMAT_DXT5 || p_image->get_format() == Image::FORMAT_DXT5_RA_AS_RG) { - squish_flags = squish::kDxt5; - } else if (p_image->get_format() == Image::FORMAT_RGTC_R) { - squish_flags = squish::kBc4; - } else if (p_image->get_format() == Image::FORMAT_RGTC_RG) { - squish_flags = squish::kBc5; - } else { - ERR_FAIL_MSG("Squish: Can't decompress unknown format: " + itos(p_image->get_format()) + "."); + int squish_flags = 0; + + switch (source_format) { + case Image::FORMAT_DXT1: + squish_flags = squish::kDxt1; + break; + + case Image::FORMAT_DXT3: + squish_flags = squish::kDxt3; + break; + + case Image::FORMAT_DXT5: + case Image::FORMAT_DXT5_RA_AS_RG: + squish_flags = squish::kDxt5; + break; + + case Image::FORMAT_RGTC_R: + squish_flags = squish::kBc4; + break; + + case Image::FORMAT_RGTC_RG: + squish_flags = squish::kBc5; + break; + + default: + ERR_FAIL_MSG("Squish: Can't decompress unknown format: " + itos(p_image->get_format()) + "."); + break; } for (int i = 0; i <= mm_count; i++) { int src_ofs = 0, mipmap_size = 0, mipmap_w = 0, mipmap_h = 0; p_image->get_mipmap_offset_size_and_dimensions(i, src_ofs, mipmap_size, mipmap_w, mipmap_h); + int dst_ofs = Image::get_image_mipmap_offset(p_image->get_width(), p_image->get_height(), target_format, i); squish::DecompressImage(&wb[dst_ofs], w, h, &rb[src_ofs], squish_flags); + w >>= 1; h >>= 1; } p_image->set_data(p_image->get_width(), p_image->get_height(), p_image->has_mipmaps(), target_format, data); - if (p_image->get_format() == Image::FORMAT_DXT5_RA_AS_RG) { + if (source_format == Image::FORMAT_DXT5_RA_AS_RG) { p_image->convert_ra_rgba8_to_rg(); } } diff --git a/modules/svg/SCsub b/modules/svg/SCsub index d0c6250b11..ff0c3c9141 100644 --- a/modules/svg/SCsub +++ b/modules/svg/SCsub @@ -43,6 +43,8 @@ thirdparty_sources = [ "src/renderer/tvgShape.cpp", "src/renderer/tvgSwCanvas.cpp", "src/renderer/tvgTaskScheduler.cpp", + "src/renderer/tvgText.cpp", + # "src/renderer/tvgWgCanvas.cpp", # renderer sw_engine "src/renderer/sw_engine/tvgSwFill.cpp", "src/renderer/sw_engine/tvgSwImage.cpp", diff --git a/modules/svg/image_loader_svg.cpp b/modules/svg/image_loader_svg.cpp index a542bbc234..affe163aeb 100644 --- a/modules/svg/image_loader_svg.cpp +++ b/modules/svg/image_loader_svg.cpp @@ -72,7 +72,7 @@ Ref<Image> ImageLoaderSVG::load_mem_svg(const uint8_t *p_svg, int p_size, float img.instantiate(); Error err = create_image_from_utf8_buffer(img, p_svg, p_size, p_scale, false); - ERR_FAIL_COND_V(err, Ref<Image>()); + ERR_FAIL_COND_V_MSG(err != OK, Ref<Image>(), vformat("ImageLoaderSVG: Failed to create SVG from buffer, error code %d.", err)); return img; } diff --git a/modules/svg/register_types.cpp b/modules/svg/register_types.cpp index 7b61749f61..82d816d833 100644 --- a/modules/svg/register_types.cpp +++ b/modules/svg/register_types.cpp @@ -34,6 +34,12 @@ #include <thorvg.h> +#ifdef THREADS_ENABLED +#define TVG_THREADS 1 +#else +#define TVG_THREADS 0 +#endif + static Ref<ImageLoaderSVG> image_loader_svg; void initialize_svg_module(ModuleInitializationLevel p_level) { @@ -42,7 +48,8 @@ void initialize_svg_module(ModuleInitializationLevel p_level) { } tvg::CanvasEngine tvgEngine = tvg::CanvasEngine::Sw; - if (tvg::Initializer::init(tvgEngine, 1) != tvg::Result::Success) { + + if (tvg::Initializer::init(tvgEngine, TVG_THREADS) != tvg::Result::Success) { return; } diff --git a/modules/text_server_adv/SCsub b/modules/text_server_adv/SCsub index 3c468e61d7..09d9b2dd67 100644 --- a/modules/text_server_adv/SCsub +++ b/modules/text_server_adv/SCsub @@ -401,6 +401,8 @@ if env["builtin_icu4c"]: "common/uloc.cpp", "common/uloc_keytype.cpp", "common/uloc_tag.cpp", + "common/ulocale.cpp", + "common/ulocbuilder.cpp", "common/umapfile.cpp", "common/umath.cpp", "common/umutablecptrie.cpp", @@ -466,7 +468,7 @@ if env["builtin_icu4c"]: ] thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources] - icu_data_name = "icudt73l.dat" + icu_data_name = "icudt74l.dat" if env.editor_build: env_icu.Depends("#thirdparty/icu4c/icudata.gen.h", "#thirdparty/icu4c/" + icu_data_name) diff --git a/modules/text_server_adv/gdextension_build/SConstruct b/modules/text_server_adv/gdextension_build/SConstruct index 4093842650..cd4331b60e 100644 --- a/modules/text_server_adv/gdextension_build/SConstruct +++ b/modules/text_server_adv/gdextension_build/SConstruct @@ -74,6 +74,8 @@ if env["thorvg_enabled"] and env["freetype_enabled"]: "src/renderer/tvgShape.cpp", "src/renderer/tvgSwCanvas.cpp", "src/renderer/tvgTaskScheduler.cpp", + "src/renderer/tvgText.cpp", + # "src/renderer/tvgWgCanvas.cpp", # renderer sw_engine "src/renderer/sw_engine/tvgSwFill.cpp", "src/renderer/sw_engine/tvgSwImage.cpp", @@ -623,6 +625,8 @@ thirdparty_icu_sources = [ "common/uloc.cpp", "common/uloc_keytype.cpp", "common/uloc_tag.cpp", + "common/ulocale.cpp", + "common/ulocbuilder.cpp", "common/umapfile.cpp", "common/umath.cpp", "common/umutablecptrie.cpp", @@ -688,7 +692,7 @@ thirdparty_icu_sources = [ ] thirdparty_icu_sources = [thirdparty_icu_dir + file for file in thirdparty_icu_sources] -icu_data_name = "icudt73l.dat" +icu_data_name = "icudt74l.dat" if env["static_icu_data"]: env_icu.Depends("../../../thirdparty/icu4c/icudata.gen.h", "../../../thirdparty/icu4c/" + icu_data_name) diff --git a/modules/text_server_adv/gdextension_build/methods.py b/modules/text_server_adv/gdextension_build/methods.py index 3c5229462c..e58bc3abec 100644 --- a/modules/text_server_adv/gdextension_build/methods.py +++ b/modules/text_server_adv/gdextension_build/methods.py @@ -99,8 +99,8 @@ def make_icu_data(target, source, env): def write_macos_plist(target, binary_name, identifier, name): - os.makedirs(f"{target}/Resourece/", exist_ok=True) - f = open(f"{target}/Resourece/Info.plist", "w") + os.makedirs(f"{target}/Resource/", exist_ok=True) + f = open(f"{target}/Resource/Info.plist", "w") f.write(f'<?xml version="1.0" encoding="UTF-8"?>\n') f.write(f'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n') diff --git a/modules/text_server_adv/register_types.h b/modules/text_server_adv/register_types.h index c11765048d..477e030e03 100644 --- a/modules/text_server_adv/register_types.h +++ b/modules/text_server_adv/register_types.h @@ -34,7 +34,7 @@ #ifdef GDEXTENSION #include <godot_cpp/core/class_db.hpp> using namespace godot; -#else +#elif defined(GODOT_MODULE) #include "modules/register_module_types.h" #endif diff --git a/modules/text_server_adv/script_iterator.h b/modules/text_server_adv/script_iterator.h index 164fdec784..f7876d6cbc 100644 --- a/modules/text_server_adv/script_iterator.h +++ b/modules/text_server_adv/script_iterator.h @@ -40,7 +40,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/string/ustring.h" diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 16046ef053..ea88278a17 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -44,7 +44,7 @@ using namespace godot; #define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get_setting_with_override(m_var) -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/config/project_settings.h" @@ -338,6 +338,7 @@ _FORCE_INLINE_ bool is_connected_to_prev(char32_t p_chr, char32_t p_pchr) { /*************************************************************************/ bool TextServerAdvanced::icu_data_loaded = false; +PackedByteArray TextServerAdvanced::icu_data; bool TextServerAdvanced::_has_feature(Feature p_feature) const { switch (p_feature) { @@ -369,7 +370,7 @@ bool TextServerAdvanced::_has_feature(Feature p_feature) const { String TextServerAdvanced::_get_name() const { #ifdef GDEXTENSION return "ICU / HarfBuzz / Graphite (GDExtension)"; -#else +#elif defined(GODOT_MODULE) return "ICU / HarfBuzz / Graphite (Built-in)"; #endif } @@ -438,7 +439,7 @@ bool TextServerAdvanced::_load_support_data(const String &p_filename) { return false; } uint64_t len = f->get_length(); - PackedByteArray icu_data = f->get_buffer(len); + icu_data = f->get_buffer(len); UErrorCode err = U_ZERO_ERROR; udata_setCommonData(icu_data.ptr(), &err); @@ -476,10 +477,10 @@ bool TextServerAdvanced::_save_support_data(const String &p_filename) const { return false; } - PackedByteArray icu_data; - icu_data.resize(U_ICUDATA_SIZE); - memcpy(icu_data.ptrw(), U_ICUDATA_ENTRY_POINT, U_ICUDATA_SIZE); - f->store_buffer(icu_data); + PackedByteArray icu_data_static; + icu_data_static.resize(U_ICUDATA_SIZE); + memcpy(icu_data_static.ptrw(), U_ICUDATA_ENTRY_POINT, U_ICUDATA_SIZE); + f->store_buffer(icu_data_static); return true; #else @@ -806,7 +807,10 @@ _FORCE_INLINE_ TextServerAdvanced::FontTexturePosition TextServerAdvanced::find_ ShelfPackTexture *ct = p_data->textures.ptrw(); for (int32_t i = 0; i < p_data->textures.size(); i++) { - if (p_image_format != ct[i].format) { + if (ct[i].image.is_null()) { + continue; + } + if (p_image_format != ct[i].image->get_format()) { continue; } if (mw > ct[i].texture_w || mh > ct[i].texture_h) { // Too big for this texture. @@ -837,12 +841,11 @@ _FORCE_INLINE_ TextServerAdvanced::FontTexturePosition TextServerAdvanced::find_ } ShelfPackTexture tex = ShelfPackTexture(texsize, texsize); - tex.format = p_image_format; - tex.imgdata.resize(texsize * texsize * p_color_size); + tex.image = Image::create_empty(texsize, texsize, false, p_image_format); { // Zero texture. - uint8_t *w = tex.imgdata.ptrw(); - ERR_FAIL_COND_V(texsize * texsize * p_color_size > tex.imgdata.size(), ret); + uint8_t *w = tex.image->ptrw(); + ERR_FAIL_COND_V(texsize * texsize * p_color_size > tex.image->data_size(), ret); // Initialize the texture to all-white pixels to prevent artifacts when the // font is displayed at a non-default scale with filtering enabled. if (p_color_size == 2) { @@ -1019,12 +1022,12 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf( msdfgen::msdfErrorCorrection(image, shape, projection, p_pixel_range, config); { - uint8_t *wr = tex.imgdata.ptrw(); + uint8_t *wr = tex.image->ptrw(); for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * 4; - ERR_FAIL_COND_V(ofs >= tex.imgdata.size(), FontGlyph()); + ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph()); wr[ofs + 0] = (uint8_t)(CLAMP(image(j, i)[0] * 256.f, 0.f, 255.f)); wr[ofs + 1] = (uint8_t)(CLAMP(image(j, i)[1] * 256.f, 0.f, 255.f)); wr[ofs + 2] = (uint8_t)(CLAMP(image(j, i)[2] * 256.f, 0.f, 255.f)); @@ -1085,12 +1088,12 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitma ShelfPackTexture &tex = p_data->textures.write[tex_pos.index]; { - uint8_t *wr = tex.imgdata.ptrw(); + uint8_t *wr = tex.image->ptrw(); for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * color_size; - ERR_FAIL_COND_V(ofs >= tex.imgdata.size(), FontGlyph()); + ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph()); switch (bitmap.pixel_mode) { case FT_PIXEL_MODE_MONO: { int byte = i * bitmap.pitch + (j >> 3); @@ -1112,14 +1115,14 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitma case FT_PIXEL_MODE_LCD: { int ofs_color = i * bitmap.pitch + (j * 3); if (p_bgra) { - wr[ofs + 0] = bitmap.buffer[ofs_color + 0]; + wr[ofs + 0] = bitmap.buffer[ofs_color + 2]; wr[ofs + 1] = bitmap.buffer[ofs_color + 1]; - wr[ofs + 2] = bitmap.buffer[ofs_color + 2]; + wr[ofs + 2] = bitmap.buffer[ofs_color + 0]; wr[ofs + 3] = 255; } else { - wr[ofs + 0] = bitmap.buffer[ofs_color + 2]; + wr[ofs + 0] = bitmap.buffer[ofs_color + 0]; wr[ofs + 1] = bitmap.buffer[ofs_color + 1]; - wr[ofs + 2] = bitmap.buffer[ofs_color + 0]; + wr[ofs + 2] = bitmap.buffer[ofs_color + 2]; wr[ofs + 3] = 255; } } break; @@ -2408,6 +2411,37 @@ int64_t TextServerAdvanced::_font_get_spacing(const RID &p_font_rid, SpacingType } } +void TextServerAdvanced::_font_set_baseline_offset(const RID &p_font_rid, float p_baseline_offset) { + FontAdvancedLinkedVariation *fdv = font_var_owner.get_or_null(p_font_rid); + if (fdv) { + if (fdv->baseline_offset != p_baseline_offset) { + fdv->baseline_offset = p_baseline_offset; + } + } else { + FontAdvanced *fd = font_owner.get_or_null(p_font_rid); + ERR_FAIL_NULL(fd); + + MutexLock lock(fd->mutex); + if (fd->baseline_offset != p_baseline_offset) { + _font_clear_cache(fd); + fd->baseline_offset = p_baseline_offset; + } + } +} + +float TextServerAdvanced::_font_get_baseline_offset(const RID &p_font_rid) const { + FontAdvancedLinkedVariation *fdv = font_var_owner.get_or_null(p_font_rid); + if (fdv) { + return fdv->baseline_offset; + } else { + FontAdvanced *fd = font_owner.get_or_null(p_font_rid); + ERR_FAIL_NULL_V(fd, 0.0); + + MutexLock lock(fd->mutex); + return fd->baseline_offset; + } +} + void TextServerAdvanced::_font_set_transform(const RID &p_font_rid, const Transform2D &p_transform) { FontAdvanced *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -2719,16 +2753,15 @@ void TextServerAdvanced::_font_set_texture_image(const RID &p_font_rid, const Ve ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; - tex.imgdata = p_image->get_data(); + tex.image = p_image; tex.texture_w = p_image->get_width(); tex.texture_h = p_image->get_height(); - tex.format = p_image->get_format(); - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = p_image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = p_image->duplicate(); img->generate_mipmaps(); } - tex.texture = ImageTexture::create_from_image(img); tex.dirty = false; } @@ -2743,7 +2776,7 @@ Ref<Image> TextServerAdvanced::_font_get_texture_image(const RID &p_font_rid, co ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; - return Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + return tex.image; } void TextServerAdvanced::_font_set_texture_offsets(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const PackedInt32Array &p_offsets) { @@ -3112,8 +3145,9 @@ RID TextServerAdvanced::_font_get_glyph_texture_rid(const RID &p_font_rid, const if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -3158,8 +3192,9 @@ Size2 TextServerAdvanced::_font_get_glyph_texture_size(const RID &p_font_rid, co if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -3242,7 +3277,7 @@ TypedArray<Vector2i> TextServerAdvanced::_font_get_kerning_list(const RID &p_fon ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), TypedArray<Vector2i>()); TypedArray<Vector2i> ret; - for (const KeyValue<Vector2i, FontForSizeAdvanced *> &E : fd->cache) { + for (const KeyValue<Vector2i, Vector2> &E : fd->cache[size]->kerning_map) { ret.push_back(E.key); } return ret; @@ -3503,6 +3538,9 @@ void TextServerAdvanced::_font_render_glyph(const RID &p_font_rid, const Vector2 } void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_canvas, int64_t p_size, const Vector2 &p_pos, int64_t p_index, const Color &p_color) const { + if (p_index == 0) { + return; // Non visual character, skip. + } FontAdvanced *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -3540,20 +3578,24 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca const FontGlyph &gl = fd->cache[size]->glyph_map[index]; if (gl.found) { + if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) { + return; // Nothing to draw. + } ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); if (gl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && (fd->cache[size]->textures[gl.texture_idx].format == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -3607,6 +3649,9 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const RID &p_canvas, int64_t p_size, int64_t p_outline_size, const Vector2 &p_pos, int64_t p_index, const Color &p_color) const { + if (p_index == 0) { + return; // Non visual character, skip. + } FontAdvanced *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -3644,20 +3689,24 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R const FontGlyph &gl = fd->cache[size]->glyph_map[index]; if (gl.found) { + if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) { + return; // Nothing to draw. + } ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); if (gl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && (fd->cache[size]->textures[gl.texture_idx].format == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -4047,6 +4096,20 @@ String TextServerAdvanced::_shaped_text_get_custom_punctuation(const RID &p_shap return sd->custom_punct; } +void TextServerAdvanced::_shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) { + _THREAD_SAFE_METHOD_ + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL(sd); + sd->el_char = p_char; +} + +int64_t TextServerAdvanced::_shaped_text_get_custom_ellipsis(const RID &p_shaped) const { + _THREAD_SAFE_METHOD_ + const ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + return sd->el_char; +} + void TextServerAdvanced::_shaped_text_set_bidi_override(const RID &p_shaped, const Array &p_override) { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL(sd); @@ -4311,6 +4374,8 @@ bool TextServerAdvanced::_shaped_text_resize_object(const RID &p_shaped, const V sd->width += gl.advance * gl.repeat; } } + sd->sort_valid = false; + sd->glyphs_logical.clear(); _realign(sd); } return true; @@ -4523,6 +4588,12 @@ bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const S if ((sd_glyphs[j].start >= bidi_run_start) && (sd_glyphs[j].end <= bidi_run_end)) { // Copy glyphs. Glyph gl = sd_glyphs[j]; + if (gl.end == p_start + p_length && ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) == GRAPHEME_IS_SOFT_HYPHEN)) { + uint32_t index = font_get_glyph_index(gl.font_rid, gl.font_size, 0x00ad, 0); + float w = font_get_glyph_advance(gl.font_rid, gl.font_size, index)[(p_new_sd->orientation == ORIENTATION_HORIZONTAL) ? 0 : 1]; + gl.index = index; + gl.advance = w; + } Variant key; bool find_embedded = false; if (gl.count == 1) { @@ -4643,22 +4714,22 @@ double TextServerAdvanced::_shaped_text_fit_to_width(const RID &p_shaped, double if (p_jst_flags.has_flag(JUSTIFICATION_TRIM_EDGE_SPACES)) { // Trim spaces. - while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { justification_width -= sd->glyphs[start_pos].advance * sd->glyphs[start_pos].repeat; sd->glyphs.write[start_pos].advance = 0; start_pos += sd->glyphs[start_pos].count; } - while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { justification_width -= sd->glyphs[end_pos].advance * sd->glyphs[end_pos].repeat; sd->glyphs.write[end_pos].advance = 0; end_pos -= sd->glyphs[end_pos].count; } } else { // Skip breaks, but do not reset size. - while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { start_pos += sd->glyphs[start_pos].count; } - while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { end_pos -= sd->glyphs[end_pos].count; } } @@ -4674,7 +4745,7 @@ double TextServerAdvanced::_shaped_text_fit_to_width(const RID &p_shaped, double elongation_count++; } } - if ((gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { + if ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN && (gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { space_count++; } } @@ -4707,7 +4778,7 @@ double TextServerAdvanced::_shaped_text_fit_to_width(const RID &p_shaped, double for (int i = start_pos; i <= end_pos; i++) { Glyph &gl = sd->glyphs.write[i]; if (gl.count > 0) { - if ((gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { + if ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN && (gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { double old_adv = gl.advance; double new_advance; if ((gl.flags & GRAPHEME_IS_VIRTUAL) == GRAPHEME_IS_VIRTUAL) { @@ -4797,6 +4868,166 @@ double TextServerAdvanced::_shaped_text_tab_align(const RID &p_shaped, const Pac return 0.0; } +RID TextServerAdvanced::_find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text) { + RID f; + // Try system fallback. + String font_name = _font_get_name(p_fdef); + BitField<FontStyle> font_style = _font_get_style(p_fdef); + int font_weight = _font_get_weight(p_fdef); + int font_stretch = _font_get_stretch(p_fdef); + Dictionary dvar = _font_get_variation_coordinates(p_fdef); + static int64_t wgth_tag = _name_to_tag("weight"); + static int64_t wdth_tag = _name_to_tag("width"); + static int64_t ital_tag = _name_to_tag("italic"); + if (dvar.has(wgth_tag)) { + font_weight = dvar[wgth_tag].operator int(); + } + if (dvar.has(wdth_tag)) { + font_stretch = dvar[wdth_tag].operator int(); + } + if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) { + font_style.set_flag(TextServer::FONT_ITALIC); + } + + String locale = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language; + PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, p_text, locale, p_script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC); +#ifdef GDEXTENSION + for (int fb = 0; fb < fallback_font_name.size(); fb++) { + const String &E = fallback_font_name[fb]; +#elif defined(GODOT_MODULE) + for (const String &E : fallback_font_name) { +#endif + SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, p_fdef, this); + if (system_fonts.has(key)) { + const SystemFontCache &sysf_cache = system_fonts[key]; + int best_score = 0; + int best_match = -1; + for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) { + const SystemFontCacheRec &F = sysf_cache.var[face_idx]; + if (unlikely(!_font_has_char(F.rid, p_text[0]))) { + continue; + } + BitField<FontStyle> style = _font_get_style(F.rid); + int weight = _font_get_weight(F.rid); + int stretch = _font_get_stretch(F.rid); + int score = (20 - Math::abs(weight - font_weight) / 50); + score += (20 - Math::abs(stretch - font_stretch) / 10); + if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { + score += 30; + } + if (score >= best_score) { + best_score = score; + best_match = face_idx; + } + if (best_score == 70) { + break; + } + } + if (best_match != -1) { + f = sysf_cache.var[best_match].rid; + } + } + if (!f.is_valid()) { + if (system_fonts.has(key)) { + const SystemFontCache &sysf_cache = system_fonts[key]; + if (sysf_cache.max_var == sysf_cache.var.size()) { + // All subfonts already tested, skip. + continue; + } + } + + if (!system_font_data.has(E)) { + system_font_data[E] = FileAccess::get_file_as_bytes(E); + } + + const PackedByteArray &font_data = system_font_data[E]; + + SystemFontCacheRec sysf; + sysf.rid = _create_font(); + _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); + + Dictionary var = dvar; + // Select matching style from collection. + int best_score = 0; + int best_match = -1; + for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) { + _font_set_face_index(sysf.rid, face_idx); + if (unlikely(!_font_has_char(sysf.rid, p_text[0]))) { + continue; + } + BitField<FontStyle> style = _font_get_style(sysf.rid); + int weight = _font_get_weight(sysf.rid); + int stretch = _font_get_stretch(sysf.rid); + int score = (20 - Math::abs(weight - font_weight) / 50); + score += (20 - Math::abs(stretch - font_stretch) / 10); + if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { + score += 30; + } + if (score >= best_score) { + best_score = score; + best_match = face_idx; + } + if (best_score == 70) { + break; + } + } + if (best_match == -1) { + _free_rid(sysf.rid); + continue; + } else { + _font_set_face_index(sysf.rid, best_match); + } + sysf.index = best_match; + + // If it's a variable font, apply weight, stretch and italic coordinates to match requested style. + if (best_score != 70) { + Dictionary ftr = _font_supported_variation_list(sysf.rid); + if (ftr.has(wdth_tag)) { + var[wdth_tag] = font_stretch; + _font_set_stretch(sysf.rid, font_stretch); + } + if (ftr.has(wgth_tag)) { + var[wgth_tag] = font_weight; + _font_set_weight(sysf.rid, font_weight); + } + if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) { + var[ital_tag] = 1; + _font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC); + } + } + + _font_set_antialiasing(sysf.rid, key.antialiasing); + _font_set_generate_mipmaps(sysf.rid, key.mipmaps); + _font_set_multichannel_signed_distance_field(sysf.rid, key.msdf); + _font_set_msdf_pixel_range(sysf.rid, key.msdf_range); + _font_set_msdf_size(sysf.rid, key.msdf_source_size); + _font_set_fixed_size(sysf.rid, key.fixed_size); + _font_set_force_autohinter(sysf.rid, key.force_autohinter); + _font_set_hinting(sysf.rid, key.hinting); + _font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning); + _font_set_variation_coordinates(sysf.rid, var); + _font_set_oversampling(sysf.rid, key.oversampling); + _font_set_embolden(sysf.rid, key.embolden); + _font_set_transform(sysf.rid, key.transform); + _font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]); + _font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]); + _font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]); + _font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]); + + if (system_fonts.has(key)) { + system_fonts[key].var.push_back(sysf); + } else { + SystemFontCache &sysf_cache = system_fonts[key]; + sysf_cache.max_var = _font_get_face_count(sysf.rid); + sysf_cache.var.push_back(sysf); + } + f = sysf.rid; + } + break; + } + return f; +} + void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_line, double p_width, BitField<TextServer::TextOverrunFlag> p_trim_flags) { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped_line); ERR_FAIL_NULL_MSG(sd, "ShapedTextDataAdvanced invalid."); @@ -4839,20 +5070,52 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ int sd_size = sd->glyphs.size(); int last_gl_font_size = sd_glyphs[sd_size - 1].font_size; + bool found_el_char = false; // Find usable fonts, if fonts from the last glyph do not have required chars. RID dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; - if (!_font_has_char(dot_gl_font_rid, '.')) { + if (!_font_has_char(dot_gl_font_rid, sd->el_char)) { const Array &fonts = spans[spans.size() - 1].fonts; for (int i = 0; i < fonts.size(); i++) { - if (_font_has_char(fonts[i], '.')) { + if (_font_has_char(fonts[i], sd->el_char)) { dot_gl_font_rid = fonts[i]; + found_el_char = true; break; } } + if (!found_el_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) { + const char32_t u32str[] = { sd->el_char, 0 }; + RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, u32str); + if (rid.is_valid()) { + dot_gl_font_rid = rid; + found_el_char = true; + } + } + } else { + found_el_char = true; + } + if (!found_el_char) { + bool found_dot_char = false; + dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; + if (!_font_has_char(dot_gl_font_rid, '.')) { + const Array &fonts = spans[spans.size() - 1].fonts; + for (int i = 0; i < fonts.size(); i++) { + if (_font_has_char(fonts[i], '.')) { + dot_gl_font_rid = fonts[i]; + found_dot_char = true; + break; + } + } + if (!found_dot_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) { + RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, "."); + if (rid.is_valid()) { + dot_gl_font_rid = rid; + } + } + } } RID whitespace_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; - if (!_font_has_char(whitespace_gl_font_rid, '.')) { + if (!_font_has_char(whitespace_gl_font_rid, ' ')) { const Array &fonts = spans[spans.size() - 1].fonts; for (int i = 0; i < fonts.size(); i++) { if (_font_has_char(fonts[i], ' ')) { @@ -4862,14 +5125,14 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ } } - int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, '.', 0) : -10; + int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, (found_el_char ? sd->el_char : '.'), 0) : -1; Vector2 dot_adv = dot_gl_font_rid.is_valid() ? _font_get_glyph_advance(dot_gl_font_rid, last_gl_font_size, dot_gl_idx) : Vector2(); - int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -10; + int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -1; Vector2 whitespace_adv = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_advance(whitespace_gl_font_rid, last_gl_font_size, whitespace_gl_idx) : Vector2(); int ellipsis_width = 0; if (add_ellipsis && whitespace_gl_font_rid.is_valid()) { - ellipsis_width = 3 * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0); + ellipsis_width = (found_el_char ? 1 : 3) * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0); } int ell_min_characters = 6; @@ -4948,7 +5211,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if (dot_gl_idx != 0) { Glyph gl; gl.count = 1; - gl.repeat = 3; + gl.repeat = (found_el_char ? 1 : 3); gl.advance = dot_adv.x; gl.index = dot_gl_idx; gl.font_rid = dot_gl_font_rid; @@ -5163,6 +5426,9 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { if (c == 0x0009 || c == 0x000b) { sd_glyphs_new[sd_shift + i].flags |= GRAPHEME_IS_TAB; } + if (c == 0x00ad) { + sd_glyphs_new[sd_shift + i].flags |= GRAPHEME_IS_SOFT_HYPHEN; + } if (is_whitespace(c)) { sd_glyphs_new[sd_shift + i].flags |= GRAPHEME_IS_SPACE; } @@ -5184,7 +5450,7 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { if (sd->breaks.has(sd_glyphs[i].end)) { if (sd->breaks[sd_glyphs[i].end] && (is_linebreak(c))) { sd_glyphs_new[sd_shift + i].flags |= GRAPHEME_IS_BREAK_HARD; - } else if (is_whitespace(c)) { + } else if (is_whitespace(c) || c == 0x00ad) { sd_glyphs_new[sd_shift + i].flags |= GRAPHEME_IS_BREAK_SOFT; } else { int count = sd_glyphs[i].count; @@ -5403,7 +5669,7 @@ bool TextServerAdvanced::_shaped_text_update_justification_ops(const RID &p_shap sd_glyphs[i].flags |= GRAPHEME_IS_ELONGATION; } if (sd->jstops.has(sd_glyphs[i].start)) { - if (c == 0xfffc) { + if (c == 0xfffc || c == 0x00ad) { continue; } if (sd->jstops[sd_glyphs[i].start]) { @@ -5529,6 +5795,11 @@ Glyph TextServerAdvanced::_shape_single_glyph(ShapedTextDataAdvanced *p_sd, char gl.x_off = Math::round((double)glyph_pos[0].x_offset / (64.0 / scale)); } gl.y_off = -Math::round((double)glyph_pos[0].y_offset / (64.0 / scale)); + if (p_sd->orientation == ORIENTATION_HORIZONTAL) { + gl.y_off += _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + } else { + gl.x_off += _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + } if ((glyph_info[0].codepoint != 0) || !u_isgraph(p_char)) { gl.flags |= GRAPHEME_IS_VALID; @@ -5557,7 +5828,7 @@ _FORCE_INLINE_ void TextServerAdvanced::_add_featuers(const Dictionary &p_source } } -void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_end, hb_script_t p_script, hb_direction_t p_direction, TypedArray<RID> p_fonts, int64_t p_span, int64_t p_fb_index, int64_t p_prev_start, int64_t p_prev_end) { +void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_end, hb_script_t p_script, hb_direction_t p_direction, TypedArray<RID> p_fonts, int64_t p_span, int64_t p_fb_index, int64_t p_prev_start, int64_t p_prev_end, RID p_prev_font) { RID f; int fs = p_sd->spans[p_span].font_size; @@ -5577,193 +5848,81 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star break; } } - String text = p_sd->text.substr(p_start, next - p_start); - - String font_name = _font_get_name(fdef); - BitField<FontStyle> font_style = _font_get_style(fdef); - int font_weight = _font_get_weight(fdef); - int font_stretch = _font_get_stretch(fdef); - Dictionary dvar = _font_get_variation_coordinates(fdef); - static int64_t wgth_tag = _name_to_tag("weight"); - static int64_t wdth_tag = _name_to_tag("width"); - static int64_t ital_tag = _name_to_tag("italic"); - if (dvar.has(wgth_tag)) { - font_weight = dvar[wgth_tag].operator int(); - } - if (dvar.has(wdth_tag)) { - font_stretch = dvar[wdth_tag].operator int(); - } - if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) { - font_style.set_flag(TextServer::FONT_ITALIC); - } - char scr_buffer[5] = { 0, 0, 0, 0, 0 }; hb_tag_to_string(hb_script_to_iso15924_tag(p_script), scr_buffer); String script_code = String(scr_buffer); - String locale = (p_sd->spans[p_span].language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_sd->spans[p_span].language; - - PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, text, locale, script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC); -#ifdef GDEXTENSION - for (int fb = 0; fb < fallback_font_name.size(); fb++) { - const String &E = fallback_font_name[fb]; -#else - for (const String &E : fallback_font_name) { -#endif - SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, fdef, this); - if (system_fonts.has(key)) { - const SystemFontCache &sysf_cache = system_fonts[key]; - int best_score = 0; - int best_match = -1; - for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) { - const SystemFontCacheRec &F = sysf_cache.var[face_idx]; - if (unlikely(!_font_has_char(F.rid, text[0]))) { - continue; - } - BitField<FontStyle> style = _font_get_style(F.rid); - int weight = _font_get_weight(F.rid); - int stretch = _font_get_stretch(F.rid); - int score = (20 - Math::abs(weight - font_weight) / 50); - score += (20 - Math::abs(stretch - font_stretch) / 10); - if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { - score += 30; - } - if (score >= best_score) { - best_score = score; - best_match = face_idx; - } - if (best_score == 70) { - break; - } - } - if (best_match != -1) { - f = sysf_cache.var[best_match].rid; - } - } - if (!f.is_valid()) { - if (system_fonts.has(key)) { - const SystemFontCache &sysf_cache = system_fonts[key]; - if (sysf_cache.max_var == sysf_cache.var.size()) { - // All subfonts already tested, skip. - continue; - } - } - - if (!system_font_data.has(E)) { - system_font_data[E] = FileAccess::get_file_as_bytes(E); - } - - const PackedByteArray &font_data = system_font_data[E]; - - SystemFontCacheRec sysf; - sysf.rid = _create_font(); - _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); - Dictionary var = dvar; - // Select matching style from collection. - int best_score = 0; - int best_match = -1; - for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) { - _font_set_face_index(sysf.rid, face_idx); - if (unlikely(!_font_has_char(sysf.rid, text[0]))) { - continue; - } - BitField<FontStyle> style = _font_get_style(sysf.rid); - int weight = _font_get_weight(sysf.rid); - int stretch = _font_get_stretch(sysf.rid); - int score = (20 - Math::abs(weight - font_weight) / 50); - score += (20 - Math::abs(stretch - font_stretch) / 10); - if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { - score += 30; - } - if (score >= best_score) { - best_score = score; - best_match = face_idx; - } - if (best_score == 70) { - break; - } - } - if (best_match == -1) { - _free_rid(sysf.rid); - continue; - } else { - _font_set_face_index(sysf.rid, best_match); - } - sysf.index = best_match; - - // If it's a variable font, apply weight, stretch and italic coordinates to match requested style. - if (best_score != 70) { - Dictionary ftr = _font_supported_variation_list(sysf.rid); - if (ftr.has(wdth_tag)) { - var[wdth_tag] = font_stretch; - _font_set_stretch(sysf.rid, font_stretch); - } - if (ftr.has(wgth_tag)) { - var[wgth_tag] = font_weight; - _font_set_weight(sysf.rid, font_weight); - } - if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) { - var[ital_tag] = 1; - _font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC); - } - } - - _font_set_antialiasing(sysf.rid, key.antialiasing); - _font_set_generate_mipmaps(sysf.rid, key.mipmaps); - _font_set_multichannel_signed_distance_field(sysf.rid, key.msdf); - _font_set_msdf_pixel_range(sysf.rid, key.msdf_range); - _font_set_msdf_size(sysf.rid, key.msdf_source_size); - _font_set_fixed_size(sysf.rid, key.fixed_size); - _font_set_force_autohinter(sysf.rid, key.force_autohinter); - _font_set_hinting(sysf.rid, key.hinting); - _font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning); - _font_set_variation_coordinates(sysf.rid, var); - _font_set_oversampling(sysf.rid, key.oversampling); - _font_set_embolden(sysf.rid, key.embolden); - _font_set_transform(sysf.rid, key.transform); - _font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]); - _font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]); - _font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]); - _font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]); - - if (system_fonts.has(key)) { - system_fonts[key].var.push_back(sysf); - } else { - SystemFontCache &sysf_cache = system_fonts[key]; - sysf_cache.max_var = _font_get_face_count(sysf.rid); - sysf_cache.var.push_back(sysf); - } - f = sysf.rid; - } - break; - } + String text = p_sd->text.substr(p_start, next - p_start); + f = _find_sys_font_for_text(fdef, script_code, p_sd->spans[p_span].language, text); } } if (!f.is_valid()) { - // No valid font, use fallback hex code boxes. - for (int i = p_start; i < p_end; i++) { + // Shaping failed, try looking up raw characters or use fallback hex code boxes. + int fb_from = (p_direction != HB_DIRECTION_RTL) ? p_start : p_end - 1; + int fb_to = (p_direction != HB_DIRECTION_RTL) ? p_end : p_start - 1; + int fb_delta = (p_direction != HB_DIRECTION_RTL) ? +1 : -1; + + for (int i = fb_from; i != fb_to; i += fb_delta) { if (p_sd->preserve_invalid || (p_sd->preserve_control && is_control(p_sd->text[i]))) { Glyph gl; gl.start = i + p_sd->start; gl.end = i + 1 + p_sd->start; gl.count = 1; - gl.index = p_sd->text[i]; gl.font_size = fs; - gl.font_rid = RID(); if (p_direction == HB_DIRECTION_RTL || p_direction == HB_DIRECTION_BTT) { gl.flags |= TextServer::GRAPHEME_IS_RTL; } - if (p_sd->orientation == ORIENTATION_HORIZONTAL) { - gl.advance = get_hex_code_box_size(fs, gl.index).x; - p_sd->ascent = MAX(p_sd->ascent, get_hex_code_box_size(fs, gl.index).y); - } else { - gl.advance = get_hex_code_box_size(fs, gl.index).y; - gl.y_off = get_hex_code_box_size(fs, gl.index).y; - gl.x_off = -Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5); - p_sd->ascent = MAX(p_sd->ascent, Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5)); - p_sd->descent = MAX(p_sd->descent, Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5)); + + bool found = false; + for (int j = 0; j <= p_fonts.size(); j++) { + RID f_rid; + if (j == p_fonts.size()) { + f_rid = p_prev_font; + } else { + f_rid = p_fonts[j]; + } + if (f_rid.is_valid() && _font_has_char(f_rid, p_sd->text[i])) { + gl.font_rid = f_rid; + gl.index = _font_get_glyph_index(gl.font_rid, fs, p_sd->text[i], 0); + if (p_sd->orientation == ORIENTATION_HORIZONTAL) { + gl.advance = _font_get_glyph_advance(gl.font_rid, fs, gl.index).x; + gl.x_off = 0; + gl.y_off = _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + p_sd->ascent = MAX(p_sd->ascent, _font_get_ascent(gl.font_rid, gl.font_size) + _font_get_spacing(gl.font_rid, SPACING_TOP)); + p_sd->descent = MAX(p_sd->descent, _font_get_descent(gl.font_rid, gl.font_size) + _font_get_spacing(gl.font_rid, SPACING_BOTTOM)); + } else { + gl.advance = _font_get_glyph_advance(gl.font_rid, fs, gl.index).y; + gl.x_off = -Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5) + _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + gl.y_off = _font_get_ascent(gl.font_rid, gl.font_size); + p_sd->ascent = MAX(p_sd->ascent, Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5)); + p_sd->descent = MAX(p_sd->descent, Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5)); + } + double scale = _font_get_scale(gl.font_rid, fs); + bool subpos = (scale != 1.0) || (_font_get_subpixel_positioning(gl.font_rid) == SUBPIXEL_POSITIONING_ONE_HALF) || (_font_get_subpixel_positioning(gl.font_rid) == SUBPIXEL_POSITIONING_ONE_QUARTER) || (_font_get_subpixel_positioning(gl.font_rid) == SUBPIXEL_POSITIONING_AUTO && fs <= SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE); + if (!subpos) { + gl.advance = Math::round(gl.advance); + gl.x_off = Math::round(gl.x_off); + } + found = true; + break; + } } + if (!found) { + gl.font_rid = RID(); + gl.index = p_sd->text[i]; + if (p_sd->orientation == ORIENTATION_HORIZONTAL) { + gl.advance = get_hex_code_box_size(fs, gl.index).x; + p_sd->ascent = MAX(p_sd->ascent, get_hex_code_box_size(fs, gl.index).y); + } else { + gl.advance = get_hex_code_box_size(fs, gl.index).y; + gl.y_off = get_hex_code_box_size(fs, gl.index).y; + gl.x_off = -Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5); + p_sd->ascent = MAX(p_sd->ascent, Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5)); + p_sd->descent = MAX(p_sd->descent, Math::round(get_hex_code_box_size(fs, gl.index).x * 0.5)); + } + } + p_sd->width += gl.advance; p_sd->glyphs.push_back(gl); @@ -5897,6 +6056,11 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star gl.x_off = Math::round((double)glyph_pos[i].x_offset / (64.0 / scale)); } gl.y_off = -Math::round((double)glyph_pos[i].y_offset / (64.0 / scale)); + if (p_sd->orientation == ORIENTATION_HORIZONTAL) { + gl.y_off += _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + } else { + gl.x_off += _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); + } } if (!last_run || i < glyph_count - 1) { // Do not add extra spacing to the last glyph of the string. @@ -5933,7 +6097,7 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star for (unsigned int i = 0; i < glyph_count; i++) { if ((w[i].flags & GRAPHEME_IS_VALID) == GRAPHEME_IS_VALID) { if (failed_subrun_start != p_end + 1) { - _shape_run(p_sd, failed_subrun_start, failed_subrun_end, p_script, p_direction, p_fonts, p_span, p_fb_index + 1, p_start, p_end); + _shape_run(p_sd, failed_subrun_start, failed_subrun_end, p_script, p_direction, p_fonts, p_span, p_fb_index + 1, p_start, p_end, (p_fb_index >= p_fonts.size()) ? f : RID()); failed_subrun_start = p_end + 1; failed_subrun_end = p_start; } @@ -5963,7 +6127,7 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star } memfree(w); if (failed_subrun_start != p_end + 1) { - _shape_run(p_sd, failed_subrun_start, failed_subrun_end, p_script, p_direction, p_fonts, p_span, p_fb_index + 1, p_start, p_end); + _shape_run(p_sd, failed_subrun_start, failed_subrun_end, p_script, p_direction, p_fonts, p_span, p_fb_index + 1, p_start, p_end, (p_fb_index >= p_fonts.size()) ? f : RID()); } p_sd->ascent = MAX(p_sd->ascent, _font_get_ascent(f, fs) + _font_get_spacing(f, SPACING_TOP)); p_sd->descent = MAX(p_sd->descent, _font_get_descent(f, fs) + _font_get_spacing(f, SPACING_BOTTOM)); @@ -6087,21 +6251,21 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { for (int i = 0; i < bidi_run_count; i++) { int32_t _bidi_run_start = 0; int32_t _bidi_run_length = end - start; - bool is_rtl = false; + bool is_ltr = false; hb_direction_t bidi_run_direction = HB_DIRECTION_INVALID; if (bidi_iter) { - is_rtl = (ubidi_getVisualRun(bidi_iter, i, &_bidi_run_start, &_bidi_run_length) == UBIDI_LTR); + is_ltr = (ubidi_getVisualRun(bidi_iter, i, &_bidi_run_start, &_bidi_run_length) == UBIDI_LTR); } switch (sd->orientation) { case ORIENTATION_HORIZONTAL: { - if (is_rtl) { + if (is_ltr) { bidi_run_direction = HB_DIRECTION_LTR; } else { bidi_run_direction = HB_DIRECTION_RTL; } } break; case ORIENTATION_VERTICAL: { - if (is_rtl) { + if (is_ltr) { bidi_run_direction = HB_DIRECTION_TTB; } else { bidi_run_direction = HB_DIRECTION_BTT; @@ -6114,9 +6278,9 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { // Shape runs. - int scr_from = (is_rtl) ? 0 : sd->script_iter->script_ranges.size() - 1; - int scr_to = (is_rtl) ? sd->script_iter->script_ranges.size() : -1; - int scr_delta = (is_rtl) ? +1 : -1; + int scr_from = (is_ltr) ? 0 : sd->script_iter->script_ranges.size() - 1; + int scr_to = (is_ltr) ? sd->script_iter->script_ranges.size() : -1; + int scr_delta = (is_ltr) ? +1 : -1; for (int j = scr_from; j != scr_to; j += scr_delta) { if ((sd->script_iter->script_ranges[j].start < bidi_run_end) && (sd->script_iter->script_ranges[j].end > bidi_run_start)) { @@ -6126,9 +6290,9 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { hb_tag_to_string(hb_script_to_iso15924_tag(sd->script_iter->script_ranges[j].script), scr_buffer); String script_code = String(scr_buffer); - int spn_from = (is_rtl) ? 0 : sd->spans.size() - 1; - int spn_to = (is_rtl) ? sd->spans.size() : -1; - int spn_delta = (is_rtl) ? +1 : -1; + int spn_from = (is_ltr) ? 0 : sd->spans.size() - 1; + int spn_to = (is_ltr) ? sd->spans.size() : -1; + int spn_delta = (is_ltr) ? +1 : -1; for (int k = spn_from; k != spn_to; k += spn_delta) { const ShapedTextDataAdvanced::Span &span = sd->spans[k]; @@ -6176,7 +6340,7 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { } fonts.append_array(fonts_scr_only); fonts.append_array(fonts_no_match); - _shape_run(sd, MAX(sd->spans[k].start - sd->start, script_run_start), MIN(sd->spans[k].end - sd->start, script_run_end), sd->script_iter->script_ranges[j].script, bidi_run_direction, fonts, k, 0, 0, 0); + _shape_run(sd, MAX(sd->spans[k].start - sd->start, script_run_start), MIN(sd->spans[k].end - sd->start, script_run_end), sd->script_iter->script_ranges[j].script, bidi_run_direction, fonts, k, 0, 0, 0, RID()); } } } @@ -6690,7 +6854,7 @@ String TextServerAdvanced::_strip_diacritics(const String &p_string) const { if (u_getCombiningClass(normalized_string[i]) == 0) { #ifdef GDEXTENSION result = result + String::chr(normalized_string[i]); -#else +#elif defined(GODOT_MODULE) result = result + normalized_string[i]; #endif } diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index f1932d9c50..3f04c17d9c 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -78,7 +78,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/extension/ext_wrappers.gen.inc" @@ -158,6 +158,7 @@ class TextServerAdvanced : public TextServerExtension { // ICU support data. static bool icu_data_loaded; + static PackedByteArray icu_data; mutable USet *allowed = nullptr; mutable USpoofChecker *sc_spoof = nullptr; mutable USpoofChecker *sc_conf = nullptr; @@ -205,8 +206,7 @@ class TextServerAdvanced : public TextServerExtension { int32_t texture_w = 1024; int32_t texture_h = 1024; - Image::Format format; - PackedByteArray imgdata; + Ref<Image> image; Ref<ImageTexture> texture; bool dirty = true; @@ -296,6 +296,7 @@ class TextServerAdvanced : public TextServerExtension { struct FontAdvancedLinkedVariation { RID base_font; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; }; struct FontAdvanced { @@ -323,6 +324,7 @@ class TextServerAdvanced : public TextServerExtension { int weight = 400; int stretch = 100; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; HashMap<Vector2i, FontForSizeAdvanced *, VariantHasher, VariantComparator> cache; @@ -501,6 +503,7 @@ class TextServerAdvanced : public TextServerExtension { double upos = 0.0; double uthk = 0.0; + char32_t el_char = 0x2026; TrimData overrun_trim_data; bool fit_width_minimum_reached = false; @@ -572,9 +575,10 @@ class TextServerAdvanced : public TextServerExtension { double embolden = 0.0; Transform2D transform; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; bool operator==(const SystemFontKey &p_b) const { - return (font_name == p_b.font_name) && (antialiasing == p_b.antialiasing) && (italic == p_b.italic) && (mipmaps == p_b.mipmaps) && (msdf == p_b.msdf) && (force_autohinter == p_b.force_autohinter) && (weight == p_b.weight) && (stretch == p_b.stretch) && (msdf_range == p_b.msdf_range) && (msdf_source_size == p_b.msdf_source_size) && (fixed_size == p_b.fixed_size) && (hinting == p_b.hinting) && (subpixel_positioning == p_b.subpixel_positioning) && (variation_coordinates == p_b.variation_coordinates) && (oversampling == p_b.oversampling) && (embolden == p_b.embolden) && (transform == p_b.transform) && (extra_spacing[SPACING_TOP] == p_b.extra_spacing[SPACING_TOP]) && (extra_spacing[SPACING_BOTTOM] == p_b.extra_spacing[SPACING_BOTTOM]) && (extra_spacing[SPACING_SPACE] == p_b.extra_spacing[SPACING_SPACE]) && (extra_spacing[SPACING_GLYPH] == p_b.extra_spacing[SPACING_GLYPH]); + return (font_name == p_b.font_name) && (antialiasing == p_b.antialiasing) && (italic == p_b.italic) && (mipmaps == p_b.mipmaps) && (msdf == p_b.msdf) && (force_autohinter == p_b.force_autohinter) && (weight == p_b.weight) && (stretch == p_b.stretch) && (msdf_range == p_b.msdf_range) && (msdf_source_size == p_b.msdf_source_size) && (fixed_size == p_b.fixed_size) && (hinting == p_b.hinting) && (subpixel_positioning == p_b.subpixel_positioning) && (variation_coordinates == p_b.variation_coordinates) && (oversampling == p_b.oversampling) && (embolden == p_b.embolden) && (transform == p_b.transform) && (extra_spacing[SPACING_TOP] == p_b.extra_spacing[SPACING_TOP]) && (extra_spacing[SPACING_BOTTOM] == p_b.extra_spacing[SPACING_BOTTOM]) && (extra_spacing[SPACING_SPACE] == p_b.extra_spacing[SPACING_SPACE]) && (extra_spacing[SPACING_GLYPH] == p_b.extra_spacing[SPACING_GLYPH]) && (baseline_offset == p_b.baseline_offset); } SystemFontKey(const String &p_font_name, bool p_italic, int p_weight, int p_stretch, RID p_font, const TextServerAdvanced *p_fb) { @@ -599,6 +603,7 @@ class TextServerAdvanced : public TextServerExtension { extra_spacing[SPACING_BOTTOM] = p_fb->_font_get_spacing(p_font, SPACING_BOTTOM); extra_spacing[SPACING_SPACE] = p_fb->_font_get_spacing(p_font, SPACING_SPACE); extra_spacing[SPACING_GLYPH] = p_fb->_font_get_spacing(p_font, SPACING_GLYPH); + baseline_offset = p_fb->_font_get_baseline_offset(p_font); } }; @@ -631,7 +636,7 @@ class TextServerAdvanced : public TextServerExtension { hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_BOTTOM], hash); hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_SPACE], hash); hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_GLYPH], hash); - + hash = hash_murmur3_one_double(p_a.baseline_offset, hash); return hash_fmix32(hash_murmur3_one_32(((int)p_a.mipmaps) | ((int)p_a.msdf << 1) | ((int)p_a.italic << 2) | ((int)p_a.force_autohinter << 3) | ((int)p_a.hinting << 4) | ((int)p_a.subpixel_positioning << 8) | ((int)p_a.antialiasing << 12), hash)); } }; @@ -644,8 +649,9 @@ class TextServerAdvanced : public TextServerExtension { int64_t _convert_pos(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const; int64_t _convert_pos_inv(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const; bool _shape_substr(ShapedTextDataAdvanced *p_new_sd, const ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_length) const; - void _shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_end, hb_script_t p_script, hb_direction_t p_direction, TypedArray<RID> p_fonts, int64_t p_span, int64_t p_fb_index, int64_t p_prev_start, int64_t p_prev_end); + void _shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_end, hb_script_t p_script, hb_direction_t p_direction, TypedArray<RID> p_fonts, int64_t p_span, int64_t p_fb_index, int64_t p_prev_start, int64_t p_prev_end, RID p_prev_font); Glyph _shape_single_glyph(ShapedTextDataAdvanced *p_sd, char32_t p_char, hb_script_t p_script, hb_direction_t p_direction, const RID &p_font, int64_t p_font_size); + _FORCE_INLINE_ RID _find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text); _FORCE_INLINE_ void _add_featuers(const Dictionary &p_source, Vector<hb_feature_t> &r_ftrs); @@ -680,11 +686,7 @@ class TextServerAdvanced : public TextServerExtension { _FORCE_INLINE_ bool operator()(const Glyph &l, const Glyph &r) const { if (l.start == r.start) { if (l.count == r.count) { - if ((l.flags & TextServer::GRAPHEME_IS_VIRTUAL) == TextServer::GRAPHEME_IS_VIRTUAL) { - return false; - } else { - return true; - } + return (l.flags & TextServer::GRAPHEME_IS_VIRTUAL) < (r.flags & TextServer::GRAPHEME_IS_VIRTUAL); } return l.count > r.count; // Sort first glyph with count & flags, order of the rest are irrelevant. } else { @@ -782,6 +784,9 @@ public: MODBIND3(font_set_spacing, const RID &, SpacingType, int64_t); MODBIND2RC(int64_t, font_get_spacing, const RID &, SpacingType); + MODBIND2(font_set_baseline_offset, const RID &, float); + MODBIND1RC(float, font_get_baseline_offset, const RID &); + MODBIND2(font_set_transform, const RID &, const Transform2D &); MODBIND1RC(Transform2D, font_get_transform, const RID &); @@ -902,6 +907,9 @@ public: MODBIND2(shaped_text_set_custom_punctuation, const RID &, const String &); MODBIND1RC(String, shaped_text_get_custom_punctuation, const RID &); + MODBIND2(shaped_text_set_custom_ellipsis, const RID &, int64_t); + MODBIND1RC(int64_t, shaped_text_get_custom_ellipsis, const RID &); + MODBIND2(shaped_text_set_orientation, const RID &, Orientation); MODBIND1RC(Orientation, shaped_text_get_orientation, const RID &); diff --git a/modules/text_server_adv/thorvg_bounds_iterator.cpp b/modules/text_server_adv/thorvg_bounds_iterator.cpp index 807f356b83..d273eef97f 100644 --- a/modules/text_server_adv/thorvg_bounds_iterator.cpp +++ b/modules/text_server_adv/thorvg_bounds_iterator.cpp @@ -35,7 +35,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/typedefs.h" diff --git a/modules/text_server_adv/thorvg_bounds_iterator.h b/modules/text_server_adv/thorvg_bounds_iterator.h index a44cbb99a7..afa2c13764 100644 --- a/modules/text_server_adv/thorvg_bounds_iterator.h +++ b/modules/text_server_adv/thorvg_bounds_iterator.h @@ -39,7 +39,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/typedefs.h" diff --git a/modules/text_server_adv/thorvg_svg_in_ot.cpp b/modules/text_server_adv/thorvg_svg_in_ot.cpp index 828f8d7d56..136ccf3aaf 100644 --- a/modules/text_server_adv/thorvg_svg_in_ot.cpp +++ b/modules/text_server_adv/thorvg_svg_in_ot.cpp @@ -38,7 +38,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/error/error_macros.h" diff --git a/modules/text_server_adv/thorvg_svg_in_ot.h b/modules/text_server_adv/thorvg_svg_in_ot.h index 034fffb5e6..ce048674fd 100644 --- a/modules/text_server_adv/thorvg_svg_in_ot.h +++ b/modules/text_server_adv/thorvg_svg_in_ot.h @@ -40,7 +40,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/os/mutex.h" diff --git a/modules/text_server_fb/gdextension_build/SConstruct b/modules/text_server_fb/gdextension_build/SConstruct index 0d2fbd97fd..0efced0bfc 100644 --- a/modules/text_server_fb/gdextension_build/SConstruct +++ b/modules/text_server_fb/gdextension_build/SConstruct @@ -69,6 +69,8 @@ if env["thorvg_enabled"] and env["freetype_enabled"]: "src/renderer/tvgShape.cpp", "src/renderer/tvgSwCanvas.cpp", "src/renderer/tvgTaskScheduler.cpp", + "src/renderer/tvgText.cpp", + # "src/renderer/tvgWgCanvas.cpp", # renderer sw_engine "src/renderer/sw_engine/tvgSwFill.cpp", "src/renderer/sw_engine/tvgSwImage.cpp", diff --git a/modules/text_server_fb/gdextension_build/methods.py b/modules/text_server_fb/gdextension_build/methods.py index 3c5229462c..e58bc3abec 100644 --- a/modules/text_server_fb/gdextension_build/methods.py +++ b/modules/text_server_fb/gdextension_build/methods.py @@ -99,8 +99,8 @@ def make_icu_data(target, source, env): def write_macos_plist(target, binary_name, identifier, name): - os.makedirs(f"{target}/Resourece/", exist_ok=True) - f = open(f"{target}/Resourece/Info.plist", "w") + os.makedirs(f"{target}/Resource/", exist_ok=True) + f = open(f"{target}/Resource/Info.plist", "w") f.write(f'<?xml version="1.0" encoding="UTF-8"?>\n') f.write(f'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n') diff --git a/modules/text_server_fb/register_types.h b/modules/text_server_fb/register_types.h index 97bc06a8f7..0933ea83c5 100644 --- a/modules/text_server_fb/register_types.h +++ b/modules/text_server_fb/register_types.h @@ -34,7 +34,7 @@ #ifdef GDEXTENSION #include <godot_cpp/core/class_db.hpp> using namespace godot; -#else +#elif defined(GODOT_MODULE) #include "modules/register_module_types.h" #endif diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index eb247cdcbe..f2d70db7a4 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -44,7 +44,7 @@ using namespace godot; #define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get_setting_with_override(m_var) -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/config/project_settings.h" @@ -73,7 +73,7 @@ using namespace godot; /*************************************************************************/ -#define OT_TAG(c1, c2, c3, c4) ((int32_t)((((uint32_t)(c1)&0xff) << 24) | (((uint32_t)(c2)&0xff) << 16) | (((uint32_t)(c3)&0xff) << 8) | ((uint32_t)(c4)&0xff))) +#define OT_TAG(c1, c2, c3, c4) ((int32_t)((((uint32_t)(c1) & 0xff) << 24) | (((uint32_t)(c2) & 0xff) << 16) | (((uint32_t)(c3) & 0xff) << 8) | ((uint32_t)(c4) & 0xff))) bool TextServerFallback::_has_feature(Feature p_feature) const { switch (p_feature) { @@ -95,7 +95,7 @@ bool TextServerFallback::_has_feature(Feature p_feature) const { String TextServerFallback::_get_name() const { #ifdef GDEXTENSION return "Fallback (GDExtension)"; -#else +#elif defined(GODOT_MODULE) return "Fallback (Built-in)"; #endif } @@ -242,7 +242,10 @@ _FORCE_INLINE_ TextServerFallback::FontTexturePosition TextServerFallback::find_ ShelfPackTexture *ct = p_data->textures.ptrw(); for (int32_t i = 0; i < p_data->textures.size(); i++) { - if (p_image_format != ct[i].format) { + if (ct[i].image.is_null()) { + continue; + } + if (p_image_format != ct[i].image->get_format()) { continue; } if (mw > ct[i].texture_w || mh > ct[i].texture_h) { // Too big for this texture. @@ -274,12 +277,11 @@ _FORCE_INLINE_ TextServerFallback::FontTexturePosition TextServerFallback::find_ } ShelfPackTexture tex = ShelfPackTexture(texsize, texsize); - tex.format = p_image_format; - tex.imgdata.resize(texsize * texsize * p_color_size); + tex.image = Image::create_empty(texsize, texsize, false, p_image_format); { // Zero texture. - uint8_t *w = tex.imgdata.ptrw(); - ERR_FAIL_COND_V(texsize * texsize * p_color_size > tex.imgdata.size(), ret); + uint8_t *w = tex.image->ptrw(); + ERR_FAIL_COND_V(texsize * texsize * p_color_size > tex.image->data_size(), ret); // Initialize the texture to all-white pixels to prevent artifacts when the // font is displayed at a non-default scale with filtering enabled. if (p_color_size == 2) { @@ -456,12 +458,12 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf( msdfgen::msdfErrorCorrection(image, shape, projection, p_pixel_range, config); { - uint8_t *wr = tex.imgdata.ptrw(); + uint8_t *wr = tex.image->ptrw(); for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * 4; - ERR_FAIL_COND_V(ofs >= tex.imgdata.size(), FontGlyph()); + ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph()); wr[ofs + 0] = (uint8_t)(CLAMP(image(j, i)[0] * 256.f, 0.f, 255.f)); wr[ofs + 1] = (uint8_t)(CLAMP(image(j, i)[1] * 256.f, 0.f, 255.f)); wr[ofs + 2] = (uint8_t)(CLAMP(image(j, i)[2] * 256.f, 0.f, 255.f)); @@ -521,12 +523,12 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitma ShelfPackTexture &tex = p_data->textures.write[tex_pos.index]; { - uint8_t *wr = tex.imgdata.ptrw(); + uint8_t *wr = tex.image->ptrw(); for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * color_size; - ERR_FAIL_COND_V(ofs >= tex.imgdata.size(), FontGlyph()); + ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph()); switch (bitmap.pixel_mode) { case FT_PIXEL_MODE_MONO: { int byte = i * bitmap.pitch + (j >> 3); @@ -548,14 +550,14 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitma case FT_PIXEL_MODE_LCD: { int ofs_color = i * bitmap.pitch + (j * 3); if (p_bgra) { - wr[ofs + 0] = bitmap.buffer[ofs_color + 0]; + wr[ofs + 0] = bitmap.buffer[ofs_color + 2]; wr[ofs + 1] = bitmap.buffer[ofs_color + 1]; - wr[ofs + 2] = bitmap.buffer[ofs_color + 2]; + wr[ofs + 2] = bitmap.buffer[ofs_color + 0]; wr[ofs + 3] = 255; } else { - wr[ofs + 0] = bitmap.buffer[ofs_color + 2]; + wr[ofs + 0] = bitmap.buffer[ofs_color + 0]; wr[ofs + 1] = bitmap.buffer[ofs_color + 1]; - wr[ofs + 2] = bitmap.buffer[ofs_color + 0]; + wr[ofs + 2] = bitmap.buffer[ofs_color + 2]; wr[ofs + 3] = 255; } } break; @@ -1403,6 +1405,37 @@ int64_t TextServerFallback::_font_get_spacing(const RID &p_font_rid, SpacingType } } +void TextServerFallback::_font_set_baseline_offset(const RID &p_font_rid, float p_baseline_offset) { + FontFallbackLinkedVariation *fdv = font_var_owner.get_or_null(p_font_rid); + if (fdv) { + if (fdv->baseline_offset != p_baseline_offset) { + fdv->baseline_offset = p_baseline_offset; + } + } else { + FontFallback *fd = font_owner.get_or_null(p_font_rid); + ERR_FAIL_NULL(fd); + + MutexLock lock(fd->mutex); + if (fd->baseline_offset != p_baseline_offset) { + _font_clear_cache(fd); + fd->baseline_offset = p_baseline_offset; + } + } +} + +float TextServerFallback::_font_get_baseline_offset(const RID &p_font_rid) const { + FontFallbackLinkedVariation *fdv = font_var_owner.get_or_null(p_font_rid); + if (fdv) { + return fdv->baseline_offset; + } else { + FontFallback *fd = font_owner.get_or_null(p_font_rid); + ERR_FAIL_NULL_V(fd, 0.0); + + MutexLock lock(fd->mutex); + return fd->baseline_offset; + } +} + void TextServerFallback::_font_set_transform(const RID &p_font_rid, const Transform2D &p_transform) { FontFallback *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -1714,16 +1747,15 @@ void TextServerFallback::_font_set_texture_image(const RID &p_font_rid, const Ve ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; - tex.imgdata = p_image->get_data(); + tex.image = p_image; tex.texture_w = p_image->get_width(); tex.texture_h = p_image->get_height(); - tex.format = p_image->get_format(); - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = p_image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = p_image->duplicate(); img->generate_mipmaps(); } - tex.texture = ImageTexture::create_from_image(img); tex.dirty = false; } @@ -1738,7 +1770,7 @@ Ref<Image> TextServerFallback::_font_get_texture_image(const RID &p_font_rid, co ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; - return Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); + return tex.image; } void TextServerFallback::_font_set_texture_offsets(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const PackedInt32Array &p_offsets) { @@ -2093,8 +2125,9 @@ RID TextServerFallback::_font_get_glyph_texture_rid(const RID &p_font_rid, const if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -2139,8 +2172,9 @@ Size2 TextServerFallback::_font_get_glyph_texture_size(const RID &p_font_rid, co if (gl[p_glyph | mod].texture_idx != -1) { if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -2439,6 +2473,9 @@ void TextServerFallback::_font_render_glyph(const RID &p_font_rid, const Vector2 } void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_canvas, int64_t p_size, const Vector2 &p_pos, int64_t p_index, const Color &p_color) const { + if (p_index == 0) { + return; // Non visual character, skip. + } FontFallback *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -2476,20 +2513,24 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca const FontGlyph &gl = fd->cache[size]->glyph_map[index]; if (gl.found) { + if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) { + return; // Nothing to draw. + } ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); if (gl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && (fd->cache[size]->textures[gl.texture_idx].format == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -2543,6 +2584,9 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const RID &p_canvas, int64_t p_size, int64_t p_outline_size, const Vector2 &p_pos, int64_t p_index, const Color &p_color) const { + if (p_index == 0) { + return; // Non visual character, skip. + } FontFallback *fd = _get_font_data(p_font_rid); ERR_FAIL_NULL(fd); @@ -2580,20 +2624,24 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R const FontGlyph &gl = fd->cache[size]->glyph_map[index]; if (gl.found) { + if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) { + return; // Nothing to draw. + } ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); if (gl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && (fd->cache[size]->textures[gl.texture_idx].format == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { if (fd->cache[size]->textures[gl.texture_idx].dirty) { ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; - Ref<Image> img = Image::create_from_data(tex.texture_w, tex.texture_h, false, tex.format, tex.imgdata); - if (fd->mipmaps) { + Ref<Image> img = tex.image; + if (fd->mipmaps && !img->has_mipmaps()) { + img = tex.image->duplicate(); img->generate_mipmaps(); } if (tex.texture.is_null()) { @@ -2905,6 +2953,20 @@ String TextServerFallback::_shaped_text_get_custom_punctuation(const RID &p_shap return sd->custom_punct; } +void TextServerFallback::_shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) { + _THREAD_SAFE_METHOD_ + ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL(sd); + sd->el_char = p_char; +} + +int64_t TextServerFallback::_shaped_text_get_custom_ellipsis(const RID &p_shaped) const { + _THREAD_SAFE_METHOD_ + const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_NULL_V(sd, 0); + return sd->el_char; +} + void TextServerFallback::_shaped_text_set_orientation(const RID &p_shaped, TextServer::Orientation p_orientation) { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL(sd); @@ -3305,6 +3367,10 @@ RID TextServerFallback::_shaped_text_substr(const RID &p_shaped, int64_t p_start for (int i = 0; i < sd_size; i++) { if ((sd_glyphs[i].start >= new_sd->start) && (sd_glyphs[i].end <= new_sd->end)) { Glyph gl = sd_glyphs[i]; + if (gl.end == p_start + p_length && ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) == GRAPHEME_IS_SOFT_HYPHEN)) { + gl.index = 0x00ad; + gl.advance = font_get_glyph_advance(gl.font_rid, gl.font_size, 0x00ad).x; + } Variant key; bool find_embedded = false; if (gl.count == 1) { @@ -3418,22 +3484,22 @@ double TextServerFallback::_shaped_text_fit_to_width(const RID &p_shaped, double if (p_jst_flags.has_flag(JUSTIFICATION_TRIM_EDGE_SPACES)) { // Trim spaces. - while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { justification_width -= sd->glyphs[start_pos].advance * sd->glyphs[start_pos].repeat; sd->glyphs.write[start_pos].advance = 0; start_pos += sd->glyphs[start_pos].count; } - while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { + while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { justification_width -= sd->glyphs[end_pos].advance * sd->glyphs[end_pos].repeat; sd->glyphs.write[end_pos].advance = 0; end_pos -= sd->glyphs[end_pos].count; } } else { // Skip breaks, but do not reset size. - while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD)) { + while ((start_pos < end_pos) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[start_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { start_pos += sd->glyphs[start_pos].count; } - while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD)) { + while ((start_pos < end_pos) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN) && ((sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (sd->glyphs[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { end_pos -= sd->glyphs[end_pos].count; } } @@ -3442,7 +3508,7 @@ double TextServerFallback::_shaped_text_fit_to_width(const RID &p_shaped, double for (int i = start_pos; i <= end_pos; i++) { const Glyph &gl = sd->glyphs[i]; if (gl.count > 0) { - if ((gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { + if ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN && (gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { space_count++; } } @@ -3453,7 +3519,7 @@ double TextServerFallback::_shaped_text_fit_to_width(const RID &p_shaped, double for (int i = start_pos; i <= end_pos; i++) { Glyph &gl = sd->glyphs.write[i]; if (gl.count > 0) { - if ((gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { + if ((gl.flags & GRAPHEME_IS_SOFT_HYPHEN) != GRAPHEME_IS_SOFT_HYPHEN && (gl.flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE && (gl.flags & GRAPHEME_IS_PUNCTUATION) != GRAPHEME_IS_PUNCTUATION) { double old_adv = gl.advance; gl.advance = MAX(gl.advance + delta_width_per_space, Math::round(0.1 * gl.font_size)); justification_width += (gl.advance - old_adv); @@ -3568,7 +3634,9 @@ bool TextServerFallback::_shaped_text_update_breaks(const RID &p_shaped) { } if (is_whitespace(c) && !is_linebreak(c)) { sd_glyphs[i].flags |= GRAPHEME_IS_SPACE; - sd_glyphs[i].flags |= GRAPHEME_IS_BREAK_SOFT; + if (c != 0x00A0 && c != 0x202F && c != 0x2060 && c != 0x2007) { // Skip for non-breaking space variants. + sd_glyphs[i].flags |= GRAPHEME_IS_BREAK_SOFT; + } } if (is_linebreak(c)) { sd_glyphs[i].flags |= GRAPHEME_IS_SPACE; @@ -3577,6 +3645,9 @@ bool TextServerFallback::_shaped_text_update_breaks(const RID &p_shaped) { if (c == 0x0009 || c == 0x000b) { sd_glyphs[i].flags |= GRAPHEME_IS_TAB; } + if (c == 0x00ad) { + sd_glyphs[i].flags |= GRAPHEME_IS_SOFT_HYPHEN; + } i += (sd_glyphs[i].count - 1); } @@ -3601,6 +3672,168 @@ bool TextServerFallback::_shaped_text_update_justification_ops(const RID &p_shap return true; } +RID TextServerFallback::_find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text) { + RID f; + // Try system fallback. + if (_font_is_allow_system_fallback(p_fdef)) { + String font_name = _font_get_name(p_fdef); + BitField<FontStyle> font_style = _font_get_style(p_fdef); + int font_weight = _font_get_weight(p_fdef); + int font_stretch = _font_get_stretch(p_fdef); + Dictionary dvar = _font_get_variation_coordinates(p_fdef); + static int64_t wgth_tag = _name_to_tag("weight"); + static int64_t wdth_tag = _name_to_tag("width"); + static int64_t ital_tag = _name_to_tag("italic"); + if (dvar.has(wgth_tag)) { + font_weight = dvar[wgth_tag].operator int(); + } + if (dvar.has(wdth_tag)) { + font_stretch = dvar[wdth_tag].operator int(); + } + if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) { + font_style.set_flag(TextServer::FONT_ITALIC); + } + + String locale = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language; + PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, p_text, locale, p_script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC); +#ifdef GDEXTENSION + for (int fb = 0; fb < fallback_font_name.size(); fb++) { + const String &E = fallback_font_name[fb]; +#elif defined(GODOT_MODULE) + for (const String &E : fallback_font_name) { +#endif + SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, p_fdef, this); + if (system_fonts.has(key)) { + const SystemFontCache &sysf_cache = system_fonts[key]; + int best_score = 0; + int best_match = -1; + for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) { + const SystemFontCacheRec &F = sysf_cache.var[face_idx]; + if (unlikely(!_font_has_char(F.rid, p_text[0]))) { + continue; + } + BitField<FontStyle> style = _font_get_style(F.rid); + int weight = _font_get_weight(F.rid); + int stretch = _font_get_stretch(F.rid); + int score = (20 - Math::abs(weight - font_weight) / 50); + score += (20 - Math::abs(stretch - font_stretch) / 10); + if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { + score += 30; + } + if (score >= best_score) { + best_score = score; + best_match = face_idx; + } + if (best_score == 70) { + break; + } + } + if (best_match != -1) { + f = sysf_cache.var[best_match].rid; + } + } + if (!f.is_valid()) { + if (system_fonts.has(key)) { + const SystemFontCache &sysf_cache = system_fonts[key]; + if (sysf_cache.max_var == sysf_cache.var.size()) { + // All subfonts already tested, skip. + continue; + } + } + + if (!system_font_data.has(E)) { + system_font_data[E] = FileAccess::get_file_as_bytes(E); + } + + const PackedByteArray &font_data = system_font_data[E]; + + SystemFontCacheRec sysf; + sysf.rid = _create_font(); + _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); + + Dictionary var = dvar; + // Select matching style from collection. + int best_score = 0; + int best_match = -1; + for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) { + _font_set_face_index(sysf.rid, face_idx); + if (unlikely(!_font_has_char(sysf.rid, p_text[0]))) { + continue; + } + BitField<FontStyle> style = _font_get_style(sysf.rid); + int weight = _font_get_weight(sysf.rid); + int stretch = _font_get_stretch(sysf.rid); + int score = (20 - Math::abs(weight - font_weight) / 50); + score += (20 - Math::abs(stretch - font_stretch) / 10); + if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { + score += 30; + } + if (score >= best_score) { + best_score = score; + best_match = face_idx; + } + if (best_score == 70) { + break; + } + } + if (best_match == -1) { + _free_rid(sysf.rid); + continue; + } else { + _font_set_face_index(sysf.rid, best_match); + } + sysf.index = best_match; + + // If it's a variable font, apply weight, stretch and italic coordinates to match requested style. + if (best_score != 70) { + Dictionary ftr = _font_supported_variation_list(sysf.rid); + if (ftr.has(wdth_tag)) { + var[wdth_tag] = font_stretch; + _font_set_stretch(sysf.rid, font_stretch); + } + if (ftr.has(wgth_tag)) { + var[wgth_tag] = font_weight; + _font_set_weight(sysf.rid, font_weight); + } + if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) { + var[ital_tag] = 1; + _font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC); + } + } + + _font_set_antialiasing(sysf.rid, key.antialiasing); + _font_set_generate_mipmaps(sysf.rid, key.mipmaps); + _font_set_multichannel_signed_distance_field(sysf.rid, key.msdf); + _font_set_msdf_pixel_range(sysf.rid, key.msdf_range); + _font_set_msdf_size(sysf.rid, key.msdf_source_size); + _font_set_fixed_size(sysf.rid, key.fixed_size); + _font_set_force_autohinter(sysf.rid, key.force_autohinter); + _font_set_hinting(sysf.rid, key.hinting); + _font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning); + _font_set_variation_coordinates(sysf.rid, var); + _font_set_oversampling(sysf.rid, key.oversampling); + _font_set_embolden(sysf.rid, key.embolden); + _font_set_transform(sysf.rid, key.transform); + _font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]); + _font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]); + _font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]); + _font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]); + + if (system_fonts.has(key)) { + system_fonts[key].var.push_back(sysf); + } else { + SystemFontCache &sysf_cache = system_fonts[key]; + sysf_cache.max_var = _font_get_face_count(sysf.rid); + sysf_cache.var.push_back(sysf); + } + f = sysf.rid; + } + break; + } + } + return f; +} + void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_line, double p_width, BitField<TextServer::TextOverrunFlag> p_trim_flags) { ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped_line); ERR_FAIL_NULL_MSG(sd, "ShapedTextDataFallback invalid."); @@ -3643,20 +3876,52 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ int sd_size = sd->glyphs.size(); int last_gl_font_size = sd_glyphs[sd_size - 1].font_size; + bool found_el_char = false; // Find usable fonts, if fonts from the last glyph do not have required chars. RID dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; - if (!_font_has_char(dot_gl_font_rid, '.')) { + if (!_font_has_char(dot_gl_font_rid, sd->el_char)) { const Array &fonts = spans[spans.size() - 1].fonts; for (int i = 0; i < fonts.size(); i++) { - if (_font_has_char(fonts[i], '.')) { + if (_font_has_char(fonts[i], sd->el_char)) { dot_gl_font_rid = fonts[i]; + found_el_char = true; break; } } + if (!found_el_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) { + const char32_t u32str[] = { sd->el_char, 0 }; + RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, u32str); + if (rid.is_valid()) { + dot_gl_font_rid = rid; + found_el_char = true; + } + } + } else { + found_el_char = true; + } + if (!found_el_char) { + bool found_dot_char = false; + dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; + if (!_font_has_char(dot_gl_font_rid, '.')) { + const Array &fonts = spans[spans.size() - 1].fonts; + for (int i = 0; i < fonts.size(); i++) { + if (_font_has_char(fonts[i], '.')) { + dot_gl_font_rid = fonts[i]; + found_dot_char = true; + break; + } + } + if (!found_dot_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) { + RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, "."); + if (rid.is_valid()) { + dot_gl_font_rid = rid; + } + } + } } RID whitespace_gl_font_rid = sd_glyphs[sd_size - 1].font_rid; - if (!_font_has_char(whitespace_gl_font_rid, '.')) { + if (!_font_has_char(whitespace_gl_font_rid, ' ')) { const Array &fonts = spans[spans.size() - 1].fonts; for (int i = 0; i < fonts.size(); i++) { if (_font_has_char(fonts[i], ' ')) { @@ -3666,14 +3931,14 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ } } - int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, '.', 0) : -10; + int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, (found_el_char ? sd->el_char : '.'), 0) : -1; Vector2 dot_adv = dot_gl_font_rid.is_valid() ? _font_get_glyph_advance(dot_gl_font_rid, last_gl_font_size, dot_gl_idx) : Vector2(); - int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -10; + int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -1; Vector2 whitespace_adv = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_advance(whitespace_gl_font_rid, last_gl_font_size, whitespace_gl_idx) : Vector2(); int ellipsis_width = 0; if (add_ellipsis && whitespace_gl_font_rid.is_valid()) { - ellipsis_width = 3 * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0); + ellipsis_width = (found_el_char ? 1 : 3) * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0); } int ell_min_characters = 6; @@ -3742,7 +4007,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if (dot_gl_idx != 0) { Glyph gl; gl.count = 1; - gl.repeat = 3; + gl.repeat = (found_el_char ? 1 : 3); gl.advance = dot_adv.x; gl.index = dot_gl_idx; gl.font_rid = dot_gl_font_rid; @@ -3873,161 +4138,7 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { RID fdef = span.fonts[0]; if (_font_is_allow_system_fallback(fdef)) { String text = sd->text.substr(j, 1); - String font_name = _font_get_name(fdef); - BitField<FontStyle> font_style = _font_get_style(fdef); - int font_weight = _font_get_weight(fdef); - int font_stretch = _font_get_stretch(fdef); - Dictionary dvar = _font_get_variation_coordinates(fdef); - static int64_t wgth_tag = _name_to_tag("weight"); - static int64_t wdth_tag = _name_to_tag("width"); - static int64_t ital_tag = _name_to_tag("italic"); - if (dvar.has(wgth_tag)) { - font_weight = dvar[wgth_tag].operator int(); - } - if (dvar.has(wdth_tag)) { - font_stretch = dvar[wdth_tag].operator int(); - } - if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) { - font_style.set_flag(TextServer::FONT_ITALIC); - } - - String locale = (span.language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : span.language; - - PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, text, locale, String(), font_weight, font_stretch, font_style & TextServer::FONT_ITALIC); -#ifdef GDEXTENSION - for (int fb = 0; fb < fallback_font_name.size(); fb++) { - const String &E = fallback_font_name[fb]; -#else - for (const String &E : fallback_font_name) { -#endif - SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, fdef, this); - if (system_fonts.has(key)) { - const SystemFontCache &sysf_cache = system_fonts[key]; - int best_score = 0; - int best_match = -1; - for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) { - const SystemFontCacheRec &F = sysf_cache.var[face_idx]; - if (unlikely(!_font_has_char(F.rid, text[0]))) { - continue; - } - BitField<FontStyle> style = _font_get_style(F.rid); - int weight = _font_get_weight(F.rid); - int stretch = _font_get_stretch(F.rid); - int score = (20 - Math::abs(weight - font_weight) / 50); - score += (20 - Math::abs(stretch - font_stretch) / 10); - if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { - score += 30; - } - if (score >= best_score) { - best_score = score; - best_match = face_idx; - } - if (best_score == 70) { - break; - } - } - if (best_match != -1) { - gl.font_rid = sysf_cache.var[best_match].rid; - } - } - if (!gl.font_rid.is_valid()) { - if (system_fonts.has(key)) { - const SystemFontCache &sysf_cache = system_fonts[key]; - if (sysf_cache.max_var == sysf_cache.var.size()) { - // All subfonts already tested, skip. - continue; - } - } - - if (!system_font_data.has(E)) { - system_font_data[E] = FileAccess::get_file_as_bytes(E); - } - - const PackedByteArray &font_data = system_font_data[E]; - - SystemFontCacheRec sysf; - sysf.rid = _create_font(); - _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); - - Dictionary var = dvar; - // Select matching style from collection. - int best_score = 0; - int best_match = -1; - for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) { - _font_set_face_index(sysf.rid, face_idx); - if (unlikely(!_font_has_char(sysf.rid, text[0]))) { - continue; - } - BitField<FontStyle> style = _font_get_style(sysf.rid); - int weight = _font_get_weight(sysf.rid); - int stretch = _font_get_stretch(sysf.rid); - int score = (20 - Math::abs(weight - font_weight) / 50); - score += (20 - Math::abs(stretch - font_stretch) / 10); - if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) { - score += 30; - } - if (score >= best_score) { - best_score = score; - best_match = face_idx; - } - if (best_score == 70) { - break; - } - } - if (best_match == -1) { - _free_rid(sysf.rid); - continue; - } else { - _font_set_face_index(sysf.rid, best_match); - } - sysf.index = best_match; - - // If it's a variable font, apply weight, stretch and italic coordinates to match requested style. - if (best_score != 70) { - Dictionary ftr = _font_supported_variation_list(sysf.rid); - if (ftr.has(wdth_tag)) { - var[wdth_tag] = font_stretch; - _font_set_stretch(sysf.rid, font_stretch); - } - if (ftr.has(wgth_tag)) { - var[wgth_tag] = font_weight; - _font_set_weight(sysf.rid, font_weight); - } - if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) { - var[ital_tag] = 1; - _font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC); - } - } - - _font_set_antialiasing(sysf.rid, key.antialiasing); - _font_set_generate_mipmaps(sysf.rid, key.mipmaps); - _font_set_multichannel_signed_distance_field(sysf.rid, key.msdf); - _font_set_msdf_pixel_range(sysf.rid, key.msdf_range); - _font_set_msdf_size(sysf.rid, key.msdf_source_size); - _font_set_fixed_size(sysf.rid, key.fixed_size); - _font_set_force_autohinter(sysf.rid, key.force_autohinter); - _font_set_hinting(sysf.rid, key.hinting); - _font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning); - _font_set_variation_coordinates(sysf.rid, var); - _font_set_oversampling(sysf.rid, key.oversampling); - _font_set_embolden(sysf.rid, key.embolden); - _font_set_transform(sysf.rid, key.transform); - _font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]); - _font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]); - _font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]); - _font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]); - - if (system_fonts.has(key)) { - system_fonts[key].var.push_back(sysf); - } else { - SystemFontCache &sysf_cache = system_fonts[key]; - sysf_cache.max_var = _font_get_face_count(sysf.rid); - sysf_cache.var.push_back(sysf); - } - gl.font_rid = sysf.rid; - } - break; - } + gl.font_rid = _find_sys_font_for_text(fdef, String(), span.language, text); } } prev_font = gl.font_rid; @@ -4039,12 +4150,12 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { if (sd->orientation == ORIENTATION_HORIZONTAL) { gl.advance = _font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x; gl.x_off = 0; - gl.y_off = 0; + gl.y_off = _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); sd->ascent = MAX(sd->ascent, _font_get_ascent(gl.font_rid, gl.font_size) + _font_get_spacing(gl.font_rid, SPACING_TOP)); sd->descent = MAX(sd->descent, _font_get_descent(gl.font_rid, gl.font_size) + _font_get_spacing(gl.font_rid, SPACING_BOTTOM)); } else { gl.advance = _font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).y; - gl.x_off = -Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5); + gl.x_off = -Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5) + _font_get_baseline_offset(gl.font_rid) * (double)(_font_get_ascent(gl.font_rid, gl.font_size) + _font_get_descent(gl.font_rid, gl.font_size)); gl.y_off = _font_get_ascent(gl.font_rid, gl.font_size); sd->ascent = MAX(sd->ascent, Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5)); sd->descent = MAX(sd->descent, Math::round(_font_get_glyph_advance(gl.font_rid, gl.font_size, gl.index).x * 0.5)); diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 5c30ea0c05..9cdf20f3fa 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -76,7 +76,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/extension/ext_wrappers.gen.inc" @@ -162,8 +162,7 @@ class TextServerFallback : public TextServerExtension { int32_t texture_w = 1024; int32_t texture_h = 1024; - Image::Format format; - PackedByteArray imgdata; + Ref<Image> image; Ref<ImageTexture> texture; bool dirty = true; @@ -248,6 +247,7 @@ class TextServerFallback : public TextServerExtension { struct FontFallbackLinkedVariation { RID base_font; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; }; struct FontFallback { @@ -275,6 +275,7 @@ class TextServerFallback : public TextServerExtension { int weight = 400; int stretch = 100; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; HashMap<Vector2i, FontForSizeFallback *, VariantHasher, VariantComparator> cache; @@ -447,6 +448,7 @@ class TextServerFallback : public TextServerExtension { double upos = 0.0; double uthk = 0.0; + char32_t el_char = 0x2026; TrimData overrun_trim_data; bool fit_width_minimum_reached = false; @@ -489,9 +491,10 @@ class TextServerFallback : public TextServerExtension { double embolden = 0.0; Transform2D transform; int extra_spacing[4] = { 0, 0, 0, 0 }; + float baseline_offset = 0.0; bool operator==(const SystemFontKey &p_b) const { - return (font_name == p_b.font_name) && (antialiasing == p_b.antialiasing) && (italic == p_b.italic) && (mipmaps == p_b.mipmaps) && (msdf == p_b.msdf) && (force_autohinter == p_b.force_autohinter) && (weight == p_b.weight) && (stretch == p_b.stretch) && (msdf_range == p_b.msdf_range) && (msdf_source_size == p_b.msdf_source_size) && (fixed_size == p_b.fixed_size) && (hinting == p_b.hinting) && (subpixel_positioning == p_b.subpixel_positioning) && (variation_coordinates == p_b.variation_coordinates) && (oversampling == p_b.oversampling) && (embolden == p_b.embolden) && (transform == p_b.transform) && (extra_spacing[SPACING_TOP] == p_b.extra_spacing[SPACING_TOP]) && (extra_spacing[SPACING_BOTTOM] == p_b.extra_spacing[SPACING_BOTTOM]) && (extra_spacing[SPACING_SPACE] == p_b.extra_spacing[SPACING_SPACE]) && (extra_spacing[SPACING_GLYPH] == p_b.extra_spacing[SPACING_GLYPH]); + return (font_name == p_b.font_name) && (antialiasing == p_b.antialiasing) && (italic == p_b.italic) && (mipmaps == p_b.mipmaps) && (msdf == p_b.msdf) && (force_autohinter == p_b.force_autohinter) && (weight == p_b.weight) && (stretch == p_b.stretch) && (msdf_range == p_b.msdf_range) && (msdf_source_size == p_b.msdf_source_size) && (fixed_size == p_b.fixed_size) && (hinting == p_b.hinting) && (subpixel_positioning == p_b.subpixel_positioning) && (variation_coordinates == p_b.variation_coordinates) && (oversampling == p_b.oversampling) && (embolden == p_b.embolden) && (transform == p_b.transform) && (extra_spacing[SPACING_TOP] == p_b.extra_spacing[SPACING_TOP]) && (extra_spacing[SPACING_BOTTOM] == p_b.extra_spacing[SPACING_BOTTOM]) && (extra_spacing[SPACING_SPACE] == p_b.extra_spacing[SPACING_SPACE]) && (extra_spacing[SPACING_GLYPH] == p_b.extra_spacing[SPACING_GLYPH]) && (baseline_offset == p_b.baseline_offset); } SystemFontKey(const String &p_font_name, bool p_italic, int p_weight, int p_stretch, RID p_font, const TextServerFallback *p_fb) { @@ -516,6 +519,7 @@ class TextServerFallback : public TextServerExtension { extra_spacing[SPACING_BOTTOM] = p_fb->_font_get_spacing(p_font, SPACING_BOTTOM); extra_spacing[SPACING_SPACE] = p_fb->_font_get_spacing(p_font, SPACING_SPACE); extra_spacing[SPACING_GLYPH] = p_fb->_font_get_spacing(p_font, SPACING_GLYPH); + baseline_offset = p_fb->_font_get_baseline_offset(p_font); } }; @@ -548,6 +552,7 @@ class TextServerFallback : public TextServerExtension { hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_BOTTOM], hash); hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_SPACE], hash); hash = hash_murmur3_one_32(p_a.extra_spacing[SPACING_GLYPH], hash); + hash = hash_murmur3_one_double(p_a.baseline_offset, hash); return hash_fmix32(hash_murmur3_one_32(((int)p_a.mipmaps) | ((int)p_a.msdf << 1) | ((int)p_a.italic << 2) | ((int)p_a.force_autohinter << 3) | ((int)p_a.hinting << 4) | ((int)p_a.subpixel_positioning << 8) | ((int)p_a.antialiasing << 12), hash)); } }; @@ -555,6 +560,7 @@ class TextServerFallback : public TextServerExtension { mutable HashMap<String, PackedByteArray> system_font_data; void _realign(ShapedTextDataFallback *p_sd) const; + _FORCE_INLINE_ RID _find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text); Mutex ft_mutex; @@ -646,6 +652,9 @@ public: MODBIND3(font_set_spacing, const RID &, SpacingType, int64_t); MODBIND2RC(int64_t, font_get_spacing, const RID &, SpacingType); + MODBIND2(font_set_baseline_offset, const RID &, float); + MODBIND1RC(float, font_get_baseline_offset, const RID &); + MODBIND2(font_set_transform, const RID &, const Transform2D &); MODBIND1RC(Transform2D, font_get_transform, const RID &); @@ -766,6 +775,9 @@ public: MODBIND2(shaped_text_set_custom_punctuation, const RID &, const String &); MODBIND1RC(String, shaped_text_get_custom_punctuation, const RID &); + MODBIND2(shaped_text_set_custom_ellipsis, const RID &, int64_t); + MODBIND1RC(int64_t, shaped_text_get_custom_ellipsis, const RID &); + MODBIND2(shaped_text_set_orientation, const RID &, Orientation); MODBIND1RC(Orientation, shaped_text_get_orientation, const RID &); diff --git a/modules/text_server_fb/thorvg_bounds_iterator.cpp b/modules/text_server_fb/thorvg_bounds_iterator.cpp index 807f356b83..d273eef97f 100644 --- a/modules/text_server_fb/thorvg_bounds_iterator.cpp +++ b/modules/text_server_fb/thorvg_bounds_iterator.cpp @@ -35,7 +35,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/typedefs.h" diff --git a/modules/text_server_fb/thorvg_bounds_iterator.h b/modules/text_server_fb/thorvg_bounds_iterator.h index a44cbb99a7..afa2c13764 100644 --- a/modules/text_server_fb/thorvg_bounds_iterator.h +++ b/modules/text_server_fb/thorvg_bounds_iterator.h @@ -39,7 +39,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/typedefs.h" diff --git a/modules/text_server_fb/thorvg_svg_in_ot.cpp b/modules/text_server_fb/thorvg_svg_in_ot.cpp index 7c8fedabc8..1ad33a88d4 100644 --- a/modules/text_server_fb/thorvg_svg_in_ot.cpp +++ b/modules/text_server_fb/thorvg_svg_in_ot.cpp @@ -38,7 +38,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/error/error_macros.h" diff --git a/modules/text_server_fb/thorvg_svg_in_ot.h b/modules/text_server_fb/thorvg_svg_in_ot.h index 034fffb5e6..ce048674fd 100644 --- a/modules/text_server_fb/thorvg_svg_in_ot.h +++ b/modules/text_server_fb/thorvg_svg_in_ot.h @@ -40,7 +40,7 @@ using namespace godot; -#else +#elif defined(GODOT_MODULE) // Headers for building as built-in module. #include "core/os/mutex.h" diff --git a/modules/tinyexr/image_loader_tinyexr.cpp b/modules/tinyexr/image_loader_tinyexr.cpp index f64bb14e4a..8720ca56f6 100644 --- a/modules/tinyexr/image_loader_tinyexr.cpp +++ b/modules/tinyexr/image_loader_tinyexr.cpp @@ -68,6 +68,7 @@ Error ImageLoaderTinyEXR::load_image(Ref<Image> p_image, Ref<FileAccess> f, BitF if (ret != TINYEXR_SUCCESS) { if (err) { ERR_PRINT(String(err)); + FreeEXRErrorMessage(err); } return ERR_FILE_CORRUPT; } @@ -86,6 +87,7 @@ Error ImageLoaderTinyEXR::load_image(Ref<Image> p_image, Ref<FileAccess> f, BitF if (ret != TINYEXR_SUCCESS) { if (err) { ERR_PRINT(String(err)); + FreeEXRErrorMessage(err); } return ERR_FILE_CORRUPT; } diff --git a/modules/upnp/upnp.cpp b/modules/upnp/upnp.cpp index aef4f394b2..2812f37eb2 100644 --- a/modules/upnp/upnp.cpp +++ b/modules/upnp/upnp.cpp @@ -265,7 +265,7 @@ void UPNP::clear_devices() { } Ref<UPNPDevice> UPNP::get_gateway() const { - ERR_FAIL_COND_V_MSG(devices.size() < 1, nullptr, "Couldn't find any UPNPDevices."); + ERR_FAIL_COND_V_MSG(devices.is_empty(), nullptr, "Couldn't find any UPNPDevices."); for (int i = 0; i < devices.size(); i++) { Ref<UPNPDevice> dev = get_device(i); diff --git a/modules/vorbis/audio_stream_ogg_vorbis.cpp b/modules/vorbis/audio_stream_ogg_vorbis.cpp index 8a265ffaf3..b235b6f96c 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.cpp +++ b/modules/vorbis/audio_stream_ogg_vorbis.cpp @@ -46,8 +46,9 @@ int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram int todo = p_frames; int beat_length_frames = -1; - bool beat_loop = vorbis_stream->has_loop(); - if (beat_loop && vorbis_stream->get_bpm() > 0 && vorbis_stream->get_beat_count() > 0) { + bool use_loop = looping_override ? looping : vorbis_stream->loop; + + if (use_loop && vorbis_stream->get_bpm() > 0 && vorbis_stream->get_beat_count() > 0) { beat_length_frames = vorbis_stream->get_beat_count() * vorbis_data->get_sampling_rate() * 60 / vorbis_stream->get_bpm(); } @@ -99,7 +100,7 @@ int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram } else **/ - if (beat_loop && beat_length_frames <= (int)frames_mixed) { + if (use_loop && beat_length_frames <= (int)frames_mixed) { // End of file when doing beat-based looping. <= used instead of == because importer editing if (!have_packets_left && !have_samples_left) { //Nothing remaining, so do nothing. @@ -125,7 +126,7 @@ int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram if (!have_packets_left && !have_samples_left) { // Actual end of file! bool is_not_empty = mixed > 0 || vorbis_stream->get_length() > 0; - if (vorbis_stream->loop && is_not_empty) { + if (use_loop && is_not_empty) { //loop seek(vorbis_stream->loop_offset); @@ -144,7 +145,7 @@ int AudioStreamPlaybackOggVorbis::_mix_internal(AudioFrame *p_buffer, int p_fram } int AudioStreamPlaybackOggVorbis::_mix_frames_vorbis(AudioFrame *p_buffer, int p_frames) { - ERR_FAIL_COND_V(!ready, 0); + ERR_FAIL_COND_V(!ready, p_frames); if (!have_samples_left) { ogg_packet *packet = nullptr; int err; @@ -156,10 +157,10 @@ int AudioStreamPlaybackOggVorbis::_mix_frames_vorbis(AudioFrame *p_buffer, int p } err = vorbis_synthesis(&block, packet); - ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis synthesis " + itos(err)); + ERR_FAIL_COND_V_MSG(err != 0, p_frames, "Error during vorbis synthesis " + itos(err)); err = vorbis_synthesis_blockin(&dsp_state, &block); - ERR_FAIL_COND_V_MSG(err != 0, 0, "Error during vorbis block processing " + itos(err)); + ERR_FAIL_COND_V_MSG(err != 0, p_frames, "Error during vorbis block processing " + itos(err)); have_packets_left = !packet->e_o_s; } @@ -176,13 +177,13 @@ int AudioStreamPlaybackOggVorbis::_mix_frames_vorbis(AudioFrame *p_buffer, int p if (info.channels > 1) { for (int frame = 0; frame < frames; frame++) { - p_buffer[frame].l = pcm[0][frame]; - p_buffer[frame].r = pcm[1][frame]; + p_buffer[frame].left = pcm[0][frame]; + p_buffer[frame].right = pcm[1][frame]; } } else { for (int frame = 0; frame < frames; frame++) { - p_buffer[frame].l = pcm[0][frame]; - p_buffer[frame].r = pcm[0][frame]; + p_buffer[frame].left = pcm[0][frame]; + p_buffer[frame].right = pcm[0][frame]; } } vorbis_synthesis_read(&dsp_state, frames); @@ -257,6 +258,25 @@ void AudioStreamPlaybackOggVorbis::tag_used_streams() { vorbis_stream->tag_used(get_playback_position()); } +void AudioStreamPlaybackOggVorbis::set_parameter(const StringName &p_name, const Variant &p_value) { + if (p_name == SNAME("looping")) { + if (p_value == Variant()) { + looping_override = false; + looping = false; + } else { + looping_override = true; + looping = p_value; + } + } +} + +Variant AudioStreamPlaybackOggVorbis::get_parameter(const StringName &p_name) const { + if (looping_override && p_name == SNAME("looping")) { + return looping; + } + return Variant(); +} + void AudioStreamPlaybackOggVorbis::seek(double p_time) { ERR_FAIL_COND(!ready); ERR_FAIL_COND(vorbis_stream.is_null()); @@ -493,6 +513,10 @@ bool AudioStreamOggVorbis::is_monophonic() const { return false; } +void AudioStreamOggVorbis::get_parameter_list(List<Parameter> *r_parameters) { + r_parameters->push_back(Parameter(PropertyInfo(Variant::BOOL, "looping", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_CHECKABLE), Variant())); +} + void AudioStreamOggVorbis::_bind_methods() { ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_buffer", "buffer"), &AudioStreamOggVorbis::load_from_buffer); ClassDB::bind_static_method("AudioStreamOggVorbis", D_METHOD("load_from_file", "path"), &AudioStreamOggVorbis::load_from_file); diff --git a/modules/vorbis/audio_stream_ogg_vorbis.h b/modules/vorbis/audio_stream_ogg_vorbis.h index 6abaeaaebd..64a7815b57 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.h +++ b/modules/vorbis/audio_stream_ogg_vorbis.h @@ -44,6 +44,8 @@ class AudioStreamPlaybackOggVorbis : public AudioStreamPlaybackResampled { uint32_t frames_mixed = 0; bool active = false; + bool looping_override = false; + bool looping = false; int loops = 0; enum { @@ -95,6 +97,9 @@ public: virtual void tag_used_streams() override; + virtual void set_parameter(const StringName &p_name, const Variant &p_value) override; + virtual Variant get_parameter(const StringName &p_name) const override; + AudioStreamPlaybackOggVorbis() {} ~AudioStreamPlaybackOggVorbis(); }; @@ -152,6 +157,8 @@ public: virtual bool is_monophonic() const override; + virtual void get_parameter_list(List<Parameter> *r_parameters) override; + AudioStreamOggVorbis(); virtual ~AudioStreamOggVorbis(); }; diff --git a/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml b/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml index 7e3af6688a..d2f6745e2f 100644 --- a/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml +++ b/modules/vorbis/doc_classes/AudioStreamOggVorbis.xml @@ -7,6 +7,7 @@ The AudioStreamOggVorbis class is a specialized [AudioStream] for handling Ogg Vorbis file formats. It offers functionality for loading and playing back Ogg Vorbis files, as well as managing looping and other playback properties. This class is part of the audio stream system, which also supports WAV files through the [AudioStreamWAV] class. </description> <tutorials> + <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <methods> <method name="load_from_buffer" qualifiers="static"> diff --git a/modules/vorbis/register_types.cpp b/modules/vorbis/register_types.cpp index 26af912999..def34220ea 100644 --- a/modules/vorbis/register_types.cpp +++ b/modules/vorbis/register_types.cpp @@ -48,8 +48,13 @@ void initialize_vorbis_module(ModuleInitializationLevel p_level) { ResourceFormatImporter::get_singleton()->add_importer(ogg_vorbis_importer); } + ClassDB::APIType prev_api = ClassDB::get_current_api(); + ClassDB::set_current_api(ClassDB::API_EDITOR); + // Required to document import options in the class reference. GDREGISTER_CLASS(ResourceImporterOggVorbis); + + ClassDB::set_current_api(prev_api); #endif GDREGISTER_CLASS(AudioStreamOggVorbis); diff --git a/modules/vorbis/resource_importer_ogg_vorbis.cpp b/modules/vorbis/resource_importer_ogg_vorbis.cpp index a8c92f06f6..7b8d14741b 100644 --- a/modules/vorbis/resource_importer_ogg_vorbis.cpp +++ b/modules/vorbis/resource_importer_ogg_vorbis.cpp @@ -90,7 +90,7 @@ bool ResourceImporterOggVorbis::has_advanced_options() const { void ResourceImporterOggVorbis::show_advanced_options(const String &p_path) { Ref<AudioStreamOggVorbis> ogg_stream = load_from_file(p_path); if (ogg_stream.is_valid()) { - AudioStreamImportSettings::get_singleton()->edit(p_path, "oggvorbisstr", ogg_stream); + AudioStreamImportSettingsDialog::get_singleton()->edit(p_path, "oggvorbisstr", ogg_stream); } } #endif @@ -212,11 +212,13 @@ Ref<AudioStreamOggVorbis> ResourceImporterOggVorbis::load_from_buffer(const Vect granule_pos = packet.granulepos; } - PackedByteArray data; - data.resize(packet.bytes); - memcpy(data.ptrw(), packet.packet, packet.bytes); - sorted_packets[granule_pos].push_back(data); - packet_count++; + if (packet.bytes > 0) { + PackedByteArray data; + data.resize(packet.bytes); + memcpy(data.ptrw(), packet.packet, packet.bytes); + sorted_packets[granule_pos].push_back(data); + packet_count++; + } } Vector<Vector<uint8_t>> packet_data; for (const KeyValue<uint64_t, Vector<Vector<uint8_t>>> &pair : sorted_packets) { @@ -224,7 +226,7 @@ Ref<AudioStreamOggVorbis> ResourceImporterOggVorbis::load_from_buffer(const Vect packet_data.push_back(packets); } } - if (initialized_stream) { + if (initialized_stream && packet_data.size() > 0) { ogg_packet_sequence->push_page(ogg_page_granulepos(&page), packet_data); } } diff --git a/modules/webp/webp_common.cpp b/modules/webp/webp_common.cpp index bc34a25733..3a2ac5a90e 100644 --- a/modules/webp/webp_common.cpp +++ b/modules/webp/webp_common.cpp @@ -59,6 +59,10 @@ Vector<uint8_t> _webp_packer(const Ref<Image> &p_image, float p_quality, bool p_ compression_method = CLAMP(compression_method, 0, 6); Ref<Image> img = p_image->duplicate(); + if (img->is_compressed()) { + Error error = img->decompress(); + ERR_FAIL_COND_V_MSG(error != OK, Vector<uint8_t>(), "Couldn't decompress image."); + } if (img->detect_alpha()) { img->convert(Image::FORMAT_RGBA8); } else { diff --git a/modules/webrtc/doc_classes/WebRTCPeerConnection.xml b/modules/webrtc/doc_classes/WebRTCPeerConnection.xml index 454f8f2ed4..8698c5755a 100644 --- a/modules/webrtc/doc_classes/WebRTCPeerConnection.xml +++ b/modules/webrtc/doc_classes/WebRTCPeerConnection.xml @@ -76,7 +76,7 @@ <method name="get_signaling_state" qualifiers="const"> <return type="int" enum="WebRTCPeerConnection.SignalingState" /> <description> - Returns the [enum SignalingState] on the local end of the connection while connecting or reconnecting to another peer. + Returns the signaling state on the local end of the connection while connecting or reconnecting to another peer. </description> </method> <method name="initialize"> diff --git a/modules/webrtc/webrtc_peer_connection.cpp b/modules/webrtc/webrtc_peer_connection.cpp index 8bad6fd784..0a50b677c4 100644 --- a/modules/webrtc/webrtc_peer_connection.cpp +++ b/modules/webrtc/webrtc_peer_connection.cpp @@ -40,14 +40,14 @@ StringName WebRTCPeerConnection::default_extension; void WebRTCPeerConnection::set_default_extension(const StringName &p_extension) { ERR_FAIL_COND_MSG(!ClassDB::is_parent_class(p_extension, WebRTCPeerConnectionExtension::get_class_static()), vformat("Can't make %s the default WebRTC extension since it does not extend WebRTCPeerConnectionExtension.", p_extension)); - default_extension = p_extension; + default_extension = StringName(p_extension, true); } WebRTCPeerConnection *WebRTCPeerConnection::create() { #ifdef WEB_ENABLED return memnew(WebRTCPeerConnectionJS); #else - if (default_extension == String()) { + if (default_extension == StringName()) { WARN_PRINT_ONCE("No default WebRTC extension configured."); return memnew(WebRTCPeerConnectionExtension); } diff --git a/modules/websocket/remote_debugger_peer_websocket.cpp b/modules/websocket/remote_debugger_peer_websocket.cpp index 791b6d9ec9..dc6833e8c3 100644 --- a/modules/websocket/remote_debugger_peer_websocket.cpp +++ b/modules/websocket/remote_debugger_peer_websocket.cpp @@ -91,7 +91,7 @@ bool RemoteDebuggerPeerWebSocket::has_message() { } Array RemoteDebuggerPeerWebSocket::get_message() { - ERR_FAIL_COND_V(in_queue.size() < 1, Array()); + ERR_FAIL_COND_V(in_queue.is_empty(), Array()); Array msg = in_queue[0]; in_queue.pop_front(); return msg; diff --git a/modules/websocket/websocket_multiplayer_peer.cpp b/modules/websocket/websocket_multiplayer_peer.cpp index 9e706dbeef..332cf93d36 100644 --- a/modules/websocket/websocket_multiplayer_peer.cpp +++ b/modules/websocket/websocket_multiplayer_peer.cpp @@ -124,7 +124,7 @@ Error WebSocketMultiplayerPeer::get_packet(const uint8_t **r_buffer, int &r_buff current_packet.data = nullptr; } - ERR_FAIL_COND_V(incoming_packets.size() == 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V(incoming_packets.is_empty(), ERR_UNAVAILABLE); current_packet = incoming_packets.front()->get(); incoming_packets.pop_front(); @@ -164,7 +164,7 @@ void WebSocketMultiplayerPeer::set_target_peer(int p_target_peer) { } int WebSocketMultiplayerPeer::get_packet_peer() const { - ERR_FAIL_COND_V(incoming_packets.size() == 0, 1); + ERR_FAIL_COND_V(incoming_packets.is_empty(), 1); return incoming_packets.front()->get().source; } diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index 9e1167c09f..caf7958f6b 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -153,6 +153,10 @@ </method> </methods> <members> + <member name="enabled_features" type="String" setter="" getter="get_enabled_features"> + A comma-separated list of features that were successfully enabled by [method XRInterface.initialize] when setting up the WebXR session. + This may include features requested by setting [member required_features] and [member optional_features]. + </member> <member name="optional_features" type="String" setter="set_optional_features" getter="get_optional_features"> A comma-seperated list of optional features used by [method XRInterface.initialize] when setting up the WebXR session. If a user's browser or device doesn't support one of the given features, initialization will continue, but you won't be able to use the requested feature. diff --git a/modules/webxr/godot_webxr.h b/modules/webxr/godot_webxr.h index a7ce7e45d2..caa7f217af 100644 --- a/modules/webxr/godot_webxr.h +++ b/modules/webxr/godot_webxr.h @@ -45,7 +45,7 @@ enum WebXRInputEvent { }; typedef void (*GodotWebXRSupportedCallback)(char *p_session_mode, int p_supported); -typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type); +typedef void (*GodotWebXRStartedCallback)(char *p_reference_space_type, char *p_enabled_features); typedef void (*GodotWebXREndedCallback)(); typedef void (*GodotWebXRFailedCallback)(char *p_message); typedef void (*GodotWebXRInputEventCallback)(int p_event_type, int p_input_source_id); @@ -85,7 +85,10 @@ extern bool godot_webxr_update_input_source( int *r_button_count, float *r_buttons, int *r_axes_count, - float *r_axes); + float *r_axes, + int *r_has_hand_data, + float *r_hand_joints, + float *r_hand_radii); extern char *godot_webxr_get_visibility_state(); extern int godot_webxr_get_bounds_geometry(float **r_points); diff --git a/modules/webxr/native/library_godot_webxr.js b/modules/webxr/native/library_godot_webxr.js index fe47de02b0..722b448fd5 100644 --- a/modules/webxr/native/library_godot_webxr.js +++ b/modules/webxr/native/library_godot_webxr.js @@ -318,9 +318,11 @@ const GodotWebXR = { // callback don't bubble up here and cause Godot to try the // next reference space. window.setTimeout(function () { - const c_str = GodotRuntime.allocString(reference_space_type); - onstarted(c_str); - GodotRuntime.free(c_str); + const reference_space_c_str = GodotRuntime.allocString(reference_space_type); + const enabled_features_c_str = GodotRuntime.allocString(Array.from(session.enabledFeatures).join(',')); + onstarted(reference_space_c_str, enabled_features_c_str); + GodotRuntime.free(reference_space_c_str); + GodotRuntime.free(enabled_features_c_str); }, 0); } @@ -479,8 +481,8 @@ const GodotWebXR = { }, godot_webxr_update_input_source__proxy: 'sync', - godot_webxr_update_input_source__sig: 'iiiiiiiiiiii', - godot_webxr_update_input_source: function (p_input_source_id, r_target_pose, r_target_ray_mode, r_touch_index, r_has_grip_pose, r_grip_pose, r_has_standard_mapping, r_button_count, r_buttons, r_axes_count, r_axes) { + godot_webxr_update_input_source__sig: 'iiiiiiiiiiiiiii', + godot_webxr_update_input_source: function (p_input_source_id, r_target_pose, r_target_ray_mode, r_touch_index, r_has_grip_pose, r_grip_pose, r_has_standard_mapping, r_button_count, r_buttons, r_axes_count, r_axes, r_has_hand_data, r_hand_joints, r_hand_radii) { if (!GodotWebXR.session || !GodotWebXR.frame) { return 0; } @@ -563,6 +565,19 @@ const GodotWebXR = { GodotRuntime.setHeapValue(r_button_count, button_count, 'i32'); GodotRuntime.setHeapValue(r_axes_count, axes_count, 'i32'); + // Hand tracking data. + let has_hand_data = false; + if (input_source.hand && r_hand_joints !== 0 && r_hand_radii !== 0) { + const hand_joint_array = new Float32Array(25 * 16); + const hand_radii_array = new Float32Array(25); + if (frame.fillPoses(input_source.hand.values(), space, hand_joint_array) && frame.fillJointRadii(input_source.hand.values(), hand_radii_array)) { + GodotRuntime.heapCopy(HEAPF32, hand_joint_array, r_hand_joints); + GodotRuntime.heapCopy(HEAPF32, hand_radii_array, r_hand_radii); + has_hand_data = true; + } + } + GodotRuntime.setHeapValue(r_has_hand_data, has_hand_data ? 1 : 0, 'i32'); + return true; }, diff --git a/modules/webxr/native/webxr.externs.js b/modules/webxr/native/webxr.externs.js index 7f7c297acc..35ad33fa93 100644 --- a/modules/webxr/native/webxr.externs.js +++ b/modules/webxr/native/webxr.externs.js @@ -229,7 +229,6 @@ XRFrame.prototype.session; XRFrame.prototype.getViewerPose = function (referenceSpace) {}; /** - * * @param {XRSpace} space * @param {XRSpace} baseSpace * @return {XRPose} @@ -237,6 +236,21 @@ XRFrame.prototype.getViewerPose = function (referenceSpace) {}; XRFrame.prototype.getPose = function (space, baseSpace) {}; /** + * @param {Array<XRSpace>} spaces + * @param {XRSpace} baseSpace + * @param {Float32Array} transforms + * @return {boolean} + */ +XRFrame.prototype.fillPoses = function (spaces, baseSpace, transforms) {}; + +/** + * @param {Array<XRJointSpace>} jointSpaces + * @param {Float32Array} radii + * @return {boolean} + */ +XRFrame.prototype.fillJointRadii = function (jointSpaces, radii) {}; + +/** * @constructor */ function XRReferenceSpace() {}; @@ -499,12 +513,52 @@ XRInputSource.prototype.targetRayMode; XRInputSource.prototype.targetRaySpace; /** + * @type {?XRHand} + */ +XRInputSource.prototype.hand; + +/** + * @constructor + */ +function XRHand() {}; + +/** + * Note: In fact, XRHand acts like a Map<string, XRJointSpace>, but I don't know + * how to represent that here. So, we're just giving the one method we call. + * + * @return {Array<XRJointSpace>} + */ +XRHand.prototype.values = function () {}; + +/** + * @type {number} + */ +XRHand.prototype.size; + +/** + * @param {string} key + * @return {XRJointSpace} + */ +XRHand.prototype.get = function (key) {}; + +/** * @constructor */ function XRSpace() {}; /** * @constructor + * @extends {XRSpace} + */ +function XRJointSpace() {}; + +/** + * @type {string} + */ +XRJointSpace.prototype.jointName; + +/** + * @constructor */ function XRPose() {}; diff --git a/modules/webxr/webxr_interface.cpp b/modules/webxr/webxr_interface.cpp index 85ed9f472e..c3efebef0f 100644 --- a/modules/webxr/webxr_interface.cpp +++ b/modules/webxr/webxr_interface.cpp @@ -41,6 +41,7 @@ void WebXRInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("set_optional_features", "optional_features"), &WebXRInterface::set_optional_features); ClassDB::bind_method(D_METHOD("get_optional_features"), &WebXRInterface::get_optional_features); ClassDB::bind_method(D_METHOD("get_reference_space_type"), &WebXRInterface::get_reference_space_type); + ClassDB::bind_method(D_METHOD("get_enabled_features"), &WebXRInterface::get_enabled_features); ClassDB::bind_method(D_METHOD("set_requested_reference_space_types", "requested_reference_space_types"), &WebXRInterface::set_requested_reference_space_types); ClassDB::bind_method(D_METHOD("get_requested_reference_space_types"), &WebXRInterface::get_requested_reference_space_types); ClassDB::bind_method(D_METHOD("is_input_source_active", "input_source_id"), &WebXRInterface::is_input_source_active); @@ -56,6 +57,7 @@ void WebXRInterface::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "optional_features", PROPERTY_HINT_NONE), "set_optional_features", "get_optional_features"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "requested_reference_space_types", PROPERTY_HINT_NONE), "set_requested_reference_space_types", "get_requested_reference_space_types"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "reference_space_type", PROPERTY_HINT_NONE), "", "get_reference_space_type"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "enabled_features", PROPERTY_HINT_NONE), "", "get_enabled_features"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "visibility_state", PROPERTY_HINT_NONE), "", "get_visibility_state"); ADD_SIGNAL(MethodInfo("session_supported", PropertyInfo(Variant::STRING, "session_mode"), PropertyInfo(Variant::BOOL, "supported"))); diff --git a/modules/webxr/webxr_interface.h b/modules/webxr/webxr_interface.h index abaa8c01f8..06c18d0486 100644 --- a/modules/webxr/webxr_interface.h +++ b/modules/webxr/webxr_interface.h @@ -62,6 +62,7 @@ public: virtual void set_requested_reference_space_types(String p_requested_reference_space_types) = 0; virtual String get_requested_reference_space_types() const = 0; virtual String get_reference_space_type() const = 0; + virtual String get_enabled_features() const = 0; virtual bool is_input_source_active(int p_input_source_id) const = 0; virtual Ref<XRPositionalTracker> get_input_source_tracker(int p_input_source_id) const = 0; virtual TargetRayMode get_input_source_target_ray_mode(int p_input_source_id) const = 0; diff --git a/modules/webxr/webxr_interface_js.cpp b/modules/webxr/webxr_interface_js.cpp index 47f20ce1a3..c6213d1aae 100644 --- a/modules/webxr/webxr_interface_js.cpp +++ b/modules/webxr/webxr_interface_js.cpp @@ -41,6 +41,7 @@ #include "scene/main/window.h" #include "servers/rendering/renderer_compositor.h" #include "servers/rendering/rendering_server_globals.h" +#include "servers/xr/xr_hand_tracker.h" #include <emscripten.h> #include <stdlib.h> @@ -49,22 +50,23 @@ void _emwebxr_on_session_supported(char *p_session_mode, int p_supported) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); String session_mode = String(p_session_mode); interface->emit_signal(SNAME("session_supported"), session_mode, p_supported ? true : false); } -void _emwebxr_on_session_started(char *p_reference_space_type) { +void _emwebxr_on_session_started(char *p_reference_space_type, char *p_enabled_features) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); String reference_space_type = String(p_reference_space_type); - static_cast<WebXRInterfaceJS *>(interface.ptr())->_set_reference_space_type(reference_space_type); + interface->_set_reference_space_type(reference_space_type); + interface->_set_enabled_features(p_enabled_features); interface->emit_signal(SNAME("session_started")); } @@ -72,7 +74,7 @@ void _emwebxr_on_session_ended() { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); interface->uninitialize(); @@ -83,7 +85,7 @@ void _emwebxr_on_session_failed(char *p_message) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); interface->uninitialize(); @@ -96,17 +98,17 @@ extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_input_event(int p_event_type, i XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); - ((WebXRInterfaceJS *)interface.ptr())->_on_input_event(p_event_type, p_input_source_id); + interface->_on_input_event(p_event_type, p_input_source_id); } extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_simple_event(char *p_signal_name) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL(xr_server); - Ref<XRInterface> interface = xr_server->find_interface("WebXR"); + Ref<WebXRInterfaceJS> interface = xr_server->find_interface("WebXR"); ERR_FAIL_COND(interface.is_null()); StringName signal_name = StringName(p_signal_name); @@ -149,14 +151,14 @@ String WebXRInterfaceJS::get_requested_reference_space_types() const { return requested_reference_space_types; } -void WebXRInterfaceJS::_set_reference_space_type(String p_reference_space_type) { - reference_space_type = p_reference_space_type; -} - String WebXRInterfaceJS::get_reference_space_type() const { return reference_space_type; } +String WebXRInterfaceJS::get_enabled_features() const { + return enabled_features; +} + bool WebXRInterfaceJS::is_input_source_active(int p_input_source_id) const { ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, false); return input_sources[p_input_source_id].active; @@ -256,7 +258,9 @@ bool WebXRInterfaceJS::initialize() { return false; } - // we must create a tracker for our head + enabled_features.clear(); + + // We must create a tracker for our head. head_transform.basis = Basis(); head_transform.origin = Vector3(); head_tracker.instantiate(); @@ -265,7 +269,7 @@ bool WebXRInterfaceJS::initialize() { head_tracker->set_tracker_desc("Players head"); xr_server->add_tracker(head_tracker); - // make this our primary interface + // Make this our primary interface. xr_server->set_primary_interface(this); // Clear render_targetsize to make sure it gets reset to the new size. @@ -301,6 +305,14 @@ void WebXRInterfaceJS::uninitialize() { head_tracker.unref(); } + for (int i = 0; i < HAND_MAX; i++) { + if (hand_trackers[i].is_valid()) { + xr_server->remove_hand_tracker(i == 0 ? "/user/left" : "/user/right"); + + hand_trackers[i].unref(); + } + } + if (xr_server->get_primary_interface() == this) { // no longer our primary interface xr_server->set_primary_interface(nullptr); @@ -309,7 +321,7 @@ void WebXRInterfaceJS::uninitialize() { godot_webxr_uninitialize(); - GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); + GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); if (texture_storage != nullptr) { for (KeyValue<unsigned int, RID> &E : texture_cache) { // Forcibly mark as not part of a render target so we can free it. @@ -321,7 +333,8 @@ void WebXRInterfaceJS::uninitialize() { } texture_cache.clear(); - reference_space_type = ""; + reference_space_type.clear(); + enabled_features.clear(); initialized = false; }; }; @@ -438,16 +451,11 @@ Projection WebXRInterfaceJS::get_projection_for_view(uint32_t p_view, double p_a } bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { - GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); + GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); if (texture_storage == nullptr) { return false; } - GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target); - if (rt == nullptr) { - return false; - } - // Cache the resources so we don't have to get them from JS twice. color_texture = _get_color_texture(); depth_texture = _get_depth_texture(); @@ -460,23 +468,9 @@ bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { // // See: https://immersive-web.github.io/layers/#xropaquetextures // - // This is why we're doing this sort of silly check: if the color and depth - // textures are the same this frame as last frame, we need to attach them - // again, despite the fact that the GLuint for them hasn't changed. - if (rt->overridden.is_overridden && rt->overridden.color == color_texture && rt->overridden.depth == depth_texture) { - GLES3::Config *config = GLES3::Config::get_singleton(); - bool use_multiview = rt->view_count > 1 && config->multiview_supported; - - glBindFramebuffer(GL_FRAMEBUFFER, rt->fbo); - if (use_multiview) { - glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, rt->color, 0, 0, rt->view_count); - glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, rt->depth, 0, 0, rt->view_count); - } else { - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rt->color, 0); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, rt->depth, 0); - } - glBindFramebuffer(GL_FRAMEBUFFER, texture_storage->system_fbo); - } + // So, even if the color and depth textures have the same GLuint as the last + // frame, we need to re-attach them again. + texture_storage->render_target_set_reattach_textures(p_render_target, true); return true; } @@ -484,7 +478,12 @@ bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { Vector<BlitToScreen> WebXRInterfaceJS::post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) { Vector<BlitToScreen> blit_to_screen; - // We don't need to do anything here. + GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); + if (texture_storage == nullptr) { + return blit_to_screen; + } + + texture_storage->render_target_set_reattach_textures(p_render_target, false); return blit_to_screen; }; @@ -513,7 +512,7 @@ RID WebXRInterfaceJS::_get_texture(unsigned int p_texture_id) { return cache->get(); } - GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); + GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); if (texture_storage == nullptr) { return RID(); } @@ -586,6 +585,9 @@ void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { float buttons[10]; int axes_count; float axes[10]; + int has_hand_data; + float hand_joints[WEBXR_HAND_JOINT_MAX * 16]; + float hand_radii[WEBXR_HAND_JOINT_MAX]; input_source.active = godot_webxr_update_input_source( p_input_source_id, @@ -598,7 +600,10 @@ void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { &button_count, buttons, &axes_count, - axes); + axes, + &has_hand_data, + hand_joints, + hand_radii); if (!input_source.active) { if (input_source.tracker.is_valid()) { @@ -689,6 +694,7 @@ void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { event->set_index(touch_index); event->set_position(position); event->set_relative(delta); + event->set_relative_screen_position(delta); Input::get_singleton()->parse_input_event(event); } } @@ -696,6 +702,56 @@ void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { touches[touch_index].position = position; } } + + if (p_input_source_id < 2) { + Ref<XRHandTracker> hand_tracker = hand_trackers[p_input_source_id]; + if (has_hand_data) { + // Transform orientations to match Godot Humanoid skeleton. + const Basis bone_adjustment( + Vector3(-1.0, 0.0, 0.0), + Vector3(0.0, 0.0, -1.0), + Vector3(0.0, -1.0, 0.0)); + + if (unlikely(hand_tracker.is_null())) { + hand_tracker.instantiate(); + hand_tracker->set_hand(p_input_source_id == 0 ? XRHandTracker::HAND_LEFT : XRHandTracker::HAND_RIGHT); + + // These flags always apply, since WebXR doesn't give us enough insight to be more fine grained. + BitField<XRHandTracker::HandJointFlags> joint_flags(XRHandTracker::HAND_JOINT_FLAG_POSITION_VALID | XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_VALID | XRHandTracker::HAND_JOINT_FLAG_POSITION_TRACKED | XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_TRACKED); + for (int godot_joint = 0; godot_joint < XRHandTracker::HAND_JOINT_MAX; godot_joint++) { + hand_tracker->set_hand_joint_flags((XRHandTracker::HandJoint)godot_joint, joint_flags); + } + + hand_trackers[p_input_source_id] = hand_tracker; + xr_server->add_hand_tracker(p_input_source_id == 0 ? "/user/left" : "/user/right", hand_tracker); + } + + hand_tracker->set_has_tracking_data(true); + for (int webxr_joint = 0; webxr_joint < WEBXR_HAND_JOINT_MAX; webxr_joint++) { + XRHandTracker::HandJoint godot_joint = (XRHandTracker::HandJoint)(webxr_joint + 1); + + Transform3D joint_transform = _js_matrix_to_transform(hand_joints + (16 * webxr_joint)); + joint_transform.basis *= bone_adjustment; + hand_tracker->set_hand_joint_transform(godot_joint, joint_transform); + + hand_tracker->set_hand_joint_radius(godot_joint, hand_radii[webxr_joint]); + } + + // WebXR doesn't have a palm joint, so we calculate it by finding the middle of the middle finger metacarpal bone. + { + // 10 is the WebXR middle finger metacarpal joint, and 12 is the offset to the transform origin. + const float *start_pos = hand_joints + (10 * 16) + 12; + // 11 is the WebXR middle finger phalanx proximal joint, and 12 is the offset to the transform origin. + const float *end_pos = hand_joints + (11 * 16) + 12; + Transform3D palm_transform; + palm_transform.origin = (Vector3(start_pos[0], start_pos[1], start_pos[2]) + Vector3(end_pos[0], end_pos[1], end_pos[2])) / 2.0; + hand_tracker->set_hand_joint_transform(XRHandTracker::HAND_JOINT_PALM, palm_transform); + } + + } else if (hand_tracker.is_valid()) { + hand_tracker->set_has_tracking_data(false); + } + } } void WebXRInterfaceJS::_on_input_event(int p_event_type, int p_input_source_id) { diff --git a/modules/webxr/webxr_interface_js.h b/modules/webxr/webxr_interface_js.h index d85eb8cad7..fc5df3a59b 100644 --- a/modules/webxr/webxr_interface_js.h +++ b/modules/webxr/webxr_interface_js.h @@ -56,6 +56,7 @@ private: String optional_features; String requested_reference_space_types; String reference_space_type; + String enabled_features; Size2 render_targetsize; RBMap<unsigned int, RID> texture_cache; @@ -73,6 +74,10 @@ private: int touch_index = -1; } input_sources[input_source_count]; + static const int WEBXR_HAND_JOINT_MAX = 25; + static const int HAND_MAX = 2; + Ref<XRHandTracker> hand_trackers[HAND_MAX]; + RID color_texture; RID depth_texture; @@ -94,8 +99,8 @@ public: virtual String get_optional_features() const override; virtual void set_requested_reference_space_types(String p_requested_reference_space_types) override; virtual String get_requested_reference_space_types() const override; - void _set_reference_space_type(String p_reference_space_type); virtual String get_reference_space_type() const override; + virtual String get_enabled_features() const override; virtual bool is_input_source_active(int p_input_source_id) const override; virtual Ref<XRPositionalTracker> get_input_source_tracker(int p_input_source_id) const override; virtual TargetRayMode get_input_source_target_ray_mode(int p_input_source_id) const override; @@ -129,6 +134,9 @@ public: void _on_input_event(int p_event_type, int p_input_source_id); + inline void _set_reference_space_type(String p_reference_space_type) { reference_space_type = p_reference_space_type; } + inline void _set_enabled_features(String p_enabled_features) { enabled_features = p_enabled_features; } + WebXRInterfaceJS(); ~WebXRInterfaceJS(); diff --git a/modules/zip/zip_packer.cpp b/modules/zip/zip_packer.cpp index 5f623476fc..e96d9da7a9 100644 --- a/modules/zip/zip_packer.cpp +++ b/modules/zip/zip_packer.cpp @@ -72,7 +72,24 @@ Error ZIPPacker::start_file(const String &p_path) { zipfi.internal_fa = 0; zipfi.external_fa = 0; - int err = zipOpenNewFileInZip(zf, p_path.utf8().get_data(), &zipfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION); + int err = zipOpenNewFileInZip4(zf, + p_path.utf8().get_data(), + &zipfi, + nullptr, + 0, + nullptr, + 0, + nullptr, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION, + 0, + -MAX_WBITS, + DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY, + nullptr, + 0, + 0, // "version made by", indicates the compatibility of the file attribute information (the `external_fa` field above). + 1 << 11); // Bit 11 is the language encoding flag. When set, filename and comment fields must be encoded using UTF-8. return err == ZIP_OK ? OK : FAILED; } |