summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--SConstruct2
-rw-r--r--core/core_bind.compat.inc16
-rw-r--r--core/core_bind.cpp6
-rw-r--r--core/core_bind.h8
-rw-r--r--core/core_constants.cpp1
-rw-r--r--core/doc_data.cpp6
-rw-r--r--core/doc_data.h22
-rw-r--r--core/extension/extension_api_dump.cpp3
-rw-r--r--core/extension/gdextension_interface.cpp10
-rw-r--r--core/extension/gdextension_interface.h16
-rw-r--r--core/io/resource.cpp22
-rw-r--r--core/io/resource_format_binary.cpp15
-rw-r--r--core/io/resource_loader.cpp154
-rw-r--r--core/io/resource_loader.h16
-rw-r--r--core/math/a_star_grid_2d.cpp185
-rw-r--r--core/math/a_star_grid_2d.h24
-rw-r--r--core/object/object.cpp70
-rw-r--r--core/object/object.h1
-rw-r--r--core/os/os.h2
-rw-r--r--core/typedefs.h3
-rw-r--r--core/variant/dictionary.cpp276
-rw-r--r--core/variant/dictionary.h16
-rw-r--r--core/variant/typed_dictionary.h342
-rw-r--r--core/variant/variant_call.cpp13
-rw-r--r--core/variant/variant_construct.cpp1
-rw-r--r--core/variant/variant_construct.h106
-rw-r--r--core/variant/variant_parser.cpp244
-rw-r--r--core/variant/variant_setget.cpp91
-rw-r--r--core/variant/variant_utility.cpp10
-rw-r--r--doc/class.xsd2
-rw-r--r--doc/classes/@GlobalScope.xml11
-rw-r--r--doc/classes/CanvasItem.xml2
-rw-r--r--doc/classes/Dictionary.xml95
-rw-r--r--doc/classes/EditorSettings.xml6
-rw-r--r--doc/classes/MeshInstance3D.xml8
-rw-r--r--doc/classes/OS.xml2
-rw-r--r--doc/classes/Object.xml49
-rw-r--r--doc/classes/PathFollow2D.xml1
-rw-r--r--doc/classes/PathFollow3D.xml1
-rw-r--r--doc/classes/RenderingServer.xml1
-rw-r--r--doc/classes/SpinBox.xml4
-rwxr-xr-xdoc/tools/make_rst.py37
-rw-r--r--drivers/gles3/rasterizer_canvas_gles3.cpp15
-rw-r--r--drivers/gles3/rasterizer_canvas_gles3.h4
-rw-r--r--drivers/gles3/rasterizer_scene_gles3.cpp2
-rw-r--r--drivers/metal/pixel_formats.mm2
-rw-r--r--drivers/unix/file_access_unix_pipe.cpp16
-rw-r--r--drivers/unix/file_access_unix_pipe.h2
-rw-r--r--drivers/unix/os_unix.cpp6
-rw-r--r--drivers/unix/os_unix.h2
-rw-r--r--drivers/windows/file_access_windows_pipe.cpp12
-rw-r--r--drivers/windows/file_access_windows_pipe.h2
-rw-r--r--editor/animation_bezier_editor.cpp4
-rw-r--r--editor/connections_dialog.cpp16
-rw-r--r--editor/doc_tools.cpp20
-rw-r--r--editor/editor_file_system.cpp75
-rw-r--r--editor/editor_file_system.h4
-rw-r--r--editor/editor_help.cpp72
-rw-r--r--editor/editor_help_search.cpp2
-rw-r--r--editor/editor_node.cpp55
-rw-r--r--editor/editor_node.h5
-rw-r--r--editor/editor_properties.cpp2
-rw-r--r--editor/editor_properties_array_dict.cpp127
-rw-r--r--editor/editor_properties_array_dict.h12
-rw-r--r--editor/editor_settings.cpp3
-rw-r--r--editor/export/export_template_manager.cpp4
-rw-r--r--editor/export/project_export.cpp2
-rw-r--r--editor/filesystem_dock.cpp7
-rw-r--r--editor/gui/editor_file_dialog.cpp3
-rw-r--r--editor/gui/editor_spin_slider.cpp4
-rw-r--r--editor/gui/scene_tree_editor.cpp57
-rw-r--r--editor/gui/scene_tree_editor.h9
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp3
-rw-r--r--editor/plugins/node_3d_editor_gizmos.cpp5
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp37
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp2
-rw-r--r--editor/scene_tree_dock.cpp76
-rw-r--r--editor/scene_tree_dock.h3
-rw-r--r--editor/script_create_dialog.cpp39
-rw-r--r--editor/themes/editor_theme_manager.cpp2
-rw-r--r--main/main.cpp7
-rw-r--r--misc/dist/macos_template.app/Contents/Info.plist11
-rw-r--r--misc/extension_api_validation/4.3-stable.expected7
-rw-r--r--modules/betsy/image_compress_betsy.cpp1
-rw-r--r--modules/gdscript/editor/gdscript_docgen.cpp91
-rw-r--r--modules/gdscript/gdscript.cpp2
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp342
-rw-r--r--modules/gdscript/gdscript_analyzer.h2
-rw-r--r--modules/gdscript/gdscript_byte_codegen.cpp84
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h1
-rw-r--r--modules/gdscript/gdscript_codegen.h1
-rw-r--r--modules/gdscript/gdscript_compiler.cpp24
-rw-r--r--modules/gdscript/gdscript_disassembler.cpp107
-rw-r--r--modules/gdscript/gdscript_editor.cpp26
-rw-r--r--modules/gdscript/gdscript_function.h43
-rw-r--r--modules/gdscript/gdscript_parser.cpp244
-rw-r--r--modules/gdscript/gdscript_parser.h9
-rw-r--r--modules/gdscript/gdscript_vm.cpp194
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.gd3
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.gd4
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.gd2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.gd4
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.gd7
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.gd20
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.out11
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.gd4
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.gd214
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd29
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out8
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg19
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd7
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg19
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd7
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg12
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd7
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg12
-rw-r--r--modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd7
-rw-r--r--modules/gdscript/tests/scripts/parser/features/typed_dictionaries.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/features/typed_dictionaries.out3
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.gd7
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.out5
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.gd7
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.gd7
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.gd9
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.out4
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/member_info.gd24
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/member_info.out24
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.gd6
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.out3
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.gd7
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.out2
-rw-r--r--modules/gdscript/tests/scripts/utils.notest.gd7
-rw-r--r--modules/interactive_music/audio_stream_interactive.cpp8
-rw-r--r--modules/interactive_music/doc_classes/AudioStreamInteractive.xml2
-rwxr-xr-xmodules/mono/build_scripts/build_assemblies.py19
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs2
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs2
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs3
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs8
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs69
-rw-r--r--modules/mono/editor/bindings_generator.cpp15
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj2
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj2
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt7
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt19
-rw-r--r--platform/macos/doc_classes/EditorExportPlatformMacOS.xml7
-rw-r--r--platform/macos/export/export_plugin.cpp9
-rw-r--r--platform/web/os_web.cpp2
-rw-r--r--platform/web/os_web.h2
-rw-r--r--platform/windows/display_server_windows.cpp106
-rw-r--r--platform/windows/display_server_windows.h10
-rw-r--r--platform/windows/os_windows.cpp17
-rw-r--r--platform/windows/os_windows.h2
-rw-r--r--scene/2d/navigation_region_2d.cpp1
-rw-r--r--scene/2d/path_2d.cpp7
-rw-r--r--scene/3d/mesh_instance_3d.cpp167
-rw-r--r--scene/3d/mesh_instance_3d.h1
-rw-r--r--scene/3d/path_3d.cpp7
-rw-r--r--scene/3d/xr_hand_modifier_3d.cpp12
-rw-r--r--scene/3d/xr_hand_modifier_3d.h2
-rw-r--r--scene/animation/animation_mixer.cpp5
-rw-r--r--scene/gui/code_edit.cpp97
-rw-r--r--scene/gui/code_edit.h3
-rw-r--r--scene/gui/file_dialog.cpp3
-rw-r--r--scene/gui/line_edit.cpp52
-rw-r--r--scene/gui/rich_text_label.cpp756
-rw-r--r--scene/gui/rich_text_label.h4
-rw-r--r--scene/gui/spin_box.cpp9
-rw-r--r--scene/gui/spin_box.h5
-rw-r--r--scene/gui/text_edit.cpp486
-rw-r--r--scene/gui/text_edit.h35
-rw-r--r--scene/resources/packed_scene.cpp117
-rw-r--r--scene/resources/packed_scene.h1
-rw-r--r--scene/resources/resource_format_text.cpp28
-rw-r--r--scene/resources/style_box_flat.cpp4
-rw-r--r--scene/resources/text_paragraph.cpp47
-rw-r--r--scene/theme/default_theme.cpp2
-rw-r--r--servers/rendering/renderer_rd/environment/sky.cpp2
-rw-r--r--tests/core/variant/test_dictionary.h39
-rwxr-xr-xtests/create_test.py15
-rw-r--r--tests/scene/test_audio_stream_wav.h18
-rw-r--r--tests/scene/test_height_map_shape_3d.h122
-rw-r--r--tests/scene/test_parallax_2d.h131
-rw-r--r--tests/scene/test_text_edit.h44
-rw-r--r--tests/test_main.cpp4
200 files changed, 5683 insertions, 1238 deletions
diff --git a/SConstruct b/SConstruct
index 0297cd6e61..5c1ccd2449 100644
--- a/SConstruct
+++ b/SConstruct
@@ -791,6 +791,8 @@ else:
# Note that this is still not complete conformance, as certain Windows-related headers
# don't compile under complete conformance.
env.Prepend(CCFLAGS=["/permissive-"])
+ # Allow use of `__cplusplus` macro to determine C++ standard universally.
+ env.Prepend(CXXFLAGS=["/Zc:__cplusplus"])
# Disable exception handling. Godot doesn't use exceptions anywhere, and this
# saves around 20% of binary size and very significant build time (GH-80513).
diff --git a/core/core_bind.compat.inc b/core/core_bind.compat.inc
index 83b7b33e38..3e8ac3c5de 100644
--- a/core/core_bind.compat.inc
+++ b/core/core_bind.compat.inc
@@ -32,6 +32,8 @@
namespace core_bind {
+// Semaphore
+
void Semaphore::_post_bind_compat_93605() {
post(1);
}
@@ -40,6 +42,16 @@ void Semaphore::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("post"), &Semaphore::_post_bind_compat_93605);
}
-}; // namespace core_bind
+// OS
+
+Dictionary OS::_execute_with_pipe_bind_compat_94434(const String &p_path, const Vector<String> &p_arguments) {
+ return execute_with_pipe(p_path, p_arguments, true);
+}
+
+void OS::_bind_compatibility_methods() {
+ ClassDB::bind_compatibility_method(D_METHOD("execute_with_pipe", "path", "arguments"), &OS::_execute_with_pipe_bind_compat_94434);
+}
+
+} // namespace core_bind
-#endif
+#endif // DISABLE_DEPRECATED
diff --git a/core/core_bind.cpp b/core/core_bind.cpp
index 4172793f9d..750598ab20 100644
--- a/core/core_bind.cpp
+++ b/core/core_bind.cpp
@@ -314,12 +314,12 @@ int OS::execute(const String &p_path, const Vector<String> &p_arguments, Array r
return exitcode;
}
-Dictionary OS::execute_with_pipe(const String &p_path, const Vector<String> &p_arguments) {
+Dictionary OS::execute_with_pipe(const String &p_path, const Vector<String> &p_arguments, bool p_blocking) {
List<String> args;
for (const String &arg : p_arguments) {
args.push_back(arg);
}
- return ::OS::get_singleton()->execute_with_pipe(p_path, args);
+ return ::OS::get_singleton()->execute_with_pipe(p_path, args, p_blocking);
}
int OS::create_instance(const Vector<String> &p_arguments) {
@@ -619,7 +619,7 @@ void OS::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_executable_path"), &OS::get_executable_path);
ClassDB::bind_method(D_METHOD("read_string_from_stdin"), &OS::read_string_from_stdin);
ClassDB::bind_method(D_METHOD("execute", "path", "arguments", "output", "read_stderr", "open_console"), &OS::execute, DEFVAL(Array()), DEFVAL(false), DEFVAL(false));
- ClassDB::bind_method(D_METHOD("execute_with_pipe", "path", "arguments"), &OS::execute_with_pipe);
+ ClassDB::bind_method(D_METHOD("execute_with_pipe", "path", "arguments", "blocking"), &OS::execute_with_pipe, DEFVAL(true));
ClassDB::bind_method(D_METHOD("create_process", "path", "arguments", "open_console"), &OS::create_process, DEFVAL(false));
ClassDB::bind_method(D_METHOD("create_instance", "arguments"), &OS::create_instance);
ClassDB::bind_method(D_METHOD("kill", "pid"), &OS::kill);
diff --git a/core/core_bind.h b/core/core_bind.h
index 122963e634..e4ba0e8f56 100644
--- a/core/core_bind.h
+++ b/core/core_bind.h
@@ -128,6 +128,12 @@ protected:
static void _bind_methods();
static OS *singleton;
+#ifndef DISABLE_DEPRECATED
+ Dictionary _execute_with_pipe_bind_compat_94434(const String &p_path, const Vector<String> &p_arguments);
+
+ static void _bind_compatibility_methods();
+#endif
+
public:
enum RenderingDriver {
RENDERING_DRIVER_VULKAN,
@@ -161,7 +167,7 @@ public:
String get_executable_path() const;
String read_string_from_stdin();
int execute(const String &p_path, const Vector<String> &p_arguments, Array r_output = Array(), bool p_read_stderr = false, bool p_open_console = false);
- Dictionary execute_with_pipe(const String &p_path, const Vector<String> &p_arguments);
+ Dictionary execute_with_pipe(const String &p_path, const Vector<String> &p_arguments, bool p_blocking = true);
int create_process(const String &p_path, const Vector<String> &p_arguments, bool p_open_console = false);
int create_instance(const Vector<String> &p_arguments);
Error kill(int p_pid);
diff --git a/core/core_constants.cpp b/core/core_constants.cpp
index 5322e39ec0..68af5abf66 100644
--- a/core/core_constants.cpp
+++ b/core/core_constants.cpp
@@ -671,6 +671,7 @@ void register_global_constants() {
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_OBJECTID);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_POINTER);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);
+ BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_DICTIONARY_TYPE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALE_ID);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALIZABLE_STRING);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_NODE_TYPE);
diff --git a/core/doc_data.cpp b/core/doc_data.cpp
index 672a36c35c..f40e878d52 100644
--- a/core/doc_data.cpp
+++ b/core/doc_data.cpp
@@ -33,6 +33,8 @@
String DocData::get_default_value_string(const Variant &p_value) {
if (p_value.get_type() == Variant::ARRAY) {
return Variant(Array(p_value, 0, StringName(), Variant())).get_construct_string().replace("\n", " ");
+ } else if (p_value.get_type() == Variant::DICTIONARY) {
+ return Variant(Dictionary(p_value, 0, StringName(), Variant(), 0, StringName(), Variant())).get_construct_string().replace("\n", " ");
} else {
return p_value.get_construct_string().replace("\n", " ");
}
@@ -57,6 +59,8 @@ void DocData::return_doc_from_retinfo(DocData::MethodDoc &p_method, const Proper
p_method.return_type = p_retinfo.class_name;
} else if (p_retinfo.type == Variant::ARRAY && p_retinfo.hint == PROPERTY_HINT_ARRAY_TYPE) {
p_method.return_type = p_retinfo.hint_string + "[]";
+ } else if (p_retinfo.type == Variant::DICTIONARY && p_retinfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ p_method.return_type = "Dictionary[" + p_retinfo.hint_string.replace(";", ", ") + "]";
} else if (p_retinfo.hint == PROPERTY_HINT_RESOURCE_TYPE) {
p_method.return_type = p_retinfo.hint_string;
} else if (p_retinfo.type == Variant::NIL && p_retinfo.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
@@ -89,6 +93,8 @@ void DocData::argument_doc_from_arginfo(DocData::ArgumentDoc &p_argument, const
p_argument.type = p_arginfo.class_name;
} else if (p_arginfo.type == Variant::ARRAY && p_arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) {
p_argument.type = p_arginfo.hint_string + "[]";
+ } else if (p_arginfo.type == Variant::DICTIONARY && p_arginfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ p_argument.type = "Dictionary[" + p_arginfo.hint_string.replace(";", ", ") + "]";
} else if (p_arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) {
p_argument.type = p_arginfo.hint_string;
} else if (p_arginfo.type == Variant::NIL) {
diff --git a/core/doc_data.h b/core/doc_data.h
index 04bd55eaba..6a7f4355db 100644
--- a/core/doc_data.h
+++ b/core/doc_data.h
@@ -522,6 +522,10 @@ public:
String type;
String data_type;
String description;
+ bool is_deprecated = false;
+ String deprecated_message;
+ bool is_experimental = false;
+ String experimental_message;
String default_value;
String keywords;
bool operator<(const ThemeItemDoc &p_theme_item) const {
@@ -550,6 +554,16 @@ public:
doc.description = p_dict["description"];
}
+ if (p_dict.has("deprecated")) {
+ doc.is_deprecated = true;
+ doc.deprecated_message = p_dict["deprecated"];
+ }
+
+ if (p_dict.has("experimental")) {
+ doc.is_experimental = true;
+ doc.experimental_message = p_dict["experimental"];
+ }
+
if (p_dict.has("default_value")) {
doc.default_value = p_dict["default_value"];
}
@@ -579,6 +593,14 @@ public:
dict["description"] = p_doc.description;
}
+ if (p_doc.is_deprecated) {
+ dict["deprecated"] = p_doc.deprecated_message;
+ }
+
+ if (p_doc.is_experimental) {
+ dict["experimental"] = p_doc.experimental_message;
+ }
+
if (!p_doc.default_value.is_empty()) {
dict["default_value"] = p_doc.default_value;
}
diff --git a/core/extension/extension_api_dump.cpp b/core/extension/extension_api_dump.cpp
index 848b6f3886..296ebc901f 100644
--- a/core/extension/extension_api_dump.cpp
+++ b/core/extension/extension_api_dump.cpp
@@ -60,6 +60,9 @@ static String get_property_info_type_name(const PropertyInfo &p_info) {
if (p_info.type == Variant::ARRAY && (p_info.hint == PROPERTY_HINT_ARRAY_TYPE)) {
return String("typedarray::") + p_info.hint_string;
}
+ if (p_info.type == Variant::DICTIONARY && (p_info.hint == PROPERTY_HINT_DICTIONARY_TYPE)) {
+ return String("typeddictionary::") + p_info.hint_string;
+ }
if (p_info.type == Variant::INT && (p_info.usage & (PROPERTY_USAGE_CLASS_IS_ENUM))) {
return String("enum::") + String(p_info.class_name);
}
diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp
index 0ebe86d0a7..ddf90f6130 100644
--- a/core/extension/gdextension_interface.cpp
+++ b/core/extension/gdextension_interface.cpp
@@ -1199,6 +1199,15 @@ static GDExtensionVariantPtr gdextension_dictionary_operator_index_const(GDExten
return (GDExtensionVariantPtr)&self->operator[](*(const Variant *)p_key);
}
+void gdextension_dictionary_set_typed(GDExtensionTypePtr p_self, GDExtensionVariantType p_key_type, GDExtensionConstStringNamePtr p_key_class_name, GDExtensionConstVariantPtr p_key_script, GDExtensionVariantType p_value_type, GDExtensionConstStringNamePtr p_value_class_name, GDExtensionConstVariantPtr p_value_script) {
+ Dictionary *self = reinterpret_cast<Dictionary *>(p_self);
+ const StringName *key_class_name = reinterpret_cast<const StringName *>(p_key_class_name);
+ const Variant *key_script = reinterpret_cast<const Variant *>(p_key_script);
+ const StringName *value_class_name = reinterpret_cast<const StringName *>(p_value_class_name);
+ const Variant *value_script = reinterpret_cast<const Variant *>(p_value_script);
+ self->set_typed((uint32_t)p_key_type, *key_class_name, *key_script, (uint32_t)p_value_type, *value_class_name, *value_script);
+}
+
/* OBJECT API */
static void gdextension_object_method_bind_call(GDExtensionMethodBindPtr p_method_bind, GDExtensionObjectPtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_arg_count, GDExtensionUninitializedVariantPtr r_return, GDExtensionCallError *r_error) {
@@ -1679,6 +1688,7 @@ void gdextension_setup_interface() {
REGISTER_INTERFACE_FUNC(array_set_typed);
REGISTER_INTERFACE_FUNC(dictionary_operator_index);
REGISTER_INTERFACE_FUNC(dictionary_operator_index_const);
+ REGISTER_INTERFACE_FUNC(dictionary_set_typed);
REGISTER_INTERFACE_FUNC(object_method_bind_call);
REGISTER_INTERFACE_FUNC(object_method_bind_ptrcall);
REGISTER_INTERFACE_FUNC(object_destroy);
diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h
index 9057e04bf3..d3132baf1b 100644
--- a/core/extension/gdextension_interface.h
+++ b/core/extension/gdextension_interface.h
@@ -2372,6 +2372,22 @@ typedef GDExtensionVariantPtr (*GDExtensionInterfaceDictionaryOperatorIndex)(GDE
*/
typedef GDExtensionVariantPtr (*GDExtensionInterfaceDictionaryOperatorIndexConst)(GDExtensionConstTypePtr p_self, GDExtensionConstVariantPtr p_key);
+/**
+ * @name dictionary_set_typed
+ * @since 4.4
+ *
+ * Makes a Dictionary into a typed Dictionary.
+ *
+ * @param p_self A pointer to the Dictionary.
+ * @param p_key_type The type of Variant the Dictionary key will store.
+ * @param p_key_class_name A pointer to a StringName with the name of the object (if p_key_type is GDEXTENSION_VARIANT_TYPE_OBJECT).
+ * @param p_key_script A pointer to a Script object (if p_key_type is GDEXTENSION_VARIANT_TYPE_OBJECT and the base class is extended by a script).
+ * @param p_value_type The type of Variant the Dictionary value will store.
+ * @param p_value_class_name A pointer to a StringName with the name of the object (if p_value_type is GDEXTENSION_VARIANT_TYPE_OBJECT).
+ * @param p_value_script A pointer to a Script object (if p_value_type is GDEXTENSION_VARIANT_TYPE_OBJECT and the base class is extended by a script).
+ */
+typedef void (*GDExtensionInterfaceDictionarySetTyped)(GDExtensionTypePtr p_self, GDExtensionVariantType p_key_type, GDExtensionConstStringNamePtr p_key_class_name, GDExtensionConstVariantPtr p_key_script, GDExtensionVariantType p_value_type, GDExtensionConstStringNamePtr p_value_class_name, GDExtensionConstVariantPtr p_value_script);
+
/* INTERFACE: Object */
/**
diff --git a/core/io/resource.cpp b/core/io/resource.cpp
index ff12dc5851..6177cba6a4 100644
--- a/core/io/resource.cpp
+++ b/core/io/resource.cpp
@@ -40,12 +40,12 @@
#include <stdio.h>
void Resource::emit_changed() {
- if (ResourceLoader::is_within_load() && MessageQueue::get_main_singleton() != MessageQueue::get_singleton() && !MessageQueue::get_singleton()->is_flushing()) {
- // Let the connection happen on the call queue, later, since signals are not thread-safe.
- call_deferred("emit_signal", CoreStringName(changed));
- } else {
- emit_signal(CoreStringName(changed));
+ if (ResourceLoader::is_within_load() && !Thread::is_main_thread()) {
+ ResourceLoader::resource_changed_emit(this);
+ return;
}
+
+ emit_signal(CoreStringName(changed));
}
void Resource::_resource_path_changed() {
@@ -166,22 +166,22 @@ bool Resource::editor_can_reload_from_file() {
}
void Resource::connect_changed(const Callable &p_callable, uint32_t p_flags) {
- if (ResourceLoader::is_within_load() && MessageQueue::get_main_singleton() != MessageQueue::get_singleton() && !MessageQueue::get_singleton()->is_flushing()) {
- // Let the check and connection happen on the call queue, later, since signals are not thread-safe.
- callable_mp(this, &Resource::connect_changed).call_deferred(p_callable, p_flags);
+ if (ResourceLoader::is_within_load() && !Thread::is_main_thread()) {
+ ResourceLoader::resource_changed_connect(this, p_callable, p_flags);
return;
}
+
if (!is_connected(CoreStringName(changed), p_callable) || p_flags & CONNECT_REFERENCE_COUNTED) {
connect(CoreStringName(changed), p_callable, p_flags);
}
}
void Resource::disconnect_changed(const Callable &p_callable) {
- if (ResourceLoader::is_within_load() && MessageQueue::get_main_singleton() != MessageQueue::get_singleton() && !MessageQueue::get_singleton()->is_flushing()) {
- // Let the check and disconnection happen on the call queue, later, since signals are not thread-safe.
- callable_mp(this, &Resource::disconnect_changed).call_deferred(p_callable);
+ if (ResourceLoader::is_within_load() && !Thread::is_main_thread()) {
+ ResourceLoader::resource_changed_disconnect(this, p_callable);
return;
}
+
if (is_connected(CoreStringName(changed), p_callable)) {
disconnect(CoreStringName(changed), p_callable);
}
diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp
index f71257fa76..41a8a569d0 100644
--- a/core/io/resource_format_binary.cpp
+++ b/core/io/resource_format_binary.cpp
@@ -857,6 +857,19 @@ Error ResourceLoaderBinary::load() {
}
}
+ if (value.get_type() == Variant::DICTIONARY) {
+ Dictionary set_dict = value;
+ bool is_get_valid = false;
+ Variant get_value = res->get(name, &is_get_valid);
+ if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) {
+ Dictionary get_dict = get_value;
+ if (!set_dict.is_same_typed(get_dict)) {
+ value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(),
+ get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script());
+ }
+ }
+ }
+
if (set_valid) {
res->set(name, value);
}
@@ -2064,6 +2077,8 @@ void ResourceFormatSaverBinaryInstance::_find_resources(const Variant &p_variant
case Variant::DICTIONARY: {
Dictionary d = p_variant;
+ _find_resources(d.get_typed_key_script());
+ _find_resources(d.get_typed_value_script());
List<Variant> keys;
d.get_key_list(&keys);
for (const Variant &E : keys) {
diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp
index 7cf101b0de..4201bfe785 100644
--- a/core/io/resource_loader.cpp
+++ b/core/io/resource_loader.cpp
@@ -31,6 +31,7 @@
#include "resource_loader.h"
#include "core/config/project_settings.h"
+#include "core/core_bind.h"
#include "core/io/file_access.h"
#include "core/io/resource_importer.h"
#include "core/object/script_language.h"
@@ -234,17 +235,22 @@ void ResourceLoader::LoadToken::clear() {
// User-facing tokens shouldn't be deleted until completely claimed.
DEV_ASSERT(user_rc == 0 && user_path.is_empty());
- if (!local_path.is_empty()) { // Empty is used for the special case where the load task is not registered.
- DEV_ASSERT(thread_load_tasks.has(local_path));
- ThreadLoadTask &load_task = thread_load_tasks[local_path];
- if (load_task.task_id && !load_task.awaited) {
- task_to_await = load_task.task_id;
+ if (!local_path.is_empty()) {
+ if (task_if_unregistered) {
+ memdelete(task_if_unregistered);
+ task_if_unregistered = nullptr;
+ } else {
+ DEV_ASSERT(thread_load_tasks.has(local_path));
+ ThreadLoadTask &load_task = thread_load_tasks[local_path];
+ if (load_task.task_id && !load_task.awaited) {
+ task_to_await = load_task.task_id;
+ }
+ // Removing a task which is still in progress would be catastrophic.
+ // Tokens must be alive until the task thread function is done.
+ DEV_ASSERT(load_task.status == THREAD_LOAD_FAILED || load_task.status == THREAD_LOAD_LOADED);
+ thread_load_tasks.erase(local_path);
}
- // Removing a task which is still in progress would be catastrophic.
- // Tokens must be alive until the task thread function is done.
- DEV_ASSERT(load_task.status == THREAD_LOAD_FAILED || load_task.status == THREAD_LOAD_LOADED);
- thread_load_tasks.erase(local_path);
- local_path.clear();
+ local_path.clear(); // Mark as already cleared.
}
}
@@ -324,6 +330,9 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
}
}
+ ThreadLoadTask *curr_load_task_backup = curr_load_task;
+ curr_load_task = &load_task;
+
// Thread-safe either if it's the current thread or a brand new one.
CallQueue *own_mq_override = nullptr;
if (load_nesting == 0) {
@@ -451,6 +460,8 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
}
DEV_ASSERT(load_paths_stack.is_empty());
}
+
+ curr_load_task = curr_load_task_backup;
}
static String _validate_local_path(const String &p_path) {
@@ -521,9 +532,7 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
Ref<LoadToken> load_token;
bool must_not_register = false;
- ThreadLoadTask unregistered_load_task; // Once set, must be valid up to the call to do the load.
ThreadLoadTask *load_task_ptr = nullptr;
- bool run_on_current_thread = false;
{
MutexLock thread_load_lock(thread_load_mutex);
@@ -578,12 +587,11 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
}
}
- // If we want to ignore cache, but there's another task loading it, we can't add this one to the map and we also have to finish within scope.
+ // If we want to ignore cache, but there's another task loading it, we can't add this one to the map.
must_not_register = ignoring_cache && thread_load_tasks.has(local_path);
if (must_not_register) {
- load_token->local_path.clear();
- unregistered_load_task = load_task;
- load_task_ptr = &unregistered_load_task;
+ load_token->task_if_unregistered = memnew(ThreadLoadTask(load_task));
+ load_task_ptr = load_token->task_if_unregistered;
} else {
DEV_ASSERT(!thread_load_tasks.has(local_path));
HashMap<String, ResourceLoader::ThreadLoadTask>::Iterator E = thread_load_tasks.insert(local_path, load_task);
@@ -591,9 +599,7 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
}
}
- run_on_current_thread = must_not_register || p_thread_mode == LOAD_THREAD_FROM_CURRENT;
-
- if (run_on_current_thread) {
+ if (p_thread_mode == LOAD_THREAD_FROM_CURRENT) {
// The current thread may happen to be a thread from the pool.
WorkerThreadPool::TaskID tid = WorkerThreadPool::get_singleton()->get_caller_task_id();
if (tid != WorkerThreadPool::INVALID_TASK_ID) {
@@ -606,11 +612,8 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
}
} // MutexLock(thread_load_mutex).
- if (run_on_current_thread) {
+ if (p_thread_mode == LOAD_THREAD_FROM_CURRENT) {
_run_load_task(load_task_ptr);
- if (must_not_register) {
- load_token->res_if_unregistered = load_task_ptr->resource;
- }
}
return load_token;
@@ -738,7 +741,10 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
*r_error = OK;
}
- if (!p_load_token.local_path.is_empty()) {
+ ThreadLoadTask *load_task_ptr = nullptr;
+ if (p_load_token.task_if_unregistered) {
+ load_task_ptr = p_load_token.task_if_unregistered;
+ } else {
if (!thread_load_tasks.has(p_load_token.local_path)) {
if (r_error) {
*r_error = ERR_BUG;
@@ -809,22 +815,51 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
load_task.error = FAILED;
}
- Ref<Resource> resource = load_task.resource;
- if (r_error) {
- *r_error = load_task.error;
- }
- return resource;
- } else {
- // Special case of an unregistered task.
- // The resource should have been loaded by now.
- Ref<Resource> resource = p_load_token.res_if_unregistered;
- if (!resource.is_valid()) {
- if (r_error) {
- *r_error = FAILED;
+ load_task_ptr = &load_task;
+ }
+
+ p_thread_load_lock.temp_unlock();
+
+ Ref<Resource> resource = load_task_ptr->resource;
+ if (r_error) {
+ *r_error = load_task_ptr->error;
+ }
+
+ if (resource.is_valid()) {
+ if (curr_load_task) {
+ // A task awaiting another => Let the awaiter accumulate the resource changed connections.
+ DEV_ASSERT(curr_load_task != load_task_ptr);
+ for (const ThreadLoadTask::ResourceChangedConnection &rcc : load_task_ptr->resource_changed_connections) {
+ curr_load_task->resource_changed_connections.push_back(rcc);
+ }
+ } else {
+ // A leaf task being awaited => Propagate the resource changed connections.
+ if (Thread::is_main_thread()) {
+ // On the main thread it's safe to migrate the connections to the standard signal mechanism.
+ for (const ThreadLoadTask::ResourceChangedConnection &rcc : load_task_ptr->resource_changed_connections) {
+ if (rcc.callable.is_valid()) {
+ rcc.source->connect_changed(rcc.callable, rcc.flags);
+ }
+ }
+ } else {
+ // On non-main threads, we have to queue and call it done when processed.
+ if (!load_task_ptr->resource_changed_connections.is_empty()) {
+ for (const ThreadLoadTask::ResourceChangedConnection &rcc : load_task_ptr->resource_changed_connections) {
+ if (rcc.callable.is_valid()) {
+ MessageQueue::get_main_singleton()->push_callable(callable_mp(rcc.source, &Resource::connect_changed).bind(rcc.callable, rcc.flags));
+ }
+ }
+ core_bind::Semaphore done;
+ MessageQueue::get_main_singleton()->push_callable(callable_mp(&done, &core_bind::Semaphore::post));
+ done.wait();
+ }
}
}
- return resource;
}
+
+ p_thread_load_lock.temp_relock();
+
+ return resource;
}
bool ResourceLoader::_ensure_load_progress() {
@@ -838,6 +873,50 @@ bool ResourceLoader::_ensure_load_progress() {
return true;
}
+void ResourceLoader::resource_changed_connect(Resource *p_source, const Callable &p_callable, uint32_t p_flags) {
+ print_lt(vformat("%d\t%ud:%s\t" FUNCTION_STR "\t%d", Thread::get_caller_id(), p_source->get_instance_id(), p_source->get_class(), p_callable.get_object_id()));
+
+ MutexLock lock(thread_load_mutex);
+
+ for (const ThreadLoadTask::ResourceChangedConnection &rcc : curr_load_task->resource_changed_connections) {
+ if (unlikely(rcc.source == p_source && rcc.callable == p_callable)) {
+ return;
+ }
+ }
+
+ ThreadLoadTask::ResourceChangedConnection rcc;
+ rcc.source = p_source;
+ rcc.callable = p_callable;
+ rcc.flags = p_flags;
+ curr_load_task->resource_changed_connections.push_back(rcc);
+}
+
+void ResourceLoader::resource_changed_disconnect(Resource *p_source, const Callable &p_callable) {
+ print_lt(vformat("%d\t%ud:%s\t" FUNCTION_STR "t%d", Thread::get_caller_id(), p_source->get_instance_id(), p_source->get_class(), p_callable.get_object_id()));
+
+ MutexLock lock(thread_load_mutex);
+
+ for (uint32_t i = 0; i < curr_load_task->resource_changed_connections.size(); ++i) {
+ const ThreadLoadTask::ResourceChangedConnection &rcc = curr_load_task->resource_changed_connections[i];
+ if (unlikely(rcc.source == p_source && rcc.callable == p_callable)) {
+ curr_load_task->resource_changed_connections.remove_at_unordered(i);
+ return;
+ }
+ }
+}
+
+void ResourceLoader::resource_changed_emit(Resource *p_source) {
+ print_lt(vformat("%d\t%ud:%s\t" FUNCTION_STR, Thread::get_caller_id(), p_source->get_instance_id(), p_source->get_class()));
+
+ MutexLock lock(thread_load_mutex);
+
+ for (const ThreadLoadTask::ResourceChangedConnection &rcc : curr_load_task->resource_changed_connections) {
+ if (unlikely(rcc.source == p_source)) {
+ rcc.callable.call();
+ }
+ }
+}
+
Ref<Resource> ResourceLoader::ensure_resource_ref_override_for_outer_load(const String &p_path, const String &p_res_type) {
ERR_FAIL_COND_V(load_nesting == 0, Ref<Resource>()); // It makes no sense to use this from nesting level 0.
const String &local_path = _validate_local_path(p_path);
@@ -1368,6 +1447,7 @@ bool ResourceLoader::timestamp_on_load = false;
thread_local int ResourceLoader::load_nesting = 0;
thread_local Vector<String> ResourceLoader::load_paths_stack;
thread_local HashMap<int, HashMap<String, Ref<Resource>>> ResourceLoader::res_ref_overrides;
+thread_local ResourceLoader::ThreadLoadTask *ResourceLoader::curr_load_task = nullptr;
SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG> &_get_res_loader_mutex() {
return ResourceLoader::thread_load_mutex;
diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h
index f75bf019fb..caaf9f8f45 100644
--- a/core/io/resource_loader.h
+++ b/core/io/resource_loader.h
@@ -106,6 +106,8 @@ class ResourceLoader {
MAX_LOADERS = 64
};
+ struct ThreadLoadTask;
+
public:
enum ThreadLoadStatus {
THREAD_LOAD_INVALID_RESOURCE,
@@ -124,7 +126,7 @@ public:
String local_path;
String user_path;
uint32_t user_rc = 0; // Having user RC implies regular RC incremented in one, until the user RC reaches zero.
- Ref<Resource> res_if_unregistered;
+ ThreadLoadTask *task_if_unregistered = nullptr;
void clear();
@@ -187,6 +189,13 @@ private:
Ref<Resource> resource;
bool use_sub_threads = false;
HashSet<String> sub_tasks;
+
+ struct ResourceChangedConnection {
+ Resource *source = nullptr;
+ Callable callable;
+ uint32_t flags = 0;
+ };
+ LocalVector<ResourceChangedConnection> resource_changed_connections;
};
static void _run_load_task(void *p_userdata);
@@ -194,6 +203,7 @@ private:
static thread_local int load_nesting;
static thread_local HashMap<int, HashMap<String, Ref<Resource>>> res_ref_overrides; // Outermost key is nesting level.
static thread_local Vector<String> load_paths_stack;
+ static thread_local ThreadLoadTask *curr_load_task;
static SafeBinaryMutex<BINARY_MUTEX_TAG> thread_load_mutex;
friend SafeBinaryMutex<BINARY_MUTEX_TAG> &_get_res_loader_mutex();
@@ -214,6 +224,10 @@ public:
static bool is_within_load() { return load_nesting > 0; };
+ static void resource_changed_connect(Resource *p_source, const Callable &p_callable, uint32_t p_flags);
+ static void resource_changed_disconnect(Resource *p_source, const Callable &p_callable);
+ static void resource_changed_emit(Resource *p_source);
+
static Ref<Resource> load(const String &p_path, const String &p_type_hint = "", ResourceFormatLoader::CacheMode p_cache_mode = ResourceFormatLoader::CACHE_MODE_REUSE, Error *r_error = nullptr);
static bool exists(const String &p_path, const String &p_type_hint = "");
diff --git a/core/math/a_star_grid_2d.cpp b/core/math/a_star_grid_2d.cpp
index b1d2f82f9d..c40ee5b4d7 100644
--- a/core/math/a_star_grid_2d.cpp
+++ b/core/math/a_star_grid_2d.cpp
@@ -127,13 +127,18 @@ void AStarGrid2D::update() {
}
points.clear();
+ solid_mask.clear();
const int32_t end_x = region.get_end().x;
const int32_t end_y = region.get_end().y;
const Vector2 half_cell_size = cell_size / 2;
+ for (int32_t x = region.position.x; x < end_x + 2; x++) {
+ solid_mask.push_back(true);
+ }
for (int32_t y = region.position.y; y < end_y; y++) {
LocalVector<Point> line;
+ solid_mask.push_back(true);
for (int32_t x = region.position.x; x < end_x; x++) {
Vector2 v = offset;
switch (cell_shape) {
@@ -150,10 +155,16 @@ void AStarGrid2D::update() {
break;
}
line.push_back(Point(Vector2i(x, y), v));
+ solid_mask.push_back(false);
}
+ solid_mask.push_back(true);
points.push_back(line);
}
+ for (int32_t x = region.position.x; x < end_x + 2; x++) {
+ solid_mask.push_back(true);
+ }
+
dirty = false;
}
@@ -207,13 +218,13 @@ AStarGrid2D::Heuristic AStarGrid2D::get_default_estimate_heuristic() const {
void AStarGrid2D::set_point_solid(const Vector2i &p_id, bool p_solid) {
ERR_FAIL_COND_MSG(dirty, "Grid is not initialized. Call the update method.");
ERR_FAIL_COND_MSG(!is_in_boundsv(p_id), vformat("Can't set if point is disabled. Point %s out of bounds %s.", p_id, region));
- _get_point_unchecked(p_id)->solid = p_solid;
+ _set_solid_unchecked(p_id, p_solid);
}
bool AStarGrid2D::is_point_solid(const Vector2i &p_id) const {
ERR_FAIL_COND_V_MSG(dirty, false, "Grid is not initialized. Call the update method.");
ERR_FAIL_COND_V_MSG(!is_in_boundsv(p_id), false, vformat("Can't get if point is disabled. Point %s out of bounds %s.", p_id, region));
- return _get_point_unchecked(p_id)->solid;
+ return _get_solid_unchecked(p_id);
}
void AStarGrid2D::set_point_weight_scale(const Vector2i &p_id, real_t p_weight_scale) {
@@ -238,7 +249,7 @@ void AStarGrid2D::fill_solid_region(const Rect2i &p_region, bool p_solid) {
for (int32_t y = safe_region.position.y; y < end_y; y++) {
for (int32_t x = safe_region.position.x; x < end_x; x++) {
- _get_point_unchecked(x, y)->solid = p_solid;
+ _set_solid_unchecked(x, y, p_solid);
}
}
}
@@ -259,13 +270,6 @@ void AStarGrid2D::fill_weight_scale_region(const Rect2i &p_region, real_t p_weig
}
AStarGrid2D::Point *AStarGrid2D::_jump(Point *p_from, Point *p_to) {
- if (!p_to || p_to->solid) {
- return nullptr;
- }
- if (p_to == end) {
- return p_to;
- }
-
int32_t from_x = p_from->id.x;
int32_t from_y = p_from->id.y;
@@ -276,72 +280,109 @@ AStarGrid2D::Point *AStarGrid2D::_jump(Point *p_from, Point *p_to) {
int32_t dy = to_y - from_y;
if (diagonal_mode == DIAGONAL_MODE_ALWAYS || diagonal_mode == DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE) {
- if (dx != 0 && dy != 0) {
- if ((_is_walkable(to_x - dx, to_y + dy) && !_is_walkable(to_x - dx, to_y)) || (_is_walkable(to_x + dx, to_y - dy) && !_is_walkable(to_x, to_y - dy))) {
- return p_to;
- }
- if (_jump(p_to, _get_point(to_x + dx, to_y)) != nullptr) {
- return p_to;
+ if (dx == 0 || dy == 0) {
+ return _forced_successor(to_x, to_y, dx, dy);
+ }
+
+ while (_is_walkable(to_x, to_y) && (diagonal_mode == DIAGONAL_MODE_ALWAYS || _is_walkable(to_x, to_y - dy) || _is_walkable(to_x - dx, to_y))) {
+ if (end->id.x == to_x && end->id.y == to_y) {
+ return end;
}
- if (_jump(p_to, _get_point(to_x, to_y + dy)) != nullptr) {
- return p_to;
+
+ if ((_is_walkable(to_x - dx, to_y + dy) && !_is_walkable(to_x - dx, to_y)) || (_is_walkable(to_x + dx, to_y - dy) && !_is_walkable(to_x, to_y - dy))) {
+ return _get_point_unchecked(to_x, to_y);
}
- } else {
- if (dx != 0) {
- if ((_is_walkable(to_x + dx, to_y + 1) && !_is_walkable(to_x, to_y + 1)) || (_is_walkable(to_x + dx, to_y - 1) && !_is_walkable(to_x, to_y - 1))) {
- return p_to;
- }
- } else {
- if ((_is_walkable(to_x + 1, to_y + dy) && !_is_walkable(to_x + 1, to_y)) || (_is_walkable(to_x - 1, to_y + dy) && !_is_walkable(to_x - 1, to_y))) {
- return p_to;
- }
+
+ if (_forced_successor(to_x + dx, to_y, dx, 0) != nullptr || _forced_successor(to_x, to_y + dy, 0, dy) != nullptr) {
+ return _get_point_unchecked(to_x, to_y);
}
+
+ to_x += dx;
+ to_y += dy;
}
- if (_is_walkable(to_x + dx, to_y + dy) && (diagonal_mode == DIAGONAL_MODE_ALWAYS || (_is_walkable(to_x + dx, to_y) || _is_walkable(to_x, to_y + dy)))) {
- return _jump(p_to, _get_point(to_x + dx, to_y + dy));
- }
+
} else if (diagonal_mode == DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES) {
- if (dx != 0 && dy != 0) {
- if ((_is_walkable(to_x + dx, to_y + dy) && !_is_walkable(to_x, to_y + dy)) || !_is_walkable(to_x + dx, to_y)) {
- return p_to;
- }
- if (_jump(p_to, _get_point(to_x + dx, to_y)) != nullptr) {
- return p_to;
+ if (dx == 0 || dy == 0) {
+ return _forced_successor(from_x, from_y, dx, dy, true);
+ }
+
+ while (_is_walkable(to_x, to_y) && _is_walkable(to_x, to_y - dy) && _is_walkable(to_x - dx, to_y)) {
+ if (end->id.x == to_x && end->id.y == to_y) {
+ return end;
}
- if (_jump(p_to, _get_point(to_x, to_y + dy)) != nullptr) {
- return p_to;
+
+ if ((_is_walkable(to_x + dx, to_y + dy) && !_is_walkable(to_x, to_y + dy)) || !_is_walkable(to_x + dx, to_y)) {
+ return _get_point_unchecked(to_x, to_y);
}
- } else {
- if (dx != 0) {
- if ((_is_walkable(to_x, to_y + 1) && !_is_walkable(to_x - dx, to_y + 1)) || (_is_walkable(to_x, to_y - 1) && !_is_walkable(to_x - dx, to_y - 1))) {
- return p_to;
- }
- } else {
- if ((_is_walkable(to_x + 1, to_y) && !_is_walkable(to_x + 1, to_y - dy)) || (_is_walkable(to_x - 1, to_y) && !_is_walkable(to_x - 1, to_y - dy))) {
- return p_to;
- }
+
+ if (_forced_successor(to_x, to_y, dx, 0) != nullptr || _forced_successor(to_x, to_y, 0, dy) != nullptr) {
+ return _get_point_unchecked(to_x, to_y);
}
+
+ to_x += dx;
+ to_y += dy;
}
- if (_is_walkable(to_x + dx, to_y + dy) && _is_walkable(to_x + dx, to_y) && _is_walkable(to_x, to_y + dy)) {
- return _jump(p_to, _get_point(to_x + dx, to_y + dy));
- }
+
} else { // DIAGONAL_MODE_NEVER
- if (dx != 0) {
- if ((_is_walkable(to_x, to_y - 1) && !_is_walkable(to_x - dx, to_y - 1)) || (_is_walkable(to_x, to_y + 1) && !_is_walkable(to_x - dx, to_y + 1))) {
- return p_to;
+ if (dy == 0) {
+ return _forced_successor(from_x, from_y, dx, 0, true);
+ }
+
+ while (_is_walkable(to_x, to_y)) {
+ if (end->id.x == to_x && end->id.y == to_y) {
+ return end;
}
- } else if (dy != 0) {
+
if ((_is_walkable(to_x - 1, to_y) && !_is_walkable(to_x - 1, to_y - dy)) || (_is_walkable(to_x + 1, to_y) && !_is_walkable(to_x + 1, to_y - dy))) {
- return p_to;
- }
- if (_jump(p_to, _get_point(to_x + 1, to_y)) != nullptr) {
- return p_to;
+ return _get_point_unchecked(to_x, to_y);
}
- if (_jump(p_to, _get_point(to_x - 1, to_y)) != nullptr) {
- return p_to;
+
+ if (_forced_successor(to_x, to_y, 1, 0, true) != nullptr || _forced_successor(to_x, to_y, -1, 0, true) != nullptr) {
+ return _get_point_unchecked(to_x, to_y);
}
+
+ to_y += dy;
}
- return _jump(p_to, _get_point(to_x + dx, to_y + dy));
+ }
+
+ return nullptr;
+}
+
+AStarGrid2D::Point *AStarGrid2D::_forced_successor(int32_t p_x, int32_t p_y, int32_t p_dx, int32_t p_dy, bool p_inclusive) {
+ // Remembering previous results can improve performance.
+ bool l_prev = false, r_prev = false, l = false, r = false;
+
+ int32_t o_x = p_x, o_y = p_y;
+ if (p_inclusive) {
+ o_x += p_dx;
+ o_y += p_dy;
+ }
+
+ int32_t l_x = p_x - p_dy, l_y = p_y - p_dx;
+ int32_t r_x = p_x + p_dy, r_y = p_y + p_dx;
+
+ while (_is_walkable(o_x, o_y)) {
+ if (end->id.x == o_x && end->id.y == o_y) {
+ return end;
+ }
+
+ l_prev = l || _is_walkable(l_x, l_y);
+ r_prev = r || _is_walkable(r_x, r_y);
+
+ l_x += p_dx;
+ l_y += p_dy;
+ r_x += p_dx;
+ r_y += p_dy;
+
+ l = _is_walkable(l_x, l_y);
+ r = _is_walkable(r_x, r_y);
+
+ if ((l && !l_prev) || (r && !r_prev)) {
+ return _get_point_unchecked(o_x, o_y);
+ }
+
+ o_x += p_dx;
+ o_y += p_dy;
}
return nullptr;
}
@@ -394,19 +435,19 @@ void AStarGrid2D::_get_nbors(Point *p_point, LocalVector<Point *> &r_nbors) {
}
}
- if (top && !top->solid) {
+ if (top && !_get_solid_unchecked(top->id)) {
r_nbors.push_back(top);
ts0 = true;
}
- if (right && !right->solid) {
+ if (right && !_get_solid_unchecked(right->id)) {
r_nbors.push_back(right);
ts1 = true;
}
- if (bottom && !bottom->solid) {
+ if (bottom && !_get_solid_unchecked(bottom->id)) {
r_nbors.push_back(bottom);
ts2 = true;
}
- if (left && !left->solid) {
+ if (left && !_get_solid_unchecked(left->id)) {
r_nbors.push_back(left);
ts3 = true;
}
@@ -436,16 +477,16 @@ void AStarGrid2D::_get_nbors(Point *p_point, LocalVector<Point *> &r_nbors) {
break;
}
- if (td0 && (top_left && !top_left->solid)) {
+ if (td0 && (top_left && !_get_solid_unchecked(top_left->id))) {
r_nbors.push_back(top_left);
}
- if (td1 && (top_right && !top_right->solid)) {
+ if (td1 && (top_right && !_get_solid_unchecked(top_right->id))) {
r_nbors.push_back(top_right);
}
- if (td2 && (bottom_right && !bottom_right->solid)) {
+ if (td2 && (bottom_right && !_get_solid_unchecked(bottom_right->id))) {
r_nbors.push_back(bottom_right);
}
- if (td3 && (bottom_left && !bottom_left->solid)) {
+ if (td3 && (bottom_left && !_get_solid_unchecked(bottom_left->id))) {
r_nbors.push_back(bottom_left);
}
}
@@ -454,7 +495,7 @@ bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point) {
last_closest_point = nullptr;
pass++;
- if (p_end_point->solid) {
+ if (_get_solid_unchecked(p_end_point->id)) {
return false;
}
@@ -500,7 +541,7 @@ bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point) {
continue;
}
} else {
- if (e->solid || e->closed_pass == pass) {
+ if (_get_solid_unchecked(e->id) || e->closed_pass == pass) {
continue;
}
weight_scale = e->weight_scale;
@@ -580,7 +621,7 @@ TypedArray<Dictionary> AStarGrid2D::get_point_data_in_region(const Rect2i &p_reg
Dictionary dict;
dict["id"] = p.id;
dict["position"] = p.pos;
- dict["solid"] = p.solid;
+ dict["solid"] = _get_solid_unchecked(p.id);
dict["weight_scale"] = p.weight_scale;
data.push_back(dict);
}
diff --git a/core/math/a_star_grid_2d.h b/core/math/a_star_grid_2d.h
index c2be0bbf29..f5ac472f09 100644
--- a/core/math/a_star_grid_2d.h
+++ b/core/math/a_star_grid_2d.h
@@ -78,7 +78,6 @@ private:
struct Point {
Vector2i id;
- bool solid = false;
Vector2 pos;
real_t weight_scale = 1.0;
@@ -111,6 +110,7 @@ private:
}
};
+ LocalVector<bool> solid_mask;
LocalVector<LocalVector<Point>> points;
Point *end = nullptr;
Point *last_closest_point = nullptr;
@@ -118,11 +118,12 @@ private:
uint64_t pass = 1;
private: // Internal routines.
+ _FORCE_INLINE_ size_t _to_mask_index(int32_t p_x, int32_t p_y) const {
+ return ((p_y - region.position.y + 1) * (region.size.x + 2)) + p_x - region.position.x + 1;
+ }
+
_FORCE_INLINE_ bool _is_walkable(int32_t p_x, int32_t p_y) const {
- if (region.has_point(Vector2i(p_x, p_y))) {
- return !points[p_y - region.position.y][p_x - region.position.x].solid;
- }
- return false;
+ return !solid_mask[_to_mask_index(p_x, p_y)];
}
_FORCE_INLINE_ Point *_get_point(int32_t p_x, int32_t p_y) {
@@ -132,6 +133,18 @@ private: // Internal routines.
return nullptr;
}
+ _FORCE_INLINE_ void _set_solid_unchecked(int32_t p_x, int32_t p_y, bool p_solid) {
+ solid_mask[_to_mask_index(p_x, p_y)] = p_solid;
+ }
+
+ _FORCE_INLINE_ void _set_solid_unchecked(const Vector2i &p_id, bool p_solid) {
+ solid_mask[_to_mask_index(p_id.x, p_id.y)] = p_solid;
+ }
+
+ _FORCE_INLINE_ bool _get_solid_unchecked(const Vector2i &p_id) const {
+ return solid_mask[_to_mask_index(p_id.x, p_id.y)];
+ }
+
_FORCE_INLINE_ Point *_get_point_unchecked(int32_t p_x, int32_t p_y) {
return &points[p_y - region.position.y][p_x - region.position.x];
}
@@ -146,6 +159,7 @@ private: // Internal routines.
void _get_nbors(Point *p_point, LocalVector<Point *> &r_nbors);
Point *_jump(Point *p_from, Point *p_to);
+ Point *_forced_successor(int32_t p_x, int32_t p_y, int32_t p_dx, int32_t p_dy, bool p_inclusive = false);
bool _solve(Point *p_begin_point, Point *p_end_point);
protected:
diff --git a/core/object/object.cpp b/core/object/object.cpp
index 4be1dc4b34..d6b7d7a7fe 100644
--- a/core/object/object.cpp
+++ b/core/object/object.cpp
@@ -1724,33 +1724,65 @@ void Object::_bind_methods() {
#define BIND_OBJ_CORE_METHOD(m_method) \
::ClassDB::add_virtual_method(get_class_static(), m_method, true, Vector<String>(), true);
- MethodInfo notification_mi("_notification", PropertyInfo(Variant::INT, "what"));
- notification_mi.arguments_metadata.push_back(GodotTypeInfo::Metadata::METADATA_INT_IS_INT32);
- BIND_OBJ_CORE_METHOD(notification_mi);
- BIND_OBJ_CORE_METHOD(MethodInfo(Variant::BOOL, "_set", PropertyInfo(Variant::STRING_NAME, "property"), PropertyInfo(Variant::NIL, "value")));
+ BIND_OBJ_CORE_METHOD(MethodInfo("_init"));
+
+ BIND_OBJ_CORE_METHOD(MethodInfo(Variant::STRING, "_to_string"));
+
+ {
+ MethodInfo mi("_notification");
+ mi.arguments.push_back(PropertyInfo(Variant::INT, "what"));
+ mi.arguments_metadata.push_back(GodotTypeInfo::Metadata::METADATA_INT_IS_INT32);
+ BIND_OBJ_CORE_METHOD(mi);
+ }
+
+ {
+ MethodInfo mi("_set");
+ mi.arguments.push_back(PropertyInfo(Variant::STRING_NAME, "property"));
+ mi.arguments.push_back(PropertyInfo(Variant::NIL, "value", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_NIL_IS_VARIANT));
+ mi.return_val.type = Variant::BOOL;
+ BIND_OBJ_CORE_METHOD(mi);
+ }
+
#ifdef TOOLS_ENABLED
- MethodInfo miget("_get", PropertyInfo(Variant::STRING_NAME, "property"));
- miget.return_val.name = "Variant";
- miget.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
- BIND_OBJ_CORE_METHOD(miget);
+ {
+ MethodInfo mi("_get");
+ mi.arguments.push_back(PropertyInfo(Variant::STRING_NAME, "property"));
+ mi.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
+ BIND_OBJ_CORE_METHOD(mi);
+ }
- MethodInfo plget("_get_property_list");
- plget.return_val.type = Variant::ARRAY;
- plget.return_val.hint = PROPERTY_HINT_ARRAY_TYPE;
- plget.return_val.hint_string = "Dictionary";
- BIND_OBJ_CORE_METHOD(plget);
+ {
+ MethodInfo mi("_get_property_list");
+ mi.return_val.type = Variant::ARRAY;
+ mi.return_val.hint = PROPERTY_HINT_ARRAY_TYPE;
+ mi.return_val.hint_string = "Dictionary";
+ BIND_OBJ_CORE_METHOD(mi);
+ }
BIND_OBJ_CORE_METHOD(MethodInfo(Variant::NIL, "_validate_property", PropertyInfo(Variant::DICTIONARY, "property")));
BIND_OBJ_CORE_METHOD(MethodInfo(Variant::BOOL, "_property_can_revert", PropertyInfo(Variant::STRING_NAME, "property")));
- MethodInfo mipgr("_property_get_revert", PropertyInfo(Variant::STRING_NAME, "property"));
- mipgr.return_val.name = "Variant";
- mipgr.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
- BIND_OBJ_CORE_METHOD(mipgr);
+ {
+ MethodInfo mi("_property_get_revert");
+ mi.arguments.push_back(PropertyInfo(Variant::STRING_NAME, "property"));
+ mi.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
+ BIND_OBJ_CORE_METHOD(mi);
+ }
+
+ // These are actually `Variant` methods, but that doesn't matter since scripts can't inherit built-in types.
+
+ BIND_OBJ_CORE_METHOD(MethodInfo(Variant::BOOL, "_iter_init", PropertyInfo(Variant::ARRAY, "iter")));
+
+ BIND_OBJ_CORE_METHOD(MethodInfo(Variant::BOOL, "_iter_next", PropertyInfo(Variant::ARRAY, "iter")));
+
+ {
+ MethodInfo mi("_iter_get");
+ mi.arguments.push_back(PropertyInfo(Variant::NIL, "iter", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_NIL_IS_VARIANT));
+ mi.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
+ BIND_OBJ_CORE_METHOD(mi);
+ }
#endif
- BIND_OBJ_CORE_METHOD(MethodInfo("_init"));
- BIND_OBJ_CORE_METHOD(MethodInfo(Variant::STRING, "_to_string"));
BIND_CONSTANT(NOTIFICATION_POSTINITIALIZE);
BIND_CONSTANT(NOTIFICATION_PREDELETE);
diff --git a/core/object/object.h b/core/object/object.h
index bc3f663baf..19e6fc5d47 100644
--- a/core/object/object.h
+++ b/core/object/object.h
@@ -86,6 +86,7 @@ enum PropertyHint {
PROPERTY_HINT_HIDE_QUATERNION_EDIT, /// Only Node3D::transform should hide the quaternion editor.
PROPERTY_HINT_PASSWORD,
PROPERTY_HINT_LAYERS_AVOIDANCE,
+ PROPERTY_HINT_DICTIONARY_TYPE,
PROPERTY_HINT_MAX,
};
diff --git a/core/os/os.h b/core/os/os.h
index 553ae4cf76..30d2a4266f 100644
--- a/core/os/os.h
+++ b/core/os/os.h
@@ -182,7 +182,7 @@ public:
virtual Vector<String> get_system_font_path_for_text(const String &p_font_name, const String &p_text, const String &p_locale = String(), const String &p_script = String(), int p_weight = 400, int p_stretch = 100, bool p_italic = false) const { return Vector<String>(); };
virtual String get_executable_path() const;
virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) = 0;
- virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) { return Dictionary(); }
+ virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking = true) { return Dictionary(); }
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) = 0;
virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) { return create_process(get_executable_path(), p_arguments, r_child_id); };
virtual Error kill(const ProcessID &p_pid) = 0;
diff --git a/core/typedefs.h b/core/typedefs.h
index 0de803293d..35c4668581 100644
--- a/core/typedefs.h
+++ b/core/typedefs.h
@@ -44,6 +44,9 @@
#include "core/error/error_list.h"
#include <cstdint>
+// Ensure that C++ standard is at least C++17. If on MSVC, also ensures that the `Zc:__cplusplus` flag is present.
+static_assert(__cplusplus >= 201703L);
+
// Turn argument to string constant:
// https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html#Stringizing
#ifndef _STR
diff --git a/core/variant/dictionary.cpp b/core/variant/dictionary.cpp
index 733d13a106..f2522a4545 100644
--- a/core/variant/dictionary.cpp
+++ b/core/variant/dictionary.cpp
@@ -32,6 +32,7 @@
#include "core/templates/hash_map.h"
#include "core/templates/safe_refcount.h"
+#include "core/variant/container_type_validate.h"
#include "core/variant/variant.h"
// required in this order by VariantInternal, do not remove this comment.
#include "core/object/class_db.h"
@@ -43,6 +44,9 @@ struct DictionaryPrivate {
SafeRefCount refcount;
Variant *read_only = nullptr; // If enabled, a pointer is used to a temporary value that is used to return read-only values.
HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator> variant_map;
+ ContainerTypeValidate typed_key;
+ ContainerTypeValidate typed_value;
+ Variant *typed_fallback = nullptr; // Allows a typed dictionary to return dummy values when attempting an invalid access.
};
void Dictionary::get_key_list(List<Variant> *p_keys) const {
@@ -120,7 +124,9 @@ Variant *Dictionary::getptr(const Variant &p_key) {
}
Variant Dictionary::get_valid(const Variant &p_key) const {
- HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::ConstIterator E(_p->variant_map.find(p_key));
+ Variant key = p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "get_valid"), Variant());
+ HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::ConstIterator E(_p->variant_map.find(key));
if (!E) {
return Variant();
@@ -129,7 +135,9 @@ Variant Dictionary::get_valid(const Variant &p_key) const {
}
Variant Dictionary::get(const Variant &p_key, const Variant &p_default) const {
- const Variant *result = getptr(p_key);
+ Variant key = p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "get"), p_default);
+ const Variant *result = getptr(key);
if (!result) {
return p_default;
}
@@ -138,10 +146,14 @@ Variant Dictionary::get(const Variant &p_key, const Variant &p_default) const {
}
Variant Dictionary::get_or_add(const Variant &p_key, const Variant &p_default) {
- const Variant *result = getptr(p_key);
+ Variant key = p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "get"), p_default);
+ const Variant *result = getptr(key);
if (!result) {
- operator[](p_key) = p_default;
- return p_default;
+ Variant value = p_default;
+ ERR_FAIL_COND_V(!_p->typed_value.validate(value, "add"), value);
+ operator[](key) = value;
+ return value;
}
return *result;
}
@@ -155,12 +167,16 @@ bool Dictionary::is_empty() const {
}
bool Dictionary::has(const Variant &p_key) const {
+ Variant key = p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "use 'has'"), false);
return _p->variant_map.has(p_key);
}
bool Dictionary::has_all(const Array &p_keys) const {
for (int i = 0; i < p_keys.size(); i++) {
- if (!has(p_keys[i])) {
+ Variant key = p_keys[i];
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "use 'has_all'"), false);
+ if (!has(key)) {
return false;
}
}
@@ -168,8 +184,10 @@ bool Dictionary::has_all(const Array &p_keys) const {
}
Variant Dictionary::find_key(const Variant &p_value) const {
+ Variant value = p_value;
+ ERR_FAIL_COND_V(!_p->typed_value.validate(value, "find_key"), Variant());
for (const KeyValue<Variant, Variant> &E : _p->variant_map) {
- if (E.value == p_value) {
+ if (E.value == value) {
return E.key;
}
}
@@ -177,8 +195,10 @@ Variant Dictionary::find_key(const Variant &p_value) const {
}
bool Dictionary::erase(const Variant &p_key) {
+ Variant key = p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "erase"), false);
ERR_FAIL_COND_V_MSG(_p->read_only, false, "Dictionary is in read-only state.");
- return _p->variant_map.erase(p_key);
+ return _p->variant_map.erase(key);
}
bool Dictionary::operator==(const Dictionary &p_dictionary) const {
@@ -238,8 +258,12 @@ void Dictionary::clear() {
void Dictionary::merge(const Dictionary &p_dictionary, bool p_overwrite) {
ERR_FAIL_COND_MSG(_p->read_only, "Dictionary is in read-only state.");
for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
- if (p_overwrite || !has(E.key)) {
- operator[](E.key) = E.value;
+ Variant key = E.key;
+ Variant value = E.value;
+ ERR_FAIL_COND(!_p->typed_key.validate(key, "merge"));
+ ERR_FAIL_COND(!_p->typed_key.validate(value, "merge"));
+ if (p_overwrite || !has(key)) {
+ operator[](key) = value;
}
}
}
@@ -256,6 +280,9 @@ void Dictionary::_unref() const {
if (_p->read_only) {
memdelete(_p->read_only);
}
+ if (_p->typed_fallback) {
+ memdelete(_p->typed_fallback);
+ }
memdelete(_p);
}
_p = nullptr;
@@ -284,6 +311,9 @@ uint32_t Dictionary::recursive_hash(int recursion_count) const {
Array Dictionary::keys() const {
Array varr;
+ if (is_typed_key()) {
+ varr.set_typed(get_typed_key_builtin(), get_typed_key_class_name(), get_typed_key_script());
+ }
if (_p->variant_map.is_empty()) {
return varr;
}
@@ -301,6 +331,9 @@ Array Dictionary::keys() const {
Array Dictionary::values() const {
Array varr;
+ if (is_typed_value()) {
+ varr.set_typed(get_typed_value_builtin(), get_typed_value_class_name(), get_typed_value_script());
+ }
if (_p->variant_map.is_empty()) {
return varr;
}
@@ -316,6 +349,146 @@ Array Dictionary::values() const {
return varr;
}
+void Dictionary::assign(const Dictionary &p_dictionary) {
+ const ContainerTypeValidate &typed_key = _p->typed_key;
+ const ContainerTypeValidate &typed_key_source = p_dictionary._p->typed_key;
+
+ const ContainerTypeValidate &typed_value = _p->typed_value;
+ const ContainerTypeValidate &typed_value_source = p_dictionary._p->typed_value;
+
+ if ((typed_key == typed_key_source || typed_key.type == Variant::NIL || (typed_key_source.type == Variant::OBJECT && typed_key.can_reference(typed_key_source))) &&
+ (typed_value == typed_value_source || typed_value.type == Variant::NIL || (typed_value_source.type == Variant::OBJECT && typed_value.can_reference(typed_value_source)))) {
+ // From same to same or,
+ // from anything to variants or,
+ // from subclasses to base classes.
+ _p->variant_map = p_dictionary._p->variant_map;
+ return;
+ }
+
+ int size = p_dictionary._p->variant_map.size();
+ HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator> variant_map = HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>(size);
+
+ Vector<Variant> key_array;
+ key_array.resize(size);
+ Variant *key_data = key_array.ptrw();
+
+ Vector<Variant> value_array;
+ value_array.resize(size);
+ Variant *value_data = value_array.ptrw();
+
+ if (typed_key == typed_key_source || typed_key.type == Variant::NIL || (typed_key_source.type == Variant::OBJECT && typed_key.can_reference(typed_key_source))) {
+ // From same to same or,
+ // from anything to variants or,
+ // from subclasses to base classes.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *key = &E.key;
+ key_data[i++] = *key;
+ }
+ } else if ((typed_key_source.type == Variant::NIL && typed_key.type == Variant::OBJECT) || (typed_key_source.type == Variant::OBJECT && typed_key_source.can_reference(typed_key))) {
+ // From variants to objects or,
+ // from base classes to subclasses.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *key = &E.key;
+ if (key->get_type() != Variant::NIL && (key->get_type() != Variant::OBJECT || !typed_key.validate_object(*key, "assign"))) {
+ ERR_FAIL_MSG(vformat(R"(Unable to convert key from "%s" to "%s".)", Variant::get_type_name(key->get_type()), Variant::get_type_name(typed_key.type)));
+ }
+ key_data[i++] = *key;
+ }
+ } else if (typed_key.type == Variant::OBJECT || typed_key_source.type == Variant::OBJECT) {
+ ERR_FAIL_MSG(vformat(R"(Cannot assign contents of "Dictionary[%s, %s]" to "Dictionary[%s, %s]".)", Variant::get_type_name(typed_key_source.type), Variant::get_type_name(typed_value_source.type),
+ Variant::get_type_name(typed_key.type), Variant::get_type_name(typed_value.type)));
+ } else if (typed_key_source.type == Variant::NIL && typed_key.type != Variant::OBJECT) {
+ // From variants to primitives.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *key = &E.key;
+ if (key->get_type() == typed_key.type) {
+ key_data[i++] = *key;
+ continue;
+ }
+ if (!Variant::can_convert_strict(key->get_type(), typed_key.type)) {
+ ERR_FAIL_MSG(vformat(R"(Unable to convert key from "%s" to "%s".)", Variant::get_type_name(key->get_type()), Variant::get_type_name(typed_key.type)));
+ }
+ Callable::CallError ce;
+ Variant::construct(typed_key.type, key_data[i++], &key, 1, ce);
+ ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert key from "%s" to "%s".)", Variant::get_type_name(key->get_type()), Variant::get_type_name(typed_key.type)));
+ }
+ } else if (Variant::can_convert_strict(typed_key_source.type, typed_key.type)) {
+ // From primitives to different convertible primitives.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *key = &E.key;
+ Callable::CallError ce;
+ Variant::construct(typed_key.type, key_data[i++], &key, 1, ce);
+ ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert key from "%s" to "%s".)", Variant::get_type_name(key->get_type()), Variant::get_type_name(typed_key.type)));
+ }
+ } else {
+ ERR_FAIL_MSG(vformat(R"(Cannot assign contents of "Dictionary[%s, %s]" to "Dictionary[%s, %s].)", Variant::get_type_name(typed_key_source.type), Variant::get_type_name(typed_value_source.type),
+ Variant::get_type_name(typed_key.type), Variant::get_type_name(typed_value.type)));
+ }
+
+ if (typed_value == typed_value_source || typed_value.type == Variant::NIL || (typed_value_source.type == Variant::OBJECT && typed_value.can_reference(typed_value_source))) {
+ // From same to same or,
+ // from anything to variants or,
+ // from subclasses to base classes.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *value = &E.value;
+ value_data[i++] = *value;
+ }
+ } else if (((typed_value_source.type == Variant::NIL && typed_value.type == Variant::OBJECT) || (typed_value_source.type == Variant::OBJECT && typed_value_source.can_reference(typed_value)))) {
+ // From variants to objects or,
+ // from base classes to subclasses.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *value = &E.value;
+ if (value->get_type() != Variant::NIL && (value->get_type() != Variant::OBJECT || !typed_value.validate_object(*value, "assign"))) {
+ ERR_FAIL_MSG(vformat(R"(Unable to convert value at key "%s" from "%s" to "%s".)", key_data[i], Variant::get_type_name(value->get_type()), Variant::get_type_name(typed_value.type)));
+ }
+ value_data[i++] = *value;
+ }
+ } else if (typed_value.type == Variant::OBJECT || typed_value_source.type == Variant::OBJECT) {
+ ERR_FAIL_MSG(vformat(R"(Cannot assign contents of "Dictionary[%s, %s]" to "Dictionary[%s, %s]".)", Variant::get_type_name(typed_key_source.type), Variant::get_type_name(typed_value_source.type),
+ Variant::get_type_name(typed_key.type), Variant::get_type_name(typed_value.type)));
+ } else if (typed_value_source.type == Variant::NIL && typed_value.type != Variant::OBJECT) {
+ // From variants to primitives.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *value = &E.value;
+ if (value->get_type() == typed_value.type) {
+ value_data[i++] = *value;
+ continue;
+ }
+ if (!Variant::can_convert_strict(value->get_type(), typed_value.type)) {
+ ERR_FAIL_MSG(vformat(R"(Unable to convert value at key "%s" from "%s" to "%s".)", key_data[i], Variant::get_type_name(value->get_type()), Variant::get_type_name(typed_value.type)));
+ }
+ Callable::CallError ce;
+ Variant::construct(typed_value.type, value_data[i++], &value, 1, ce);
+ ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert value at key "%s" from "%s" to "%s".)", key_data[i - 1], Variant::get_type_name(value->get_type()), Variant::get_type_name(typed_value.type)));
+ }
+ } else if (Variant::can_convert_strict(typed_value_source.type, typed_value.type)) {
+ // From primitives to different convertible primitives.
+ int i = 0;
+ for (const KeyValue<Variant, Variant> &E : p_dictionary._p->variant_map) {
+ const Variant *value = &E.value;
+ Callable::CallError ce;
+ Variant::construct(typed_value.type, value_data[i++], &value, 1, ce);
+ ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert value at key "%s" from "%s" to "%s".)", key_data[i - 1], Variant::get_type_name(value->get_type()), Variant::get_type_name(typed_value.type)));
+ }
+ } else {
+ ERR_FAIL_MSG(vformat(R"(Cannot assign contents of "Dictionary[%s, %s]" to "Dictionary[%s, %s].)", Variant::get_type_name(typed_key_source.type), Variant::get_type_name(typed_value_source.type),
+ Variant::get_type_name(typed_key.type), Variant::get_type_name(typed_value.type)));
+ }
+
+ for (int i = 0; i < size; i++) {
+ variant_map.insert(key_data[i], value_data[i]);
+ }
+
+ _p->variant_map = variant_map;
+}
+
const Variant *Dictionary::next(const Variant *p_key) const {
if (p_key == nullptr) {
// caller wants to get the first element
@@ -324,7 +497,9 @@ const Variant *Dictionary::next(const Variant *p_key) const {
}
return nullptr;
}
- HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::Iterator E = _p->variant_map.find(*p_key);
+ Variant key = *p_key;
+ ERR_FAIL_COND_V(!_p->typed_key.validate(key, "next"), nullptr);
+ HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>::Iterator E = _p->variant_map.find(key);
if (!E) {
return nullptr;
@@ -354,6 +529,8 @@ bool Dictionary::is_read_only() const {
Dictionary Dictionary::recursive_duplicate(bool p_deep, int recursion_count) const {
Dictionary n;
+ n._p->typed_key = _p->typed_key;
+ n._p->typed_value = _p->typed_value;
if (recursion_count > MAX_RECURSION) {
ERR_PRINT("Max recursion reached");
@@ -374,6 +551,76 @@ Dictionary Dictionary::recursive_duplicate(bool p_deep, int recursion_count) con
return n;
}
+void Dictionary::set_typed(uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script) {
+ ERR_FAIL_COND_MSG(_p->read_only, "Dictionary is in read-only state.");
+ ERR_FAIL_COND_MSG(_p->variant_map.size() > 0, "Type can only be set when dictionary is empty.");
+ ERR_FAIL_COND_MSG(_p->refcount.get() > 1, "Type can only be set when dictionary has no more than one user.");
+ ERR_FAIL_COND_MSG(_p->typed_key.type != Variant::NIL || _p->typed_value.type != Variant::NIL, "Type can only be set once.");
+ ERR_FAIL_COND_MSG((p_key_class_name != StringName() && p_key_type != Variant::OBJECT) || (p_value_class_name != StringName() && p_value_type != Variant::OBJECT), "Class names can only be set for type OBJECT.");
+ Ref<Script> key_script = p_key_script;
+ ERR_FAIL_COND_MSG(key_script.is_valid() && p_key_class_name == StringName(), "Script class can only be set together with base class name.");
+ Ref<Script> value_script = p_value_script;
+ ERR_FAIL_COND_MSG(value_script.is_valid() && p_value_class_name == StringName(), "Script class can only be set together with base class name.");
+
+ _p->typed_key.type = Variant::Type(p_key_type);
+ _p->typed_key.class_name = p_key_class_name;
+ _p->typed_key.script = key_script;
+ _p->typed_key.where = "TypedDictionary.Key";
+
+ _p->typed_value.type = Variant::Type(p_value_type);
+ _p->typed_value.class_name = p_value_class_name;
+ _p->typed_value.script = value_script;
+ _p->typed_value.where = "TypedDictionary.Value";
+}
+
+bool Dictionary::is_typed() const {
+ return is_typed_key() || is_typed_value();
+}
+
+bool Dictionary::is_typed_key() const {
+ return _p->typed_key.type != Variant::NIL;
+}
+
+bool Dictionary::is_typed_value() const {
+ return _p->typed_value.type != Variant::NIL;
+}
+
+bool Dictionary::is_same_typed(const Dictionary &p_other) const {
+ return is_same_typed_key(p_other) && is_same_typed_value(p_other);
+}
+
+bool Dictionary::is_same_typed_key(const Dictionary &p_other) const {
+ return _p->typed_key == p_other._p->typed_key;
+}
+
+bool Dictionary::is_same_typed_value(const Dictionary &p_other) const {
+ return _p->typed_value == p_other._p->typed_value;
+}
+
+uint32_t Dictionary::get_typed_key_builtin() const {
+ return _p->typed_key.type;
+}
+
+uint32_t Dictionary::get_typed_value_builtin() const {
+ return _p->typed_value.type;
+}
+
+StringName Dictionary::get_typed_key_class_name() const {
+ return _p->typed_key.class_name;
+}
+
+StringName Dictionary::get_typed_value_class_name() const {
+ return _p->typed_value.class_name;
+}
+
+Variant Dictionary::get_typed_key_script() const {
+ return _p->typed_key.script;
+}
+
+Variant Dictionary::get_typed_value_script() const {
+ return _p->typed_value.script;
+}
+
void Dictionary::operator=(const Dictionary &p_dictionary) {
if (this == &p_dictionary) {
return;
@@ -385,6 +632,13 @@ const void *Dictionary::id() const {
return _p;
}
+Dictionary::Dictionary(const Dictionary &p_base, uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script) {
+ _p = memnew(DictionaryPrivate);
+ _p->refcount.init();
+ set_typed(p_key_type, p_key_class_name, p_key_script, p_value_type, p_value_class_name, p_value_script);
+ assign(p_base);
+}
+
Dictionary::Dictionary(const Dictionary &p_from) {
_p = nullptr;
_ref(p_from);
diff --git a/core/variant/dictionary.h b/core/variant/dictionary.h
index 67178ee7b7..57fbefc8f2 100644
--- a/core/variant/dictionary.h
+++ b/core/variant/dictionary.h
@@ -80,6 +80,7 @@ public:
uint32_t recursive_hash(int recursion_count) const;
void operator=(const Dictionary &p_dictionary);
+ void assign(const Dictionary &p_dictionary);
const Variant *next(const Variant *p_key = nullptr) const;
Array keys() const;
@@ -88,11 +89,26 @@ public:
Dictionary duplicate(bool p_deep = false) const;
Dictionary recursive_duplicate(bool p_deep, int recursion_count) const;
+ void set_typed(uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script);
+ bool is_typed() const;
+ bool is_typed_key() const;
+ bool is_typed_value() const;
+ bool is_same_typed(const Dictionary &p_other) const;
+ bool is_same_typed_key(const Dictionary &p_other) const;
+ bool is_same_typed_value(const Dictionary &p_other) const;
+ uint32_t get_typed_key_builtin() const;
+ uint32_t get_typed_value_builtin() const;
+ StringName get_typed_key_class_name() const;
+ StringName get_typed_value_class_name() const;
+ Variant get_typed_key_script() const;
+ Variant get_typed_value_script() const;
+
void make_read_only();
bool is_read_only() const;
const void *id() const;
+ Dictionary(const Dictionary &p_base, uint32_t p_key_type, const StringName &p_key_class_name, const Variant &p_key_script, uint32_t p_value_type, const StringName &p_value_class_name, const Variant &p_value_script);
Dictionary(const Dictionary &p_from);
Dictionary();
~Dictionary();
diff --git a/core/variant/typed_dictionary.h b/core/variant/typed_dictionary.h
new file mode 100644
index 0000000000..67fc33b4fc
--- /dev/null
+++ b/core/variant/typed_dictionary.h
@@ -0,0 +1,342 @@
+/**************************************************************************/
+/* typed_dictionary.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 TYPED_DICTIONARY_H
+#define TYPED_DICTIONARY_H
+
+#include "core/object/object.h"
+#include "core/variant/binder_common.h"
+#include "core/variant/dictionary.h"
+#include "core/variant/method_ptrcall.h"
+#include "core/variant/type_info.h"
+#include "core/variant/variant.h"
+
+template <typename K, typename V>
+class TypedDictionary : public Dictionary {
+public:
+ _FORCE_INLINE_ void operator=(const Dictionary &p_dictionary) {
+ ERR_FAIL_COND_MSG(!is_same_typed(p_dictionary), "Cannot assign a dictionary with a different element type.");
+ Dictionary::operator=(p_dictionary);
+ }
+ _FORCE_INLINE_ TypedDictionary(const Variant &p_variant) :
+ TypedDictionary(Dictionary(p_variant)) {
+ }
+ _FORCE_INLINE_ TypedDictionary(const Dictionary &p_dictionary) {
+ set_typed(Variant::OBJECT, K::get_class_static(), Variant(), Variant::OBJECT, V::get_class_static(), Variant());
+ if (is_same_typed(p_dictionary)) {
+ Dictionary::operator=(p_dictionary);
+ } else {
+ assign(p_dictionary);
+ }
+ }
+ _FORCE_INLINE_ TypedDictionary() {
+ set_typed(Variant::OBJECT, K::get_class_static(), Variant(), Variant::OBJECT, V::get_class_static(), Variant());
+ }
+};
+
+template <typename K, typename V>
+struct VariantInternalAccessor<TypedDictionary<K, V>> {
+ static _FORCE_INLINE_ TypedDictionary<K, V> get(const Variant *v) { return *VariantInternal::get_dictionary(v); }
+ static _FORCE_INLINE_ void set(Variant *v, const TypedDictionary<K, V> &p_dictionary) { *VariantInternal::get_dictionary(v) = p_dictionary; }
+};
+
+template <typename K, typename V>
+struct VariantInternalAccessor<const TypedDictionary<K, V> &> {
+ static _FORCE_INLINE_ TypedDictionary<K, V> get(const Variant *v) { return *VariantInternal::get_dictionary(v); }
+ static _FORCE_INLINE_ void set(Variant *v, const TypedDictionary<K, V> &p_dictionary) { *VariantInternal::get_dictionary(v) = p_dictionary; }
+};
+
+template <typename K, typename V>
+struct PtrToArg<TypedDictionary<K, V>> {
+ _FORCE_INLINE_ static TypedDictionary<K, V> convert(const void *p_ptr) {
+ return TypedDictionary<K, V>(*reinterpret_cast<const Dictionary *>(p_ptr));
+ }
+ typedef Dictionary EncodeT;
+ _FORCE_INLINE_ static void encode(TypedDictionary<K, V> p_val, void *p_ptr) {
+ *(Dictionary *)p_ptr = p_val;
+ }
+};
+
+template <typename K, typename V>
+struct PtrToArg<const TypedDictionary<K, V> &> {
+ typedef Dictionary EncodeT;
+ _FORCE_INLINE_ static TypedDictionary<K, V>
+ convert(const void *p_ptr) {
+ return TypedDictionary<K, V>(*reinterpret_cast<const Dictionary *>(p_ptr));
+ }
+};
+
+template <typename K, typename V>
+struct GetTypeInfo<TypedDictionary<K, V>> {
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY;
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE;
+ static inline PropertyInfo get_class_info() {
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, vformat("%s;%s", K::get_class_static(), V::get_class_static()));
+ }
+};
+
+template <typename K, typename V>
+struct GetTypeInfo<const TypedDictionary<K, V> &> {
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY;
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE;
+ static inline PropertyInfo get_class_info() {
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, vformat("%s;%s", K::get_class_static(), V::get_class_static()));
+ }
+};
+
+// Specialization for the rest of the Variant types.
+
+#define MAKE_TYPED_DICTIONARY_WITH_OBJECT(m_type, m_variant_type) \
+ template <typename T> \
+ class TypedDictionary<T, m_type> : public Dictionary { \
+ public: \
+ _FORCE_INLINE_ void operator=(const Dictionary &p_dictionary) { \
+ ERR_FAIL_COND_MSG(!is_same_typed(p_dictionary), "Cannot assign an dictionary with a different element type."); \
+ Dictionary::operator=(p_dictionary); \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Variant &p_variant) : \
+ TypedDictionary(Dictionary(p_variant)) { \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Dictionary &p_dictionary) { \
+ set_typed(Variant::OBJECT, T::get_class_static(), Variant(), m_variant_type, StringName(), Variant()); \
+ if (is_same_typed(p_dictionary)) { \
+ Dictionary::operator=(p_dictionary); \
+ } else { \
+ assign(p_dictionary); \
+ } \
+ } \
+ _FORCE_INLINE_ TypedDictionary() { \
+ set_typed(Variant::OBJECT, T::get_class_static(), Variant(), m_variant_type, StringName(), Variant()); \
+ } \
+ }; \
+ template <typename T> \
+ struct GetTypeInfo<TypedDictionary<T, m_type>> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", T::get_class_static(), m_variant_type == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type))); \
+ } \
+ }; \
+ template <typename T> \
+ struct GetTypeInfo<const TypedDictionary<T, m_type> &> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", T::get_class_static(), m_variant_type == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type))); \
+ } \
+ }; \
+ template <typename T> \
+ class TypedDictionary<m_type, T> : public Dictionary { \
+ public: \
+ _FORCE_INLINE_ void operator=(const Dictionary &p_dictionary) { \
+ ERR_FAIL_COND_MSG(!is_same_typed(p_dictionary), "Cannot assign an dictionary with a different element type."); \
+ Dictionary::operator=(p_dictionary); \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Variant &p_variant) : \
+ TypedDictionary(Dictionary(p_variant)) { \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Dictionary &p_dictionary) { \
+ set_typed(m_variant_type, StringName(), Variant(), Variant::OBJECT, T::get_class_static(), Variant()); \
+ if (is_same_typed(p_dictionary)) { \
+ Dictionary::operator=(p_dictionary); \
+ } else { \
+ assign(p_dictionary); \
+ } \
+ } \
+ _FORCE_INLINE_ TypedDictionary() { \
+ set_typed(m_variant_type, StringName(), Variant(), Variant::OBJECT, T::get_class_static(), Variant()); \
+ } \
+ }; \
+ template <typename T> \
+ struct GetTypeInfo<TypedDictionary<m_type, T>> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", m_variant_type == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type), T::get_class_static())); \
+ } \
+ }; \
+ template <typename T> \
+ struct GetTypeInfo<const TypedDictionary<m_type, T> &> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", m_variant_type == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type), T::get_class_static())); \
+ } \
+ };
+
+#define MAKE_TYPED_DICTIONARY_EXPANDED(m_type_key, m_variant_type_key, m_type_value, m_variant_type_value) \
+ template <> \
+ class TypedDictionary<m_type_key, m_type_value> : public Dictionary { \
+ public: \
+ _FORCE_INLINE_ void operator=(const Dictionary &p_dictionary) { \
+ ERR_FAIL_COND_MSG(!is_same_typed(p_dictionary), "Cannot assign an dictionary with a different element type."); \
+ Dictionary::operator=(p_dictionary); \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Variant &p_variant) : \
+ TypedDictionary(Dictionary(p_variant)) { \
+ } \
+ _FORCE_INLINE_ TypedDictionary(const Dictionary &p_dictionary) { \
+ set_typed(m_variant_type_key, StringName(), Variant(), m_variant_type_value, StringName(), Variant()); \
+ if (is_same_typed(p_dictionary)) { \
+ Dictionary::operator=(p_dictionary); \
+ } else { \
+ assign(p_dictionary); \
+ } \
+ } \
+ _FORCE_INLINE_ TypedDictionary() { \
+ set_typed(m_variant_type_key, StringName(), Variant(), m_variant_type_value, StringName(), Variant()); \
+ } \
+ }; \
+ template <> \
+ struct GetTypeInfo<TypedDictionary<m_type_key, m_type_value>> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", m_variant_type_key == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type_key), \
+ m_variant_type_value == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type_value))); \
+ } \
+ }; \
+ template <> \
+ struct GetTypeInfo<const TypedDictionary<m_type_key, m_type_value> &> { \
+ static const Variant::Type VARIANT_TYPE = Variant::DICTIONARY; \
+ static const GodotTypeInfo::Metadata METADATA = GodotTypeInfo::METADATA_NONE; \
+ static inline PropertyInfo get_class_info() { \
+ return PropertyInfo(Variant::DICTIONARY, String(), PROPERTY_HINT_DICTIONARY_TYPE, \
+ vformat("%s;%s", m_variant_type_key == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type_key), \
+ m_variant_type_value == Variant::NIL ? "Variant" : Variant::get_type_name(m_variant_type_value))); \
+ } \
+ };
+
+#define MAKE_TYPED_DICTIONARY_NIL(m_type, m_variant_type) \
+ MAKE_TYPED_DICTIONARY_WITH_OBJECT(m_type, m_variant_type) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, bool, Variant::BOOL) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, uint8_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, int8_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, uint16_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, int16_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, uint32_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, int32_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, uint64_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, int64_t, Variant::INT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, float, Variant::FLOAT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, double, Variant::FLOAT) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, String, Variant::STRING) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Vector2, Variant::VECTOR2) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Vector2i, Variant::VECTOR2I) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Rect2, Variant::RECT2) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Rect2i, Variant::RECT2I) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Vector3, Variant::VECTOR3) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Vector3i, Variant::VECTOR3I) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Transform2D, Variant::TRANSFORM2D) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Plane, Variant::PLANE) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Quaternion, Variant::QUATERNION) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, AABB, Variant::AABB) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Basis, Variant::BASIS) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Transform3D, Variant::TRANSFORM3D) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Color, Variant::COLOR) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, StringName, Variant::STRING_NAME) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, NodePath, Variant::NODE_PATH) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, RID, Variant::RID) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Callable, Variant::CALLABLE) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Signal, Variant::SIGNAL) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Dictionary, Variant::DICTIONARY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Array, Variant::ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedByteArray, Variant::PACKED_BYTE_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedInt32Array, Variant::PACKED_INT32_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedInt64Array, Variant::PACKED_INT64_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedFloat32Array, Variant::PACKED_FLOAT32_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedFloat64Array, Variant::PACKED_FLOAT64_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedStringArray, Variant::PACKED_STRING_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedVector2Array, Variant::PACKED_VECTOR2_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedVector3Array, Variant::PACKED_VECTOR3_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedColorArray, Variant::PACKED_COLOR_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, PackedVector4Array, Variant::PACKED_VECTOR4_ARRAY) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, IPAddress, Variant::STRING)
+
+#define MAKE_TYPED_DICTIONARY(m_type, m_variant_type) \
+ MAKE_TYPED_DICTIONARY_EXPANDED(m_type, m_variant_type, Variant, Variant::NIL) \
+ MAKE_TYPED_DICTIONARY_NIL(m_type, m_variant_type)
+
+MAKE_TYPED_DICTIONARY_NIL(Variant, Variant::NIL)
+MAKE_TYPED_DICTIONARY(bool, Variant::BOOL)
+MAKE_TYPED_DICTIONARY(uint8_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(int8_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(uint16_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(int16_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(uint32_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(int32_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(uint64_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(int64_t, Variant::INT)
+MAKE_TYPED_DICTIONARY(float, Variant::FLOAT)
+MAKE_TYPED_DICTIONARY(double, Variant::FLOAT)
+MAKE_TYPED_DICTIONARY(String, Variant::STRING)
+MAKE_TYPED_DICTIONARY(Vector2, Variant::VECTOR2)
+MAKE_TYPED_DICTIONARY(Vector2i, Variant::VECTOR2I)
+MAKE_TYPED_DICTIONARY(Rect2, Variant::RECT2)
+MAKE_TYPED_DICTIONARY(Rect2i, Variant::RECT2I)
+MAKE_TYPED_DICTIONARY(Vector3, Variant::VECTOR3)
+MAKE_TYPED_DICTIONARY(Vector3i, Variant::VECTOR3I)
+MAKE_TYPED_DICTIONARY(Transform2D, Variant::TRANSFORM2D)
+MAKE_TYPED_DICTIONARY(Plane, Variant::PLANE)
+MAKE_TYPED_DICTIONARY(Quaternion, Variant::QUATERNION)
+MAKE_TYPED_DICTIONARY(AABB, Variant::AABB)
+MAKE_TYPED_DICTIONARY(Basis, Variant::BASIS)
+MAKE_TYPED_DICTIONARY(Transform3D, Variant::TRANSFORM3D)
+MAKE_TYPED_DICTIONARY(Color, Variant::COLOR)
+MAKE_TYPED_DICTIONARY(StringName, Variant::STRING_NAME)
+MAKE_TYPED_DICTIONARY(NodePath, Variant::NODE_PATH)
+MAKE_TYPED_DICTIONARY(RID, Variant::RID)
+MAKE_TYPED_DICTIONARY(Callable, Variant::CALLABLE)
+MAKE_TYPED_DICTIONARY(Signal, Variant::SIGNAL)
+MAKE_TYPED_DICTIONARY(Dictionary, Variant::DICTIONARY)
+MAKE_TYPED_DICTIONARY(Array, Variant::ARRAY)
+MAKE_TYPED_DICTIONARY(PackedByteArray, Variant::PACKED_BYTE_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedInt32Array, Variant::PACKED_INT32_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedInt64Array, Variant::PACKED_INT64_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedFloat32Array, Variant::PACKED_FLOAT32_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedFloat64Array, Variant::PACKED_FLOAT64_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedStringArray, Variant::PACKED_STRING_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedVector2Array, Variant::PACKED_VECTOR2_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedVector3Array, Variant::PACKED_VECTOR3_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedColorArray, Variant::PACKED_COLOR_ARRAY)
+MAKE_TYPED_DICTIONARY(PackedVector4Array, Variant::PACKED_VECTOR4_ARRAY)
+MAKE_TYPED_DICTIONARY(IPAddress, Variant::STRING)
+
+#undef MAKE_TYPED_DICTIONARY
+#undef MAKE_TYPED_DICTIONARY_NIL
+#undef MAKE_TYPED_DICTIONARY_EXPANDED
+#undef MAKE_TYPED_DICTIONARY_WITH_OBJECT
+
+#endif // TYPED_DICTIONARY_H
diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp
index 83f1f981b3..2da94de875 100644
--- a/core/variant/variant_call.cpp
+++ b/core/variant/variant_call.cpp
@@ -2254,6 +2254,7 @@ static void _register_variant_builtin_methods_misc() {
bind_method(Dictionary, size, sarray(), varray());
bind_method(Dictionary, is_empty, sarray(), varray());
bind_method(Dictionary, clear, sarray(), varray());
+ bind_method(Dictionary, assign, sarray("dictionary"), varray());
bind_method(Dictionary, merge, sarray("dictionary", "overwrite"), varray(false));
bind_method(Dictionary, merged, sarray("dictionary", "overwrite"), varray(false));
bind_method(Dictionary, has, sarray("key"), varray());
@@ -2266,6 +2267,18 @@ static void _register_variant_builtin_methods_misc() {
bind_method(Dictionary, duplicate, sarray("deep"), varray(false));
bind_method(Dictionary, get, sarray("key", "default"), varray(Variant()));
bind_method(Dictionary, get_or_add, sarray("key", "default"), varray(Variant()));
+ bind_method(Dictionary, is_typed, sarray(), varray());
+ bind_method(Dictionary, is_typed_key, sarray(), varray());
+ bind_method(Dictionary, is_typed_value, sarray(), varray());
+ bind_method(Dictionary, is_same_typed, sarray("dictionary"), varray());
+ bind_method(Dictionary, is_same_typed_key, sarray("dictionary"), varray());
+ bind_method(Dictionary, is_same_typed_value, sarray("dictionary"), varray());
+ bind_method(Dictionary, get_typed_key_builtin, sarray(), varray());
+ bind_method(Dictionary, get_typed_value_builtin, sarray(), varray());
+ bind_method(Dictionary, get_typed_key_class_name, sarray(), varray());
+ bind_method(Dictionary, get_typed_value_class_name, sarray(), varray());
+ bind_method(Dictionary, get_typed_key_script, sarray(), varray());
+ bind_method(Dictionary, get_typed_value_script, sarray(), varray());
bind_method(Dictionary, make_read_only, sarray(), varray());
bind_method(Dictionary, is_read_only, sarray(), varray());
bind_method(Dictionary, recursive_equal, sarray("dictionary", "recursion_count"), varray());
diff --git a/core/variant/variant_construct.cpp b/core/variant/variant_construct.cpp
index 1edae407c2..fb75a874e7 100644
--- a/core/variant/variant_construct.cpp
+++ b/core/variant/variant_construct.cpp
@@ -198,6 +198,7 @@ void Variant::_register_variant_constructors() {
add_constructor<VariantConstructNoArgs<Dictionary>>(sarray());
add_constructor<VariantConstructor<Dictionary, Dictionary>>(sarray("from"));
+ add_constructor<VariantConstructorTypedDictionary>(sarray("base", "key_type", "key_class_name", "key_script", "value_type", "value_class_name", "value_script"));
add_constructor<VariantConstructNoArgs<Array>>(sarray());
add_constructor<VariantConstructor<Array, Array>>(sarray("from"));
diff --git a/core/variant/variant_construct.h b/core/variant/variant_construct.h
index a46fadb198..68210a9451 100644
--- a/core/variant/variant_construct.h
+++ b/core/variant/variant_construct.h
@@ -400,6 +400,112 @@ public:
}
};
+class VariantConstructorTypedDictionary {
+public:
+ static void construct(Variant &r_ret, const Variant **p_args, Callable::CallError &r_error) {
+ if (p_args[0]->get_type() != Variant::DICTIONARY) {
+ r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
+ r_error.argument = 0;
+ r_error.expected = Variant::DICTIONARY;
+ return;
+ }
+
+ if (p_args[1]->get_type() != Variant::INT) {
+ r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
+ r_error.argument = 1;
+ r_error.expected = Variant::INT;
+ return;
+ }
+
+ if (p_args[2]->get_type() != Variant::STRING_NAME) {
+ r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
+ r_error.argument = 2;
+ r_error.expected = Variant::STRING_NAME;
+ return;
+ }
+
+ if (p_args[4]->get_type() != Variant::INT) {
+ r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
+ r_error.argument = 4;
+ r_error.expected = Variant::INT;
+ return;
+ }
+
+ if (p_args[5]->get_type() != Variant::STRING_NAME) {
+ r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
+ r_error.argument = 5;
+ r_error.expected = Variant::STRING_NAME;
+ return;
+ }
+
+ const Dictionary &base_dict = *VariantGetInternalPtr<Dictionary>::get_ptr(p_args[0]);
+ const uint32_t key_type = p_args[1]->operator uint32_t();
+ const StringName &key_class_name = *VariantGetInternalPtr<StringName>::get_ptr(p_args[2]);
+ const uint32_t value_type = p_args[4]->operator uint32_t();
+ const StringName &value_class_name = *VariantGetInternalPtr<StringName>::get_ptr(p_args[5]);
+ r_ret = Dictionary(base_dict, key_type, key_class_name, *p_args[3], value_type, value_class_name, *p_args[6]);
+ }
+
+ static inline void validated_construct(Variant *r_ret, const Variant **p_args) {
+ const Dictionary &base_dict = *VariantGetInternalPtr<Dictionary>::get_ptr(p_args[0]);
+ const uint32_t key_type = p_args[1]->operator uint32_t();
+ const StringName &key_class_name = *VariantGetInternalPtr<StringName>::get_ptr(p_args[2]);
+ const uint32_t value_type = p_args[4]->operator uint32_t();
+ const StringName &value_class_name = *VariantGetInternalPtr<StringName>::get_ptr(p_args[5]);
+ *r_ret = Dictionary(base_dict, key_type, key_class_name, *p_args[3], value_type, value_class_name, *p_args[6]);
+ }
+
+ static void ptr_construct(void *base, const void **p_args) {
+ const Dictionary &base_dict = PtrToArg<Dictionary>::convert(p_args[0]);
+ const uint32_t key_type = PtrToArg<uint32_t>::convert(p_args[1]);
+ const StringName &key_class_name = PtrToArg<StringName>::convert(p_args[2]);
+ const Variant &key_script = PtrToArg<Variant>::convert(p_args[3]);
+ const uint32_t value_type = PtrToArg<uint32_t>::convert(p_args[4]);
+ const StringName &value_class_name = PtrToArg<StringName>::convert(p_args[5]);
+ const Variant &value_script = PtrToArg<Variant>::convert(p_args[6]);
+ Dictionary dst_arr = Dictionary(base_dict, key_type, key_class_name, key_script, value_type, value_class_name, value_script);
+
+ PtrConstruct<Dictionary>::construct(dst_arr, base);
+ }
+
+ static int get_argument_count() {
+ return 7;
+ }
+
+ static Variant::Type get_argument_type(int p_arg) {
+ switch (p_arg) {
+ case 0: {
+ return Variant::DICTIONARY;
+ } break;
+ case 1: {
+ return Variant::INT;
+ } break;
+ case 2: {
+ return Variant::STRING_NAME;
+ } break;
+ case 3: {
+ return Variant::NIL;
+ } break;
+ case 4: {
+ return Variant::INT;
+ } break;
+ case 5: {
+ return Variant::STRING_NAME;
+ } break;
+ case 6: {
+ return Variant::NIL;
+ } break;
+ default: {
+ return Variant::NIL;
+ } break;
+ }
+ }
+
+ static Variant::Type get_base_type() {
+ return Variant::DICTIONARY;
+ }
+};
+
class VariantConstructorTypedArray {
public:
static void construct(Variant &r_ret, const Variant **p_args, Callable::CallError &r_error) {
diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp
index 9a0dd712ed..f5f96456d3 100644
--- a/core/variant/variant_parser.cpp
+++ b/core/variant/variant_parser.cpp
@@ -1140,6 +1140,146 @@ Error VariantParser::parse_value(Token &token, Variant &value, Stream *p_stream,
return ERR_PARSE_ERROR;
}
}
+ } else if (id == "Dictionary") {
+ Error err = OK;
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_BRACKET_OPEN) {
+ r_err_str = "Expected '['";
+ return ERR_PARSE_ERROR;
+ }
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_IDENTIFIER) {
+ r_err_str = "Expected type identifier for key";
+ return ERR_PARSE_ERROR;
+ }
+
+ static HashMap<StringName, Variant::Type> builtin_types;
+ if (builtin_types.is_empty()) {
+ for (int i = 1; i < Variant::VARIANT_MAX; i++) {
+ builtin_types[Variant::get_type_name((Variant::Type)i)] = (Variant::Type)i;
+ }
+ }
+
+ Dictionary dict;
+ Variant::Type key_type = Variant::NIL;
+ StringName key_class_name;
+ Variant key_script;
+ bool got_comma_token = false;
+ if (builtin_types.has(token.value)) {
+ key_type = builtin_types.get(token.value);
+ } else if (token.value == "Resource" || token.value == "SubResource" || token.value == "ExtResource") {
+ Variant resource;
+ err = parse_value(token, resource, p_stream, line, r_err_str, p_res_parser);
+ if (err) {
+ if (token.value == "Resource" && err == ERR_PARSE_ERROR && r_err_str == "Expected '('" && token.type == TK_COMMA) {
+ err = OK;
+ r_err_str = String();
+ key_type = Variant::OBJECT;
+ key_class_name = token.value;
+ got_comma_token = true;
+ } else {
+ return err;
+ }
+ } else {
+ Ref<Script> script = resource;
+ if (script.is_valid() && script->is_valid()) {
+ key_type = Variant::OBJECT;
+ key_class_name = script->get_instance_base_type();
+ key_script = script;
+ }
+ }
+ } else if (ClassDB::class_exists(token.value)) {
+ key_type = Variant::OBJECT;
+ key_class_name = token.value;
+ }
+
+ if (!got_comma_token) {
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_COMMA) {
+ r_err_str = "Expected ',' after key type";
+ return ERR_PARSE_ERROR;
+ }
+ }
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_IDENTIFIER) {
+ r_err_str = "Expected type identifier for value";
+ return ERR_PARSE_ERROR;
+ }
+
+ Variant::Type value_type = Variant::NIL;
+ StringName value_class_name;
+ Variant value_script;
+ bool got_bracket_token = false;
+ if (builtin_types.has(token.value)) {
+ value_type = builtin_types.get(token.value);
+ } else if (token.value == "Resource" || token.value == "SubResource" || token.value == "ExtResource") {
+ Variant resource;
+ err = parse_value(token, resource, p_stream, line, r_err_str, p_res_parser);
+ if (err) {
+ if (token.value == "Resource" && err == ERR_PARSE_ERROR && r_err_str == "Expected '('" && token.type == TK_BRACKET_CLOSE) {
+ err = OK;
+ r_err_str = String();
+ value_type = Variant::OBJECT;
+ value_class_name = token.value;
+ got_comma_token = true;
+ } else {
+ return err;
+ }
+ } else {
+ Ref<Script> script = resource;
+ if (script.is_valid() && script->is_valid()) {
+ value_type = Variant::OBJECT;
+ value_class_name = script->get_instance_base_type();
+ value_script = script;
+ }
+ }
+ } else if (ClassDB::class_exists(token.value)) {
+ value_type = Variant::OBJECT;
+ value_class_name = token.value;
+ }
+
+ if (key_type != Variant::NIL || value_type != Variant::NIL) {
+ dict.set_typed(key_type, key_class_name, key_script, value_type, value_class_name, value_script);
+ }
+
+ if (!got_bracket_token) {
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_BRACKET_CLOSE) {
+ r_err_str = "Expected ']'";
+ return ERR_PARSE_ERROR;
+ }
+ }
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_PARENTHESIS_OPEN) {
+ r_err_str = "Expected '('";
+ return ERR_PARSE_ERROR;
+ }
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_CURLY_BRACKET_OPEN) {
+ r_err_str = "Expected '{'";
+ return ERR_PARSE_ERROR;
+ }
+
+ Dictionary values;
+ err = _parse_dictionary(values, p_stream, line, r_err_str, p_res_parser);
+ if (err) {
+ return err;
+ }
+
+ get_token(p_stream, token, line, r_err_str);
+ if (token.type != TK_PARENTHESIS_CLOSE) {
+ r_err_str = "Expected ')'";
+ return ERR_PARSE_ERROR;
+ }
+
+ dict.assign(values);
+
+ value = dict;
} else if (id == "Array") {
Error err = OK;
@@ -2036,40 +2176,109 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str
case Variant::DICTIONARY: {
Dictionary dict = p_variant;
+
+ if (dict.is_typed()) {
+ p_store_string_func(p_store_string_ud, "Dictionary[");
+
+ Variant::Type key_builtin_type = (Variant::Type)dict.get_typed_key_builtin();
+ StringName key_class_name = dict.get_typed_key_class_name();
+ Ref<Script> key_script = dict.get_typed_key_script();
+
+ if (key_script.is_valid()) {
+ String resource_text;
+ if (p_encode_res_func) {
+ resource_text = p_encode_res_func(p_encode_res_ud, key_script);
+ }
+ if (resource_text.is_empty() && key_script->get_path().is_resource_file()) {
+ resource_text = "Resource(\"" + key_script->get_path() + "\")";
+ }
+
+ if (!resource_text.is_empty()) {
+ p_store_string_func(p_store_string_ud, resource_text);
+ } else {
+ ERR_PRINT("Failed to encode a path to a custom script for a dictionary key type.");
+ p_store_string_func(p_store_string_ud, key_class_name);
+ }
+ } else if (key_class_name != StringName()) {
+ p_store_string_func(p_store_string_ud, key_class_name);
+ } else if (key_builtin_type == Variant::NIL) {
+ p_store_string_func(p_store_string_ud, "Variant");
+ } else {
+ p_store_string_func(p_store_string_ud, Variant::get_type_name(key_builtin_type));
+ }
+
+ p_store_string_func(p_store_string_ud, ", ");
+
+ Variant::Type value_builtin_type = (Variant::Type)dict.get_typed_value_builtin();
+ StringName value_class_name = dict.get_typed_value_class_name();
+ Ref<Script> value_script = dict.get_typed_value_script();
+
+ if (value_script.is_valid()) {
+ String resource_text;
+ if (p_encode_res_func) {
+ resource_text = p_encode_res_func(p_encode_res_ud, value_script);
+ }
+ if (resource_text.is_empty() && value_script->get_path().is_resource_file()) {
+ resource_text = "Resource(\"" + value_script->get_path() + "\")";
+ }
+
+ if (!resource_text.is_empty()) {
+ p_store_string_func(p_store_string_ud, resource_text);
+ } else {
+ ERR_PRINT("Failed to encode a path to a custom script for a dictionary value type.");
+ p_store_string_func(p_store_string_ud, value_class_name);
+ }
+ } else if (value_class_name != StringName()) {
+ p_store_string_func(p_store_string_ud, value_class_name);
+ } else if (value_builtin_type == Variant::NIL) {
+ p_store_string_func(p_store_string_ud, "Variant");
+ } else {
+ p_store_string_func(p_store_string_ud, Variant::get_type_name(value_builtin_type));
+ }
+
+ p_store_string_func(p_store_string_ud, "](");
+ }
+
if (unlikely(p_recursion_count > MAX_RECURSION)) {
ERR_PRINT("Max recursion reached");
p_store_string_func(p_store_string_ud, "{}");
} else {
- p_recursion_count++;
-
List<Variant> keys;
dict.get_key_list(&keys);
keys.sort();
- if (keys.is_empty()) { // Avoid unnecessary line break.
+ if (keys.is_empty()) {
+ // Avoid unnecessary line break.
p_store_string_func(p_store_string_ud, "{}");
- break;
- }
+ } else {
+ p_recursion_count++;
- p_store_string_func(p_store_string_ud, "{\n");
- for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
- write(E->get(), p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat);
- p_store_string_func(p_store_string_ud, ": ");
- write(dict[E->get()], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat);
- if (E->next()) {
- p_store_string_func(p_store_string_ud, ",\n");
- } else {
- p_store_string_func(p_store_string_ud, "\n");
+ p_store_string_func(p_store_string_ud, "{\n");
+
+ for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
+ write(E->get(), p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat);
+ p_store_string_func(p_store_string_ud, ": ");
+ write(dict[E->get()], p_store_string_func, p_store_string_ud, p_encode_res_func, p_encode_res_ud, p_recursion_count, p_compat);
+ if (E->next()) {
+ p_store_string_func(p_store_string_ud, ",\n");
+ } else {
+ p_store_string_func(p_store_string_ud, "\n");
+ }
}
+
+ p_store_string_func(p_store_string_ud, "}");
}
+ }
- p_store_string_func(p_store_string_ud, "}");
+ if (dict.is_typed()) {
+ p_store_string_func(p_store_string_ud, ")");
}
} break;
case Variant::ARRAY: {
Array array = p_variant;
- if (array.get_typed_builtin() != Variant::NIL) {
+
+ if (array.is_typed()) {
p_store_string_func(p_store_string_ud, "Array[");
Variant::Type builtin_type = (Variant::Type)array.get_typed_builtin();
@@ -2107,6 +2316,7 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str
p_recursion_count++;
p_store_string_func(p_store_string_ud, "[");
+
bool first = true;
for (const Variant &var : array) {
if (first) {
@@ -2120,7 +2330,7 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str
p_store_string_func(p_store_string_ud, "]");
}
- if (array.get_typed_builtin() != Variant::NIL) {
+ if (array.is_typed()) {
p_store_string_func(p_store_string_ud, ")");
}
} break;
diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp
index 0cd4b86fe7..b60ff83cf1 100644
--- a/core/variant/variant_setget.cpp
+++ b/core/variant/variant_setget.cpp
@@ -703,6 +703,50 @@ struct VariantIndexedSetGet_Array {
static uint64_t get_indexed_size(const Variant *base) { return 0; }
};
+struct VariantIndexedSetGet_Dictionary {
+ static void get(const Variant *base, int64_t index, Variant *value, bool *oob) {
+ const Variant *ptr = VariantGetInternalPtr<Dictionary>::get_ptr(base)->getptr(index);
+ if (!ptr) {
+ *oob = true;
+ return;
+ }
+ *value = *ptr;
+ *oob = false;
+ }
+ static void ptr_get(const void *base, int64_t index, void *member) {
+ // Avoid ptrconvert for performance.
+ const Dictionary &v = *reinterpret_cast<const Dictionary *>(base);
+ const Variant *ptr = v.getptr(index);
+ NULL_TEST(ptr);
+ PtrToArg<Variant>::encode(*ptr, member);
+ }
+ static void set(Variant *base, int64_t index, const Variant *value, bool *valid, bool *oob) {
+ if (VariantGetInternalPtr<Dictionary>::get_ptr(base)->is_read_only()) {
+ *valid = false;
+ *oob = true;
+ return;
+ }
+ (*VariantGetInternalPtr<Dictionary>::get_ptr(base))[index] = *value;
+ *oob = false;
+ *valid = true;
+ }
+ static void validated_set(Variant *base, int64_t index, const Variant *value, bool *oob) {
+ if (VariantGetInternalPtr<Dictionary>::get_ptr(base)->is_read_only()) {
+ *oob = true;
+ return;
+ }
+ (*VariantGetInternalPtr<Dictionary>::get_ptr(base))[index] = *value;
+ *oob = false;
+ }
+ static void ptr_set(void *base, int64_t index, const void *member) {
+ Dictionary &v = *reinterpret_cast<Dictionary *>(base);
+ v[index] = PtrToArg<Variant>::convert(member);
+ }
+ static Variant::Type get_index_type() { return Variant::NIL; }
+ static uint32_t get_index_usage() { return PROPERTY_USAGE_DEFAULT; }
+ static uint64_t get_indexed_size(const Variant *base) { return VariantGetInternalPtr<Dictionary>::get_ptr(base)->size(); }
+};
+
struct VariantIndexedSetGet_String {
static void get(const Variant *base, int64_t index, Variant *value, bool *oob) {
int64_t length = VariantGetInternalPtr<String>::get_ptr(base)->length();
@@ -789,51 +833,6 @@ struct VariantIndexedSetGet_String {
static uint64_t get_indexed_size(const Variant *base) { return VariantInternal::get_string(base)->length(); }
};
-#define INDEXED_SETGET_STRUCT_DICT(m_base_type) \
- struct VariantIndexedSetGet_##m_base_type { \
- static void get(const Variant *base, int64_t index, Variant *value, bool *oob) { \
- const Variant *ptr = VariantGetInternalPtr<m_base_type>::get_ptr(base)->getptr(index); \
- if (!ptr) { \
- *oob = true; \
- return; \
- } \
- *value = *ptr; \
- *oob = false; \
- } \
- static void ptr_get(const void *base, int64_t index, void *member) { \
- /* avoid ptrconvert for performance*/ \
- const m_base_type &v = *reinterpret_cast<const m_base_type *>(base); \
- const Variant *ptr = v.getptr(index); \
- NULL_TEST(ptr); \
- PtrToArg<Variant>::encode(*ptr, member); \
- } \
- static void set(Variant *base, int64_t index, const Variant *value, bool *valid, bool *oob) { \
- if (VariantGetInternalPtr<m_base_type>::get_ptr(base)->is_read_only()) { \
- *valid = false; \
- *oob = true; \
- return; \
- } \
- (*VariantGetInternalPtr<m_base_type>::get_ptr(base))[index] = *value; \
- *oob = false; \
- *valid = true; \
- } \
- static void validated_set(Variant *base, int64_t index, const Variant *value, bool *oob) { \
- if (VariantGetInternalPtr<m_base_type>::get_ptr(base)->is_read_only()) { \
- *oob = true; \
- return; \
- } \
- (*VariantGetInternalPtr<m_base_type>::get_ptr(base))[index] = *value; \
- *oob = false; \
- } \
- static void ptr_set(void *base, int64_t index, const void *member) { \
- m_base_type &v = *reinterpret_cast<m_base_type *>(base); \
- v[index] = PtrToArg<Variant>::convert(member); \
- } \
- static Variant::Type get_index_type() { return Variant::NIL; } \
- static uint32_t get_index_usage() { return PROPERTY_USAGE_DEFAULT; } \
- static uint64_t get_indexed_size(const Variant *base) { return VariantGetInternalPtr<m_base_type>::get_ptr(base)->size(); } \
- };
-
INDEXED_SETGET_STRUCT_BULTIN_NUMERIC(Vector2, double, real_t, 2)
INDEXED_SETGET_STRUCT_BULTIN_NUMERIC(Vector2i, int64_t, int32_t, 2)
INDEXED_SETGET_STRUCT_BULTIN_NUMERIC(Vector3, double, real_t, 3)
@@ -858,8 +857,6 @@ INDEXED_SETGET_STRUCT_TYPED(PackedStringArray, String)
INDEXED_SETGET_STRUCT_TYPED(PackedColorArray, Color)
INDEXED_SETGET_STRUCT_TYPED(PackedVector4Array, Vector4)
-INDEXED_SETGET_STRUCT_DICT(Dictionary)
-
struct VariantIndexedSetterGetterInfo {
void (*setter)(Variant *base, int64_t index, const Variant *value, bool *valid, bool *oob) = nullptr;
void (*getter)(const Variant *base, int64_t index, Variant *value, bool *oob) = nullptr;
diff --git a/core/variant/variant_utility.cpp b/core/variant/variant_utility.cpp
index 7534a154a1..384fe6c4a6 100644
--- a/core/variant/variant_utility.cpp
+++ b/core/variant/variant_utility.cpp
@@ -452,12 +452,14 @@ Variant VariantUtilityFunctions::lerp(const Variant &from, const Variant &to, do
case Variant::QUATERNION:
case Variant::BASIS:
case Variant::COLOR:
+ case Variant::TRANSFORM2D:
+ case Variant::TRANSFORM3D:
break;
default:
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::NIL;
- return R"(Argument "from" must be "int", "float", "Vector2", "Vector3", "Vector4", "Quaternion", "Basis, or "Color".)";
+ return R"(Argument "from" must be "int", "float", "Vector2", "Vector3", "Vector4", "Color", "Quaternion", "Basis", "Transform2D", or "Transform3D".)";
}
if (from.get_type() != to.get_type()) {
@@ -490,6 +492,12 @@ Variant VariantUtilityFunctions::lerp(const Variant &from, const Variant &to, do
case Variant::BASIS: {
return VariantInternalAccessor<Basis>::get(&from).slerp(VariantInternalAccessor<Basis>::get(&to), weight);
} break;
+ case Variant::TRANSFORM2D: {
+ return VariantInternalAccessor<Transform2D>::get(&from).interpolate_with(VariantInternalAccessor<Transform2D>::get(&to), weight);
+ } break;
+ case Variant::TRANSFORM3D: {
+ return VariantInternalAccessor<Transform3D>::get(&from).interpolate_with(VariantInternalAccessor<Transform3D>::get(&to), weight);
+ } break;
case Variant::COLOR: {
return VariantInternalAccessor<Color>::get(&from).lerp(VariantInternalAccessor<Color>::get(&to), weight);
} break;
diff --git a/doc/class.xsd b/doc/class.xsd
index f0e1241053..28e02e870d 100644
--- a/doc/class.xsd
+++ b/doc/class.xsd
@@ -246,6 +246,8 @@
<xs:attribute type="xs:string" name="data_type" />
<xs:attribute type="xs:string" name="type" />
<xs:attribute type="xs:string" name="default" use="optional" />
+ <xs:attribute type="xs:string" name="deprecated" use="optional" />
+ <xs:attribute type="xs:string" name="experimental" use="optional" />
<xs:attribute type="xs:string" name="keywords" use="optional" />
</xs:extension>
</xs:simpleContent>
diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 49b8e34f8f..f222cbc969 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -621,13 +621,13 @@
<param index="1" name="to" type="Variant" />
<param index="2" name="weight" type="Variant" />
<description>
- Linearly interpolates between two values by the factor defined in [param weight]. To perform interpolation, [param weight] should be between [code]0.0[/code] and [code]1.0[/code] (inclusive). However, values outside this range are allowed and can be used to perform [i]extrapolation[/i]. If this is not desired, use [method clamp] on the result of this function.
- Both [param from] and [param to] must be the same type. Supported types: [int], [float], [Vector2], [Vector3], [Vector4], [Color], [Quaternion], [Basis].
+ Linearly interpolates between two values by the factor defined in [param weight]. To perform interpolation, [param weight] should be between [code]0.0[/code] and [code]1.0[/code] (inclusive). However, values outside this range are allowed and can be used to perform [i]extrapolation[/i]. If this is not desired, use [method clampf] to limit [param weight].
+ Both [param from] and [param to] must be the same type. Supported types: [int], [float], [Vector2], [Vector3], [Vector4], [Color], [Quaternion], [Basis], [Transform2D], [Transform3D].
[codeblock]
lerp(0, 4, 0.75) # Returns 3.0
[/codeblock]
See also [method inverse_lerp] which performs the reverse of this operation. To perform eased interpolation with [method lerp], combine it with [method ease] or [method smoothstep]. See also [method remap] to map a continuous series of values to another.
- [b]Note:[/b] For better type safety, use [method lerpf], [method Vector2.lerp], [method Vector3.lerp], [method Vector4.lerp], [method Color.lerp], [method Quaternion.slerp] or [method Basis.slerp].
+ [b]Note:[/b] For better type safety, use [method lerpf], [method Vector2.lerp], [method Vector3.lerp], [method Vector4.lerp], [method Color.lerp], [method Quaternion.slerp], [method Basis.slerp], [method Transform2D.interpolate_with], or [method Transform3D.interpolate_with].
</description>
</method>
<method name="lerp_angle" keywords="interpolate">
@@ -2915,6 +2915,9 @@
<constant name="PROPERTY_HINT_ARRAY_TYPE" value="31" enum="PropertyHint">
Hints that a property is an [Array] with the stored type specified in the hint string.
</constant>
+ <constant name="PROPERTY_HINT_DICTIONARY_TYPE" value="38" enum="PropertyHint">
+ Hints that a property is a [Dictionary] with the stored types specified in the hint string.
+ </constant>
<constant name="PROPERTY_HINT_LOCALE_ID" value="32" enum="PropertyHint">
Hints that a string property is a locale code. Editing it will show a locale dialog for picking language and country.
</constant>
@@ -2930,7 +2933,7 @@
<constant name="PROPERTY_HINT_PASSWORD" value="36" enum="PropertyHint">
Hints that a string property is a password, and every character is replaced with the secret character.
</constant>
- <constant name="PROPERTY_HINT_MAX" value="38" enum="PropertyHint">
+ <constant name="PROPERTY_HINT_MAX" value="39" enum="PropertyHint">
Represents the size of the [enum PropertyHint] enum.
</constant>
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags" is_bitfield="true">
diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml
index 186ee1b9c4..0a0223c550 100644
--- a/doc/classes/CanvasItem.xml
+++ b/doc/classes/CanvasItem.xml
@@ -96,6 +96,7 @@
<param index="3" name="texture" type="Texture2D" default="null" />
<description>
Draws a colored polygon of any number of points, convex or concave. Unlike [method draw_polygon], a single color must be specified for the whole polygon.
+ [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method draw_mesh], [method draw_multimesh], or [method RenderingServer.canvas_item_add_triangle_array].
</description>
</method>
<method name="draw_dashed_line">
@@ -251,6 +252,7 @@
<param index="3" name="texture" type="Texture2D" default="null" />
<description>
Draws a solid polygon of any number of points, convex or concave. Unlike [method draw_colored_polygon], each point's color can be changed individually. See also [method draw_polyline] and [method draw_polyline_colors]. If you need more flexibility (such as being able to use bones), use [method RenderingServer.canvas_item_add_triangle_array] instead.
+ [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method draw_mesh], [method draw_multimesh], or [method RenderingServer.canvas_item_add_triangle_array].
</description>
</method>
<method name="draw_polyline">
diff --git a/doc/classes/Dictionary.xml b/doc/classes/Dictionary.xml
index b8b4fc7b08..feb2a07e4a 100644
--- a/doc/classes/Dictionary.xml
+++ b/doc/classes/Dictionary.xml
@@ -150,6 +150,19 @@
</constructor>
<constructor name="Dictionary">
<return type="Dictionary" />
+ <param index="0" name="base" type="Dictionary" />
+ <param index="1" name="key_type" type="int" />
+ <param index="2" name="key_class_name" type="StringName" />
+ <param index="3" name="key_script" type="Variant" />
+ <param index="4" name="value_type" type="int" />
+ <param index="5" name="value_class_name" type="StringName" />
+ <param index="6" name="value_script" type="Variant" />
+ <description>
+ Creates a typed dictionary from the [param base] dictionary. A typed dictionary can only contain keys and values of the given types, or that inherit from the given classes, as described by this constructor's parameters.
+ </description>
+ </constructor>
+ <constructor name="Dictionary">
+ <return type="Dictionary" />
<param index="0" name="from" type="Dictionary" />
<description>
Returns the same dictionary as [param from]. If you need a copy of the dictionary, use [method duplicate].
@@ -157,6 +170,13 @@
</constructor>
</constructors>
<methods>
+ <method name="assign">
+ <return type="void" />
+ <param index="0" name="dictionary" type="Dictionary" />
+ <description>
+ Assigns elements of another [param dictionary] into the dictionary. Resizes the dictionary to match [param dictionary]. Performs type conversions if the dictionary is typed.
+ </description>
+ </method>
<method name="clear">
<return type="void" />
<description>
@@ -202,6 +222,42 @@
Gets a value and ensures the key is set. If the [param key] exists in the dictionary, this behaves like [method get]. Otherwise, the [param default] value is inserted into the dictionary and returned.
</description>
</method>
+ <method name="get_typed_key_builtin" qualifiers="const">
+ <return type="int" />
+ <description>
+ Returns the built-in [Variant] type of the typed dictionary's keys as a [enum Variant.Type] constant. If the keys are not typed, returns [constant TYPE_NIL]. See also [method is_typed_key].
+ </description>
+ </method>
+ <method name="get_typed_key_class_name" qualifiers="const">
+ <return type="StringName" />
+ <description>
+ Returns the [b]built-in[/b] class name of the typed dictionary's keys, if the built-in [Variant] type is [constant TYPE_OBJECT]. Otherwise, returns an empty [StringName]. See also [method is_typed_key] and [method Object.get_class].
+ </description>
+ </method>
+ <method name="get_typed_key_script" qualifiers="const">
+ <return type="Variant" />
+ <description>
+ Returns the [Script] instance associated with this typed dictionary's keys, or [code]null[/code] if it does not exist. See also [method is_typed_key].
+ </description>
+ </method>
+ <method name="get_typed_value_builtin" qualifiers="const">
+ <return type="int" />
+ <description>
+ Returns the built-in [Variant] type of the typed dictionary's values as a [enum Variant.Type] constant. If the values are not typed, returns [constant TYPE_NIL]. See also [method is_typed_value].
+ </description>
+ </method>
+ <method name="get_typed_value_class_name" qualifiers="const">
+ <return type="StringName" />
+ <description>
+ Returns the [b]built-in[/b] class name of the typed dictionary's values, if the built-in [Variant] type is [constant TYPE_OBJECT]. Otherwise, returns an empty [StringName]. See also [method is_typed_value] and [method Object.get_class].
+ </description>
+ </method>
+ <method name="get_typed_value_script" qualifiers="const">
+ <return type="Variant" />
+ <description>
+ Returns the [Script] instance associated with this typed dictionary's values, or [code]null[/code] if it does not exist. See also [method is_typed_value].
+ </description>
+ </method>
<method name="has" qualifiers="const">
<return type="bool" />
<param index="0" name="key" type="Variant" />
@@ -284,6 +340,45 @@
Returns [code]true[/code] if the dictionary is read-only. See [method make_read_only]. Dictionaries are automatically read-only if declared with [code]const[/code] keyword.
</description>
</method>
+ <method name="is_same_typed" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="dictionary" type="Dictionary" />
+ <description>
+ Returns [code]true[/code] if the dictionary is typed the same as [param dictionary].
+ </description>
+ </method>
+ <method name="is_same_typed_key" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="dictionary" type="Dictionary" />
+ <description>
+ Returns [code]true[/code] if the dictionary's keys are typed the same as [param dictionary]'s keys.
+ </description>
+ </method>
+ <method name="is_same_typed_value" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="dictionary" type="Dictionary" />
+ <description>
+ Returns [code]true[/code] if the dictionary's values are typed the same as [param dictionary]'s values.
+ </description>
+ </method>
+ <method name="is_typed" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if the dictionary is typed. Typed dictionaries can only store keys/values of their associated type and provide type safety for the [code][][/code] operator. Methods of typed dictionary still return [Variant].
+ </description>
+ </method>
+ <method name="is_typed_key" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if the dictionary's keys are typed.
+ </description>
+ </method>
+ <method name="is_typed_value" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if the dictionary's values are typed.
+ </description>
+ </method>
<method name="keys" qualifiers="const">
<return type="Array" />
<description>
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index d2118f1942..1de63b4a39 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -207,8 +207,11 @@
If [code]true[/code], displays folders in the FileSystem dock's bottom pane when split mode is enabled. If [code]false[/code], only files will be displayed in the bottom pane. Split mode can be toggled by pressing the icon next to the [code]res://[/code] folder path.
[b]Note:[/b] This setting has no effect when split mode is disabled (which is the default).
</member>
+ <member name="docks/filesystem/other_file_extensions" type="String" setter="" getter="">
+ A comma separated list of unsupported file extensions to show in the FileSystem dock, e.g. [code]"ico,icns"[/code].
+ </member>
<member name="docks/filesystem/textfile_extensions" type="String" setter="" getter="">
- List of file extensions to consider as editable text files in the FileSystem dock (by double-clicking on the files).
+ A comma separated list of file extensions to consider as editable text files in the FileSystem dock (by double-clicking on the files), e.g. [code]"txt,md,cfg,ini,log,json,yml,yaml,toml,xml"[/code].
</member>
<member name="docks/filesystem/thumbnail_size" type="int" setter="" getter="">
The thumbnail size to use in the FileSystem dock (in pixels). See also [member filesystem/file_dialog/thumbnail_size].
@@ -970,6 +973,7 @@
- [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets.
- [b]Same as Editor[/b] will launch the Play window in the same window as the Editor.
- [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window.
+ - [b]Launch in PiP mode[/b] will launch the Play window directly in picture-in-picture (PiP) mode if PiP mode is supported and enabled. When maximized, the Play window will occupy the same window as the Editor.
[b]Note:[/b] Only available in the Android editor.
</member>
<member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
diff --git a/doc/classes/MeshInstance3D.xml b/doc/classes/MeshInstance3D.xml
index abbb4c4eeb..d8e2c43566 100644
--- a/doc/classes/MeshInstance3D.xml
+++ b/doc/classes/MeshInstance3D.xml
@@ -21,6 +21,14 @@
[b]Performance:[/b] [Mesh] data needs to be received from the GPU, stalling the [RenderingServer] in the process.
</description>
</method>
+ <method name="bake_mesh_from_current_skeleton_pose">
+ <return type="ArrayMesh" />
+ <param index="0" name="existing" type="ArrayMesh" default="null" />
+ <description>
+ Takes a snapshot of the current animated skeleton pose of the skinned mesh and bakes it to the provided [param existing] mesh. If no [param existing] mesh is provided a new [ArrayMesh] is created, baked, and returned. Requires a skeleton with a registered skin to work. Blendshapes are ignored. Mesh surface materials are not copied.
+ [b]Performance:[/b] [Mesh] data needs to be retrieved from the GPU, stalling the [RenderingServer] in the process.
+ </description>
+ </method>
<method name="create_convex_collision">
<return type="void" />
<param index="0" name="clean" type="bool" default="true" />
diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml
index 9675f5af50..777950c075 100644
--- a/doc/classes/OS.xml
+++ b/doc/classes/OS.xml
@@ -132,8 +132,10 @@
<return type="Dictionary" />
<param index="0" name="path" type="String" />
<param index="1" name="arguments" type="PackedStringArray" />
+ <param index="2" name="blocking" type="bool" default="true" />
<description>
Creates a new process that runs independently of Godot with redirected IO. It will not terminate when Godot terminates. The path specified in [param path] must exist and be an executable file or macOS [code].app[/code] bundle. The path is resolved based on the current platform. The [param arguments] are used in the given order and separated by a space.
+ If [param blocking] is [code]false[/code], created pipes work in non-blocking mode, i.e. read and write operations will return immediately. Use [method FileAccess.get_error] to check if the last read/write operation was successful.
If the process cannot be created, this method returns an empty [Dictionary]. Otherwise, this method returns a [Dictionary] with the following keys:
- [code]"stdio"[/code] - [FileAccess] to access the process stdin and stdout pipes (read/write).
- [code]"stderr"[/code] - [FileAccess] to access the process stderr pipe (read only).
diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml
index 0cfa3a5d4a..a331c05e47 100644
--- a/doc/classes/Object.xml
+++ b/doc/classes/Object.xml
@@ -192,6 +192,55 @@
[b]Note:[/b] If [method _init] is defined with [i]required[/i] parameters, the Object with script may only be created directly. If any other means (such as [method PackedScene.instantiate] or [method Node.duplicate]) are used, the script's initialization will fail.
</description>
</method>
+ <method name="_iter_get" qualifiers="virtual">
+ <return type="Variant" />
+ <param index="0" name="iter" type="Variant" />
+ <description>
+ Returns the current iterable value. [param iter] stores the iteration state, but unlike [method _iter_init] and [method _iter_next] the state is supposed to be read-only, so there is no [Array] wrapper.
+ </description>
+ </method>
+ <method name="_iter_init" qualifiers="virtual">
+ <return type="bool" />
+ <param index="0" name="iter" type="Array" />
+ <description>
+ Initializes the iterator. [param iter] stores the iteration state. Since GDScript does not support passing arguments by reference, a single-element array is used as a wrapper. Returns [code]true[/code] so long as the iterator has not reached the end.
+ Example:
+ [codeblock]
+ class MyRange:
+ var _from
+ var _to
+
+ func _init(from, to):
+ assert(from &lt;= to)
+ _from = from
+ _to = to
+
+ func _iter_init(iter):
+ iter[0] = _from
+ return iter[0] &lt; _to
+
+ func _iter_next(iter):
+ iter[0] += 1
+ return iter[0] &lt; _to
+
+ func _iter_get(iter):
+ return iter
+
+ func _ready():
+ var my_range = MyRange.new(2, 5)
+ for x in my_range:
+ print(x) # Prints 2, 3, 4.
+ [/codeblock]
+ [b]Note:[/b] Alternatively, you can ignore [param iter] and use the object's state instead, see [url=$DOCS_URL/tutorials/scripting/gdscript/gdscript_advanced.html#custom-iterators]online docs[/url] for an example. Note that in this case you will not be able to reuse the same iterator instance in nested loops. Also, make sure you reset the iterator state in this method if you want to reuse the same instance multiple times.
+ </description>
+ </method>
+ <method name="_iter_next" qualifiers="virtual">
+ <return type="bool" />
+ <param index="0" name="iter" type="Array" />
+ <description>
+ Moves the iterator to the next iteration. [param iter] stores the iteration state. Since GDScript does not support passing arguments by reference, a single-element array is used as a wrapper. Returns [code]true[/code] so long as the iterator has not reached the end.
+ </description>
+ </method>
<method name="_notification" qualifiers="virtual">
<return type="void" />
<param index="0" name="what" type="int" />
diff --git a/doc/classes/PathFollow2D.xml b/doc/classes/PathFollow2D.xml
index 61db34fb02..7444bfc23a 100644
--- a/doc/classes/PathFollow2D.xml
+++ b/doc/classes/PathFollow2D.xml
@@ -26,6 +26,7 @@
</member>
<member name="progress_ratio" type="float" setter="set_progress_ratio" getter="get_progress_ratio" default="0.0">
The distance along the path as a number in the range 0.0 (for the first vertex) to 1.0 (for the last). This is just another way of expressing the progress within the path, as the offset supplied is multiplied internally by the path's length.
+ It can be set or get only if the [PathFollow2D] is the child of a [Path2D] which is part of the scene tree, and that this [Path2D] has a [Curve2D] with a non-zero length. Otherwise, trying to set this field will print an error, and getting this field will return [code]0.0[/code].
</member>
<member name="rotates" type="bool" setter="set_rotates" getter="is_rotating" default="true">
If [code]true[/code], this node rotates to follow the path, with the +X direction facing forward on the path.
diff --git a/doc/classes/PathFollow3D.xml b/doc/classes/PathFollow3D.xml
index b505c6ea8b..bcd04d51ca 100644
--- a/doc/classes/PathFollow3D.xml
+++ b/doc/classes/PathFollow3D.xml
@@ -36,6 +36,7 @@
</member>
<member name="progress_ratio" type="float" setter="set_progress_ratio" getter="get_progress_ratio" default="0.0">
The distance from the first vertex, considering 0.0 as the first vertex and 1.0 as the last. This is just another way of expressing the progress within the path, as the progress supplied is multiplied internally by the path's length.
+ It can be set or get only if the [PathFollow3D] is the child of a [Path3D] which is part of the scene tree, and that this [Path3D] has a [Curve3D] with a non-zero length. Otherwise, trying to set this field will print an error, and getting this field will return [code]0.0[/code].
</member>
<member name="rotation_mode" type="int" setter="set_rotation_mode" getter="get_rotation_mode" enum="PathFollow3D.RotationMode" default="3">
Allows or forbids rotation on one or more axes, depending on the [enum RotationMode] constants being used.
diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml
index 4cdfba17e9..cea9a4cec4 100644
--- a/doc/classes/RenderingServer.xml
+++ b/doc/classes/RenderingServer.xml
@@ -329,6 +329,7 @@
<param index="4" name="texture" type="RID" default="RID()" />
<description>
Draws a 2D polygon on the [CanvasItem] pointed to by the [param item] [RID]. If you need more flexibility (such as being able to use bones), use [method canvas_item_add_triangle_array] instead. See also [method CanvasItem.draw_polygon].
+ [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method CanvasItem.draw_mesh], [method CanvasItem.draw_multimesh], or [method canvas_item_add_triangle_array].
</description>
</method>
<method name="canvas_item_add_polyline">
diff --git a/doc/classes/SpinBox.xml b/doc/classes/SpinBox.xml
index 3fb30a81b8..6da6a301fe 100644
--- a/doc/classes/SpinBox.xml
+++ b/doc/classes/SpinBox.xml
@@ -98,12 +98,12 @@
Vertical separation between the up and down buttons.
</theme_item>
<theme_item name="buttons_width" data_type="constant" type="int" default="16">
- Width of the up and down buttons. If smaller than any icon set on the buttons, the respective icon may overlap neighboring elements, unless [theme_item set_min_buttons_width_from_icons] is different than [code]0[/code].
+ Width of the up and down buttons. If smaller than any icon set on the buttons, the respective icon may overlap neighboring elements. If smaller than [code]0[/code], the width is automatically adjusted from the icon size.
</theme_item>
<theme_item name="field_and_buttons_separation" data_type="constant" type="int" default="2">
Width of the horizontal separation between the text input field ([LineEdit]) and the buttons.
</theme_item>
- <theme_item name="set_min_buttons_width_from_icons" data_type="constant" type="int" default="1">
+ <theme_item name="set_min_buttons_width_from_icons" data_type="constant" type="int" default="1" deprecated="This property exists only for compatibility with older themes. Setting [theme_item buttons_width] to a negative value has the same effect.">
If not [code]0[/code], the minimum button width corresponds to the widest of all icons set on those buttons, even if [theme_item buttons_width] is smaller.
</theme_item>
<theme_item name="down" data_type="icon" type="Texture2D">
diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py
index 761a7f8f4a..101660881b 100755
--- a/doc/tools/make_rst.py
+++ b/doc/tools/make_rst.py
@@ -1311,9 +1311,6 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
if property_def.text is not None and property_def.text.strip() != "":
f.write(f"{format_text_block(property_def.text.strip(), property_def, state)}\n\n")
- if property_def.type_name.type_name in PACKED_ARRAY_TYPES:
- tmp = f"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [{property_def.type_name.type_name}] for more details."
- f.write(f"{format_text_block(tmp, property_def, state)}\n\n")
elif property_def.deprecated is None and property_def.experimental is None:
f.write(".. container:: contribute\n\n\t")
f.write(
@@ -1323,6 +1320,11 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
+ "\n\n"
)
+ # Add copy note to built-in properties returning `Packed*Array`.
+ if property_def.type_name.type_name in PACKED_ARRAY_TYPES:
+ copy_note = f"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [{property_def.type_name.type_name}] for more details."
+ f.write(f"{format_text_block(copy_note, property_def, state)}\n\n")
+
index += 1
# Constructor, Method, Operator descriptions
@@ -1507,24 +1509,23 @@ def make_type(klass: str, state: State) -> str:
if klass.find("*") != -1: # Pointer, ignore
return f"``{klass}``"
- link_type = klass
- is_array = False
+ def resolve_type(link_type: str) -> str:
+ if link_type in state.classes:
+ return f":ref:`{link_type}<class_{link_type}>`"
+ else:
+ print_error(f'{state.current_class}.xml: Unresolved type "{link_type}".', state)
+ return f"``{link_type}``"
- if link_type.endswith("[]"): # Typed array, strip [] to link to contained type.
- link_type = link_type[:-2]
- is_array = True
+ if klass.endswith("[]"): # Typed array, strip [] to link to contained type.
+ return f":ref:`Array<class_Array>`\\[{resolve_type(klass[:-len('[]')])}\\]"
- if link_type in state.classes:
- type_rst = f":ref:`{link_type}<class_{link_type}>`"
- if is_array:
- type_rst = f":ref:`Array<class_Array>`\\[{type_rst}\\]"
- return type_rst
+ if klass.startswith("Dictionary["): # Typed dictionary, split elements to link contained types.
+ parts = klass[len("Dictionary[") : -len("]")].partition(", ")
+ key = parts[0]
+ value = parts[2]
+ return f":ref:`Dictionary<class_Dictionary>`\\[{resolve_type(key)}, {resolve_type(value)}\\]"
- print_error(f'{state.current_class}.xml: Unresolved type "{link_type}".', state)
- type_rst = f"``{link_type}``"
- if is_array:
- type_rst = f":ref:`Array<class_Array>`\\[{type_rst}\\]"
- return type_rst
+ return resolve_type(klass)
def make_enum(t: str, is_bitfield: bool, state: State) -> str:
diff --git a/drivers/gles3/rasterizer_canvas_gles3.cpp b/drivers/gles3/rasterizer_canvas_gles3.cpp
index b9206f310e..df7aba265b 100644
--- a/drivers/gles3/rasterizer_canvas_gles3.cpp
+++ b/drivers/gles3/rasterizer_canvas_gles3.cpp
@@ -468,7 +468,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
update_skeletons = false;
}
// Canvas group begins here, render until before this item
- _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info);
+ _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info, material_screen_texture_mipmaps_cached);
item_count = 0;
if (ci->canvas_group_owner->canvas_group->mode != RS::CANVAS_GROUP_MODE_TRANSPARENT) {
@@ -499,7 +499,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
mesh_storage->update_mesh_instances();
update_skeletons = false;
}
- _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, true, r_render_info);
+ _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, true, r_render_info, material_screen_texture_mipmaps_cached);
item_count = 0;
if (ci->canvas_group->blur_mipmaps) {
@@ -523,7 +523,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
}
//render anything pending, including clearing if no items
- _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info);
+ _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info, material_screen_texture_mipmaps_cached);
item_count = 0;
texture_storage->render_target_copy_to_back_buffer(p_to_render_target, back_buffer_rect, backbuffer_gen_mipmaps);
@@ -553,7 +553,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
mesh_storage->update_mesh_instances();
update_skeletons = false;
}
- _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, canvas_group_owner != nullptr, r_render_info);
+ _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, canvas_group_owner != nullptr, r_render_info, material_screen_texture_mipmaps_cached);
//then reset
item_count = 0;
}
@@ -573,10 +573,10 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_
state.current_instance_buffer_index = 0;
}
-void RasterizerCanvasGLES3::_render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer, RenderingMethod::RenderInfo *r_render_info) {
+void RasterizerCanvasGLES3::_render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer, RenderingMethod::RenderInfo *r_render_info, bool p_backbuffer_has_mipmaps) {
GLES3::MaterialStorage *material_storage = GLES3::MaterialStorage::get_singleton();
- canvas_begin(p_to_render_target, p_to_backbuffer);
+ canvas_begin(p_to_render_target, p_to_backbuffer, p_backbuffer_has_mipmaps);
if (p_item_count <= 0) {
// Nothing to draw, just call canvas_begin() to clear the render target and return.
@@ -2169,7 +2169,7 @@ bool RasterizerCanvasGLES3::free(RID p_rid) {
void RasterizerCanvasGLES3::update() {
}
-void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backbuffer) {
+void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backbuffer, bool p_backbuffer_has_mipmaps) {
GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton();
GLES3::Config *config = GLES3::Config::get_singleton();
@@ -2184,6 +2184,7 @@ void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backb
glBindFramebuffer(GL_FRAMEBUFFER, render_target->fbo);
glActiveTexture(GL_TEXTURE0 + config->max_texture_image_units - 4);
glBindTexture(GL_TEXTURE_2D, render_target->backbuffer);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, p_backbuffer_has_mipmaps ? render_target->mipmap_count - 1 : 0);
}
if (render_target->is_transparent || p_to_backbuffer) {
diff --git a/drivers/gles3/rasterizer_canvas_gles3.h b/drivers/gles3/rasterizer_canvas_gles3.h
index 027f717eb7..9c0d0abccb 100644
--- a/drivers/gles3/rasterizer_canvas_gles3.h
+++ b/drivers/gles3/rasterizer_canvas_gles3.h
@@ -335,7 +335,7 @@ public:
typedef void Texture;
- void canvas_begin(RID p_to_render_target, bool p_to_backbuffer);
+ void canvas_begin(RID p_to_render_target, bool p_to_backbuffer, bool p_backbuffer_has_mipmaps);
//virtual void draw_window_margins(int *black_margin, RID *black_image) override;
void draw_lens_distortion_rect(const Rect2 &p_rect, float p_k1, float p_k2, const Vector2 &p_eye_center, float p_oversample);
@@ -361,7 +361,7 @@ public:
void _prepare_canvas_texture(RID p_texture, RS::CanvasItemTextureFilter p_base_filter, RS::CanvasItemTextureRepeat p_base_repeat, uint32_t &r_index, Size2 &r_texpixel_size);
void canvas_render_items(RID p_to_render_target, Item *p_item_list, const Color &p_modulate, Light *p_light_list, Light *p_directional_list, const Transform2D &p_canvas_transform, RS::CanvasItemTextureFilter p_default_filter, RS::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_vertices_to_pixel, bool &r_sdf_used, RenderingMethod::RenderInfo *r_render_info = nullptr) override;
- void _render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer = false, RenderingMethod::RenderInfo *r_render_info = nullptr);
+ void _render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer = false, RenderingMethod::RenderInfo *r_render_info = nullptr, bool p_backbuffer_has_mipmaps = false);
void _record_item_commands(const Item *p_item, RID p_render_target, const Transform2D &p_canvas_transform_inverse, Item *&current_clip, GLES3::CanvasShaderData::BlendMode p_blend_mode, Light *p_lights, uint32_t &r_index, bool &r_break_batch, bool &r_sdf_used, const Point2 &p_repeat_offset);
void _render_batch(Light *p_lights, uint32_t p_index, RenderingMethod::RenderInfo *r_render_info = nullptr);
bool _bind_material(GLES3::CanvasMaterialData *p_material_data, CanvasShaderGLES3::ShaderVariant p_variant, uint64_t p_specialization);
diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp
index 3ed8042f3f..8b6d3e3268 100644
--- a/drivers/gles3/rasterizer_scene_gles3.cpp
+++ b/drivers/gles3/rasterizer_scene_gles3.cpp
@@ -912,7 +912,7 @@ void RasterizerSceneGLES3::_update_sky_radiance(RID p_env, const Projection &p_p
RS::SkyMode sky_mode = sky->mode;
if (sky_mode == RS::SKY_MODE_AUTOMATIC) {
- if (shader_data->uses_time || shader_data->uses_position) {
+ if ((shader_data->uses_time || shader_data->uses_position) && sky->radiance_size == 256) {
update_single_frame = true;
sky_mode = RS::SKY_MODE_REALTIME;
} else if (shader_data->uses_light || shader_data->ubo_size > 0) {
diff --git a/drivers/metal/pixel_formats.mm b/drivers/metal/pixel_formats.mm
index ac737b3f0a..36edbab99a 100644
--- a/drivers/metal/pixel_formats.mm
+++ b/drivers/metal/pixel_formats.mm
@@ -1200,7 +1200,7 @@ void PixelFormats::modifyMTLFormatCapabilities(id<MTLDevice> p_device) {
// Disable for iOS simulator last.
#if TARGET_OS_SIMULATOR
- if (![mtlDevice supportsFamily:MTLGPUFamilyApple5]) {
+ if (![p_device supportsFamily:MTLGPUFamilyApple5]) {
disableAllMTLPixFmtCaps(R8Unorm_sRGB);
disableAllMTLPixFmtCaps(RG8Unorm_sRGB);
disableAllMTLPixFmtCaps(B5G6R5Unorm);
diff --git a/drivers/unix/file_access_unix_pipe.cpp b/drivers/unix/file_access_unix_pipe.cpp
index 34758e8c7d..0a78429dec 100644
--- a/drivers/unix/file_access_unix_pipe.cpp
+++ b/drivers/unix/file_access_unix_pipe.cpp
@@ -41,7 +41,7 @@
#include <sys/types.h>
#include <unistd.h>
-Error FileAccessUnixPipe::open_existing(int p_rfd, int p_wfd) {
+Error FileAccessUnixPipe::open_existing(int p_rfd, int p_wfd, bool p_blocking) {
// Open pipe using handles created by pipe(fd) call in the OS.execute_with_pipe.
_close();
@@ -51,6 +51,11 @@ Error FileAccessUnixPipe::open_existing(int p_rfd, int p_wfd) {
fd[0] = p_rfd;
fd[1] = p_wfd;
+ if (!p_blocking) {
+ fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK);
+ fcntl(fd[1], F_SETFL, fcntl(fd[1], F_GETFL) | O_NONBLOCK);
+ }
+
last_error = OK;
return OK;
}
@@ -74,7 +79,7 @@ Error FileAccessUnixPipe::open_internal(const String &p_path, int p_mode_flags)
ERR_FAIL_COND_V_MSG(!S_ISFIFO(st.st_mode), ERR_ALREADY_IN_USE, "Pipe name is already used by file.");
}
- int f = ::open(path.utf8().get_data(), O_RDWR | O_CLOEXEC);
+ int f = ::open(path.utf8().get_data(), O_RDWR | O_CLOEXEC | O_NONBLOCK);
if (f < 0) {
switch (errno) {
case ENOENT: {
@@ -129,8 +134,11 @@ uint64_t FileAccessUnixPipe::get_buffer(uint8_t *p_dst, uint64_t p_length) const
ERR_FAIL_COND_V_MSG(fd[0] < 0, -1, "Pipe must be opened before use.");
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
- uint64_t read = ::read(fd[0], p_dst, p_length);
- if (read == p_length) {
+ ssize_t read = ::read(fd[0], p_dst, p_length);
+ if (read == -1) {
+ last_error = ERR_FILE_CANT_READ;
+ read = 0;
+ } else if (read != (ssize_t)p_length) {
last_error = ERR_FILE_CANT_READ;
} else {
last_error = OK;
diff --git a/drivers/unix/file_access_unix_pipe.h b/drivers/unix/file_access_unix_pipe.h
index 19acdb5a37..1a4199f239 100644
--- a/drivers/unix/file_access_unix_pipe.h
+++ b/drivers/unix/file_access_unix_pipe.h
@@ -50,7 +50,7 @@ class FileAccessUnixPipe : public FileAccess {
void _close();
public:
- Error open_existing(int p_rfd, int p_wfd);
+ Error open_existing(int p_rfd, int p_wfd, bool p_blocking);
virtual Error open_internal(const String &p_path, int p_mode_flags) override; ///< open a file
virtual bool is_open() const override; ///< true when file is open
diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp
index ce2553456d..8a9b130068 100644
--- a/drivers/unix/os_unix.cpp
+++ b/drivers/unix/os_unix.cpp
@@ -493,7 +493,7 @@ Dictionary OS_Unix::get_memory_info() const {
return meminfo;
}
-Dictionary OS_Unix::execute_with_pipe(const String &p_path, const List<String> &p_arguments) {
+Dictionary OS_Unix::execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking) {
#define CLEAN_PIPES \
if (pipe_in[0] >= 0) { \
::close(pipe_in[0]); \
@@ -578,11 +578,11 @@ Dictionary OS_Unix::execute_with_pipe(const String &p_path, const List<String> &
Ref<FileAccessUnixPipe> main_pipe;
main_pipe.instantiate();
- main_pipe->open_existing(pipe_out[0], pipe_in[1]);
+ main_pipe->open_existing(pipe_out[0], pipe_in[1], p_blocking);
Ref<FileAccessUnixPipe> err_pipe;
err_pipe.instantiate();
- err_pipe->open_existing(pipe_err[0], 0);
+ err_pipe->open_existing(pipe_err[0], 0, p_blocking);
ProcessInfo pi;
process_map_mutex.lock();
diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h
index df269a59d3..3add5df055 100644
--- a/drivers/unix/os_unix.h
+++ b/drivers/unix/os_unix.h
@@ -83,7 +83,7 @@ public:
virtual Dictionary get_memory_info() const override;
virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override;
- virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override;
+ virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking = true) override;
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
virtual Error kill(const ProcessID &p_pid) override;
virtual int get_process_id() const override;
diff --git a/drivers/windows/file_access_windows_pipe.cpp b/drivers/windows/file_access_windows_pipe.cpp
index 0c953b14aa..9bf0f4d852 100644
--- a/drivers/windows/file_access_windows_pipe.cpp
+++ b/drivers/windows/file_access_windows_pipe.cpp
@@ -35,7 +35,7 @@
#include "core/os/os.h"
#include "core/string/print_string.h"
-Error FileAccessWindowsPipe::open_existing(HANDLE p_rfd, HANDLE p_wfd) {
+Error FileAccessWindowsPipe::open_existing(HANDLE p_rfd, HANDLE p_wfd, bool p_blocking) {
// Open pipe using handles created by CreatePipe(rfd, wfd, NULL, 4096) call in the OS.execute_with_pipe.
_close();
@@ -44,6 +44,12 @@ Error FileAccessWindowsPipe::open_existing(HANDLE p_rfd, HANDLE p_wfd) {
fd[0] = p_rfd;
fd[1] = p_wfd;
+ if (!p_blocking) {
+ DWORD mode = PIPE_READMODE_BYTE | PIPE_NOWAIT;
+ SetNamedPipeHandleState(fd[0], &mode, nullptr, nullptr);
+ SetNamedPipeHandleState(fd[1], &mode, nullptr, nullptr);
+ }
+
last_error = OK;
return OK;
}
@@ -58,7 +64,7 @@ Error FileAccessWindowsPipe::open_internal(const String &p_path, int p_mode_flag
HANDLE h = CreateFileW((LPCWSTR)path.utf16().get_data(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) {
- h = CreateNamedPipeW((LPCWSTR)path.utf16().get_data(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, nullptr);
+ h = CreateNamedPipeW((LPCWSTR)path.utf16().get_data(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT, 1, 4096, 4096, 0, nullptr);
if (h == INVALID_HANDLE_VALUE) {
last_error = ERR_FILE_CANT_OPEN;
return last_error;
@@ -100,7 +106,7 @@ uint64_t FileAccessWindowsPipe::get_buffer(uint8_t *p_dst, uint64_t p_length) co
ERR_FAIL_COND_V_MSG(fd[0] == 0, -1, "Pipe must be opened before use.");
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
- DWORD read = -1;
+ DWORD read = 0;
if (!ReadFile(fd[0], p_dst, p_length, &read, nullptr) || read != p_length) {
last_error = ERR_FILE_CANT_READ;
} else {
diff --git a/drivers/windows/file_access_windows_pipe.h b/drivers/windows/file_access_windows_pipe.h
index 4e9bd036ae..1eb3c6ef2f 100644
--- a/drivers/windows/file_access_windows_pipe.h
+++ b/drivers/windows/file_access_windows_pipe.h
@@ -49,7 +49,7 @@ class FileAccessWindowsPipe : public FileAccess {
void _close();
public:
- Error open_existing(HANDLE p_rfd, HANDLE p_wfd);
+ Error open_existing(HANDLE p_rfd, HANDLE p_wfd, bool p_blocking);
virtual Error open_internal(const String &p_path, int p_mode_flags) override; ///< open a file
virtual bool is_open() const override; ///< true when file is open
diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp
index feb5f3d20d..24fcfd3930 100644
--- a/editor/animation_bezier_editor.cpp
+++ b/editor/animation_bezier_editor.cpp
@@ -174,7 +174,7 @@ void AnimationBezierTrackEdit::_draw_track(int p_track, const Color &p_color) {
}
if (lines.size() >= 2) {
- draw_multiline(lines, p_color, Math::round(EDSCALE));
+ draw_multiline(lines, p_color, Math::round(EDSCALE), true);
}
}
}
@@ -208,7 +208,7 @@ void AnimationBezierTrackEdit::_draw_line_clipped(const Vector2 &p_from, const V
from = from.lerp(to, c);
}
- draw_line(from, to, p_color, Math::round(EDSCALE));
+ draw_line(from, to, p_color, Math::round(EDSCALE), true);
}
void AnimationBezierTrackEdit::_notification(int p_what) {
diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp
index eb0ab1174b..063241fd1b 100644
--- a/editor/connections_dialog.cpp
+++ b/editor/connections_dialog.cpp
@@ -576,6 +576,22 @@ String ConnectDialog::get_signature(const MethodInfo &p_method, PackedStringArra
type_name = "Array";
}
break;
+ case Variant::DICTIONARY:
+ type_name = "Dictionary";
+ if (pi.hint == PROPERTY_HINT_DICTIONARY_TYPE && !pi.hint_string.is_empty()) {
+ String key_hint = pi.hint_string.get_slice(";", 0);
+ String value_hint = pi.hint_string.get_slice(";", 1);
+ if (key_hint.is_empty() || key_hint.begins_with("res://")) {
+ key_hint = "Variant";
+ }
+ if (value_hint.is_empty() || value_hint.begins_with("res://")) {
+ value_hint = "Variant";
+ }
+ if (key_hint != "Variant" || value_hint != "Variant") {
+ type_name += "[" + key_hint + ", " + value_hint + "]";
+ }
+ }
+ break;
case Variant::OBJECT:
if (pi.class_name != StringName()) {
type_name = pi.class_name;
diff --git a/editor/doc_tools.cpp b/editor/doc_tools.cpp
index bf5b717c19..79e0c7ebd1 100644
--- a/editor/doc_tools.cpp
+++ b/editor/doc_tools.cpp
@@ -277,6 +277,10 @@ static void merge_theme_properties(Vector<DocData::ThemeItemDoc> &p_to, const Ve
// Check found entry on name and data type.
if (to.name == from.name && to.data_type == from.data_type) {
to.description = from.description;
+ to.is_deprecated = from.is_deprecated;
+ to.deprecated_message = from.deprecated_message;
+ to.is_experimental = from.is_experimental;
+ to.experimental_message = from.experimental_message;
to.keywords = from.keywords;
}
}
@@ -571,6 +575,8 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) {
prop.type = retinfo.class_name;
} else if (retinfo.type == Variant::ARRAY && retinfo.hint == PROPERTY_HINT_ARRAY_TYPE) {
prop.type = retinfo.hint_string + "[]";
+ } else if (retinfo.type == Variant::DICTIONARY && retinfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ prop.type = "Dictionary[" + retinfo.hint_string.replace(";", ", ") + "]";
} else if (retinfo.hint == PROPERTY_HINT_RESOURCE_TYPE) {
prop.type = retinfo.hint_string;
} else if (retinfo.type == Variant::NIL && retinfo.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
@@ -1420,6 +1426,14 @@ Error DocTools::_load(Ref<XMLParser> parser) {
prop2.type = parser->get_named_attribute_value("type");
ERR_FAIL_COND_V(!parser->has_attribute("data_type"), ERR_FILE_CORRUPT);
prop2.data_type = parser->get_named_attribute_value("data_type");
+ if (parser->has_attribute("deprecated")) {
+ prop2.is_deprecated = true;
+ prop2.deprecated_message = parser->get_named_attribute_value("deprecated");
+ }
+ if (parser->has_attribute("experimental")) {
+ prop2.is_experimental = true;
+ prop2.experimental_message = parser->get_named_attribute_value("experimental");
+ }
if (parser->has_attribute("keywords")) {
prop2.keywords = parser->get_named_attribute_value("keywords");
}
@@ -1738,6 +1752,12 @@ Error DocTools::save_classes(const String &p_default_path, const HashMap<String,
if (!ti.default_value.is_empty()) {
additional_attributes += String(" default=\"") + ti.default_value.xml_escape(true) + "\"";
}
+ if (ti.is_deprecated) {
+ additional_attributes += " deprecated=\"" + ti.deprecated_message.xml_escape(true) + "\"";
+ }
+ if (ti.is_experimental) {
+ additional_attributes += " experimental=\"" + ti.experimental_message.xml_escape(true) + "\"";
+ }
if (!ti.keywords.is_empty()) {
additional_attributes += String(" keywords=\"") + ti.keywords.xml_escape(true) + "\"";
}
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index 3d3caf59eb..b1b64b5d60 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -410,11 +410,13 @@ void EditorFileSystem::_scan_filesystem() {
new_filesystem->parent = nullptr;
ScannedDirectory *sd;
+ HashSet<String> *processed_files = nullptr;
// On the first scan, the first_scan_root_dir is created in _first_scan_filesystem.
if (first_scan) {
sd = first_scan_root_dir;
// Will be updated on scan.
ResourceUID::get_singleton()->clear();
+ processed_files = memnew(HashSet<String>());
} else {
Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES);
sd = memnew(ScannedDirectory);
@@ -422,14 +424,18 @@ void EditorFileSystem::_scan_filesystem() {
nb_files_total = _scan_new_dir(sd, d);
}
- _process_file_system(sd, new_filesystem, sp);
+ _process_file_system(sd, new_filesystem, sp, processed_files);
+ if (first_scan) {
+ _process_removed_files(*processed_files);
+ }
dep_update_list.clear();
file_cache.clear(); //clear caches, no longer needed
if (first_scan) {
memdelete(first_scan_root_dir);
first_scan_root_dir = nullptr;
+ memdelete(processed_files);
} else {
//on the first scan this is done from the main thread after re-importing
_save_filesystem_cache();
@@ -990,7 +996,7 @@ int EditorFileSystem::_scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da)
return nb_files_total_scan;
}
-void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) {
+void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *r_processed_files) {
p_dir->modified_time = FileAccess::get_modified_time(p_scan_dir->full_path);
for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) {
@@ -998,7 +1004,7 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
sub_dir->parent = p_dir;
sub_dir->name = scan_sub_dir->name;
p_dir->subdirs.push_back(sub_dir);
- _process_file_system(scan_sub_dir, sub_dir, p_progress);
+ _process_file_system(scan_sub_dir, sub_dir, p_progress, r_processed_files);
}
for (const String &scan_file : p_scan_dir->files) {
@@ -1014,6 +1020,10 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
fi->file = scan_file;
p_dir->files.push_back(fi);
+ if (r_processed_files) {
+ r_processed_files->insert(path);
+ }
+
FileCache *fc = file_cache.getptr(path);
uint64_t mt = FileAccess::get_modified_time(path);
@@ -1065,7 +1075,7 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path);
fi->modified_time = 0;
fi->import_modified_time = 0;
- fi->import_valid = fi->type == "TextFile" ? true : ResourceLoader::is_import_valid(path);
+ fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);
ItemAction ia;
ia.action = ItemAction::ACTION_FILE_TEST_REIMPORT;
@@ -1108,6 +1118,9 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
if (fi->type == "" && textfile_extensions.has(ext)) {
fi->type = "TextFile";
}
+ if (fi->type == "" && other_file_extensions.has(ext)) {
+ fi->type = "OtherFile";
+ }
fi->uid = ResourceLoader::get_resource_uid(path);
fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path);
fi->deps = _get_dependencies(path);
@@ -1139,6 +1152,20 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir,
}
}
+void EditorFileSystem::_process_removed_files(const HashSet<String> &p_processed_files) {
+ for (const KeyValue<String, EditorFileSystem::FileCache> &kv : file_cache) {
+ if (!p_processed_files.has(kv.key)) {
+ if (ClassDB::is_parent_class(kv.value.type, SNAME("Script"))) {
+ // A script has been removed from disk since the last startup. The documentation needs to be updated.
+ // There's no need to add the path in update_script_paths since that is exclusively for updating global class names,
+ // which is handled in _first_scan_filesystem before the full scan to ensure plugins and autoloads can be created.
+ MutexLock update_script_lock(update_script_mutex);
+ update_script_paths_documentation.insert(kv.key);
+ }
+ }
+ }
+}
+
void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) {
uint64_t current_mtime = FileAccess::get_modified_time(p_dir->get_path());
@@ -1206,7 +1233,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanPr
int nb_files_dir = _scan_new_dir(&sd, d);
p_progress.hi += nb_files_dir;
diff_nb_files += nb_files_dir;
- _process_file_system(&sd, efd, p_progress);
+ _process_file_system(&sd, efd, p_progress, nullptr);
ItemAction ia;
ia.action = ItemAction::ACTION_DIR_ADD;
@@ -1239,8 +1266,11 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanPr
if (fi->type == "" && textfile_extensions.has(ext)) {
fi->type = "TextFile";
}
+ if (fi->type == "" && other_file_extensions.has(ext)) {
+ fi->type = "OtherFile";
+ }
fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path);
- fi->import_valid = fi->type == "TextFile" ? true : ResourceLoader::is_import_valid(path);
+ fi->import_valid = (fi->type == "TextFile" || fi->type == "OtherFile") ? true : ResourceLoader::is_import_valid(path);
fi->import_group_file = ResourceLoader::get_import_group_file(path);
{
@@ -1872,7 +1902,12 @@ void EditorFileSystem::_update_script_classes() {
EditorProgress *ep = nullptr;
if (update_script_paths.size() > 1) {
- ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size()));
+ if (MessageQueue::get_singleton()->is_flushing()) {
+ // Use background progress when message queue is flushing.
+ ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size(), false, true));
+ } else {
+ ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size()));
+ }
}
int step_count = 0;
@@ -1911,7 +1946,12 @@ void EditorFileSystem::_update_script_documentation() {
EditorProgress *ep = nullptr;
if (update_script_paths_documentation.size() > 1) {
- ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size()));
+ if (MessageQueue::get_singleton()->is_flushing()) {
+ // Use background progress when message queue is flushing.
+ ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size(), false, true));
+ } else {
+ ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size()));
+ }
}
int step_count = 0;
@@ -2084,6 +2124,9 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {
if (type.is_empty() && textfile_extensions.has(file.get_extension())) {
type = "TextFile";
}
+ if (type.is_empty() && other_file_extensions.has(file.get_extension())) {
+ type = "OtherFile";
+ }
String script_class = ResourceLoader::get_resource_script_class(file);
ResourceUID::ID uid = ResourceLoader::get_resource_uid(file);
@@ -2103,7 +2146,7 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {
EditorFileSystemDirectory::FileInfo *fi = memnew(EditorFileSystemDirectory::FileInfo);
fi->file = file_name;
fi->import_modified_time = 0;
- fi->import_valid = type == "TextFile" ? true : ResourceLoader::is_import_valid(file);
+ fi->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);
if (idx == fs->files.size()) {
fs->files.push_back(fi);
@@ -2127,7 +2170,7 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) {
fs->files[cpos]->import_group_file = ResourceLoader::get_import_group_file(file);
fs->files[cpos]->modified_time = FileAccess::get_modified_time(file);
fs->files[cpos]->deps = _get_dependencies(file);
- fs->files[cpos]->import_valid = type == "TextFile" ? true : ResourceLoader::is_import_valid(file);
+ fs->files[cpos]->import_valid = (type == "TextFile" || type == "OtherFile") ? true : ResourceLoader::is_import_valid(file);
if (uid != ResourceUID::INVALID_ID) {
if (ResourceUID::get_singleton()->has_id(uid)) {
@@ -2385,6 +2428,9 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector
if (fs->files[cpos]->type == "" && textfile_extensions.has(file.get_extension())) {
fs->files[cpos]->type = "TextFile";
}
+ if (fs->files[cpos]->type == "" && other_file_extensions.has(file.get_extension())) {
+ fs->files[cpos]->type = "OtherFile";
+ }
fs->files[cpos]->import_valid = err == OK;
if (ResourceUID::get_singleton()->has_id(uid)) {
@@ -3085,6 +3131,7 @@ void EditorFileSystem::_update_extensions() {
valid_extensions.clear();
import_extensions.clear();
textfile_extensions.clear();
+ other_file_extensions.clear();
List<String> extensionsl;
ResourceLoader::get_recognized_extensions_for_type("", &extensionsl);
@@ -3100,6 +3147,14 @@ void EditorFileSystem::_update_extensions() {
valid_extensions.insert(E);
textfile_extensions.insert(E);
}
+ const Vector<String> other_file_ext = ((String)(EDITOR_GET("docks/filesystem/other_file_extensions"))).split(",", false);
+ for (const String &E : other_file_ext) {
+ if (valid_extensions.has(E)) {
+ continue;
+ }
+ valid_extensions.insert(E);
+ other_file_extensions.insert(E);
+ }
extensionsl.clear();
ResourceFormatImporter::get_singleton()->get_recognized_extensions(&extensionsl);
diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h
index 2ceb3fe4a5..9adab1ed24 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -235,11 +235,12 @@ class EditorFileSystem : public Node {
int _insert_actions_delete_files_directory(EditorFileSystemDirectory *p_dir);
HashSet<String> textfile_extensions;
+ HashSet<String> other_file_extensions;
HashSet<String> valid_extensions;
HashSet<String> import_extensions;
int _scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da);
- void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress);
+ void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *p_processed_files);
Thread thread_sources;
bool scanning_changes = false;
@@ -287,6 +288,7 @@ class EditorFileSystem : public Node {
void _update_script_classes();
void _update_script_documentation();
void _process_update_pending();
+ void _process_removed_files(const HashSet<String> &p_processed_files);
Mutex update_scene_mutex;
HashSet<String> update_scene_paths;
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 9b0c05d910..fe758bf99b 100644
--- a/editor/editor_help.cpp
+++ b/editor/editor_help.cpp
@@ -376,10 +376,10 @@ static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_i
}
p_rt->push_color(type_color);
- bool add_array = false;
+ bool add_typed_container = false;
if (can_ref) {
if (link_t.ends_with("[]")) {
- add_array = true;
+ add_typed_container = true;
link_t = link_t.trim_suffix("[]");
display_t = display_t.trim_suffix("[]");
@@ -387,6 +387,22 @@ static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_i
p_rt->add_text("Array");
p_rt->pop(); // meta
p_rt->add_text("[");
+ } else if (link_t.begins_with("Dictionary[")) {
+ add_typed_container = true;
+ link_t = link_t.trim_prefix("Dictionary[").trim_suffix("]");
+ display_t = display_t.trim_prefix("Dictionary[").trim_suffix("]");
+
+ p_rt->push_meta("#Dictionary", RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+ p_rt->add_text("Dictionary");
+ p_rt->pop(); // meta
+ p_rt->add_text("[");
+ p_rt->push_meta("#" + link_t.get_slice(", ", 0), RichTextLabel::META_UNDERLINE_ON_HOVER); // class
+ p_rt->add_text(_contextualize_class_specifier(display_t.get_slice(", ", 0), p_class));
+ p_rt->pop(); // meta
+ p_rt->add_text(", ");
+
+ link_t = link_t.get_slice(", ", 1);
+ display_t = _contextualize_class_specifier(display_t.get_slice(", ", 1), p_class);
} else if (is_bitfield) {
p_rt->push_color(Color(type_color, 0.5));
p_rt->push_hint(TTR("This value is an integer composed as a bitmask of the following flags."));
@@ -405,7 +421,7 @@ static void _add_type_to_rt(const String &p_type, const String &p_enum, bool p_i
p_rt->add_text(display_t);
if (can_ref) {
p_rt->pop(); // meta
- if (add_array) {
+ if (add_typed_container) {
p_rt->add_text("]");
} else if (is_bitfield) {
p_rt->push_color(Color(type_color, 0.5));
@@ -1456,10 +1472,31 @@ void EditorHelp::_update_doc() {
_push_normal_font();
class_desc->push_color(theme_cache.comment_color);
+ bool has_prev_text = false;
+
+ if (theme_item.is_deprecated) {
+ has_prev_text = true;
+ DEPRECATED_DOC_MSG(HANDLE_DOC(theme_item.deprecated_message), TTR("This theme property may be changed or removed in future versions."));
+ }
+
+ if (theme_item.is_experimental) {
+ if (has_prev_text) {
+ class_desc->add_newline();
+ class_desc->add_newline();
+ }
+ has_prev_text = true;
+ EXPERIMENTAL_DOC_MSG(HANDLE_DOC(theme_item.experimental_message), TTR("This theme property may be changed or removed in future versions."));
+ }
+
const String descr = HANDLE_DOC(theme_item.description);
if (!descr.is_empty()) {
+ if (has_prev_text) {
+ class_desc->add_newline();
+ class_desc->add_newline();
+ }
+ has_prev_text = true;
_add_text(descr);
- } else {
+ } else if (!has_prev_text) {
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
class_desc->add_text(" ");
class_desc->push_color(theme_cache.comment_color);
@@ -2220,12 +2257,6 @@ void EditorHelp::_update_doc() {
}
has_prev_text = true;
_add_text(descr);
- // Add copy note to built-in properties returning Packed*Array.
- if (!cd.is_script_doc && packed_array_types.has(prop.type)) {
- class_desc->add_newline();
- class_desc->add_newline();
- _add_text(vformat(TTR("[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details."), prop.type));
- }
} else if (!has_prev_text) {
class_desc->add_image(get_editor_theme_icon(SNAME("Error")));
class_desc->add_text(" ");
@@ -2238,6 +2269,13 @@ void EditorHelp::_update_doc() {
class_desc->pop(); // color
}
+ // Add copy note to built-in properties returning `Packed*Array`.
+ if (!cd.is_script_doc && packed_array_types.has(prop.type)) {
+ class_desc->add_newline();
+ class_desc->add_newline();
+ _add_text(vformat(TTR("[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details."), prop.type));
+ }
+
class_desc->pop(); // color
_pop_normal_font();
class_desc->pop(); // indent
@@ -3380,6 +3418,20 @@ EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringNam
for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) {
HelpData current;
current.description = HANDLE_DOC(theme_item.description);
+ if (theme_item.is_deprecated) {
+ if (theme_item.deprecated_message.is_empty()) {
+ current.deprecated_message = TTR("This theme property may be changed or removed in future versions.");
+ } else {
+ current.deprecated_message = HANDLE_DOC(theme_item.deprecated_message);
+ }
+ }
+ if (theme_item.is_experimental) {
+ if (theme_item.experimental_message.is_empty()) {
+ current.experimental_message = TTR("This theme property may be changed or removed in future versions.");
+ } else {
+ current.experimental_message = HANDLE_DOC(theme_item.experimental_message);
+ }
+ }
if (theme_item.name == p_theme_item_name) {
result = current;
diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp
index eb97337b37..987c7f9c31 100644
--- a/editor/editor_help_search.cpp
+++ b/editor/editor_help_search.cpp
@@ -1248,7 +1248,7 @@ TreeItem *EditorHelpSearch::Runner::_create_property_item(TreeItem *p_parent, co
TreeItem *EditorHelpSearch::Runner::_create_theme_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ThemeItemDoc> &p_match) {
String tooltip = p_match.doc->type + " " + p_class_doc->name + "." + p_match.doc->name;
tooltip += _build_keywords_tooltip(p_match.doc->keywords);
- return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberTheme"), p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, false, false, p_match.name ? String() : p_match.keyword);
+ return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberTheme"), p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword);
}
TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const StringName &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword) {
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 47b5fa4d1a..6899a35ded 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -175,7 +175,7 @@ static const String REMOVE_ANDROID_BUILD_TEMPLATE_MESSAGE = "The Android build t
static const String INSTALL_ANDROID_BUILD_TEMPLATE_MESSAGE = "This will set up your project for gradle Android builds by installing the source template to \"%s\".\nNote that in order to make gradle builds instead of using pre-built APKs, the \"Use Gradle Build\" option should be enabled in the Android export preset.";
bool EditorProgress::step(const String &p_state, int p_step, bool p_force_refresh) {
- if (Thread::is_main_thread()) {
+ if (!force_background && Thread::is_main_thread()) {
return EditorNode::progress_task_step(task, p_state, p_step, p_force_refresh);
} else {
EditorNode::progress_task_step_bg(task, p_step);
@@ -183,17 +183,18 @@ bool EditorProgress::step(const String &p_state, int p_step, bool p_force_refres
}
}
-EditorProgress::EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel) {
- if (Thread::is_main_thread()) {
+EditorProgress::EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel, bool p_force_background) {
+ if (!p_force_background && Thread::is_main_thread()) {
EditorNode::progress_add_task(p_task, p_label, p_amount, p_can_cancel);
} else {
EditorNode::progress_add_task_bg(p_task, p_label, p_amount);
}
task = p_task;
+ force_background = p_force_background;
}
EditorProgress::~EditorProgress() {
- if (Thread::is_main_thread()) {
+ if (!force_background && Thread::is_main_thread()) {
EditorNode::progress_end_task(task);
} else {
EditorNode::progress_end_task_bg(task);
@@ -824,6 +825,7 @@ void EditorNode::_notification(int p_what) {
if (EditorSettings::get_singleton()->check_changed_settings_in_group("docks/filesystem")) {
HashSet<String> updated_textfile_extensions;
+ HashSet<String> updated_other_file_extensions;
bool extensions_match = true;
const Vector<String> textfile_ext = ((String)(EDITOR_GET("docks/filesystem/textfile_extensions"))).split(",", false);
for (const String &E : textfile_ext) {
@@ -832,9 +834,17 @@ void EditorNode::_notification(int p_what) {
extensions_match = false;
}
}
+ const Vector<String> other_file_ext = ((String)(EDITOR_GET("docks/filesystem/other_file_extensions"))).split(",", false);
+ for (const String &E : other_file_ext) {
+ updated_other_file_extensions.insert(E);
+ if (extensions_match && !other_file_extensions.has(E)) {
+ extensions_match = false;
+ }
+ }
- if (!extensions_match || updated_textfile_extensions.size() < textfile_extensions.size()) {
+ if (!extensions_match || updated_textfile_extensions.size() < textfile_extensions.size() || updated_other_file_extensions.size() < other_file_extensions.size()) {
textfile_extensions = updated_textfile_extensions;
+ other_file_extensions = updated_other_file_extensions;
EditorFileSystem::get_singleton()->scan();
}
}
@@ -1325,6 +1335,9 @@ Error EditorNode::load_resource(const String &p_resource, bool p_ignore_broken_d
res = ResourceLoader::load(p_resource, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);
} else if (textfile_extensions.has(p_resource.get_extension())) {
res = ScriptEditor::get_singleton()->open_file(p_resource);
+ } else if (other_file_extensions.has(p_resource.get_extension())) {
+ OS::get_singleton()->shell_open(ProjectSettings::get_singleton()->globalize_path(p_resource));
+ return OK;
}
ERR_FAIL_COND_V(!res.is_valid(), ERR_CANT_OPEN);
@@ -5239,8 +5252,8 @@ void EditorNode::_copy_warning(const String &p_str) {
}
void EditorNode::_save_editor_layout() {
- if (waiting_for_first_scan) {
- return; // Scanning, do not touch docks.
+ if (!load_editor_layout_done) {
+ return;
}
Ref<ConfigFile> config;
config.instantiate();
@@ -5296,22 +5309,22 @@ void EditorNode::_load_editor_layout() {
if (overridden_default_layout >= 0) {
_layout_menu_option(overridden_default_layout);
}
- return;
- }
-
- ep.step(TTR("Loading docks..."), 1, true);
- editor_dock_manager->load_docks_from_config(config, "docks");
+ } else {
+ ep.step(TTR("Loading docks..."), 1, true);
+ editor_dock_manager->load_docks_from_config(config, "docks");
- ep.step(TTR("Reopening scenes..."), 2, true);
- _load_open_scenes_from_config(config);
+ ep.step(TTR("Reopening scenes..."), 2, true);
+ _load_open_scenes_from_config(config);
- ep.step(TTR("Loading central editor layout..."), 3, true);
- _load_central_editor_layout_from_config(config);
+ ep.step(TTR("Loading central editor layout..."), 3, true);
+ _load_central_editor_layout_from_config(config);
- ep.step(TTR("Loading plugin window layout..."), 4, true);
- editor_data.set_plugin_window_layout(config);
+ ep.step(TTR("Loading plugin window layout..."), 4, true);
+ editor_data.set_plugin_window_layout(config);
- ep.step(TTR("Editor layout ready."), 5, true);
+ ep.step(TTR("Editor layout ready."), 5, true);
+ }
+ load_editor_layout_done = true;
}
void EditorNode::_save_central_editor_layout_to_config(Ref<ConfigFile> p_config_file) {
@@ -6979,6 +6992,10 @@ EditorNode::EditorNode() {
for (const String &E : textfile_ext) {
textfile_extensions.insert(E);
}
+ const Vector<String> other_file_ext = ((String)(EDITOR_GET("docks/filesystem/other_file_extensions"))).split(",", false);
+ for (const String &E : other_file_ext) {
+ other_file_extensions.insert(E);
+ }
resource_preview = memnew(EditorResourcePreview);
add_child(resource_preview);
diff --git a/editor/editor_node.h b/editor/editor_node.h
index bdf9b26a7a..e9d2c28528 100644
--- a/editor/editor_node.h
+++ b/editor/editor_node.h
@@ -124,9 +124,10 @@ class WindowWrapper;
struct EditorProgress {
String task;
+ bool force_background = false;
bool step(const String &p_state, int p_step = -1, bool p_force_refresh = true);
- EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel = false);
+ EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel = false, bool p_force_background = false);
~EditorProgress();
};
@@ -461,6 +462,7 @@ private:
bool requested_first_scan = false;
bool waiting_for_first_scan = true;
+ bool load_editor_layout_done = false;
int current_menu_option = 0;
@@ -487,6 +489,7 @@ private:
String import_reload_fn;
HashSet<String> textfile_extensions;
+ HashSet<String> other_file_extensions;
HashSet<FileDialog *> file_dialogs;
HashSet<EditorFileDialog *> editor_file_dialogs;
diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp
index 19a4165041..123d903220 100644
--- a/editor/editor_properties.cpp
+++ b/editor/editor_properties.cpp
@@ -3771,7 +3771,7 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
return editor;
} else {
EditorPropertyDictionary *editor = memnew(EditorPropertyDictionary);
- editor->setup(p_hint);
+ editor->setup(p_hint, p_hint_text);
return editor;
}
} break;
diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp
index f5d016629f..f03eef4d4d 100644
--- a/editor/editor_properties_array_dict.cpp
+++ b/editor/editor_properties_array_dict.cpp
@@ -863,6 +863,26 @@ EditorPropertyArray::EditorPropertyArray() {
///////////////////// DICTIONARY ///////////////////////////
+void EditorPropertyDictionary::initialize_dictionary(Variant &p_dictionary) {
+ if (key_subtype != Variant::NIL || value_subtype != Variant::NIL) {
+ Dictionary dict;
+ StringName key_subtype_class;
+ Ref<Script> key_subtype_script;
+ if (key_subtype == Variant::OBJECT && !key_subtype_hint_string.is_empty() && ClassDB::class_exists(key_subtype_hint_string)) {
+ key_subtype_class = key_subtype_hint_string;
+ }
+ StringName value_subtype_class;
+ Ref<Script> value_subtype_script;
+ if (value_subtype == Variant::OBJECT && !value_subtype_hint_string.is_empty() && ClassDB::class_exists(value_subtype_hint_string)) {
+ value_subtype_class = value_subtype_hint_string;
+ }
+ dict.set_typed(key_subtype, key_subtype_class, key_subtype_script, value_subtype, value_subtype_class, value_subtype_script);
+ p_dictionary = dict;
+ } else {
+ VariantInternal::initialize(&p_dictionary, Variant::DICTIONARY);
+ }
+}
+
void EditorPropertyDictionary::_property_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing) {
if (p_value.get_type() == Variant::OBJECT && p_value.is_null()) {
p_value = Variant(); // `EditorResourcePicker` resets to `Ref<Resource>()`. See GH-82716.
@@ -914,16 +934,29 @@ void EditorPropertyDictionary::_create_new_property_slot(int p_idx) {
EditorProperty *prop = memnew(EditorPropertyNil);
hbox->add_child(prop);
- Button *edit_btn = memnew(Button);
- edit_btn->set_icon(get_editor_theme_icon(SNAME("Edit")));
- edit_btn->set_disabled(is_read_only());
- edit_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorPropertyDictionary::_change_type).bind(edit_btn, slots.size()));
- hbox->add_child(edit_btn);
+ bool use_key = p_idx == EditorPropertyDictionaryObject::NEW_KEY_INDEX;
+ bool is_untyped_dict = (use_key ? key_subtype : value_subtype) == Variant::NIL;
+
+ if (is_untyped_dict) {
+ Button *edit_btn = memnew(Button);
+ edit_btn->set_icon(get_editor_theme_icon(SNAME("Edit")));
+ edit_btn->set_disabled(is_read_only());
+ edit_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorPropertyDictionary::_change_type).bind(edit_btn, slots.size()));
+ hbox->add_child(edit_btn);
+ } else if (p_idx >= 0) {
+ Button *remove_btn = memnew(Button);
+ remove_btn->set_icon(get_editor_theme_icon(SNAME("Remove")));
+ remove_btn->set_disabled(is_read_only());
+ remove_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorPropertyDictionary::_remove_pressed).bind(slots.size()));
+ hbox->add_child(remove_btn);
+ }
+
if (add_panel) {
add_panel->get_child(0)->add_child(hbox);
} else {
property_vbox->add_child(hbox);
}
+
Slot slot;
slot.prop = prop;
slot.object = object;
@@ -969,15 +1002,70 @@ void EditorPropertyDictionary::_change_type_menu(int p_index) {
}
}
-void EditorPropertyDictionary::setup(PropertyHint p_hint) {
- property_hint = p_hint;
+void EditorPropertyDictionary::setup(PropertyHint p_hint, const String &p_hint_string) {
+ PackedStringArray types = p_hint_string.split(";");
+ if (types.size() > 0 && !types[0].is_empty()) {
+ String key = types[0];
+ int hint_key_subtype_separator = key.find(":");
+ if (hint_key_subtype_separator >= 0) {
+ String key_subtype_string = key.substr(0, hint_key_subtype_separator);
+ int slash_pos = key_subtype_string.find("/");
+ if (slash_pos >= 0) {
+ key_subtype_hint = PropertyHint(key_subtype_string.substr(slash_pos + 1, key_subtype_string.size() - slash_pos - 1).to_int());
+ key_subtype_string = key_subtype_string.substr(0, slash_pos);
+ }
+
+ key_subtype_hint_string = key.substr(hint_key_subtype_separator + 1, key.size() - hint_key_subtype_separator - 1);
+ key_subtype = Variant::Type(key_subtype_string.to_int());
+
+ Variant new_key = object->get_new_item_key();
+ VariantInternal::initialize(&new_key, key_subtype);
+ object->set_new_item_key(new_key);
+ }
+ }
+ if (types.size() > 1 && !types[1].is_empty()) {
+ String value = types[1];
+ int hint_value_subtype_separator = value.find(":");
+ if (hint_value_subtype_separator >= 0) {
+ String value_subtype_string = value.substr(0, hint_value_subtype_separator);
+ int slash_pos = value_subtype_string.find("/");
+ if (slash_pos >= 0) {
+ value_subtype_hint = PropertyHint(value_subtype_string.substr(slash_pos + 1, value_subtype_string.size() - slash_pos - 1).to_int());
+ value_subtype_string = value_subtype_string.substr(0, slash_pos);
+ }
+
+ value_subtype_hint_string = value.substr(hint_value_subtype_separator + 1, value.size() - hint_value_subtype_separator - 1);
+ value_subtype = Variant::Type(value_subtype_string.to_int());
+
+ Variant new_value = object->get_new_item_value();
+ VariantInternal::initialize(&new_value, value_subtype);
+ object->set_new_item_value(new_value);
+ }
+ }
}
void EditorPropertyDictionary::update_property() {
Variant updated_val = get_edited_property_value();
+ String dict_type_name = "Dictionary";
+ if (key_subtype != Variant::NIL || value_subtype != Variant::NIL) {
+ String key_subtype_name = "Variant";
+ if (key_subtype == Variant::OBJECT && (key_subtype_hint == PROPERTY_HINT_RESOURCE_TYPE || key_subtype_hint == PROPERTY_HINT_NODE_TYPE)) {
+ key_subtype_name = key_subtype_hint_string;
+ } else if (key_subtype != Variant::NIL) {
+ key_subtype_name = Variant::get_type_name(key_subtype);
+ }
+ String value_subtype_name = "Variant";
+ if (value_subtype == Variant::OBJECT && (value_subtype_hint == PROPERTY_HINT_RESOURCE_TYPE || value_subtype_hint == PROPERTY_HINT_NODE_TYPE)) {
+ value_subtype_name = value_subtype_hint_string;
+ } else if (value_subtype != Variant::NIL) {
+ value_subtype_name = Variant::get_type_name(value_subtype);
+ }
+ dict_type_name += vformat("[%s, %s]", key_subtype_name, value_subtype_name);
+ }
+
if (updated_val.get_type() != Variant::DICTIONARY) {
- edit->set_text(TTR("Dictionary (Nil)")); // This provides symmetry with the array property.
+ edit->set_text(vformat(TTR("(Nil) %s"), dict_type_name)); // This provides symmetry with the array property.
edit->set_pressed(false);
if (container) {
set_bottom_editor(nullptr);
@@ -993,7 +1081,7 @@ void EditorPropertyDictionary::update_property() {
Dictionary dict = updated_val;
object->set_dict(updated_val);
- edit->set_text(vformat(TTR("Dictionary (size %d)"), dict.size()));
+ edit->set_text(vformat(TTR("%s (size %d)"), dict_type_name, dict.size()));
bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property());
if (edit->is_pressed() != unfolded) {
@@ -1074,7 +1162,9 @@ void EditorPropertyDictionary::update_property() {
editor->setup("Object");
new_prop = editor;
} else {
- new_prop = EditorInspector::instantiate_property_editor(this, value_type, "", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE);
+ bool use_key = slot.index == EditorPropertyDictionaryObject::NEW_KEY_INDEX;
+ new_prop = EditorInspector::instantiate_property_editor(this, value_type, "", use_key ? key_subtype_hint : value_subtype_hint,
+ use_key ? key_subtype_hint_string : value_subtype_hint_string, PROPERTY_USAGE_NONE);
}
new_prop->set_selectable(false);
new_prop->set_use_folding(is_using_folding());
@@ -1108,6 +1198,13 @@ void EditorPropertyDictionary::update_property() {
}
}
+void EditorPropertyDictionary::_remove_pressed(int p_slot_index) {
+ Dictionary dict = object->get_dict().duplicate();
+ dict.erase(dict.get_key_at_index(p_slot_index));
+
+ emit_changed(get_edited_property(), dict);
+}
+
void EditorPropertyDictionary::_object_id_selected(const StringName &p_property, ObjectID p_id) {
emit_signal(SNAME("object_id_selected"), p_property, p_id);
}
@@ -1140,7 +1237,7 @@ void EditorPropertyDictionary::_notification(int p_what) {
void EditorPropertyDictionary::_edit_pressed() {
Variant prop_val = get_edited_property_value();
if (prop_val.get_type() == Variant::NIL && edit->is_pressed()) {
- VariantInternal::initialize(&prop_val, Variant::DICTIONARY);
+ initialize_dictionary(prop_val);
emit_changed(get_edited_property(), prop_val);
}
@@ -1187,6 +1284,14 @@ EditorPropertyDictionary::EditorPropertyDictionary() {
change_type->connect(SceneStringName(id_pressed), callable_mp(this, &EditorPropertyDictionary::_change_type_menu));
changing_type_index = -1;
has_borders = true;
+
+ key_subtype = Variant::NIL;
+ key_subtype_hint = PROPERTY_HINT_NONE;
+ key_subtype_hint_string = "";
+
+ value_subtype = Variant::NIL;
+ value_subtype_hint = PROPERTY_HINT_NONE;
+ value_subtype_hint_string = "";
}
///////////////////// LOCALIZABLE STRING ///////////////////////////
diff --git a/editor/editor_properties_array_dict.h b/editor/editor_properties_array_dict.h
index 024c04956f..84c3f975be 100644
--- a/editor/editor_properties_array_dict.h
+++ b/editor/editor_properties_array_dict.h
@@ -219,7 +219,6 @@ class EditorPropertyDictionary : public EditorProperty {
EditorSpinSlider *size_sliderv = nullptr;
Button *button_add_item = nullptr;
EditorPaginator *paginator = nullptr;
- PropertyHint property_hint;
LocalVector<Slot> slots;
void _create_new_property_slot(int p_idx);
@@ -231,12 +230,21 @@ class EditorPropertyDictionary : public EditorProperty {
void _add_key_value();
void _object_id_selected(const StringName &p_property, ObjectID p_id);
+ void _remove_pressed(int p_slot_index);
+
+ Variant::Type key_subtype;
+ PropertyHint key_subtype_hint;
+ String key_subtype_hint_string;
+ Variant::Type value_subtype;
+ PropertyHint value_subtype_hint;
+ String value_subtype_hint_string;
+ void initialize_dictionary(Variant &p_dictionary);
protected:
void _notification(int p_what);
public:
- void setup(PropertyHint p_hint);
+ void setup(PropertyHint p_hint, const String &p_hint_string = "");
virtual void update_property() override;
virtual bool is_colored(ColorationMode p_mode) override;
EditorPropertyDictionary();
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 644916e83f..b1a91fa3dd 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -593,6 +593,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "docks/filesystem/thumbnail_size", 64, "32,128,16")
_initial_set("docks/filesystem/always_show_folders", true);
_initial_set("docks/filesystem/textfile_extensions", "txt,md,cfg,ini,log,json,yml,yaml,toml,xml");
+ _initial_set("docks/filesystem/other_file_extensions", "ico,icns");
// Property editor
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "docks/property_editor/auto_refresh_interval", 0.2, "0.01,1,0.001"); // Update 5 times per second by default.
@@ -824,7 +825,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
#endif
// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt'
- String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
+ String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3";
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
int default_play_window_pip_mode = 0;
diff --git a/editor/export/export_template_manager.cpp b/editor/export/export_template_manager.cpp
index 2e4f2da9a8..5360b6fb60 100644
--- a/editor/export/export_template_manager.cpp
+++ b/editor/export/export_template_manager.cpp
@@ -571,7 +571,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
unzClose(pkg);
_update_template_status();
- EditorSettings::get_singleton()->set_meta("export_template_download_directory", p_file.get_base_dir());
+ EditorSettings::get_singleton()->set("_export_template_download_directory", p_file.get_base_dir());
return true;
}
@@ -1102,7 +1102,7 @@ ExportTemplateManager::ExportTemplateManager() {
install_file_dialog->set_title(TTR("Select Template File"));
install_file_dialog->set_access(FileDialog::ACCESS_FILESYSTEM);
install_file_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_FILE);
- install_file_dialog->set_current_dir(EditorSettings::get_singleton()->get_meta("export_template_download_directory", ""));
+ install_file_dialog->set_current_dir(EDITOR_DEF("_export_template_download_directory", ""));
install_file_dialog->add_filter("*.tpz", TTR("Godot Export Templates"));
install_file_dialog->connect("file_selected", callable_mp(this, &ExportTemplateManager::_install_file_selected).bind(false));
add_child(install_file_dialog);
diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp
index 03e9fba12d..3ad8ab0b19 100644
--- a/editor/export/project_export.cpp
+++ b/editor/export/project_export.cpp
@@ -903,7 +903,7 @@ bool ProjectExportDialog::_fill_tree(EditorFileSystemDirectory *p_dir, TreeItem
if (p_export_filter == EditorExportPreset::EXPORT_SELECTED_SCENES && type != "PackedScene") {
continue;
}
- if (type == "TextFile") {
+ if (type == "TextFile" || type == "OtherFile") {
continue;
}
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index 99cd2ad526..f7e81b329c 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -271,7 +271,7 @@ bool FileSystemDock::_create_tree(TreeItem *p_parent, EditorFileSystemDirectory
List<FileInfo> file_list;
for (int i = 0; i < p_dir->get_file_count(); i++) {
String file_type = p_dir->get_file_type(i);
- if (file_type != "TextFile" && _is_file_type_disabled_by_feature_profile(file_type)) {
+ if (file_type != "TextFile" && file_type != "OtherFile" && _is_file_type_disabled_by_feature_profile(file_type)) {
// If type is disabled, file won't be displayed.
continue;
}
@@ -3418,6 +3418,11 @@ void FileSystemDock::_file_list_empty_clicked(const Vector2 &p_pos, MouseButton
current_path = current_path_line_edit->get_text();
+ // Favorites isn't a directory so don't show menu.
+ if (current_path == "Favorites") {
+ return;
+ }
+
file_list_popup->clear();
file_list_popup->reset_size();
diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp
index 7aa19509e1..b6aa3c2215 100644
--- a/editor/gui/editor_file_dialog.cpp
+++ b/editor/gui/editor_file_dialog.cpp
@@ -115,6 +115,8 @@ void EditorFileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_file
file_name = ProjectSettings::get_singleton()->localize_path(file_name);
}
}
+ selected_options = p_selected_options;
+
String f = files[0];
if (mode == FILE_MODE_OPEN_FILES) {
emit_signal(SNAME("files_selected"), files);
@@ -146,7 +148,6 @@ void EditorFileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_file
}
file->set_text(f);
dir->set_text(f.get_base_dir());
- selected_options = p_selected_options;
filter->select(p_filter);
}
diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp
index 5372d33b4c..731f682605 100644
--- a/editor/gui/editor_spin_slider.cpp
+++ b/editor/gui/editor_spin_slider.cpp
@@ -665,6 +665,10 @@ bool EditorSpinSlider::is_grabbing() const {
}
void EditorSpinSlider::_focus_entered() {
+ if (is_read_only()) {
+ return;
+ }
+
_ensure_input_popup();
value_input->set_text(get_text_value());
value_input_popup->set_size(get_size());
diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp
index 52ba98b4d5..a7a14de527 100644
--- a/editor/gui/scene_tree_editor.cpp
+++ b/editor/gui/scene_tree_editor.cpp
@@ -174,15 +174,40 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
EditorDockManager::get_singleton()->focus_dock(NodeDock::get_singleton());
NodeDock::get_singleton()->show_groups();
} else if (p_id == BUTTON_UNIQUE) {
- undo_redo->create_action(TTR("Disable Scene Unique Name"));
- undo_redo->add_do_method(n, "set_unique_name_in_owner", false);
- undo_redo->add_undo_method(n, "set_unique_name_in_owner", true);
- undo_redo->add_do_method(this, "_update_tree");
- undo_redo->add_undo_method(this, "_update_tree");
- undo_redo->commit_action();
+ bool ask_before_revoking_unique_name = EDITOR_GET("docks/scene_tree/ask_before_revoking_unique_name");
+ revoke_node = n;
+ if (ask_before_revoking_unique_name) {
+ String msg = vformat(TTR("Revoke unique name for node \"%s\"?"), n->get_name());
+ ask_before_revoke_checkbox->set_pressed(false);
+ revoke_dialog_label->set_text(msg);
+ revoke_dialog->reset_size();
+ revoke_dialog->popup_centered();
+ } else {
+ _revoke_unique_name();
+ }
}
}
+void SceneTreeEditor::_update_ask_before_revoking_unique_name() {
+ if (ask_before_revoke_checkbox->is_pressed()) {
+ EditorSettings::get_singleton()->set("docks/scene_tree/ask_before_revoking_unique_name", false);
+ ask_before_revoke_checkbox->set_pressed(false);
+ }
+
+ _revoke_unique_name();
+}
+
+void SceneTreeEditor::_revoke_unique_name() {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+
+ undo_redo->create_action(TTR("Disable Scene Unique Name"));
+ undo_redo->add_do_method(revoke_node, "set_unique_name_in_owner", false);
+ undo_redo->add_undo_method(revoke_node, "set_unique_name_in_owner", true);
+ undo_redo->add_do_method(this, "_update_tree");
+ undo_redo->add_undo_method(this, "_update_tree");
+ undo_redo->commit_action();
+}
+
void SceneTreeEditor::_toggle_visible(Node *p_node) {
if (p_node->has_method("is_visible") && p_node->has_method("set_visible")) {
bool v = bool(p_node->call("is_visible"));
@@ -491,10 +516,14 @@ void SceneTreeEditor::_update_node_tooltip(Node *p_node, TreeItem *p_item) {
String tooltip = p_node->get_name();
if (p_node == get_scene_node() && p_node->get_scene_inherited_state().is_valid()) {
- p_item->add_button(0, get_editor_theme_icon(SNAME("InstanceOptions")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+ if (p_item->get_button_by_id(0, BUTTON_SUBSCENE) == -1) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("InstanceOptions")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+ }
tooltip += String("\n" + TTR("Inherits:") + " " + p_node->get_scene_inherited_state()->get_path());
} else if (p_node != get_scene_node() && !p_node->get_scene_file_path().is_empty() && can_open_instance) {
- p_item->add_button(0, get_editor_theme_icon(SNAME("InstanceOptions")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+ if (p_item->get_button_by_id(0, BUTTON_SUBSCENE) == -1) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("InstanceOptions")), BUTTON_SUBSCENE, false, TTR("Open in Editor"));
+ }
tooltip += String("\n" + TTR("Instance:") + " " + p_node->get_scene_file_path());
}
@@ -1620,6 +1649,18 @@ SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_ope
update_node_tooltip_delay->set_one_shot(true);
add_child(update_node_tooltip_delay);
+ revoke_dialog = memnew(ConfirmationDialog);
+ revoke_dialog->set_ok_button_text(TTR("Revoke"));
+ add_child(revoke_dialog);
+ revoke_dialog->connect(SceneStringName(confirmed), callable_mp(this, &SceneTreeEditor::_update_ask_before_revoking_unique_name));
+ VBoxContainer *vb = memnew(VBoxContainer);
+ revoke_dialog->add_child(vb);
+ revoke_dialog_label = memnew(Label);
+ vb->add_child(revoke_dialog_label);
+ ask_before_revoke_checkbox = memnew(CheckBox(TTR("Don't Ask Again")));
+ ask_before_revoke_checkbox->set_tooltip_text(TTR("This dialog can also be enabled/disabled in the Editor Settings: Docks > Scene Tree > Ask Before Revoking Unique Name."));
+ vb->add_child(ask_before_revoke_checkbox);
+
script_types = memnew(List<StringName>);
ClassDB::get_inheriters_from_class("Script", script_types);
}
diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h
index b4d9644f16..9633993b8b 100644
--- a/editor/gui/scene_tree_editor.h
+++ b/editor/gui/scene_tree_editor.h
@@ -31,6 +31,7 @@
#ifndef SCENE_TREE_EDITOR_H
#define SCENE_TREE_EDITOR_H
+#include "scene/gui/check_box.h"
#include "scene/gui/check_button.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/tree.h"
@@ -68,6 +69,11 @@ class SceneTreeEditor : public Control {
AcceptDialog *error = nullptr;
AcceptDialog *warning = nullptr;
+ ConfirmationDialog *revoke_dialog = nullptr;
+ Label *revoke_dialog_label = nullptr;
+ CheckBox *ask_before_revoke_checkbox = nullptr;
+ Node *revoke_node = nullptr;
+
bool auto_expand_selected = true;
bool connect_to_script_mode = false;
bool connecting_signal = false;
@@ -144,6 +150,9 @@ class SceneTreeEditor : public Control {
Vector<StringName> valid_types;
+ void _update_ask_before_revoking_unique_name();
+ void _revoke_unique_name();
+
public:
// Public for use with callable_mp.
void _update_tree(bool p_scroll_to_selected = false);
diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp
index 6e41e98360..4b7bf1674f 100644
--- a/editor/plugins/canvas_item_editor_plugin.cpp
+++ b/editor/plugins/canvas_item_editor_plugin.cpp
@@ -4120,7 +4120,8 @@ void CanvasItemEditor::_notification(int p_what) {
}
} break;
- case NOTIFICATION_APPLICATION_FOCUS_OUT: {
+ case NOTIFICATION_APPLICATION_FOCUS_OUT:
+ case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
if (drag_type != DRAG_NONE) {
_reset_drag();
viewport->queue_redraw();
diff --git a/editor/plugins/node_3d_editor_gizmos.cpp b/editor/plugins/node_3d_editor_gizmos.cpp
index d4d918743d..8aff3c9aec 100644
--- a/editor/plugins/node_3d_editor_gizmos.cpp
+++ b/editor/plugins/node_3d_editor_gizmos.cpp
@@ -431,12 +431,13 @@ void EditorNode3DGizmo::add_handles(const Vector<Vector3> &p_handles, const Ref<
colors.resize(p_handles.size());
Color *w = colors.ptrw();
for (int i = 0; i < p_handles.size(); i++) {
+ int id = p_ids.is_empty() ? i : p_ids[i];
+
Color col(1, 1, 1, 1);
- if (is_handle_highlighted(i, p_secondary)) {
+ if (is_handle_highlighted(id, p_secondary)) {
col = Color(0, 0, 1, 0.9);
}
- int id = p_ids.is_empty() ? i : p_ids[i];
if (!is_current_hover_gizmo || current_hover_handle != id || p_secondary != current_hover_handle_secondary) {
col.a = 0.8;
}
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index 8679dc25a4..44673e7224 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -1975,7 +1975,7 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
}
}
- if (_edit.mode != TRANSFORM_NONE) {
+ if (!_edit.instant && _edit.mode != TRANSFORM_NONE) {
Node3D *selected = spatial_editor->get_single_selected_node();
Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected) : nullptr;
@@ -2399,15 +2399,30 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
if (ED_IS_SHORTCUT("spatial_editor/cancel_transform", p_event) && _edit.mode != TRANSFORM_NONE) {
cancel_transform();
}
- if (!is_freelook_active()) {
- if (ED_IS_SHORTCUT("spatial_editor/instant_translate", p_event)) {
- begin_transform(TRANSFORM_TRANSLATE, true);
+ if (!is_freelook_active() && !k->is_echo()) {
+ if (ED_IS_SHORTCUT("spatial_editor/instant_translate", p_event) && _edit.mode != TRANSFORM_TRANSLATE) {
+ if (_edit.mode == TRANSFORM_NONE) {
+ begin_transform(TRANSFORM_TRANSLATE, true);
+ } else if (_edit.instant) {
+ commit_transform();
+ begin_transform(TRANSFORM_TRANSLATE, true);
+ }
}
- if (ED_IS_SHORTCUT("spatial_editor/instant_rotate", p_event)) {
- begin_transform(TRANSFORM_ROTATE, true);
+ if (ED_IS_SHORTCUT("spatial_editor/instant_rotate", p_event) && _edit.mode != TRANSFORM_ROTATE) {
+ if (_edit.mode == TRANSFORM_NONE) {
+ begin_transform(TRANSFORM_ROTATE, true);
+ } else if (_edit.instant) {
+ commit_transform();
+ begin_transform(TRANSFORM_ROTATE, true);
+ }
}
- if (ED_IS_SHORTCUT("spatial_editor/instant_scale", p_event)) {
- begin_transform(TRANSFORM_SCALE, true);
+ if (ED_IS_SHORTCUT("spatial_editor/instant_scale", p_event) && _edit.mode != TRANSFORM_SCALE) {
+ if (_edit.mode == TRANSFORM_NONE) {
+ begin_transform(TRANSFORM_SCALE, true);
+ } else if (_edit.instant) {
+ commit_transform();
+ begin_transform(TRANSFORM_SCALE, true);
+ }
}
}
@@ -3077,8 +3092,11 @@ void Node3DEditorViewport::_notification(int p_what) {
update_preview_node = false;
} break;
+ case NOTIFICATION_APPLICATION_FOCUS_OUT:
case NOTIFICATION_WM_WINDOW_FOCUS_OUT: {
set_freelook_active(false);
+ cursor.region_select = false;
+ surface->queue_redraw();
} break;
case NOTIFICATION_ENTER_TREE: {
@@ -7847,6 +7865,9 @@ void Node3DEditor::_sun_environ_settings_pressed() {
sun_environ_popup->set_position(pos - Vector2(sun_environ_popup->get_contents_minimum_size().width / 2, 0));
sun_environ_popup->reset_size();
sun_environ_popup->popup();
+ // Grabbing the focus is required for Shift modifier checking to be functional
+ // (when the Add sun/environment buttons are pressed).
+ sun_environ_popup->grab_focus();
}
void Node3DEditor::_add_sun_to_scene(bool p_already_added_environment) {
diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index 054239d99f..bf8dab92f8 100644
--- a/editor/plugins/visual_shader_editor_plugin.cpp
+++ b/editor/plugins/visual_shader_editor_plugin.cpp
@@ -2995,8 +2995,8 @@ void VisualShaderEditor::_frame_title_popup_show(const Point2 &p_position, int p
}
frame_title_change_edit->set_text(node->get_title());
frame_title_change_popup->set_meta("id", p_node_id);
- frame_title_change_popup->popup();
frame_title_change_popup->set_position(p_position);
+ frame_title_change_popup->popup();
// Select current text.
frame_title_change_edit->grab_focus();
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index baa77cb41d..ed64ccfa4c 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -2247,8 +2247,7 @@ void SceneTreeDock::_node_reparent(NodePath p_path, bool p_keep_global_xform) {
}
void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, Vector<Node *> p_nodes, bool p_keep_global_xform) {
- Node *new_parent = p_new_parent;
- ERR_FAIL_NULL(new_parent);
+ ERR_FAIL_NULL(p_new_parent);
if (p_nodes.size() == 0) {
return; // Nothing to reparent.
@@ -2288,7 +2287,7 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
// Prevent selecting the hovered node and keep the reparented node(s) selected instead.
hovered_but_reparenting = true;
- Node *validate = new_parent;
+ Node *validate = p_new_parent;
while (validate) {
ERR_FAIL_COND_MSG(p_nodes.has(validate), "Selection changed at some point. Can't reparent.");
validate = validate->get_parent();
@@ -2310,7 +2309,7 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
// No undo implemented for this yet.
Node *node = p_nodes[ni];
- fill_path_renames(node, new_parent, &path_renames);
+ fill_path_renames(node, p_new_parent, &path_renames);
former_names.push_back(node->get_name());
List<Node *> owned;
@@ -2320,7 +2319,7 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
owners.push_back(E);
}
- bool same_parent = new_parent == node->get_parent();
+ bool same_parent = p_new_parent == node->get_parent();
if (same_parent && node->get_index(false) < p_position_in_parent + ni) {
inc--; // If the child will generate a gap when moved, adjust.
}
@@ -2331,17 +2330,17 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
need_edit = select_node_hovered_at_end_of_drag;
} else {
undo_redo->add_do_method(node->get_parent(), "remove_child", node);
- undo_redo->add_do_method(new_parent, "add_child", node, true);
+ undo_redo->add_do_method(p_new_parent, "add_child", node, true);
}
int new_position_in_parent = p_position_in_parent == -1 ? -1 : p_position_in_parent + inc;
if (new_position_in_parent >= 0 || same_parent) {
- undo_redo->add_do_method(new_parent, "move_child", node, new_position_in_parent);
+ undo_redo->add_do_method(p_new_parent, "move_child", node, new_position_in_parent);
}
EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton();
String old_name = former_names[ni];
- String new_name = new_parent->validate_child_name(node);
+ String new_name = p_new_parent->validate_child_name(node);
// Name was modified, fix the path renames.
if (old_name.casecmp_to(new_name) != 0) {
@@ -2366,8 +2365,12 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
}
}
- undo_redo->add_do_method(ed, "live_debug_reparent_node", edited_scene->get_path_to(node), edited_scene->get_path_to(new_parent), new_name, new_position_in_parent);
- undo_redo->add_undo_method(ed, "live_debug_reparent_node", NodePath(String(edited_scene->get_path_to(new_parent)).path_join(new_name)), edited_scene->get_path_to(node->get_parent()), node->get_name(), node->get_index(false));
+ // FIXME: Live editing for "Reparent to New Node" option is broken.
+ // We must get the path to `p_new_parent` *after* it was added to the scene.
+ if (p_new_parent->is_inside_tree()) {
+ undo_redo->add_do_method(ed, "live_debug_reparent_node", edited_scene->get_path_to(node), edited_scene->get_path_to(p_new_parent), new_name, new_position_in_parent);
+ undo_redo->add_undo_method(ed, "live_debug_reparent_node", NodePath(String(edited_scene->get_path_to(p_new_parent)).path_join(new_name)), edited_scene->get_path_to(node->get_parent()), node->get_name(), node->get_index(false));
+ }
if (p_keep_global_xform) {
if (Object::cast_to<Node2D>(node)) {
@@ -2387,7 +2390,7 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
undo_redo->add_do_method(AnimationPlayerEditor::get_singleton()->get_track_editor(), "set_root", node);
}
- undo_redo->add_undo_method(new_parent, "remove_child", node);
+ undo_redo->add_undo_method(p_new_parent, "remove_child", node);
undo_redo->add_undo_method(node, "set_name", former_names[ni]);
inc++;
@@ -2405,7 +2408,7 @@ void SceneTreeDock::_do_reparent(Node *p_new_parent, int p_position_in_parent, V
}
int child_pos = node->get_index(false);
- bool reparented_to_container = Object::cast_to<Container>(new_parent) && Object::cast_to<Control>(node);
+ bool reparented_to_container = Object::cast_to<Container>(p_new_parent) && Object::cast_to<Control>(node);
undo_redo->add_undo_method(node->get_parent(), "add_child", node, true);
undo_redo->add_undo_method(node->get_parent(), "move_child", node, child_pos);
@@ -2785,10 +2788,10 @@ void SceneTreeDock::_selection_changed() {
_update_script_button();
}
-void SceneTreeDock::_do_create(Node *p_parent) {
+Node *SceneTreeDock::_do_create(Node *p_parent) {
Variant c = create_dialog->instantiate_selected();
Node *child = Object::cast_to<Node>(c);
- ERR_FAIL_NULL(child);
+ ERR_FAIL_NULL_V(child, nullptr);
String new_name = p_parent->validate_child_name(child);
if (GLOBAL_GET("editor/naming/node_name_casing").operator int() != NAME_CASING_PASCAL_CASE) {
@@ -2802,8 +2805,6 @@ void SceneTreeDock::_do_create(Node *p_parent) {
if (edited_scene) {
undo_redo->add_do_method(p_parent, "add_child", child, true);
undo_redo->add_do_method(child, "set_owner", edited_scene);
- undo_redo->add_do_method(editor_selection, "clear");
- undo_redo->add_do_method(editor_selection, "add_node", child);
undo_redo->add_do_reference(child);
undo_redo->add_undo_method(p_parent, "remove_child", child);
@@ -2818,28 +2819,34 @@ void SceneTreeDock::_do_create(Node *p_parent) {
undo_redo->add_undo_method(EditorNode::get_singleton(), "set_edited_scene", (Object *)nullptr);
}
+ undo_redo->add_do_method(this, "_post_do_create", child);
undo_redo->commit_action();
- _push_item(c);
+
+ return child;
+}
+
+void SceneTreeDock::_post_do_create(Node *p_child) {
editor_selection->clear();
- editor_selection->add_node(child);
- if (Object::cast_to<Control>(c)) {
- //make editor more comfortable, so some controls don't appear super shrunk
- Control *ct = Object::cast_to<Control>(c);
+ editor_selection->add_node(p_child);
+ _push_item(p_child);
- Size2 ms = ct->get_minimum_size();
+ // Make editor more comfortable, so some controls don't appear super shrunk.
+ Control *control = Object::cast_to<Control>(p_child);
+ if (control) {
+ Size2 ms = control->get_minimum_size();
if (ms.width < 4) {
ms.width = 40;
}
if (ms.height < 4) {
ms.height = 40;
}
- if (ct->is_layout_rtl()) {
- ct->set_position(ct->get_position() - Vector2(ms.x, 0));
+ if (control->is_layout_rtl()) {
+ control->set_position(control->get_position() - Vector2(ms.x, 0));
}
- ct->set_size(ms);
+ control->set_size(ms);
}
- emit_signal(SNAME("node_created"), c);
+ emit_signal(SNAME("node_created"), p_child);
}
void SceneTreeDock::_create() {
@@ -2922,22 +2929,24 @@ void SceneTreeDock::_create() {
}
Node *parent = nullptr;
+ int original_position = -1;
if (only_one_top_node) {
parent = top_node->get_parent();
+ original_position = top_node->get_index();
} else {
parent = top_node->get_parent()->get_parent();
}
- _do_create(parent);
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action_for_history(TTR("Reparent to New Node"), editor_data->get_current_edited_scene_history_id());
+
+ Node *last_created = _do_create(parent);
Vector<Node *> nodes;
for (Node *E : selection) {
nodes.push_back(E);
}
- // This works because editor_selection was cleared and populated with last created node in _do_create()
- Node *last_created = editor_selection->get_selected_node_list().front()->get();
-
if (center_parent) {
// Find parent type and only average positions of relevant nodes.
Node3D *parent_node_3d = Object::cast_to<Node3D>(last_created);
@@ -2976,6 +2985,11 @@ void SceneTreeDock::_create() {
}
_do_reparent(last_created, -1, nodes, true);
+
+ if (only_one_top_node) {
+ undo_redo->add_do_method(parent, "move_child", last_created, original_position);
+ }
+ undo_redo->commit_action();
}
scene_tree->get_scene_tree()->grab_focus();
@@ -4390,6 +4404,7 @@ void SceneTreeDock::_edit_subresource(int p_idx, const PopupMenu *p_from_menu) {
}
void SceneTreeDock::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_post_do_create"), &SceneTreeDock::_post_do_create);
ClassDB::bind_method(D_METHOD("_set_owners"), &SceneTreeDock::_set_owners);
ClassDB::bind_method(D_METHOD("_reparent_nodes_to_root"), &SceneTreeDock::_reparent_nodes_to_root);
ClassDB::bind_method(D_METHOD("_reparent_nodes_to_paths_with_transform_and_name"), &SceneTreeDock::_reparent_nodes_to_paths_with_transform_and_name);
@@ -4655,6 +4670,7 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec
EDITOR_DEF("interface/editors/show_scene_tree_root_selection", true);
EDITOR_DEF("interface/editors/derive_script_globals_by_name", true);
EDITOR_DEF("docks/scene_tree/ask_before_deleting_related_animation_tracks", true);
+ EDITOR_DEF("docks/scene_tree/ask_before_revoking_unique_name", true);
EDITOR_DEF("_use_favorites_root_selection", false);
Resource::_update_configuration_warning = _update_configuration_warning;
diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h
index f01e6598cf..1807ec5896 100644
--- a/editor/scene_tree_dock.h
+++ b/editor/scene_tree_dock.h
@@ -179,7 +179,8 @@ class SceneTreeDock : public VBoxContainer {
bool first_enter = true;
void _create();
- void _do_create(Node *p_parent);
+ Node *_do_create(Node *p_parent);
+ void _post_do_create(Node *p_child);
Node *scene_root = nullptr;
Node *edited_scene = nullptr;
Node *pending_click_select = nullptr;
diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp
index f8673ddd44..0cb4952b04 100644
--- a/editor/script_create_dialog.cpp
+++ b/editor/script_create_dialog.cpp
@@ -111,15 +111,7 @@ static Vector<String> _get_hierarchy(const String &p_class_name) {
void ScriptCreateDialog::_notification(int p_what) {
switch (p_what) {
- case NOTIFICATION_ENTER_TREE:
- case NOTIFICATION_THEME_CHANGED: {
- for (int i = 0; i < ScriptServer::get_language_count(); i++) {
- Ref<Texture2D> language_icon = get_editor_theme_icon(ScriptServer::get_language(i)->get_type());
- if (language_icon.is_valid()) {
- language_menu->set_item_icon(i, language_icon);
- }
- }
-
+ case NOTIFICATION_ENTER_TREE: {
String last_language = EditorSettings::get_singleton()->get_project_metadata("script_setup", "last_selected_language", "");
if (!last_language.is_empty()) {
for (int i = 0; i < language_menu->get_item_count(); i++) {
@@ -131,9 +123,16 @@ void ScriptCreateDialog::_notification(int p_what) {
} else {
language_menu->select(default_language);
}
- if (EditorSettings::get_singleton()->has_meta("script_setup_use_script_templates")) {
- is_using_templates = bool(EditorSettings::get_singleton()->get_meta("script_setup_use_script_templates"));
- use_templates->set_pressed(is_using_templates);
+ is_using_templates = EDITOR_DEF("_script_setup_use_script_templates", false);
+ use_templates->set_pressed(is_using_templates);
+ } break;
+
+ case NOTIFICATION_THEME_CHANGED: {
+ for (int i = 0; i < ScriptServer::get_language_count(); i++) {
+ Ref<Texture2D> language_icon = get_editor_theme_icon(ScriptServer::get_language(i)->get_type());
+ if (language_icon.is_valid()) {
+ language_menu->set_item_icon(i, language_icon);
+ }
}
path_button->set_icon(get_editor_theme_icon(SNAME("Folder")));
@@ -297,12 +296,9 @@ void ScriptCreateDialog::_template_changed(int p_template) {
EditorSettings::get_singleton()->set_project_metadata("script_setup", "templates_dictionary", dic_templates_project);
} else {
// Save template info to editor dictionary (not a project template).
- Dictionary dic_templates;
- if (EditorSettings::get_singleton()->has_meta("script_setup_templates_dictionary")) {
- dic_templates = (Dictionary)EditorSettings::get_singleton()->get_meta("script_setup_templates_dictionary");
- }
+ Dictionary dic_templates = EDITOR_GET("_script_setup_templates_dictionary");
dic_templates[parent_name->get_text()] = sinfo.get_hash();
- EditorSettings::get_singleton()->set_meta("script_setup_templates_dictionary", dic_templates);
+ EditorSettings::get_singleton()->set("_script_setup_templates_dictionary", dic_templates);
// Remove template from project dictionary as we last used an editor level template.
Dictionary dic_templates_project = EditorSettings::get_singleton()->get_project_metadata("script_setup", "templates_dictionary", Dictionary());
if (dic_templates_project.has(parent_name->get_text())) {
@@ -415,7 +411,7 @@ void ScriptCreateDialog::_built_in_pressed() {
void ScriptCreateDialog::_use_template_pressed() {
is_using_templates = use_templates->is_pressed();
- EditorSettings::get_singleton()->set_meta("script_setup_use_script_templates", is_using_templates);
+ EditorSettings::get_singleton()->set("_script_setup_use_script_templates", is_using_templates);
validation_panel->update();
}
@@ -509,10 +505,7 @@ void ScriptCreateDialog::_update_template_menu() {
if (is_language_using_templates) {
// Get the latest templates used for each type of node from project settings then global settings.
Dictionary last_local_templates = EditorSettings::get_singleton()->get_project_metadata("script_setup", "templates_dictionary", Dictionary());
- Dictionary last_global_templates;
- if (EditorSettings::get_singleton()->has_meta("script_setup_templates_dictionary")) {
- last_global_templates = (Dictionary)EditorSettings::get_singleton()->get_meta("script_setup_templates_dictionary");
- }
+ Dictionary last_global_templates = EDITOR_GET("_script_setup_templates_dictionary");
String inherits_base_type = parent_name->get_text();
// If it inherits from a script, get its parent class first.
@@ -825,6 +818,8 @@ void ScriptCreateDialog::_bind_methods() {
}
ScriptCreateDialog::ScriptCreateDialog() {
+ EDITOR_DEF("_script_setup_templates_dictionary", Dictionary());
+
/* Main Controls */
GridContainer *gc = memnew(GridContainer);
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 7517bc3cb9..119508f59b 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -1507,7 +1507,9 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the
p_theme->set_constant("buttons_vertical_separation", "SpinBox", 0);
p_theme->set_constant("field_and_buttons_separation", "SpinBox", 2);
p_theme->set_constant("buttons_width", "SpinBox", 16);
+#ifndef DISABLE_DEPRECATED
p_theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1);
+#endif
}
// ProgressBar.
diff --git a/main/main.cpp b/main/main.cpp
index db5e6ec398..1af08bc036 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -3548,13 +3548,16 @@ int Main::start() {
gdscript_docs_path = E->next()->get();
#endif
} else if (E->get() == "--export-release") {
+ ERR_FAIL_COND_V_MSG(!editor && !found_project, EXIT_FAILURE, "Please provide a valid project path when exporting, aborting.");
editor = true; //needs editor
_export_preset = E->next()->get();
} else if (E->get() == "--export-debug") {
+ ERR_FAIL_COND_V_MSG(!editor && !found_project, EXIT_FAILURE, "Please provide a valid project path when exporting, aborting.");
editor = true; //needs editor
_export_preset = E->next()->get();
export_debug = true;
} else if (E->get() == "--export-pack") {
+ ERR_FAIL_COND_V_MSG(!editor && !found_project, EXIT_FAILURE, "Please provide a valid project path when exporting, aborting.");
editor = true;
_export_preset = E->next()->get();
export_pack_only = true;
@@ -3566,6 +3569,8 @@ int Main::start() {
if (parsed_pair) {
E = E->next();
}
+ } else if (E->get().begins_with("--export-")) {
+ ERR_FAIL_V_MSG(EXIT_FAILURE, "Missing export preset name, aborting.");
}
#ifdef TOOLS_ENABLED
// Handle case where no path is given to --doctool.
@@ -4409,7 +4414,7 @@ bool Main::iteration() {
}
#ifdef TOOLS_ENABLED
- if (wait_for_import && EditorFileSystem::get_singleton()->doing_first_scan()) {
+ if (wait_for_import && EditorFileSystem::get_singleton() && EditorFileSystem::get_singleton()->doing_first_scan()) {
exit = false;
}
#endif
diff --git a/misc/dist/macos_template.app/Contents/Info.plist b/misc/dist/macos_template.app/Contents/Info.plist
index 78bb559c0e..708498dd9c 100644
--- a/misc/dist/macos_template.app/Contents/Info.plist
+++ b/misc/dist/macos_template.app/Contents/Info.plist
@@ -49,12 +49,17 @@ $usage_descriptions
<string>NSApplication</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.$app_category</string>
- <key>LSMinimumSystemVersion</key>
- <string>$min_version</string>
+ <key>LSArchitecturePriority</key>
+ <array>
+ <string>arm64</string>
+ <string>x86_64</string>
+ </array>
<key>LSMinimumSystemVersionByArchitecture</key>
<dict>
+ <key>arm64</key>
+ <string>$min_version_arm64</string>
<key>x86_64</key>
- <string>$min_version</string>
+ <string>$min_version_x86_64</string>
</dict>
<key>NSHighResolutionCapable</key>
$highres
diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected
index 882f96a0dc..ad77e30e11 100644
--- a/misc/extension_api_validation/4.3-stable.expected
+++ b/misc/extension_api_validation/4.3-stable.expected
@@ -73,3 +73,10 @@ Validate extension JSON: Error: Field 'classes/EditorInterface/methods/popup_nod
Validate extension JSON: Error: Field 'classes/EditorInterface/methods/popup_property_selector/arguments': size changed value in new API, from 3 to 4.
Added optional argument to popup_property_selector and popup_node_selector to specify the current value.
+
+
+GH-94434
+--------
+Validate extension JSON: Error: Field 'classes/OS/methods/execute_with_pipe/arguments': size changed value in new API, from 2 to 3.
+
+Optional argument added. Compatibility method registered.
diff --git a/modules/betsy/image_compress_betsy.cpp b/modules/betsy/image_compress_betsy.cpp
index c17651da45..bc72203b2f 100644
--- a/modules/betsy/image_compress_betsy.cpp
+++ b/modules/betsy/image_compress_betsy.cpp
@@ -125,6 +125,7 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) {
}
if (err != OK) {
+ compute_shader->print_errors("Betsy compress shader");
memdelete(rd);
if (rcd != nullptr) {
memdelete(rcd);
diff --git a/modules/gdscript/editor/gdscript_docgen.cpp b/modules/gdscript/editor/gdscript_docgen.cpp
index 35b69fab8c..32ef429b0d 100644
--- a/modules/gdscript/editor/gdscript_docgen.cpp
+++ b/modules/gdscript/editor/gdscript_docgen.cpp
@@ -84,6 +84,15 @@ void GDScriptDocGen::_doctype_from_gdtype(const GDType &p_gdtype, String &r_type
return;
}
}
+ if (p_gdtype.builtin_type == Variant::DICTIONARY && p_gdtype.has_container_element_types()) {
+ String key, value;
+ _doctype_from_gdtype(p_gdtype.get_container_element_type_or_variant(0), key, r_enum);
+ _doctype_from_gdtype(p_gdtype.get_container_element_type_or_variant(1), value, r_enum);
+ if (key != "Variant" || value != "Variant") {
+ r_type = "Dictionary[" + key + ", " + value + "]";
+ return;
+ }
+ }
r_type = Variant::get_type_name(p_gdtype.builtin_type);
return;
case GDType::NATIVE:
@@ -155,34 +164,82 @@ String GDScriptDocGen::_docvalue_from_variant(const Variant &p_variant, int p_re
return "<Object>";
case Variant::DICTIONARY: {
const Dictionary dict = p_variant;
+ String result;
- if (dict.is_empty()) {
- return "{}";
- }
+ if (dict.is_typed()) {
+ result += "Dictionary[";
+
+ Ref<Script> key_script = dict.get_typed_key_script();
+ if (key_script.is_valid()) {
+ if (key_script->get_global_name() != StringName()) {
+ result += key_script->get_global_name();
+ } else if (!key_script->get_path().get_file().is_empty()) {
+ result += key_script->get_path().get_file();
+ } else {
+ result += dict.get_typed_key_class_name();
+ }
+ } else if (dict.get_typed_key_class_name() != StringName()) {
+ result += dict.get_typed_key_class_name();
+ } else if (dict.is_typed_key()) {
+ result += Variant::get_type_name((Variant::Type)dict.get_typed_key_builtin());
+ } else {
+ result += "Variant";
+ }
+
+ result += ", ";
- if (p_recursion_level > MAX_RECURSION_LEVEL) {
- return "{...}";
+ Ref<Script> value_script = dict.get_typed_value_script();
+ if (value_script.is_valid()) {
+ if (value_script->get_global_name() != StringName()) {
+ result += value_script->get_global_name();
+ } else if (!value_script->get_path().get_file().is_empty()) {
+ result += value_script->get_path().get_file();
+ } else {
+ result += dict.get_typed_value_class_name();
+ }
+ } else if (dict.get_typed_value_class_name() != StringName()) {
+ result += dict.get_typed_value_class_name();
+ } else if (dict.is_typed_value()) {
+ result += Variant::get_type_name((Variant::Type)dict.get_typed_value_builtin());
+ } else {
+ result += "Variant";
+ }
+
+ result += "](";
}
- List<Variant> keys;
- dict.get_key_list(&keys);
- keys.sort();
+ if (dict.is_empty()) {
+ result += "{}";
+ } else if (p_recursion_level > MAX_RECURSION_LEVEL) {
+ result += "{...}";
+ } else {
+ result += "{";
+
+ List<Variant> keys;
+ dict.get_key_list(&keys);
+ keys.sort();
- String data;
- for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
- if (E->prev()) {
- data += ", ";
+ for (List<Variant>::Element *E = keys.front(); E; E = E->next()) {
+ if (E->prev()) {
+ result += ", ";
+ }
+ result += _docvalue_from_variant(E->get(), p_recursion_level + 1) + ": " + _docvalue_from_variant(dict[E->get()], p_recursion_level + 1);
}
- data += _docvalue_from_variant(E->get(), p_recursion_level + 1) + ": " + _docvalue_from_variant(dict[E->get()], p_recursion_level + 1);
+
+ result += "}";
+ }
+
+ if (dict.is_typed()) {
+ result += ")";
}
- return "{" + data + "}";
+ return result;
} break;
case Variant::ARRAY: {
const Array array = p_variant;
String result;
- if (array.get_typed_builtin() != Variant::NIL) {
+ if (array.is_typed()) {
result += "Array[";
Ref<Script> script = array.get_typed_script();
@@ -209,16 +266,18 @@ String GDScriptDocGen::_docvalue_from_variant(const Variant &p_variant, int p_re
result += "[...]";
} else {
result += "[";
+
for (int i = 0; i < array.size(); i++) {
if (i > 0) {
result += ", ";
}
result += _docvalue_from_variant(array[i], p_recursion_level + 1);
}
+
result += "]";
}
- if (array.get_typed_builtin() != Variant::NIL) {
+ if (array.is_typed()) {
result += ")";
}
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index e3f2a61090..276a12f5de 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -138,7 +138,7 @@ void GDScript::_super_implicit_constructor(GDScript *p_script, GDScriptInstance
}
}
ERR_FAIL_NULL(p_script->implicit_initializer);
- if (likely(valid)) {
+ if (likely(p_script->valid)) {
p_script->implicit_initializer->call(p_instance, nullptr, 0, r_error);
} else {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD;
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 7e29a9c0fe..7f0d5005cb 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -724,6 +724,18 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::Type
result.set_container_element_type(0, container_type);
}
}
+ if (result.builtin_type == Variant::DICTIONARY) {
+ GDScriptParser::DataType key_type = type_from_metatype(resolve_datatype(p_type->get_container_type_or_null(0)));
+ if (key_type.kind != GDScriptParser::DataType::VARIANT) {
+ key_type.is_constant = false;
+ result.set_container_element_type(0, key_type);
+ }
+ GDScriptParser::DataType value_type = type_from_metatype(resolve_datatype(p_type->get_container_type_or_null(1)));
+ if (value_type.kind != GDScriptParser::DataType::VARIANT) {
+ value_type.is_constant = false;
+ result.set_container_element_type(1, value_type);
+ }
+ }
} else if (class_exists(first)) {
// Native engine classes.
result.kind = GDScriptParser::DataType::NATIVE;
@@ -884,11 +896,16 @@ GDScriptParser::DataType GDScriptAnalyzer::resolve_datatype(GDScriptParser::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);
+ push_error(R"(Typed arrays require exactly one collection element type.)", p_type);
+ return bad_type;
+ }
+ } else if (result.builtin_type == Variant::DICTIONARY) {
+ if (p_type->container_types.size() != 2) {
+ push_error(R"(Typed dictionaries require exactly two collection element types.)", p_type);
return bad_type;
}
} else {
- push_error("Only arrays can specify collection element types.", p_type);
+ push_error(R"(Only arrays and dictionaries can specify collection element types.)", p_type);
return bad_type;
}
}
@@ -1926,6 +1943,11 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi
if (has_specified_type && specified_type.has_container_element_type(0)) {
update_array_literal_element_type(array, specified_type.get_container_element_type(0));
}
+ } else if (p_assignable->initializer->type == GDScriptParser::Node::DICTIONARY) {
+ GDScriptParser::DictionaryNode *dictionary = static_cast<GDScriptParser::DictionaryNode *>(p_assignable->initializer);
+ if (has_specified_type && specified_type.has_container_element_types()) {
+ update_dictionary_literal_element_type(dictionary, specified_type.get_container_element_type_or_variant(0), specified_type.get_container_element_type_or_variant(1));
+ }
}
if (is_constant && !p_assignable->initializer->is_constant) {
@@ -1987,7 +2009,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(0) && !initializer_type.has_container_element_type(0)) {
+ } else if ((specified_type.has_container_element_type(0) && !initializer_type.has_container_element_type(0)) || (specified_type.has_container_element_type(1) && !initializer_type.has_container_element_type(1))) {
mark_node_unsafe(p_assignable->initializer);
#ifdef DEBUG_ENABLED
} else if (specified_type.builtin_type == Variant::INT && initializer_type.builtin_type == Variant::FLOAT) {
@@ -2229,8 +2251,12 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) {
} else if (!is_type_compatible(specified_type, variable_type)) {
p_for->use_conversion_assign = true;
}
- if (p_for->list && p_for->list->type == GDScriptParser::Node::ARRAY) {
- update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_for->list), specified_type);
+ if (p_for->list) {
+ if (p_for->list->type == GDScriptParser::Node::ARRAY) {
+ update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_for->list), specified_type);
+ } else if (p_for->list->type == GDScriptParser::Node::DICTIONARY) {
+ update_dictionary_literal_element_type(static_cast<GDScriptParser::DictionaryNode *>(p_for->list), specified_type, GDScriptParser::DataType::get_variant_type());
+ }
}
}
p_for->variable->set_datatype(specified_type);
@@ -2432,6 +2458,9 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) {
} else {
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));
+ } else if (p_return->return_value->type == GDScriptParser::Node::DICTIONARY && has_expected_type && expected_type.has_container_element_types()) {
+ update_dictionary_literal_element_type(static_cast<GDScriptParser::DictionaryNode *>(p_return->return_value),
+ expected_type.get_container_element_type_or_variant(0), expected_type.get_container_element_type_or_variant(1));
}
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");
@@ -2678,6 +2707,54 @@ void GDScriptAnalyzer::update_array_literal_element_type(GDScriptParser::ArrayNo
p_array->set_datatype(array_type);
}
+// When a dictionary literal is stored (or passed as function argument) to a typed context, we then assume the dictionary is typed.
+// This function determines which type is that (if any).
+void GDScriptAnalyzer::update_dictionary_literal_element_type(GDScriptParser::DictionaryNode *p_dictionary, const GDScriptParser::DataType &p_key_element_type, const GDScriptParser::DataType &p_value_element_type) {
+ GDScriptParser::DataType expected_key_type = p_key_element_type;
+ GDScriptParser::DataType expected_value_type = p_value_element_type;
+ expected_key_type.container_element_types.clear(); // Nested types (like `Dictionary[String, Array[int]]`) are not currently supported.
+ expected_value_type.container_element_types.clear();
+
+ for (int i = 0; i < p_dictionary->elements.size(); i++) {
+ GDScriptParser::ExpressionNode *key_element_node = p_dictionary->elements[i].key;
+ if (key_element_node->is_constant) {
+ update_const_expression_builtin_type(key_element_node, expected_key_type, "include");
+ }
+ const GDScriptParser::DataType &actual_key_type = key_element_node->get_datatype();
+ if (actual_key_type.has_no_type() || actual_key_type.is_variant() || !actual_key_type.is_hard_type()) {
+ mark_node_unsafe(key_element_node);
+ } else if (!is_type_compatible(expected_key_type, actual_key_type, true, p_dictionary)) {
+ if (is_type_compatible(actual_key_type, expected_key_type)) {
+ mark_node_unsafe(key_element_node);
+ } else {
+ push_error(vformat(R"(Cannot have a key of type "%s" in a dictionary of type "Dictionary[%s, %s]".)", actual_key_type.to_string(), expected_key_type.to_string(), expected_value_type.to_string()), key_element_node);
+ return;
+ }
+ }
+
+ GDScriptParser::ExpressionNode *value_element_node = p_dictionary->elements[i].value;
+ if (value_element_node->is_constant) {
+ update_const_expression_builtin_type(value_element_node, expected_value_type, "include");
+ }
+ const GDScriptParser::DataType &actual_value_type = value_element_node->get_datatype();
+ if (actual_value_type.has_no_type() || actual_value_type.is_variant() || !actual_value_type.is_hard_type()) {
+ mark_node_unsafe(value_element_node);
+ } else if (!is_type_compatible(expected_value_type, actual_value_type, true, p_dictionary)) {
+ if (is_type_compatible(actual_value_type, expected_value_type)) {
+ mark_node_unsafe(value_element_node);
+ } else {
+ push_error(vformat(R"(Cannot have a value of type "%s" in a dictionary of type "Dictionary[%s, %s]".)", actual_value_type.to_string(), expected_key_type.to_string(), expected_value_type.to_string()), value_element_node);
+ return;
+ }
+ }
+ }
+
+ GDScriptParser::DataType dictionary_type = p_dictionary->get_datatype();
+ dictionary_type.set_container_element_type(0, expected_key_type);
+ dictionary_type.set_container_element_type(1, expected_value_type);
+ p_dictionary->set_datatype(dictionary_type);
+}
+
void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assignment) {
reduce_expression(p_assignment->assigned_value);
@@ -2770,9 +2847,12 @@ 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.
+ // Check if assigned value is an array/dictionary literal, so we can make it a typed container too if appropriate.
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));
+ } else if (p_assignment->assigned_value->type == GDScriptParser::Node::DICTIONARY && assignee_type.is_hard_type() && assignee_type.has_container_element_types()) {
+ update_dictionary_literal_element_type(static_cast<GDScriptParser::DictionaryNode *>(p_assignment->assigned_value),
+ assignee_type.get_container_element_type_or_variant(0), assignee_type.get_container_element_type_or_variant(1));
}
if (p_assignment->operation == GDScriptParser::AssignmentNode::OP_NONE && assignee_type.is_hard_type() && p_assignment->assigned_value->is_constant) {
@@ -2850,8 +2930,8 @@ 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(0) && !op_type.has_container_element_type(0)) {
- // typed array assignee and untyped array result
+ } else if ((assignee_type.has_container_element_type(0) && !op_type.has_container_element_type(0)) || (assignee_type.has_container_element_type(1) && !op_type.has_container_element_type(1))) {
+ // Typed assignee and untyped result.
mark_node_unsafe(p_assignment);
}
}
@@ -3049,10 +3129,13 @@ const char *check_for_renamed_identifier(String identifier, GDScriptParser::Node
void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_await, bool p_is_root) {
bool all_is_constant = true;
HashMap<int, GDScriptParser::ArrayNode *> arrays; // For array literal to potentially type when passing.
+ HashMap<int, GDScriptParser::DictionaryNode *> dictionaries; // Same, but for dictionaries.
for (int i = 0; i < p_call->arguments.size(); i++) {
reduce_expression(p_call->arguments[i]);
if (p_call->arguments[i]->type == GDScriptParser::Node::ARRAY) {
arrays[i] = static_cast<GDScriptParser::ArrayNode *>(p_call->arguments[i]);
+ } else if (p_call->arguments[i]->type == GDScriptParser::Node::DICTIONARY) {
+ dictionaries[i] = static_cast<GDScriptParser::DictionaryNode *>(p_call->arguments[i]);
}
all_is_constant = all_is_constant && p_call->arguments[i]->is_constant;
}
@@ -3245,6 +3328,26 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call);
}
}
+
+#ifdef DEBUG_ENABLED
+ // Consider `Signal(self, "my_signal")` as an implicit use of the signal.
+ if (builtin_type == Variant::SIGNAL && p_call->arguments.size() >= 2) {
+ const GDScriptParser::ExpressionNode *object_arg = p_call->arguments[0];
+ if (object_arg && object_arg->type == GDScriptParser::Node::SELF) {
+ const GDScriptParser::ExpressionNode *signal_arg = p_call->arguments[1];
+ if (signal_arg && signal_arg->is_constant) {
+ const StringName &signal_name = signal_arg->reduced_value;
+ if (parser->current_class->has_member(signal_name)) {
+ const GDScriptParser::ClassNode::Member &member = parser->current_class->get_member(signal_name);
+ if (member.type == GDScriptParser::ClassNode::Member::SIGNAL) {
+ member.signal->usages++;
+ }
+ }
+ }
+ }
+ }
+#endif
+
p_call->set_datatype(call_type);
return;
} else if (GDScriptUtilityFunctions::function_exists(function_name)) {
@@ -3437,6 +3540,14 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
update_array_literal_element_type(E.value, par_types.get(index).get_container_element_type(0));
}
}
+ for (const KeyValue<int, GDScriptParser::DictionaryNode *> &E : dictionaries) {
+ int index = E.key;
+ if (index < par_types.size() && par_types.get(index).is_hard_type() && par_types.get(index).has_container_element_types()) {
+ GDScriptParser::DataType key = par_types.get(index).get_container_element_type_or_variant(0);
+ GDScriptParser::DataType value = par_types.get(index).get_container_element_type_or_variant(1);
+ update_dictionary_literal_element_type(E.value, key, value);
+ }
+ }
validate_call_arg(par_types, default_arg_count, method_flags.has_flag(METHOD_FLAG_VARARG), p_call);
if (base_type.kind == GDScriptParser::DataType::ENUM && base_type.is_meta_type) {
@@ -3479,6 +3590,20 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
parser->push_warning(p_call, GDScriptWarning::STATIC_CALLED_ON_INSTANCE, p_call->function_name, caller_type);
}
+
+ // Consider `emit_signal()`, `connect()`, and `disconnect()` as implicit uses of the signal.
+ if (is_self && (p_call->function_name == SNAME("emit_signal") || p_call->function_name == SNAME("connect") || p_call->function_name == SNAME("disconnect")) && !p_call->arguments.is_empty()) {
+ const GDScriptParser::ExpressionNode *signal_arg = p_call->arguments[0];
+ if (signal_arg && signal_arg->is_constant) {
+ const StringName &signal_name = signal_arg->reduced_value;
+ if (parser->current_class->has_member(signal_name)) {
+ const GDScriptParser::ClassNode::Member &member = parser->current_class->get_member(signal_name);
+ if (member.type == GDScriptParser::ClassNode::Member::SIGNAL) {
+ member.signal->usages++;
+ }
+ }
+ }
+ }
#endif // DEBUG_ENABLED
call_type = return_type;
@@ -3567,6 +3692,11 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) {
update_array_literal_element_type(static_cast<GDScriptParser::ArrayNode *>(p_cast->operand), cast_type.get_container_element_type(0));
}
+ if (p_cast->operand->type == GDScriptParser::Node::DICTIONARY && cast_type.has_container_element_types()) {
+ update_dictionary_literal_element_type(static_cast<GDScriptParser::DictionaryNode *>(p_cast->operand),
+ cast_type.get_container_element_type_or_variant(0), cast_type.get_container_element_type_or_variant(1));
+ }
+
if (!cast_type.is_variant()) {
GDScriptParser::DataType op_type = p_cast->operand->get_datatype();
if (op_type.is_variant() || !op_type.is_hard_type()) {
@@ -4591,10 +4721,23 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
reduce_identifier_from_base(p_subscript->attribute, &base_type);
GDScriptParser::DataType attr_type = p_subscript->attribute->get_datatype();
if (attr_type.is_set()) {
- valid = !attr_type.is_pseudo_type || p_can_be_pseudo_type;
- result_type = attr_type;
- p_subscript->is_constant = p_subscript->attribute->is_constant;
- p_subscript->reduced_value = p_subscript->attribute->reduced_value;
+ if (base_type.builtin_type == Variant::DICTIONARY && base_type.has_container_element_types()) {
+ Variant::Type key_type = base_type.get_container_element_type_or_variant(0).builtin_type;
+ valid = key_type == Variant::NIL || key_type == Variant::STRING || key_type == Variant::STRING_NAME;
+ if (base_type.has_container_element_type(1)) {
+ result_type = base_type.get_container_element_type(1);
+ result_type.type_source = base_type.type_source;
+ } else {
+ result_type.builtin_type = Variant::NIL;
+ result_type.kind = GDScriptParser::DataType::VARIANT;
+ result_type.type_source = GDScriptParser::DataType::UNDETECTED;
+ }
+ } else {
+ valid = !attr_type.is_pseudo_type || p_can_be_pseudo_type;
+ result_type = attr_type;
+ p_subscript->is_constant = p_subscript->attribute->is_constant;
+ p_subscript->reduced_value = p_subscript->attribute->reduced_value;
+ }
} else if (!base_type.is_meta_type || !base_type.is_constant) {
valid = base_type.kind != GDScriptParser::DataType::BUILTIN;
#ifdef DEBUG_ENABLED
@@ -4701,8 +4844,40 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
case Variant::SIGNAL:
case Variant::STRING_NAME:
break;
- // Here for completeness.
+ // Support depends on if the dictionary has a typed key, otherwise anything is valid.
case Variant::DICTIONARY:
+ if (base_type.has_container_element_type(0)) {
+ GDScriptParser::DataType key_type = base_type.get_container_element_type(0);
+ switch (index_type.builtin_type) {
+ // Null value will be treated as an empty object, allow.
+ case Variant::NIL:
+ error = key_type.builtin_type != Variant::OBJECT;
+ break;
+ // Objects are parsed for validity in a similar manner to container types.
+ case Variant::OBJECT:
+ if (key_type.builtin_type == Variant::OBJECT) {
+ error = !key_type.can_reference(index_type);
+ } else {
+ error = key_type.builtin_type != Variant::NIL;
+ }
+ break;
+ // String and StringName interchangeable in this context.
+ case Variant::STRING:
+ case Variant::STRING_NAME:
+ error = key_type.builtin_type != Variant::STRING_NAME && key_type.builtin_type != Variant::STRING;
+ break;
+ // Ints are valid indices for floats, but not the other way around.
+ case Variant::INT:
+ error = key_type.builtin_type != Variant::INT && key_type.builtin_type != Variant::FLOAT;
+ break;
+ // All other cases require the types to match exactly.
+ default:
+ error = key_type.builtin_type != index_type.builtin_type;
+ break;
+ }
+ }
+ break;
+ // Here for completeness.
case Variant::VARIANT_MAX:
break;
}
@@ -4791,7 +4966,6 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
case Variant::PROJECTION:
case Variant::PLANE:
case Variant::COLOR:
- case Variant::DICTIONARY:
case Variant::OBJECT:
result_type.kind = GDScriptParser::DataType::VARIANT;
result_type.type_source = GDScriptParser::DataType::UNDETECTED;
@@ -4806,6 +4980,16 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri
result_type.type_source = GDScriptParser::DataType::UNDETECTED;
}
break;
+ // Can have two element types, but we only care about the value.
+ case Variant::DICTIONARY:
+ if (base_type.has_container_element_type(1)) {
+ result_type = base_type.get_container_element_type(1);
+ result_type.type_source = base_type.type_source;
+ } else {
+ result_type.kind = GDScriptParser::DataType::VARIANT;
+ result_type.type_source = GDScriptParser::DataType::UNDETECTED;
+ }
+ break;
// Here for completeness.
case Variant::VARIANT_MAX:
break;
@@ -4985,7 +5169,9 @@ Variant GDScriptAnalyzer::make_array_reduced_value(GDScriptParser::ArrayNode *p_
}
Variant GDScriptAnalyzer::make_dictionary_reduced_value(GDScriptParser::DictionaryNode *p_dictionary, bool &is_reduced) {
- Dictionary dictionary;
+ Dictionary dictionary = p_dictionary->get_datatype().has_container_element_types()
+ ? make_dictionary_from_element_datatype(p_dictionary->get_datatype().get_container_element_type_or_variant(0), p_dictionary->get_datatype().get_container_element_type_or_variant(1))
+ : Dictionary();
for (int i = 0; i < p_dictionary->elements.size(); i++) {
const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i];
@@ -5072,6 +5258,49 @@ Array GDScriptAnalyzer::make_array_from_element_datatype(const GDScriptParser::D
return array;
}
+Dictionary GDScriptAnalyzer::make_dictionary_from_element_datatype(const GDScriptParser::DataType &p_key_element_datatype, const GDScriptParser::DataType &p_value_element_datatype, const GDScriptParser::Node *p_source_node) {
+ Dictionary dictionary;
+ StringName key_name;
+ Variant key_script;
+ StringName value_name;
+ Variant value_script;
+
+ if (p_key_element_datatype.builtin_type == Variant::OBJECT) {
+ Ref<Script> script_type = p_key_element_datatype.script_type;
+ if (p_key_element_datatype.kind == GDScriptParser::DataType::CLASS && script_type.is_null()) {
+ Error err = OK;
+ Ref<GDScript> scr = get_depended_shallow_script(p_key_element_datatype.script_path, err);
+ if (err) {
+ push_error(vformat(R"(Error while getting cache for script "%s".)", p_key_element_datatype.script_path), p_source_node);
+ return dictionary;
+ }
+ script_type.reference_ptr(scr->find_class(p_key_element_datatype.class_type->fqcn));
+ }
+
+ key_name = p_key_element_datatype.native_type;
+ key_script = script_type;
+ }
+
+ if (p_value_element_datatype.builtin_type == Variant::OBJECT) {
+ Ref<Script> script_type = p_value_element_datatype.script_type;
+ if (p_value_element_datatype.kind == GDScriptParser::DataType::CLASS && script_type.is_null()) {
+ Error err = OK;
+ Ref<GDScript> scr = get_depended_shallow_script(p_value_element_datatype.script_path, err);
+ if (err) {
+ push_error(vformat(R"(Error while getting cache for script "%s".)", p_value_element_datatype.script_path), p_source_node);
+ return dictionary;
+ }
+ script_type.reference_ptr(scr->find_class(p_value_element_datatype.class_type->fqcn));
+ }
+
+ value_name = p_value_element_datatype.native_type;
+ value_script = script_type;
+ }
+
+ dictionary.set_typed(p_key_element_datatype.builtin_type, key_name, key_script, p_value_element_datatype.builtin_type, value_name, value_script);
+ return dictionary;
+}
+
Variant GDScriptAnalyzer::make_variable_default_value(GDScriptParser::VariableNode *p_variable) {
Variant result = Variant();
@@ -5087,6 +5316,10 @@ Variant GDScriptAnalyzer::make_variable_default_value(GDScriptParser::VariableNo
if (datatype.kind == GDScriptParser::DataType::BUILTIN && datatype.builtin_type != Variant::OBJECT) {
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 if (datatype.builtin_type == Variant::DICTIONARY && datatype.has_container_element_types()) {
+ GDScriptParser::DataType key = datatype.get_container_element_type_or_variant(0);
+ GDScriptParser::DataType value = datatype.get_container_element_type_or_variant(1);
+ result = make_dictionary_from_element_datatype(key, value);
} else {
VariantInternal::initialize(&result, datatype.builtin_type);
}
@@ -5115,6 +5348,22 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_variant(const Variant &p_va
} else if (array.get_typed_builtin() != Variant::NIL) {
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::DICTIONARY) {
+ const Dictionary &dict = p_value;
+ if (dict.get_typed_key_script()) {
+ result.set_container_element_type(0, type_from_metatype(make_script_meta_type(dict.get_typed_key_script())));
+ } else if (dict.get_typed_key_class_name()) {
+ result.set_container_element_type(0, type_from_metatype(make_native_meta_type(dict.get_typed_key_class_name())));
+ } else if (dict.get_typed_key_builtin() != Variant::NIL) {
+ result.set_container_element_type(0, type_from_metatype(make_builtin_meta_type((Variant::Type)dict.get_typed_key_builtin())));
+ }
+ if (dict.get_typed_value_script()) {
+ result.set_container_element_type(1, type_from_metatype(make_script_meta_type(dict.get_typed_value_script())));
+ } else if (dict.get_typed_value_class_name()) {
+ result.set_container_element_type(1, type_from_metatype(make_native_meta_type(dict.get_typed_value_class_name())));
+ } else if (dict.get_typed_value_builtin() != Variant::NIL) {
+ result.set_container_element_type(1, type_from_metatype(make_builtin_meta_type((Variant::Type)dict.get_typed_value_builtin())));
+ }
} else if (p_value.get_type() == Variant::OBJECT) {
// Object is treated as a native type, not a builtin type.
result.kind = GDScriptParser::DataType::NATIVE;
@@ -5247,6 +5496,60 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
}
elem_type.is_constant = false;
result.set_container_element_type(0, elem_type);
+ } else if (p_property.type == Variant::DICTIONARY && p_property.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ // Check element type.
+ StringName key_elem_type_name = p_property.hint_string.get_slice(";", 0);
+ GDScriptParser::DataType key_elem_type;
+ key_elem_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+
+ Variant::Type key_elem_builtin_type = GDScriptParser::get_builtin_type(key_elem_type_name);
+ if (key_elem_builtin_type < Variant::VARIANT_MAX) {
+ // Builtin type.
+ key_elem_type.kind = GDScriptParser::DataType::BUILTIN;
+ key_elem_type.builtin_type = key_elem_builtin_type;
+ } else if (class_exists(key_elem_type_name)) {
+ key_elem_type.kind = GDScriptParser::DataType::NATIVE;
+ key_elem_type.builtin_type = Variant::OBJECT;
+ key_elem_type.native_type = key_elem_type_name;
+ } else if (ScriptServer::is_global_class(key_elem_type_name)) {
+ // Just load this as it shouldn't be a GDScript.
+ Ref<Script> script = ResourceLoader::load(ScriptServer::get_global_class_path(key_elem_type_name));
+ key_elem_type.kind = GDScriptParser::DataType::SCRIPT;
+ key_elem_type.builtin_type = Variant::OBJECT;
+ key_elem_type.native_type = script->get_instance_base_type();
+ key_elem_type.script_type = script;
+ } else {
+ ERR_FAIL_V_MSG(result, "Could not find element type from property hint of a typed dictionary.");
+ }
+ key_elem_type.is_constant = false;
+
+ StringName value_elem_type_name = p_property.hint_string.get_slice(";", 1);
+ GDScriptParser::DataType value_elem_type;
+ value_elem_type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+
+ Variant::Type value_elem_builtin_type = GDScriptParser::get_builtin_type(value_elem_type_name);
+ if (value_elem_builtin_type < Variant::VARIANT_MAX) {
+ // Builtin type.
+ value_elem_type.kind = GDScriptParser::DataType::BUILTIN;
+ value_elem_type.builtin_type = value_elem_builtin_type;
+ } else if (class_exists(value_elem_type_name)) {
+ value_elem_type.kind = GDScriptParser::DataType::NATIVE;
+ value_elem_type.builtin_type = Variant::OBJECT;
+ value_elem_type.native_type = value_elem_type_name;
+ } else if (ScriptServer::is_global_class(value_elem_type_name)) {
+ // Just load this as it shouldn't be a GDScript.
+ Ref<Script> script = ResourceLoader::load(ScriptServer::get_global_class_path(value_elem_type_name));
+ value_elem_type.kind = GDScriptParser::DataType::SCRIPT;
+ value_elem_type.builtin_type = Variant::OBJECT;
+ value_elem_type.native_type = script->get_instance_base_type();
+ value_elem_type.script_type = script;
+ } else {
+ ERR_FAIL_V_MSG(result, "Could not find element type from property hint of a typed dictionary.");
+ }
+ value_elem_type.is_constant = false;
+
+ result.set_container_element_type(0, key_elem_type);
+ result.set_container_element_type(1, value_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()) {
@@ -5667,6 +5970,15 @@ bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType &
valid = p_target.get_container_element_type(0) == p_source.get_container_element_type(0);
}
}
+ if (valid && p_target.builtin_type == Variant::DICTIONARY && p_source.builtin_type == Variant::DICTIONARY) {
+ // Check the element types.
+ 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);
+ }
+ if (valid && p_target.has_container_element_type(1) && p_source.has_container_element_type(1)) {
+ valid = p_target.get_container_element_type(1) == p_source.get_container_element_type(1);
+ }
+ }
return valid;
}
diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h
index 25e5aa9a2c..3b781409a4 100644
--- a/modules/gdscript/gdscript_analyzer.h
+++ b/modules/gdscript/gdscript_analyzer.h
@@ -125,6 +125,7 @@ class GDScriptAnalyzer {
// Helpers.
Array make_array_from_element_datatype(const GDScriptParser::DataType &p_element_datatype, const GDScriptParser::Node *p_source_node = nullptr);
+ Dictionary make_dictionary_from_element_datatype(const GDScriptParser::DataType &p_key_element_datatype, const GDScriptParser::DataType &p_value_element_datatype, const GDScriptParser::Node *p_source_node = nullptr);
GDScriptParser::DataType type_from_variant(const Variant &p_value, const GDScriptParser::Node *p_source);
static GDScriptParser::DataType type_from_metatype(const GDScriptParser::DataType &p_meta_type);
GDScriptParser::DataType type_from_property(const PropertyInfo &p_property, bool p_is_arg = false, bool p_is_readonly = false) const;
@@ -137,6 +138,7 @@ class GDScriptAnalyzer {
GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, bool &r_valid, const GDScriptParser::Node *p_source);
void update_const_expression_builtin_type(GDScriptParser::ExpressionNode *p_expression, const GDScriptParser::DataType &p_type, const char *p_usage, bool p_is_cast = false);
void update_array_literal_element_type(GDScriptParser::ArrayNode *p_array, const GDScriptParser::DataType &p_element_type);
+ void update_dictionary_literal_element_type(GDScriptParser::DictionaryNode *p_dictionary, const GDScriptParser::DataType &p_key_element_type, const GDScriptParser::DataType &p_value_element_type);
bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr);
void push_error(const String &p_message, const GDScriptParser::Node *p_origin = nullptr);
void mark_node_unsafe(const GDScriptParser::Node *p_node);
diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp
index 4cda3d3037..b77c641eb5 100644
--- a/modules/gdscript/gdscript_byte_codegen.cpp
+++ b/modules/gdscript/gdscript_byte_codegen.cpp
@@ -634,6 +634,18 @@ void GDScriptByteCodeGenerator::write_type_test(const Address &p_target, const A
append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
append(element_type.builtin_type);
append(element_type.native_type);
+ } else if (p_type.builtin_type == Variant::DICTIONARY && p_type.has_container_element_types()) {
+ const GDScriptDataType &key_element_type = p_type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_element_type = p_type.get_container_element_type_or_variant(1);
+ append_opcode(GDScriptFunction::OPCODE_TYPE_TEST_DICTIONARY);
+ append(p_target);
+ append(p_source);
+ append(get_constant_pos(key_element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(value_element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(key_element_type.builtin_type);
+ append(key_element_type.native_type);
+ append(value_element_type.builtin_type);
+ append(value_element_type.native_type);
} else {
append_opcode(GDScriptFunction::OPCODE_TYPE_TEST_BUILTIN);
append(p_target);
@@ -889,6 +901,18 @@ void GDScriptByteCodeGenerator::write_assign_with_conversion(const Address &p_ta
append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
append(element_type.builtin_type);
append(element_type.native_type);
+ } else if (p_target.type.builtin_type == Variant::DICTIONARY && p_target.type.has_container_element_types()) {
+ const GDScriptDataType &key_type = p_target.type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_type = p_target.type.get_container_element_type_or_variant(1);
+ append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_DICTIONARY);
+ append(p_target);
+ append(p_source);
+ append(get_constant_pos(key_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(value_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(key_type.builtin_type);
+ append(key_type.native_type);
+ append(value_type.builtin_type);
+ append(value_type.native_type);
} else {
append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_BUILTIN);
append(p_target);
@@ -935,6 +959,18 @@ void GDScriptByteCodeGenerator::write_assign(const Address &p_target, const Addr
append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
append(element_type.builtin_type);
append(element_type.native_type);
+ } else if (p_target.type.kind == GDScriptDataType::BUILTIN && p_target.type.builtin_type == Variant::DICTIONARY && p_target.type.has_container_element_types()) {
+ const GDScriptDataType &key_type = p_target.type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_type = p_target.type.get_container_element_type_or_variant(1);
+ append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_DICTIONARY);
+ append(p_target);
+ append(p_source);
+ append(get_constant_pos(key_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(value_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(key_type.builtin_type);
+ append(key_type.native_type);
+ append(value_type.builtin_type);
+ append(value_type.native_type);
} else if (p_target.type.kind == GDScriptDataType::BUILTIN && p_source.type.kind == GDScriptDataType::BUILTIN && p_target.type.builtin_type != p_source.type.builtin_type) {
// Need conversion.
append_opcode(GDScriptFunction::OPCODE_ASSIGN_TYPED_BUILTIN);
@@ -1434,6 +1470,23 @@ void GDScriptByteCodeGenerator::write_construct_dictionary(const Address &p_targ
ct.cleanup();
}
+void GDScriptByteCodeGenerator::write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector<Address> &p_arguments) {
+ append_opcode_and_argcount(GDScriptFunction::OPCODE_CONSTRUCT_TYPED_DICTIONARY, 3 + p_arguments.size());
+ for (int i = 0; i < p_arguments.size(); i++) {
+ append(p_arguments[i]);
+ }
+ CallTarget ct = get_call_target(p_target);
+ append(ct.target);
+ append(get_constant_pos(p_key_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(p_value_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(p_arguments.size() / 2); // This is number of key-value pairs, so only half of actual arguments.
+ append(p_key_type.builtin_type);
+ append(p_key_type.native_type);
+ append(p_value_type.builtin_type);
+ append(p_value_type.native_type);
+ ct.cleanup();
+}
+
void GDScriptByteCodeGenerator::write_await(const Address &p_target, const Address &p_operand) {
append_opcode(GDScriptFunction::OPCODE_AWAIT);
append(p_operand);
@@ -1711,6 +1764,19 @@ void GDScriptByteCodeGenerator::write_return(const Address &p_return_value) {
append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
append(element_type.builtin_type);
append(element_type.native_type);
+ } else if (function->return_type.kind == GDScriptDataType::BUILTIN && function->return_type.builtin_type == Variant::DICTIONARY &&
+ function->return_type.has_container_element_types()) {
+ // Typed dictionary.
+ const GDScriptDataType &key_type = function->return_type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_type = function->return_type.get_container_element_type_or_variant(1);
+ append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_DICTIONARY);
+ append(p_return_value);
+ append(get_constant_pos(key_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(value_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(key_type.builtin_type);
+ append(key_type.native_type);
+ append(value_type.builtin_type);
+ append(value_type.native_type);
} else if (function->return_type.kind == GDScriptDataType::BUILTIN && p_return_value.type.kind == GDScriptDataType::BUILTIN && function->return_type.builtin_type != p_return_value.type.builtin_type) {
// Add conversion.
append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_BUILTIN);
@@ -1735,6 +1801,17 @@ void GDScriptByteCodeGenerator::write_return(const Address &p_return_value) {
append(get_constant_pos(element_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
append(element_type.builtin_type);
append(element_type.native_type);
+ } else if (function->return_type.builtin_type == Variant::DICTIONARY && function->return_type.has_container_element_types()) {
+ const GDScriptDataType &key_type = function->return_type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_type = function->return_type.get_container_element_type_or_variant(1);
+ append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_DICTIONARY);
+ append(p_return_value);
+ append(get_constant_pos(key_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(get_constant_pos(value_type.script_type) | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS));
+ append(key_type.builtin_type);
+ append(key_type.native_type);
+ append(value_type.builtin_type);
+ append(value_type.native_type);
} else {
append_opcode(GDScriptFunction::OPCODE_RETURN_TYPED_BUILTIN);
append(p_return_value);
@@ -1803,6 +1880,13 @@ void GDScriptByteCodeGenerator::clear_address(const Address &p_address) {
case Variant::BOOL:
write_assign_false(p_address);
break;
+ case Variant::DICTIONARY:
+ if (p_address.type.has_container_element_types()) {
+ write_construct_typed_dictionary(p_address, p_address.type.get_container_element_type_or_variant(0), p_address.type.get_container_element_type_or_variant(1), Vector<GDScriptCodeGenerator::Address>());
+ } else {
+ write_construct(p_address, p_address.type.builtin_type, Vector<GDScriptCodeGenerator::Address>());
+ }
+ break;
case Variant::ARRAY:
if (p_address.type.has_container_element_type(0)) {
write_construct_typed_array(p_address, p_address.type.get_container_element_type(0), Vector<GDScriptCodeGenerator::Address>());
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index 34f56a2f5c..6303db71fd 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -529,6 +529,7 @@ public:
virtual void write_construct_array(const Address &p_target, const Vector<Address> &p_arguments) override;
virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector<Address> &p_arguments) override;
virtual void write_construct_dictionary(const Address &p_target, const Vector<Address> &p_arguments) override;
+ virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector<Address> &p_arguments) override;
virtual void write_await(const Address &p_target, const Address &p_operand) override;
virtual void write_if(const Address &p_condition) override;
virtual void write_else() override;
diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h
index c1c0b61395..f3c4acf1c3 100644
--- a/modules/gdscript/gdscript_codegen.h
+++ b/modules/gdscript/gdscript_codegen.h
@@ -142,6 +142,7 @@ public:
virtual void write_construct_array(const Address &p_target, const Vector<Address> &p_arguments) = 0;
virtual void write_construct_typed_array(const Address &p_target, const GDScriptDataType &p_element_type, const Vector<Address> &p_arguments) = 0;
virtual void write_construct_dictionary(const Address &p_target, const Vector<Address> &p_arguments) = 0;
+ virtual void write_construct_typed_dictionary(const Address &p_target, const GDScriptDataType &p_key_type, const GDScriptDataType &p_value_type, const Vector<Address> &p_arguments) = 0;
virtual void write_await(const Address &p_target, const Address &p_operand) = 0;
virtual void write_if(const Address &p_condition) = 0;
virtual void write_else() = 0;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index d8b44a558f..eebf282633 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -532,10 +532,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
Vector<GDScriptCodeGenerator::Address> elements;
// Create the result temporary first since it's the last to be killed.
- GDScriptDataType dict_type;
- dict_type.has_type = true;
- dict_type.kind = GDScriptDataType::BUILTIN;
- dict_type.builtin_type = Variant::DICTIONARY;
+ GDScriptDataType dict_type = _gdtype_from_datatype(dn->get_datatype(), codegen.script);
GDScriptCodeGenerator::Address result = codegen.add_temporary(dict_type);
for (int i = 0; i < dn->elements.size(); i++) {
@@ -566,7 +563,11 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
elements.push_back(element);
}
- gen->write_construct_dictionary(result, elements);
+ if (dict_type.has_container_element_types()) {
+ gen->write_construct_typed_dictionary(result, dict_type.get_container_element_type_or_variant(0), dict_type.get_container_element_type_or_variant(1), elements);
+ } else {
+ gen->write_construct_dictionary(result, elements);
+ }
for (int i = 0; i < elements.size(); i++) {
if (elements[i].mode == GDScriptCodeGenerator::Address::TEMPORARY) {
@@ -2325,8 +2326,11 @@ 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(0)) {
+ if (field_type.builtin_type == Variant::ARRAY && 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.builtin_type == Variant::DICTIONARY && field_type.has_container_element_types()) {
+ codegen.generator->write_construct_typed_dictionary(dst_address, field_type.get_container_element_type_or_variant(0),
+ field_type.get_container_element_type_or_variant(1), Vector<GDScriptCodeGenerator::Address>());
} else if (field_type.kind == GDScriptDataType::BUILTIN) {
codegen.generator->write_construct(dst_address, field_type.builtin_type, Vector<GDScriptCodeGenerator::Address>());
}
@@ -2515,11 +2519,17 @@ 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(0)) {
+ if (field_type.builtin_type == Variant::ARRAY && 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(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.builtin_type == Variant::DICTIONARY && field_type.has_container_element_types()) {
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ codegen.generator->write_construct_typed_dictionary(temp, field_type.get_container_element_type_or_variant(0),
+ field_type.get_container_element_type_or_variant(1), 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) {
GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
codegen.generator->write_construct(temp, field_type.builtin_type, Vector<GDScriptCodeGenerator::Address>());
diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp
index 0331045078..bc063693a3 100644
--- a/modules/gdscript/gdscript_disassembler.cpp
+++ b/modules/gdscript/gdscript_disassembler.cpp
@@ -176,6 +176,47 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr += 6;
} break;
+ case OPCODE_TYPE_TEST_DICTIONARY: {
+ text += "type test ";
+ text += DADDR(1);
+ text += " = ";
+ text += DADDR(2);
+ text += " is Dictionary[";
+
+ Ref<Script> key_script_type = get_constant(_code_ptr[ip + 3] & ADDR_MASK);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + 5];
+ StringName key_native_type = get_global_name(_code_ptr[ip + 6]);
+
+ if (key_script_type.is_valid() && key_script_type->is_valid()) {
+ text += "script(";
+ text += GDScript::debug_get_script_name(key_script_type);
+ text += ")";
+ } else if (key_native_type != StringName()) {
+ text += key_native_type;
+ } else {
+ text += Variant::get_type_name(key_builtin_type);
+ }
+
+ text += ", ";
+
+ Ref<Script> value_script_type = get_constant(_code_ptr[ip + 4] & ADDR_MASK);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + 7];
+ StringName value_native_type = get_global_name(_code_ptr[ip + 8]);
+
+ if (value_script_type.is_valid() && value_script_type->is_valid()) {
+ text += "script(";
+ text += GDScript::debug_get_script_name(value_script_type);
+ text += ")";
+ } else if (value_native_type != StringName()) {
+ text += value_native_type;
+ } else {
+ text += Variant::get_type_name(value_builtin_type);
+ }
+
+ text += "]";
+
+ incr += 9;
+ } break;
case OPCODE_TYPE_TEST_NATIVE: {
text += "type test ";
text += DADDR(1);
@@ -399,6 +440,14 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr += 6;
} break;
+ case OPCODE_ASSIGN_TYPED_DICTIONARY: {
+ text += "assign typed dictionary ";
+ text += DADDR(1);
+ text += " = ";
+ text += DADDR(2);
+
+ incr += 9;
+ } break;
case OPCODE_ASSIGN_TYPED_NATIVE: {
text += "assign typed native (";
text += DADDR(3);
@@ -564,6 +613,58 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr += 3 + argc * 2;
} break;
+ case OPCODE_CONSTRUCT_TYPED_DICTIONARY: {
+ int instr_var_args = _code_ptr[++ip];
+ int argc = _code_ptr[ip + 1 + instr_var_args];
+
+ Ref<Script> key_script_type = get_constant(_code_ptr[ip + argc * 2 + 2] & ADDR_MASK);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + argc * 2 + 5];
+ StringName key_native_type = get_global_name(_code_ptr[ip + argc * 2 + 6]);
+
+ String key_type_name;
+ if (key_script_type.is_valid() && key_script_type->is_valid()) {
+ key_type_name = "script(" + GDScript::debug_get_script_name(key_script_type) + ")";
+ } else if (key_native_type != StringName()) {
+ key_type_name = key_native_type;
+ } else {
+ key_type_name = Variant::get_type_name(key_builtin_type);
+ }
+
+ Ref<Script> value_script_type = get_constant(_code_ptr[ip + argc * 2 + 3] & ADDR_MASK);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + argc * 2 + 7];
+ StringName value_native_type = get_global_name(_code_ptr[ip + argc * 2 + 8]);
+
+ String value_type_name;
+ if (value_script_type.is_valid() && value_script_type->is_valid()) {
+ value_type_name = "script(" + GDScript::debug_get_script_name(value_script_type) + ")";
+ } else if (value_native_type != StringName()) {
+ value_type_name = value_native_type;
+ } else {
+ value_type_name = Variant::get_type_name(value_builtin_type);
+ }
+
+ text += "make_typed_dict (";
+ text += key_type_name;
+ text += ", ";
+ text += value_type_name;
+ text += ") ";
+
+ text += DADDR(1 + argc * 2);
+ text += " = {";
+
+ for (int i = 0; i < argc; i++) {
+ if (i > 0) {
+ text += ", ";
+ }
+ text += DADDR(1 + i * 2 + 0);
+ text += ": ";
+ text += DADDR(1 + i * 2 + 1);
+ }
+
+ text += "}";
+
+ incr += 9 + argc * 2;
+ } break;
case OPCODE_CALL:
case OPCODE_CALL_RETURN:
case OPCODE_CALL_ASYNC: {
@@ -978,6 +1079,12 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const {
incr += 5;
} break;
+ case OPCODE_RETURN_TYPED_DICTIONARY: {
+ text += "return typed dictionary ";
+ text += DADDR(1);
+
+ incr += 8;
+ } break;
case OPCODE_RETURN_TYPED_NATIVE: {
text += "return typed native (";
text += DADDR(2);
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index f7eda11c8e..524f528f76 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -697,6 +697,10 @@ static String _get_visual_datatype(const PropertyInfo &p_info, bool p_is_arg, co
return _trim_parent_class(class_name, p_base_class);
} else if (p_info.type == Variant::ARRAY && p_info.hint == PROPERTY_HINT_ARRAY_TYPE && !p_info.hint_string.is_empty()) {
return "Array[" + _trim_parent_class(p_info.hint_string, p_base_class) + "]";
+ } else if (p_info.type == Variant::DICTIONARY && p_info.hint == PROPERTY_HINT_DICTIONARY_TYPE && !p_info.hint_string.is_empty()) {
+ const String key = p_info.hint_string.get_slice(";", 0);
+ const String value = p_info.hint_string.get_slice(";", 1);
+ return "Dictionary[" + _trim_parent_class(key, p_base_class) + ", " + _trim_parent_class(value, p_base_class) + "]";
} else if (p_info.type == Variant::NIL) {
if (p_is_arg || (p_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT)) {
return "Variant";
@@ -2112,7 +2116,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
// Look in blocks first.
int last_assign_line = -1;
const GDScriptParser::ExpressionNode *last_assigned_expression = nullptr;
- GDScriptParser::DataType id_type;
+ GDScriptCompletionIdentifier id_type;
GDScriptParser::SuiteNode *suite = p_context.current_suite;
bool is_function_parameter = false;
@@ -2134,7 +2138,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
if (can_be_local && suite && suite->has_local(p_identifier->name)) {
const GDScriptParser::SuiteNode::Local &local = suite->get_local(p_identifier->name);
- id_type = local.get_datatype();
+ id_type.type = local.get_datatype();
// Check initializer as the first assignment.
switch (local.type) {
@@ -2172,7 +2176,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
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;
+ id_type = base_identifier;
}
}
}
@@ -2212,7 +2216,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
c.current_line = type_test->operand->start_line;
c.current_suite = suite;
if (type_test->test_datatype.is_hard_type()) {
- id_type = type_test->test_datatype;
+ id_type.type = type_test->test_datatype;
if (last_assign_line < c.current_line) {
// Override last assignment.
last_assign_line = c.current_line;
@@ -2230,10 +2234,10 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
c.current_line = last_assign_line;
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)) {
+ if (id_type.type.is_set() && assigned_type.type.is_set() && !GDScriptAnalyzer::check_type_compatibility(id_type.type, assigned_type.type)) {
// The assigned type is incompatible. The annotated type takes priority.
+ r_type = id_type;
r_type.assigned_expression = last_assigned_expression;
- r_type.type = id_type;
} else {
r_type = assigned_type;
}
@@ -2251,8 +2255,8 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
GDScriptParser::FunctionNode *parent_function = base_type.class_type->get_member(p_context.current_function->identifier->name).function;
if (parent_function->parameters_indices.has(p_identifier->name)) {
const GDScriptParser::ParameterNode *parameter = parent_function->parameters[parent_function->parameters_indices[p_identifier->name]];
- if ((!id_type.is_set() || id_type.is_variant()) && parameter->get_datatype().is_hard_type()) {
- id_type = parameter->get_datatype();
+ if ((!id_type.type.is_set() || id_type.type.is_variant()) && parameter->get_datatype().is_hard_type()) {
+ id_type.type = parameter->get_datatype();
}
if (parameter->initializer) {
GDScriptParser::CompletionContext c = p_context;
@@ -2268,7 +2272,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
base_type = base_type.class_type->base_type;
break;
case GDScriptParser::DataType::NATIVE: {
- if (id_type.is_set() && !id_type.is_variant()) {
+ if (id_type.type.is_set() && !id_type.type.is_variant()) {
base_type = GDScriptParser::DataType();
break;
}
@@ -2289,8 +2293,8 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
}
}
- if (id_type.is_set() && !id_type.is_variant()) {
- r_type.type = id_type;
+ if (id_type.type.is_set() && !id_type.type.is_variant()) {
+ r_type = id_type;
return true;
}
diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h
index ac4bab6d84..6433072b55 100644
--- a/modules/gdscript/gdscript_function.h
+++ b/modules/gdscript/gdscript_function.h
@@ -93,6 +93,41 @@ public:
} else {
valid = false;
}
+ } else if (valid && builtin_type == Variant::DICTIONARY && has_container_element_types()) {
+ Dictionary dictionary = p_variant;
+ if (dictionary.is_typed()) {
+ if (dictionary.is_typed_key()) {
+ GDScriptDataType key = get_container_element_type_or_variant(0);
+ Variant::Type key_builtin_type = (Variant::Type)dictionary.get_typed_key_builtin();
+ StringName key_native_type = dictionary.get_typed_key_class_name();
+ Ref<Script> key_script_type_ref = dictionary.get_typed_key_script();
+
+ if (key_script_type_ref.is_valid()) {
+ valid = (key.kind == SCRIPT || key.kind == GDSCRIPT) && key.script_type == key_script_type_ref.ptr();
+ } else if (key_native_type != StringName()) {
+ valid = key.kind == NATIVE && key.native_type == key_native_type;
+ } else {
+ valid = key.kind == BUILTIN && key.builtin_type == key_builtin_type;
+ }
+ }
+
+ if (valid && dictionary.is_typed_value()) {
+ GDScriptDataType value = get_container_element_type_or_variant(1);
+ Variant::Type value_builtin_type = (Variant::Type)dictionary.get_typed_value_builtin();
+ StringName value_native_type = dictionary.get_typed_value_class_name();
+ Ref<Script> value_script_type_ref = dictionary.get_typed_value_script();
+
+ if (value_script_type_ref.is_valid()) {
+ valid = (value.kind == SCRIPT || value.kind == GDSCRIPT) && value.script_type == value_script_type_ref.ptr();
+ } else if (value_native_type != StringName()) {
+ valid = value.kind == NATIVE && value.native_type == value_native_type;
+ } else {
+ valid = value.kind == BUILTIN && value.builtin_type == value_builtin_type;
+ }
+ }
+ } else {
+ valid = false;
+ }
} else if (!valid && p_allow_implicit_conversion) {
valid = Variant::can_convert_strict(var_type, builtin_type);
}
@@ -156,6 +191,10 @@ public:
}
return true;
case Variant::DICTIONARY:
+ if (has_container_element_types()) {
+ return get_container_element_type_or_variant(0).can_contain_object() || get_container_element_type_or_variant(1).can_contain_object();
+ }
+ return true;
case Variant::NIL:
case Variant::OBJECT:
return true;
@@ -220,6 +259,7 @@ public:
OPCODE_OPERATOR_VALIDATED,
OPCODE_TYPE_TEST_BUILTIN,
OPCODE_TYPE_TEST_ARRAY,
+ OPCODE_TYPE_TEST_DICTIONARY,
OPCODE_TYPE_TEST_NATIVE,
OPCODE_TYPE_TEST_SCRIPT,
OPCODE_SET_KEYED,
@@ -242,6 +282,7 @@ public:
OPCODE_ASSIGN_FALSE,
OPCODE_ASSIGN_TYPED_BUILTIN,
OPCODE_ASSIGN_TYPED_ARRAY,
+ OPCODE_ASSIGN_TYPED_DICTIONARY,
OPCODE_ASSIGN_TYPED_NATIVE,
OPCODE_ASSIGN_TYPED_SCRIPT,
OPCODE_CAST_TO_BUILTIN,
@@ -252,6 +293,7 @@ public:
OPCODE_CONSTRUCT_ARRAY,
OPCODE_CONSTRUCT_TYPED_ARRAY,
OPCODE_CONSTRUCT_DICTIONARY,
+ OPCODE_CONSTRUCT_TYPED_DICTIONARY,
OPCODE_CALL,
OPCODE_CALL_RETURN,
OPCODE_CALL_ASYNC,
@@ -280,6 +322,7 @@ public:
OPCODE_RETURN,
OPCODE_RETURN_TYPED_BUILTIN,
OPCODE_RETURN_TYPED_ARRAY,
+ OPCODE_RETURN_TYPED_DICTIONARY,
OPCODE_RETURN_TYPED_NATIVE,
OPCODE_RETURN_TYPED_SCRIPT,
OPCODE_ITERATE_BEGIN,
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index b6db6a940b..65aa150be3 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -249,7 +249,7 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet
if (!for_completion) {
return;
}
- if (completion_context.node != p_for_node) {
+ if (p_for_node == nullptr || completion_context.node != p_for_node) {
return;
}
CompletionContext context;
@@ -264,8 +264,8 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet
completion_context = context;
}
-void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node, int p_argument) {
- if (!for_completion) {
+void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node, int p_argument, bool p_force) {
+ if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) {
return;
}
if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) {
@@ -283,8 +283,8 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node
completion_context = context;
}
-void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Type p_builtin_type) {
- if (!for_completion) {
+void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force) {
+ if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) {
return;
}
if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) {
@@ -2471,7 +2471,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr
}
// Completion can appear whenever an expression is expected.
- make_completion_context(COMPLETION_IDENTIFIER, nullptr);
+ make_completion_context(COMPLETION_IDENTIFIER, nullptr, -1, false);
GDScriptTokenizer::Token token = current;
GDScriptTokenizer::Token::Type token_type = token.type;
@@ -2488,8 +2488,17 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr
advance(); // Only consume the token if there's a valid rule.
+ // After a token was consumed, update the completion context regardless of a previously set context.
+
ExpressionNode *previous_operand = (this->*prefix_rule)(nullptr, p_can_assign);
+#ifdef TOOLS_ENABLED
+ // HACK: We can't create a context in parse_identifier since it is used in places were we don't want completion.
+ if (previous_operand != nullptr && previous_operand->type == GDScriptParser::Node::IDENTIFIER && prefix_rule == static_cast<ParseFunction>(&GDScriptParser::parse_identifier)) {
+ make_completion_context(COMPLETION_IDENTIFIER, previous_operand);
+ }
+#endif
+
while (p_precedence <= get_rule(current.type)->precedence) {
if (previous_operand == nullptr || (p_stop_on_assign && current.type == GDScriptTokenizer::Token::EQUAL) || lambda_ended) {
return previous_operand;
@@ -2924,6 +2933,11 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode
}
assignment->assignee = p_previous_operand;
assignment->assigned_value = parse_expression(false);
+#ifdef TOOLS_ENABLED
+ if (assignment->assigned_value != nullptr && assignment->assigned_value->type == GDScriptParser::Node::IDENTIFIER) {
+ override_completion_context(assignment->assigned_value, COMPLETION_ASSIGN, assignment);
+ }
+#endif
if (assignment->assigned_value == nullptr) {
push_error(R"(Expected an expression after "=".)");
}
@@ -3554,7 +3568,7 @@ GDScriptParser::TypeNode *GDScriptParser::parse_type(bool p_allow_void) {
type->type_chain.push_back(type_element);
if (match(GDScriptTokenizer::Token::BRACKET_OPEN)) {
- // Typed collection (like Array[int]).
+ // Typed collection (like Array[int], Dictionary[String, int]).
bool first_pass = true;
do {
TypeNode *container_type = parse_type(false); // Don't allow void for element type.
@@ -4371,6 +4385,14 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
export_type.type_source = variable->datatype.type_source;
}
+ bool is_dict = false;
+ if (export_type.builtin_type == Variant::DICTIONARY && export_type.has_container_element_types()) {
+ is_dict = true;
+ DataType inner_type = export_type.get_container_element_type_or_variant(1);
+ export_type = export_type.get_container_element_type_or_variant(0);
+ export_type.set_container_element_type(0, inner_type); // Store earlier extracted value within key to separately parse after.
+ }
+
bool use_default_variable_type_check = true;
if (p_annotation->name == SNAME("@export_range")) {
@@ -4398,8 +4420,13 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
}
if (export_type.is_variant() || export_type.has_no_type()) {
- push_error(R"(Cannot use simple "@export" annotation because the type of the initialized value can't be inferred.)", p_annotation);
- return false;
+ if (is_dict) {
+ // Dictionary allowed to have a variant key/value.
+ export_type.kind = GDScriptParser::DataType::BUILTIN;
+ } else {
+ push_error(R"(Cannot use simple "@export" annotation because the type of the initialized value can't be inferred.)", p_annotation);
+ return false;
+ }
}
switch (export_type.kind) {
@@ -4459,6 +4486,90 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), p_annotation);
return false;
}
+
+ if (is_dict) {
+ String key_prefix = itos(variable->export_info.type);
+ if (variable->export_info.hint) {
+ key_prefix += "/" + itos(variable->export_info.hint);
+ }
+ key_prefix += ":" + variable->export_info.hint_string;
+
+ // Now parse value.
+ export_type = export_type.get_container_element_type(0);
+
+ if (export_type.is_variant() || export_type.has_no_type()) {
+ export_type.kind = GDScriptParser::DataType::BUILTIN;
+ }
+ switch (export_type.kind) {
+ case GDScriptParser::DataType::BUILTIN:
+ variable->export_info.type = export_type.builtin_type;
+ variable->export_info.hint = PROPERTY_HINT_NONE;
+ variable->export_info.hint_string = String();
+ break;
+ case GDScriptParser::DataType::NATIVE:
+ case GDScriptParser::DataType::SCRIPT:
+ case GDScriptParser::DataType::CLASS: {
+ const StringName class_name = _find_narrowest_native_or_global_class(export_type);
+ if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) {
+ variable->export_info.type = Variant::OBJECT;
+ variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE;
+ variable->export_info.hint_string = class_name;
+ } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) {
+ variable->export_info.type = Variant::OBJECT;
+ variable->export_info.hint = PROPERTY_HINT_NODE_TYPE;
+ variable->export_info.hint_string = class_name;
+ } else {
+ push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation);
+ return false;
+ }
+ } break;
+ case GDScriptParser::DataType::ENUM: {
+ if (export_type.is_meta_type) {
+ variable->export_info.type = Variant::DICTIONARY;
+ } else {
+ variable->export_info.type = Variant::INT;
+ variable->export_info.hint = PROPERTY_HINT_ENUM;
+
+ String enum_hint_string;
+ bool first = true;
+ for (const KeyValue<StringName, int64_t> &E : export_type.enum_values) {
+ if (!first) {
+ enum_hint_string += ",";
+ } else {
+ first = false;
+ }
+ enum_hint_string += E.key.operator String().capitalize().xml_escape();
+ enum_hint_string += ":";
+ enum_hint_string += String::num_int64(E.value).xml_escape();
+ }
+
+ variable->export_info.hint_string = enum_hint_string;
+ variable->export_info.usage |= PROPERTY_USAGE_CLASS_IS_ENUM;
+ variable->export_info.class_name = String(export_type.native_type).replace("::", ".");
+ }
+ } break;
+ default:
+ push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation);
+ return false;
+ }
+
+ if (variable->export_info.hint == PROPERTY_HINT_NODE_TYPE && !ClassDB::is_parent_class(p_class->base_type.native_type, SNAME("Node"))) {
+ push_error(vformat(R"(Node export is only supported in Node-derived classes, but the current class inherits "%s".)", p_class->base_type.to_string()), p_annotation);
+ return false;
+ }
+
+ String value_prefix = itos(variable->export_info.type);
+ if (variable->export_info.hint) {
+ value_prefix += "/" + itos(variable->export_info.hint);
+ }
+ value_prefix += ":" + variable->export_info.hint_string;
+
+ variable->export_info.type = Variant::DICTIONARY;
+ variable->export_info.hint = PROPERTY_HINT_TYPE_STRING;
+ variable->export_info.hint_string = key_prefix + ";" + value_prefix;
+ variable->export_info.usage = PROPERTY_USAGE_DEFAULT;
+ variable->export_info.class_name = StringName();
+ }
} else if (p_annotation->name == SNAME("@export_enum")) {
use_default_variable_type_check = false;
@@ -4780,7 +4891,10 @@ String GDScriptParser::DataType::to_string() const {
return "null";
}
if (builtin_type == Variant::ARRAY && has_container_element_type(0)) {
- return vformat("Array[%s]", container_element_types[0].to_string());
+ return vformat("Array[%s]", get_container_element_type(0).to_string());
+ }
+ if (builtin_type == Variant::DICTIONARY && has_container_element_types()) {
+ return vformat("Dictionary[%s, %s]", get_container_element_type_or_variant(0).to_string(), get_container_element_type_or_variant(1).to_string());
}
return Variant::get_type_name(builtin_type);
case NATIVE:
@@ -4869,6 +4983,72 @@ PropertyInfo GDScriptParser::DataType::to_property_info(const String &p_name) co
case UNRESOLVED:
break;
}
+ } else if (builtin_type == Variant::DICTIONARY && has_container_element_types()) {
+ const DataType key_type = get_container_element_type_or_variant(0);
+ const DataType value_type = get_container_element_type_or_variant(1);
+ if ((key_type.kind == VARIANT && value_type.kind == VARIANT) || key_type.kind == RESOLVING ||
+ key_type.kind == UNRESOLVED || value_type.kind == RESOLVING || value_type.kind == UNRESOLVED) {
+ break;
+ }
+ String key_hint, value_hint;
+ switch (key_type.kind) {
+ case BUILTIN:
+ key_hint = Variant::get_type_name(key_type.builtin_type);
+ break;
+ case NATIVE:
+ key_hint = key_type.native_type;
+ break;
+ case SCRIPT:
+ if (key_type.script_type.is_valid() && key_type.script_type->get_global_name() != StringName()) {
+ key_hint = key_type.script_type->get_global_name();
+ } else {
+ key_hint = key_type.native_type;
+ }
+ break;
+ case CLASS:
+ if (key_type.class_type != nullptr && key_type.class_type->get_global_name() != StringName()) {
+ key_hint = key_type.class_type->get_global_name();
+ } else {
+ key_hint = key_type.native_type;
+ }
+ break;
+ case ENUM:
+ key_hint = String(key_type.native_type).replace("::", ".");
+ break;
+ default:
+ key_hint = "Variant";
+ break;
+ }
+ switch (value_type.kind) {
+ case BUILTIN:
+ value_hint = Variant::get_type_name(value_type.builtin_type);
+ break;
+ case NATIVE:
+ value_hint = value_type.native_type;
+ break;
+ case SCRIPT:
+ if (value_type.script_type.is_valid() && value_type.script_type->get_global_name() != StringName()) {
+ value_hint = value_type.script_type->get_global_name();
+ } else {
+ value_hint = value_type.native_type;
+ }
+ break;
+ case CLASS:
+ if (value_type.class_type != nullptr && value_type.class_type->get_global_name() != StringName()) {
+ value_hint = value_type.class_type->get_global_name();
+ } else {
+ value_hint = value_type.native_type;
+ }
+ break;
+ case ENUM:
+ value_hint = String(value_type.native_type).replace("::", ".");
+ break;
+ default:
+ value_hint = "Variant";
+ break;
+ }
+ result.hint = PROPERTY_HINT_DICTIONARY_TYPE;
+ result.hint_string = key_hint + ";" + value_hint;
}
break;
case NATIVE:
@@ -4953,6 +5133,50 @@ GDScriptParser::DataType GDScriptParser::DataType::get_typed_container_type() co
return type;
}
+bool GDScriptParser::DataType::can_reference(const GDScriptParser::DataType &p_other) const {
+ if (p_other.is_meta_type) {
+ return false;
+ } else if (builtin_type != p_other.builtin_type) {
+ return false;
+ } else if (builtin_type != Variant::OBJECT) {
+ return true;
+ }
+
+ if (native_type == StringName()) {
+ return true;
+ } else if (p_other.native_type == StringName()) {
+ return false;
+ } else if (native_type != p_other.native_type && !ClassDB::is_parent_class(p_other.native_type, native_type)) {
+ return false;
+ }
+
+ Ref<Script> script = script_type;
+ if (kind == GDScriptParser::DataType::CLASS && script.is_null()) {
+ Error err = OK;
+ Ref<GDScript> scr = GDScriptCache::get_shallow_script(script_path, err);
+ ERR_FAIL_COND_V_MSG(err, false, vformat(R"(Error while getting cache for script "%s".)", script_path));
+ script.reference_ptr(scr->find_class(class_type->fqcn));
+ }
+
+ Ref<Script> script_other = p_other.script_type;
+ if (p_other.kind == GDScriptParser::DataType::CLASS && script_other.is_null()) {
+ Error err = OK;
+ Ref<GDScript> scr = GDScriptCache::get_shallow_script(p_other.script_path, err);
+ ERR_FAIL_COND_V_MSG(err, false, vformat(R"(Error while getting cache for script "%s".)", p_other.script_path));
+ script_other.reference_ptr(scr->find_class(p_other.class_type->fqcn));
+ }
+
+ if (script.is_null()) {
+ return true;
+ } else if (script_other.is_null()) {
+ return false;
+ } else if (script != script_other && !script_other->inherits_script(script)) {
+ return false;
+ }
+
+ return true;
+}
+
void GDScriptParser::complete_extents(Node *p_node) {
while (!nodes_in_progress.is_empty() && nodes_in_progress.back()->get() != p_node) {
ERR_PRINT("Parser bug: Mismatch in extents tracking stack.");
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 43c5a48fa7..7840474a89 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -189,6 +189,8 @@ public:
GDScriptParser::DataType get_typed_container_type() const;
+ bool can_reference(const DataType &p_other) const;
+
bool operator==(const DataType &p_other) const {
if (type_source == UNDETECTED || p_other.type_source == UNDETECTED) {
return true; // Can be considered equal for parsing purposes.
@@ -1455,8 +1457,11 @@ private:
}
void apply_pending_warnings();
#endif
- void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1);
- void make_completion_context(CompletionType p_type, Variant::Type p_builtin_type);
+ // Setting p_force to false will prevent the completion context from being update if a context was already set before.
+ // This should only be done when we push context before we consumed any tokens for the corresponding structure.
+ // See parse_precedence for an example.
+ void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1, bool p_force = true);
+ void make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force = true);
// In some cases it might become necessary to alter the completion context after parsing a subexpression.
// For example to not override COMPLETE_CALL_ARGUMENTS with COMPLETION_NONE from string literals.
void override_completion_context(const Node *p_for_node, CompletionType p_type, Node *p_node, int p_argument = -1);
diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index ddb0cf9502..4617a0dbb9 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -84,9 +84,15 @@ static String _get_var_type(const Variant *p_var) {
if (p_var->get_type() == Variant::ARRAY) {
basestr = "Array";
const Array *p_array = VariantInternal::get_array(p_var);
- Variant::Type builtin_type = (Variant::Type)p_array->get_typed_builtin();
- if (builtin_type != Variant::NIL) {
- basestr += "[" + _get_element_type(builtin_type, p_array->get_typed_class_name(), p_array->get_typed_script()) + "]";
+ if (p_array->is_typed()) {
+ basestr += "[" + _get_element_type((Variant::Type)p_array->get_typed_builtin(), p_array->get_typed_class_name(), p_array->get_typed_script()) + "]";
+ }
+ } else if (p_var->get_type() == Variant::DICTIONARY) {
+ basestr = "Dictionary";
+ const Dictionary *p_dictionary = VariantInternal::get_dictionary(p_var);
+ if (p_dictionary->is_typed()) {
+ basestr += "[" + _get_element_type((Variant::Type)p_dictionary->get_typed_key_builtin(), p_dictionary->get_typed_key_class_name(), p_dictionary->get_typed_key_script()) +
+ ", " + _get_element_type((Variant::Type)p_dictionary->get_typed_value_builtin(), p_dictionary->get_typed_value_class_name(), p_dictionary->get_typed_value_script()) + "]";
}
} else {
basestr = Variant::get_type_name(p_var->get_type());
@@ -120,6 +126,16 @@ Variant GDScriptFunction::_get_default_variant_for_data_type(const GDScriptDataT
}
return array;
+ } else if (p_data_type.builtin_type == Variant::DICTIONARY) {
+ Dictionary dict;
+ // Typed dictionary.
+ if (p_data_type.has_container_element_types()) {
+ const GDScriptDataType &key_type = p_data_type.get_container_element_type_or_variant(0);
+ const GDScriptDataType &value_type = p_data_type.get_container_element_type_or_variant(1);
+ dict.set_typed(key_type.builtin_type, key_type.native_type, key_type.script_type, value_type.builtin_type, value_type.native_type, value_type.script_type);
+ }
+
+ return dict;
} else {
Callable::CallError ce;
Variant variant;
@@ -153,6 +169,9 @@ String GDScriptFunction::_get_call_error(const String &p_where, const Variant **
if (p_err.expected == Variant::ARRAY && p_argptrs[p_err.argument]->get_type() == p_err.expected) {
return "Invalid type in " + p_where + ". The array of argument " + itos(p_err.argument + 1) + " (" + _get_var_type(p_argptrs[p_err.argument]) + ") does not have the same element type as the expected typed array argument.";
}
+ if (p_err.expected == Variant::DICTIONARY && p_argptrs[p_err.argument]->get_type() == p_err.expected) {
+ return "Invalid type in " + p_where + ". The dictionary of argument " + itos(p_err.argument + 1) + " (" + _get_var_type(p_argptrs[p_err.argument]) + ") does not have the same element type as the expected typed dictionary argument.";
+ }
#endif // DEBUG_ENABLED
return "Invalid type in " + p_where + ". Cannot convert argument " + itos(p_err.argument + 1) + " from " + Variant::get_type_name(p_argptrs[p_err.argument]->get_type()) + " to " + Variant::get_type_name(Variant::Type(p_err.expected)) + ".";
case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS:
@@ -215,6 +234,7 @@ void (*type_init_function_table[])(Variant *) = {
&&OPCODE_OPERATOR_VALIDATED, \
&&OPCODE_TYPE_TEST_BUILTIN, \
&&OPCODE_TYPE_TEST_ARRAY, \
+ &&OPCODE_TYPE_TEST_DICTIONARY, \
&&OPCODE_TYPE_TEST_NATIVE, \
&&OPCODE_TYPE_TEST_SCRIPT, \
&&OPCODE_SET_KEYED, \
@@ -237,6 +257,7 @@ void (*type_init_function_table[])(Variant *) = {
&&OPCODE_ASSIGN_FALSE, \
&&OPCODE_ASSIGN_TYPED_BUILTIN, \
&&OPCODE_ASSIGN_TYPED_ARRAY, \
+ &&OPCODE_ASSIGN_TYPED_DICTIONARY, \
&&OPCODE_ASSIGN_TYPED_NATIVE, \
&&OPCODE_ASSIGN_TYPED_SCRIPT, \
&&OPCODE_CAST_TO_BUILTIN, \
@@ -247,6 +268,7 @@ void (*type_init_function_table[])(Variant *) = {
&&OPCODE_CONSTRUCT_ARRAY, \
&&OPCODE_CONSTRUCT_TYPED_ARRAY, \
&&OPCODE_CONSTRUCT_DICTIONARY, \
+ &&OPCODE_CONSTRUCT_TYPED_DICTIONARY, \
&&OPCODE_CALL, \
&&OPCODE_CALL_RETURN, \
&&OPCODE_CALL_ASYNC, \
@@ -275,6 +297,7 @@ void (*type_init_function_table[])(Variant *) = {
&&OPCODE_RETURN, \
&&OPCODE_RETURN_TYPED_BUILTIN, \
&&OPCODE_RETURN_TYPED_ARRAY, \
+ &&OPCODE_RETURN_TYPED_DICTIONARY, \
&&OPCODE_RETURN_TYPED_NATIVE, \
&&OPCODE_RETURN_TYPED_SCRIPT, \
&&OPCODE_ITERATE_BEGIN, \
@@ -548,7 +571,12 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
return _get_default_variant_for_data_type(return_type);
}
if (argument_types[i].kind == GDScriptDataType::BUILTIN) {
- if (argument_types[i].builtin_type == Variant::ARRAY && argument_types[i].has_container_element_type(0)) {
+ if (argument_types[i].builtin_type == Variant::DICTIONARY && argument_types[i].has_container_element_types()) {
+ const GDScriptDataType &arg_key_type = argument_types[i].get_container_element_type_or_variant(0);
+ const GDScriptDataType &arg_value_type = argument_types[i].get_container_element_type_or_variant(1);
+ Dictionary dict(p_args[i]->operator Dictionary(), arg_key_type.builtin_type, arg_key_type.native_type, arg_key_type.script_type, arg_value_type.builtin_type, arg_value_type.native_type, arg_value_type.script_type);
+ memnew_placement(&stack[i + 3], Variant(dict));
+ } else if (argument_types[i].builtin_type == Variant::ARRAY && argument_types[i].has_container_element_type(0)) {
const GDScriptDataType &arg_type = argument_types[i].container_element_types[0];
Array array(p_args[i]->operator Array(), arg_type.builtin_type, arg_type.native_type, arg_type.script_type);
memnew_placement(&stack[i + 3], Variant(array));
@@ -827,6 +855,36 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
DISPATCH_OPCODE;
+ OPCODE(OPCODE_TYPE_TEST_DICTIONARY) {
+ CHECK_SPACE(9);
+
+ GET_VARIANT_PTR(dst, 0);
+ GET_VARIANT_PTR(value, 1);
+
+ GET_VARIANT_PTR(key_script_type, 2);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + 5];
+ int key_native_type_idx = _code_ptr[ip + 6];
+ GD_ERR_BREAK(key_native_type_idx < 0 || key_native_type_idx >= _global_names_count);
+ const StringName key_native_type = _global_names_ptr[key_native_type_idx];
+
+ GET_VARIANT_PTR(value_script_type, 3);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + 7];
+ int value_native_type_idx = _code_ptr[ip + 8];
+ GD_ERR_BREAK(value_native_type_idx < 0 || value_native_type_idx >= _global_names_count);
+ const StringName value_native_type = _global_names_ptr[value_native_type_idx];
+
+ bool result = false;
+ if (value->get_type() == Variant::DICTIONARY) {
+ Dictionary *dictionary = VariantInternal::get_dictionary(value);
+ result = dictionary->get_typed_key_builtin() == ((uint32_t)key_builtin_type) && dictionary->get_typed_key_class_name() == key_native_type && dictionary->get_typed_key_script() == *key_script_type &&
+ dictionary->get_typed_value_builtin() == ((uint32_t)value_builtin_type) && dictionary->get_typed_value_class_name() == value_native_type && dictionary->get_typed_value_script() == *value_script_type;
+ }
+
+ *dst = result;
+ ip += 9;
+ }
+ DISPATCH_OPCODE;
+
OPCODE(OPCODE_TYPE_TEST_NATIVE) {
CHECK_SPACE(4);
@@ -1384,6 +1442,50 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
DISPATCH_OPCODE;
+ OPCODE(OPCODE_ASSIGN_TYPED_DICTIONARY) {
+ CHECK_SPACE(9);
+ GET_VARIANT_PTR(dst, 0);
+ GET_VARIANT_PTR(src, 1);
+
+ GET_VARIANT_PTR(key_script_type, 2);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + 5];
+ int key_native_type_idx = _code_ptr[ip + 6];
+ GD_ERR_BREAK(key_native_type_idx < 0 || key_native_type_idx >= _global_names_count);
+ const StringName key_native_type = _global_names_ptr[key_native_type_idx];
+
+ GET_VARIANT_PTR(value_script_type, 3);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + 7];
+ int value_native_type_idx = _code_ptr[ip + 8];
+ GD_ERR_BREAK(value_native_type_idx < 0 || value_native_type_idx >= _global_names_count);
+ const StringName value_native_type = _global_names_ptr[value_native_type_idx];
+
+ if (src->get_type() != Variant::DICTIONARY) {
+#ifdef DEBUG_ENABLED
+ err_text = vformat(R"(Trying to assign a value of type "%s" to a variable of type "Dictionary[%s, %s]".)",
+ _get_var_type(src), _get_element_type(key_builtin_type, key_native_type, *key_script_type),
+ _get_element_type(value_builtin_type, value_native_type, *value_script_type));
+#endif // DEBUG_ENABLED
+ OPCODE_BREAK;
+ }
+
+ Dictionary *dictionary = VariantInternal::get_dictionary(src);
+
+ if (dictionary->get_typed_key_builtin() != ((uint32_t)key_builtin_type) || dictionary->get_typed_key_class_name() != key_native_type || dictionary->get_typed_key_script() != *key_script_type ||
+ dictionary->get_typed_value_builtin() != ((uint32_t)value_builtin_type) || dictionary->get_typed_value_class_name() != value_native_type || dictionary->get_typed_value_script() != *value_script_type) {
+#ifdef DEBUG_ENABLED
+ err_text = vformat(R"(Trying to assign a dictionary of type "%s" to a variable of type "Dictionary[%s, %s]".)",
+ _get_var_type(src), _get_element_type(key_builtin_type, key_native_type, *key_script_type),
+ _get_element_type(value_builtin_type, value_native_type, *value_script_type));
+#endif // DEBUG_ENABLED
+ OPCODE_BREAK;
+ }
+
+ *dst = *src;
+
+ ip += 9;
+ }
+ DISPATCH_OPCODE;
+
OPCODE(OPCODE_ASSIGN_TYPED_NATIVE) {
CHECK_SPACE(4);
GET_VARIANT_PTR(dst, 0);
@@ -1703,12 +1805,51 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
GET_INSTRUCTION_ARG(dst, argc * 2);
+ *dst = Variant(); // Clear potential previous typed dictionary.
+
*dst = dict;
ip += 2;
}
DISPATCH_OPCODE;
+ OPCODE(OPCODE_CONSTRUCT_TYPED_DICTIONARY) {
+ LOAD_INSTRUCTION_ARGS
+ CHECK_SPACE(6 + instr_arg_count);
+ ip += instr_arg_count;
+
+ int argc = _code_ptr[ip + 1];
+
+ GET_INSTRUCTION_ARG(key_script_type, argc * 2 + 1);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + 2];
+ int key_native_type_idx = _code_ptr[ip + 3];
+ GD_ERR_BREAK(key_native_type_idx < 0 || key_native_type_idx >= _global_names_count);
+ const StringName key_native_type = _global_names_ptr[key_native_type_idx];
+
+ GET_INSTRUCTION_ARG(value_script_type, argc * 2 + 2);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + 4];
+ int value_native_type_idx = _code_ptr[ip + 5];
+ GD_ERR_BREAK(value_native_type_idx < 0 || value_native_type_idx >= _global_names_count);
+ const StringName value_native_type = _global_names_ptr[value_native_type_idx];
+
+ Dictionary dict;
+
+ for (int i = 0; i < argc; i++) {
+ GET_INSTRUCTION_ARG(k, i * 2 + 0);
+ GET_INSTRUCTION_ARG(v, i * 2 + 1);
+ dict[*k] = *v;
+ }
+
+ GET_INSTRUCTION_ARG(dst, argc * 2);
+
+ *dst = Variant(); // Clear potential previous typed dictionary.
+
+ *dst = Dictionary(dict, key_builtin_type, key_native_type, *key_script_type, value_builtin_type, value_native_type, *value_script_type);
+
+ ip += 6;
+ }
+ DISPATCH_OPCODE;
+
OPCODE(OPCODE_CALL_ASYNC)
OPCODE(OPCODE_CALL_RETURN)
OPCODE(OPCODE_CALL) {
@@ -2657,6 +2798,51 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
OPCODE_BREAK;
}
+ OPCODE(OPCODE_RETURN_TYPED_DICTIONARY) {
+ CHECK_SPACE(8);
+ GET_VARIANT_PTR(r, 0);
+
+ GET_VARIANT_PTR(key_script_type, 1);
+ Variant::Type key_builtin_type = (Variant::Type)_code_ptr[ip + 4];
+ int key_native_type_idx = _code_ptr[ip + 5];
+ GD_ERR_BREAK(key_native_type_idx < 0 || key_native_type_idx >= _global_names_count);
+ const StringName key_native_type = _global_names_ptr[key_native_type_idx];
+
+ GET_VARIANT_PTR(value_script_type, 2);
+ Variant::Type value_builtin_type = (Variant::Type)_code_ptr[ip + 6];
+ int value_native_type_idx = _code_ptr[ip + 7];
+ GD_ERR_BREAK(value_native_type_idx < 0 || value_native_type_idx >= _global_names_count);
+ const StringName value_native_type = _global_names_ptr[value_native_type_idx];
+
+ if (r->get_type() != Variant::DICTIONARY) {
+#ifdef DEBUG_ENABLED
+ err_text = vformat(R"(Trying to return a value of type "%s" where expected return type is "Dictionary[%s, %s]".)",
+ _get_var_type(r), _get_element_type(key_builtin_type, key_native_type, *key_script_type),
+ _get_element_type(value_builtin_type, value_native_type, *value_script_type));
+#endif // DEBUG_ENABLED
+ OPCODE_BREAK;
+ }
+
+ Dictionary *dictionary = VariantInternal::get_dictionary(r);
+
+ if (dictionary->get_typed_key_builtin() != ((uint32_t)key_builtin_type) || dictionary->get_typed_key_class_name() != key_native_type || dictionary->get_typed_key_script() != *key_script_type ||
+ dictionary->get_typed_value_builtin() != ((uint32_t)value_builtin_type) || dictionary->get_typed_value_class_name() != value_native_type || dictionary->get_typed_value_script() != *value_script_type) {
+#ifdef DEBUG_ENABLED
+ err_text = vformat(R"(Trying to return a dictionary of type "%s" where expected return type is "Dictionary[%s, %s]".)",
+ _get_var_type(r), _get_element_type(key_builtin_type, key_native_type, *key_script_type),
+ _get_element_type(value_builtin_type, value_native_type, *value_script_type));
+#endif // DEBUG_ENABLED
+ OPCODE_BREAK;
+ }
+
+ retvalue = *dictionary;
+
+#ifdef DEBUG_ENABLED
+ exit_ok = true;
+#endif // DEBUG_ENABLED
+ OPCODE_BREAK;
+ }
+
OPCODE(OPCODE_RETURN_TYPED_NATIVE) {
CHECK_SPACE(3);
GET_VARIANT_PTR(r, 0);
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.gd b/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.gd
new file mode 100644
index 0000000000..c180cca03c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.gd
@@ -0,0 +1,3 @@
+func test():
+ for key: int in { "a": 1 }:
+ print(key)
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.out b/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.out
new file mode 100644
index 0000000000..8530783673
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/for_loop_wrong_specified_type_with_literal_dictionary.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot include a value of type "String" as "int".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.gd b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.gd
new file mode 100644
index 0000000000..75d1b7fe62
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.gd
@@ -0,0 +1,4 @@
+func test():
+ var differently: Dictionary[float, float] = { 1.0: 0.0 }
+ var typed: Dictionary[int, int] = differently
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.out b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.out
new file mode 100644
index 0000000000..e05d4be8c9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assign_differently_typed.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot assign a value of type Dictionary[float, float] to variable "typed" with specified type Dictionary[int, int].
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.gd b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.gd
new file mode 100644
index 0000000000..e0af71823a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.gd
@@ -0,0 +1,2 @@
+func test():
+ const dict: Dictionary[int, int] = { "Hello": "World" }
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.out b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.out
new file mode 100644
index 0000000000..8530783673
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_assignment.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot include a value of type "String" as "int".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.gd b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.gd
new file mode 100644
index 0000000000..814ba12aef
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.gd
@@ -0,0 +1,4 @@
+func test():
+ var unconvertible := 1
+ var typed: Dictionary[Object, Object] = { unconvertible: unconvertible }
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.out b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.out
new file mode 100644
index 0000000000..9d6c9d9144
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_init_with_unconvertible_in_literal.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot have a key of type "int" in a dictionary of type "Dictionary[Object, Object]".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.gd b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.gd
new file mode 100644
index 0000000000..73d8ce2b96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.gd
@@ -0,0 +1,7 @@
+func expect_typed(typed: Dictionary[int, int]):
+ print(typed.size())
+
+func test():
+ var differently: Dictionary[float, float] = { 1.0: 0.0 }
+ expect_typed(differently)
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.out b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.out
new file mode 100644
index 0000000000..302109cf8a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_dictionary_pass_differently_to_typed.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Invalid argument for "expect_typed()" function: argument 1 should be "Dictionary[int, int]" but is "Dictionary[float, float]".
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.gd
new file mode 100644
index 0000000000..65f5e7da07
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.gd
@@ -0,0 +1,20 @@
+func print_untyped(dictionary = { 0: 1 }) -> void:
+ print(dictionary)
+ print(dictionary.get_typed_key_builtin())
+ print(dictionary.get_typed_value_builtin())
+
+func print_inferred(dictionary := { 2: 3 }) -> void:
+ print(dictionary)
+ print(dictionary.get_typed_key_builtin())
+ print(dictionary.get_typed_value_builtin())
+
+func print_typed(dictionary: Dictionary[int, int] = { 4: 5 }) -> void:
+ print(dictionary)
+ print(dictionary.get_typed_key_builtin())
+ print(dictionary.get_typed_value_builtin())
+
+func test():
+ print_untyped()
+ print_inferred()
+ print_typed()
+ print('ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.out b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.out
new file mode 100644
index 0000000000..c31561bee3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_as_default_parameter.out
@@ -0,0 +1,11 @@
+GDTEST_OK
+{ 0: 1 }
+0
+0
+{ 2: 3 }
+0
+0
+{ 4: 5 }
+2
+2
+ok
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.gd
new file mode 100644
index 0000000000..0aa3de2c4a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.gd
@@ -0,0 +1,4 @@
+func test():
+ var dict := { 0: 0 }
+ dict[0] = 1
+ print(dict[0])
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.out b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.out
new file mode 100644
index 0000000000..a7f1357bb2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_inferred_access_isnt_constant.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+1
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.gd
new file mode 100644
index 0000000000..9d3fffd1de
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.gd
@@ -0,0 +1,214 @@
+class A: pass
+class B extends A: pass
+
+enum E { E0 = 391, E1 = 193 }
+
+func floats_identity(floats: Dictionary[float, float]): return floats
+
+class Members:
+ var one: Dictionary[int, int] = { 104: 401 }
+ var two: Dictionary[int, int] = one
+
+ func check_passing() -> bool:
+ Utils.check(str(one) == '{ 104: 401 }')
+ Utils.check(str(two) == '{ 104: 401 }')
+ two[582] = 285
+ Utils.check(str(one) == '{ 104: 401, 582: 285 }')
+ Utils.check(str(two) == '{ 104: 401, 582: 285 }')
+ two = { 486: 684 }
+ Utils.check(str(one) == '{ 104: 401, 582: 285 }')
+ Utils.check(str(two) == '{ 486: 684 }')
+ return true
+
+
+@warning_ignore("unsafe_method_access")
+@warning_ignore("assert_always_true")
+@warning_ignore("return_value_discarded")
+func test():
+ var untyped_basic = { 459: 954 }
+ Utils.check(str(untyped_basic) == '{ 459: 954 }')
+ Utils.check(untyped_basic.get_typed_key_builtin() == TYPE_NIL)
+ Utils.check(untyped_basic.get_typed_value_builtin() == TYPE_NIL)
+
+ var inferred_basic := { 366: 663 }
+ Utils.check(str(inferred_basic) == '{ 366: 663 }')
+ Utils.check(inferred_basic.get_typed_key_builtin() == TYPE_NIL)
+ Utils.check(inferred_basic.get_typed_value_builtin() == TYPE_NIL)
+
+ var typed_basic: Dictionary = { 521: 125 }
+ Utils.check(str(typed_basic) == '{ 521: 125 }')
+ Utils.check(typed_basic.get_typed_key_builtin() == TYPE_NIL)
+ Utils.check(typed_basic.get_typed_value_builtin() == TYPE_NIL)
+
+
+ var empty_floats: Dictionary[float, float] = {}
+ Utils.check(str(empty_floats) == '{ }')
+ Utils.check(empty_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(empty_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ untyped_basic = empty_floats
+ Utils.check(untyped_basic.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(untyped_basic.get_typed_value_builtin() == TYPE_FLOAT)
+
+ inferred_basic = empty_floats
+ Utils.check(inferred_basic.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(inferred_basic.get_typed_value_builtin() == TYPE_FLOAT)
+
+ typed_basic = empty_floats
+ Utils.check(typed_basic.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(typed_basic.get_typed_value_builtin() == TYPE_FLOAT)
+
+ empty_floats[705.0] = 507.0
+ untyped_basic[430.0] = 34.0
+ inferred_basic[263.0] = 362.0
+ typed_basic[518.0] = 815.0
+ Utils.check(str(empty_floats) == '{ 705: 507, 430: 34, 263: 362, 518: 815 }')
+ Utils.check(str(untyped_basic) == '{ 705: 507, 430: 34, 263: 362, 518: 815 }')
+ Utils.check(str(inferred_basic) == '{ 705: 507, 430: 34, 263: 362, 518: 815 }')
+ Utils.check(str(typed_basic) == '{ 705: 507, 430: 34, 263: 362, 518: 815 }')
+
+
+ const constant_float := 950.0
+ const constant_int := 170
+ var typed_float := 954.0
+ var filled_floats: Dictionary[float, float] = { constant_float: constant_int, typed_float: empty_floats[430.0] + empty_floats[263.0] }
+ Utils.check(str(filled_floats) == '{ 950: 170, 954: 396 }')
+ Utils.check(filled_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(filled_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ var casted_floats := { empty_floats[263.0] * 2: empty_floats[263.0] / 2 } as Dictionary[float, float]
+ Utils.check(str(casted_floats) == '{ 724: 181 }')
+ Utils.check(casted_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(casted_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ var returned_floats = (func () -> Dictionary[float, float]: return { 554: 455 }).call()
+ Utils.check(str(returned_floats) == '{ 554: 455 }')
+ Utils.check(returned_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(returned_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ var passed_floats = floats_identity({ 663.0 if randf() > 0.5 else 663.0: 366.0 if randf() <= 0.5 else 366.0 })
+ Utils.check(str(passed_floats) == '{ 663: 366 }')
+ Utils.check(passed_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(passed_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ var default_floats = (func (floats: Dictionary[float, float] = { 364.0: 463.0 }): return floats).call()
+ Utils.check(str(default_floats) == '{ 364: 463 }')
+ Utils.check(default_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(default_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+ var typed_int := 556
+ var converted_floats: Dictionary[float, float] = { typed_int: typed_int }
+ converted_floats[498.0] = 894
+ Utils.check(str(converted_floats) == '{ 556: 556, 498: 894 }')
+ Utils.check(converted_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(converted_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+
+ const constant_basic = { 228: 822 }
+ Utils.check(str(constant_basic) == '{ 228: 822 }')
+ Utils.check(constant_basic.get_typed_key_builtin() == TYPE_NIL)
+ Utils.check(constant_basic.get_typed_value_builtin() == TYPE_NIL)
+
+ const constant_floats: Dictionary[float, float] = { constant_float - constant_basic[228] - constant_int: constant_float + constant_basic[228] + constant_int }
+ Utils.check(str(constant_floats) == '{ -42: 1942 }')
+ Utils.check(constant_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(constant_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+
+ var source_floats: Dictionary[float, float] = { 999.74: 47.999 }
+ untyped_basic = source_floats
+ var destination_floats: Dictionary[float, float] = untyped_basic
+ destination_floats[999.74] -= 0.999
+ Utils.check(str(source_floats) == '{ 999.74: 47 }')
+ Utils.check(str(untyped_basic) == '{ 999.74: 47 }')
+ Utils.check(str(destination_floats) == '{ 999.74: 47 }')
+ Utils.check(destination_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(destination_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+
+ var duplicated_floats := empty_floats.duplicate()
+ duplicated_floats.erase(705.0)
+ duplicated_floats.erase(430.0)
+ duplicated_floats.erase(518.0)
+ duplicated_floats[263.0] *= 3
+ Utils.check(str(duplicated_floats) == '{ 263: 1086 }')
+ Utils.check(duplicated_floats.get_typed_key_builtin() == TYPE_FLOAT)
+ Utils.check(duplicated_floats.get_typed_value_builtin() == TYPE_FLOAT)
+
+
+ var b_objects: Dictionary[int, B] = { 0: B.new(), 1: B.new() as A, 2: null }
+ Utils.check(b_objects.size() == 3)
+ Utils.check(b_objects.get_typed_value_builtin() == TYPE_OBJECT)
+ Utils.check(b_objects.get_typed_value_script() == B)
+
+ var a_objects: Dictionary[int, A] = { 0: A.new(), 1: B.new(), 2: null, 3: b_objects[0] }
+ Utils.check(a_objects.size() == 4)
+ Utils.check(a_objects.get_typed_value_builtin() == TYPE_OBJECT)
+ Utils.check(a_objects.get_typed_value_script() == A)
+
+ var a_passed = (func check_a_passing(p_objects: Dictionary[int, A]): return p_objects.size()).call(a_objects)
+ Utils.check(a_passed == 4)
+
+ var b_passed = (func check_b_passing(basic: Dictionary): return basic[0] != null).call(b_objects)
+ Utils.check(b_passed == true)
+
+
+ var empty_strings: Dictionary[String, String] = {}
+ var empty_bools: Dictionary[bool, bool] = {}
+ var empty_basic_one := {}
+ var empty_basic_two := {}
+ Utils.check(empty_strings == empty_bools)
+ Utils.check(empty_basic_one == empty_basic_two)
+ Utils.check(empty_strings.hash() == empty_bools.hash())
+ Utils.check(empty_basic_one.hash() == empty_basic_two.hash())
+
+
+ var assign_source: Dictionary[int, int] = { 527: 725 }
+ var assign_target: Dictionary[int, int] = {}
+ assign_target.assign(assign_source)
+ Utils.check(str(assign_source) == '{ 527: 725 }')
+ Utils.check(str(assign_target) == '{ 527: 725 }')
+ assign_source[657] = 756
+ Utils.check(str(assign_source) == '{ 527: 725, 657: 756 }')
+ Utils.check(str(assign_target) == '{ 527: 725 }')
+
+
+ var defaults_passed = (func check_defaults_passing(one: Dictionary[int, int] = {}, two := one):
+ one[887] = 788
+ two[198] = 891
+ Utils.check(str(one) == '{ 887: 788, 198: 891 }')
+ Utils.check(str(two) == '{ 887: 788, 198: 891 }')
+ two = {130: 31}
+ Utils.check(str(one) == '{ 887: 788, 198: 891 }')
+ Utils.check(str(two) == '{ 130: 31 }')
+ return true
+ ).call()
+ Utils.check(defaults_passed == true)
+
+
+ var members := Members.new()
+ var members_passed := members.check_passing()
+ Utils.check(members_passed == true)
+
+
+ var typed_enums: Dictionary[E, E] = {}
+ typed_enums[E.E0] = E.E1
+ Utils.check(str(typed_enums) == '{ 391: 193 }')
+ Utils.check(typed_enums.get_typed_key_builtin() == TYPE_INT)
+ Utils.check(typed_enums.get_typed_value_builtin() == TYPE_INT)
+
+ const const_enums: Dictionary[E, E] = {}
+ Utils.check(const_enums.get_typed_key_builtin() == TYPE_INT)
+ Utils.check(const_enums.get_typed_key_class_name() == &'')
+ Utils.check(const_enums.get_typed_value_builtin() == TYPE_INT)
+ Utils.check(const_enums.get_typed_value_class_name() == &'')
+
+
+ var a := A.new()
+ var b := B.new()
+ var typed_natives: Dictionary[RefCounted, RefCounted] = { a: b }
+ var typed_scripts = Dictionary(typed_natives, TYPE_OBJECT, "RefCounted", A, TYPE_OBJECT, "RefCounted", B)
+ Utils.check(typed_scripts[a] == b)
+
+
+ print('ok')
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.out b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_usage.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.gd
new file mode 100644
index 0000000000..f4a23ade14
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.gd
@@ -0,0 +1,9 @@
+class Inner:
+ var prop = "Inner"
+
+var dict: Dictionary[int, Inner] = { 0: Inner.new() }
+
+
+func test():
+ var element: Inner = dict[0]
+ print(element.prop)
diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.out b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.out
new file mode 100644
index 0000000000..8f250d2632
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/typed_dictionary_with_custom_class.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+Inner
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd
index d937dfdcfe..37f118dc5d 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.gd
@@ -1,12 +1,29 @@
-signal s1()
-signal s2()
-signal s3()
+# Doesn't produce the warning:
+signal used_as_first_class_signal()
+signal used_with_signal_constructor()
+signal used_with_signal_emit()
+signal used_with_object_emit_signal()
+signal used_with_object_connect()
+signal used_with_object_disconnect()
+signal used_with_self_prefix()
+
+# Produce the warning:
+signal used_with_dynamic_name()
+signal just_unused()
@warning_ignore("unused_signal")
-signal s4()
+signal unused_but_ignored()
func no_exec():
- s1.emit()
- print(s2)
+ print(used_as_first_class_signal)
+ print(Signal(self, "used_with_signal_constructor"))
+ used_with_signal_emit.emit()
+ print(emit_signal("used_with_object_emit_signal"))
+ print(connect("used_with_object_connect", Callable()))
+ disconnect("used_with_object_disconnect", Callable())
+ print(self.emit_signal("used_with_self_prefix"))
+
+ var dynamic_name := "used_with_dynamic_name"
+ print(emit_signal(dynamic_name))
func test():
pass
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out
index ff57017830..39ddf91c76 100644
--- a/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unused_signal.out
@@ -1,5 +1,9 @@
GDTEST_OK
>> WARNING
->> Line: 3
+>> Line: 11
>> UNUSED_SIGNAL
->> The signal "s3" is declared but never explicitly used in the class.
+>> The signal "used_with_dynamic_name" is declared but never explicitly used in the class.
+>> WARNING
+>> Line: 12
+>> UNUSED_SIGNAL
+>> The signal "just_unused" is declared but never explicitly used in the class.
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg
new file mode 100644
index 0000000000..e4759ac76b
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg
@@ -0,0 +1,19 @@
+[output]
+include=[
+ {"display": "new"},
+ {"display": "SIZE_EXPAND"},
+ {"display": "SIZE_EXPAND_FILL"},
+ {"display": "SIZE_FILL"},
+ {"display": "SIZE_SHRINK_BEGIN"},
+ {"display": "SIZE_SHRINK_CENTER"},
+ {"display": "SIZE_SHRINK_END"},
+]
+exclude=[
+ {"display": "Control.SIZE_EXPAND"},
+ {"display": "Control.SIZE_EXPAND_FILL"},
+ {"display": "Control.SIZE_FILL"},
+ {"display": "Control.SIZE_SHRINK_BEGIN"},
+ {"display": "Control.SIZE_SHRINK_CENTER"},
+ {"display": "Control.SIZE_SHRINK_END"},
+ {"display": "contro_var"}
+]
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd
new file mode 100644
index 0000000000..4aeafb2e0a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd
@@ -0,0 +1,7 @@
+extends Control
+
+var contro_var
+
+func _ready():
+ size_flags_horizontal = Control.➡
+ pass
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg
new file mode 100644
index 0000000000..e4759ac76b
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg
@@ -0,0 +1,19 @@
+[output]
+include=[
+ {"display": "new"},
+ {"display": "SIZE_EXPAND"},
+ {"display": "SIZE_EXPAND_FILL"},
+ {"display": "SIZE_FILL"},
+ {"display": "SIZE_SHRINK_BEGIN"},
+ {"display": "SIZE_SHRINK_CENTER"},
+ {"display": "SIZE_SHRINK_END"},
+]
+exclude=[
+ {"display": "Control.SIZE_EXPAND"},
+ {"display": "Control.SIZE_EXPAND_FILL"},
+ {"display": "Control.SIZE_FILL"},
+ {"display": "Control.SIZE_SHRINK_BEGIN"},
+ {"display": "Control.SIZE_SHRINK_CENTER"},
+ {"display": "Control.SIZE_SHRINK_END"},
+ {"display": "contro_var"}
+]
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd
new file mode 100644
index 0000000000..47e9bd5a67
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd
@@ -0,0 +1,7 @@
+extends Control
+
+var contro_var
+
+func _ready():
+ size_flags_horizontal = Control.SIZE➡
+ pass
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg
new file mode 100644
index 0000000000..5cc4ec5fd3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg
@@ -0,0 +1,12 @@
+[output]
+include=[
+ {"display": "Control.SIZE_EXPAND"},
+ {"display": "Control.SIZE_EXPAND_FILL"},
+ {"display": "Control.SIZE_FILL"},
+ {"display": "Control.SIZE_SHRINK_BEGIN"},
+ {"display": "Control.SIZE_SHRINK_CENTER"},
+ {"display": "Control.SIZE_SHRINK_END"},
+]
+exclude=[
+ {"display": "contro_var"}
+]
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd
new file mode 100644
index 0000000000..5c96720bd3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd
@@ -0,0 +1,7 @@
+extends Control
+
+var contro_var
+
+func _ready():
+ size_flags_horizontal = Con➡
+ pass
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg
new file mode 100644
index 0000000000..5cc4ec5fd3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg
@@ -0,0 +1,12 @@
+[output]
+include=[
+ {"display": "Control.SIZE_EXPAND"},
+ {"display": "Control.SIZE_EXPAND_FILL"},
+ {"display": "Control.SIZE_FILL"},
+ {"display": "Control.SIZE_SHRINK_BEGIN"},
+ {"display": "Control.SIZE_SHRINK_CENTER"},
+ {"display": "Control.SIZE_SHRINK_END"},
+]
+exclude=[
+ {"display": "contro_var"}
+]
diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd
new file mode 100644
index 0000000000..d8bf13e51d
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd
@@ -0,0 +1,7 @@
+extends Control
+
+var contro_var
+
+func _ready():
+ size_flags_horizontal = ➡
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.gd b/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.gd
new file mode 100644
index 0000000000..57e6489484
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.gd
@@ -0,0 +1,5 @@
+func test():
+ var my_dictionary: Dictionary[int, String] = { 1: "one", 2: "two", 3: "three" }
+ var inferred_dictionary := { 1: "one", 2: "two", 3: "three" } # This is Dictionary[int, String].
+ print(my_dictionary)
+ print(inferred_dictionary)
diff --git a/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.out b/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.out
new file mode 100644
index 0000000000..6021c338ee
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/typed_dictionaries.out
@@ -0,0 +1,3 @@
+GDTEST_OK
+{ 1: "one", 2: "two", 3: "three" }
+{ 1: "one", 2: "two", 3: "three" }
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.gd
new file mode 100644
index 0000000000..75004742a2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.gd
@@ -0,0 +1,4 @@
+func test():
+ var basic := { 1: 1 }
+ var typed: Dictionary[int, int] = basic
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.out
new file mode 100644
index 0000000000..cadb17f570
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_basic_to_typed.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/typed_dictionary_assign_basic_to_typed.gd
+>> 3
+>> Trying to assign a dictionary of type "Dictionary" to a variable of type "Dictionary[int, int]".
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.gd
new file mode 100644
index 0000000000..e5ab4a1a85
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.gd
@@ -0,0 +1,4 @@
+func test():
+ var differently: Variant = { 1.0: 0.0 } as Dictionary[float, float]
+ var typed: Dictionary[int, int] = differently
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.out
new file mode 100644
index 0000000000..fe1e5d1285
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_differently_typed.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/typed_dictionary_assign_differently_typed.gd
+>> 3
+>> Trying to assign a dictionary of type "Dictionary[float, float]" to a variable of type "Dictionary[int, int]".
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.gd
new file mode 100644
index 0000000000..6cc0e57255
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.gd
@@ -0,0 +1,7 @@
+class Foo: pass
+class Bar extends Foo: pass
+class Baz extends Foo: pass
+
+func test():
+ var typed: Dictionary[Bar, Bar] = { Baz.new() as Foo: Baz.new() as Foo }
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.out
new file mode 100644
index 0000000000..18a4c360e2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_assign_wrong_to_typed.out
@@ -0,0 +1,5 @@
+GDTEST_RUNTIME_ERROR
+>> ERROR
+>> Method/function failed.
+>> Unable to convert key from "Object" to "Object".
+not ok
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.gd
new file mode 100644
index 0000000000..8f7d732584
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.gd
@@ -0,0 +1,7 @@
+func expect_typed(typed: Dictionary[int, int]):
+ print(typed.size())
+
+func test():
+ var basic := { 1: 1 }
+ expect_typed(basic)
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.out
new file mode 100644
index 0000000000..fb45461701
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_basic_to_typed.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/typed_dictionary_pass_basic_to_typed.gd
+>> 6
+>> Invalid type in function 'expect_typed' in base 'RefCounted (typed_dictionary_pass_basic_to_typed.gd)'. The dictionary of argument 1 (Dictionary) does not have the same element type as the expected typed dictionary argument.
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.gd b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.gd
new file mode 100644
index 0000000000..978a9fdfee
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.gd
@@ -0,0 +1,7 @@
+func expect_typed(typed: Dictionary[int, int]):
+ print(typed.size())
+
+func test():
+ var differently: Variant = { 1.0: 0.0 } as Dictionary[float, float]
+ expect_typed(differently)
+ print('not ok')
diff --git a/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.out b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.out
new file mode 100644
index 0000000000..4036a1bf01
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/typed_dictionary_pass_differently_to_typed.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/typed_dictionary_pass_differently_to_typed.gd
+>> 6
+>> Invalid type in function 'expect_typed' in base 'RefCounted (typed_dictionary_pass_differently_to_typed.gd)'. The dictionary of argument 1 (Dictionary[float, float]) does not have the same element type as the expected typed dictionary argument.
diff --git a/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.gd b/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.gd
index bc899a3a6f..393500bd9b 100644
--- a/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.gd
+++ b/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.gd
@@ -28,13 +28,18 @@ func test():
prints(var_to_str(e), var_to_str(elem))
print("Test String-keys dictionary.")
- var d1 := {a = 1, b = 2, c = 3}
+ var d1 := { a = 1, b = 2, c = 3 }
for k: StringName in d1:
var key := k
prints(var_to_str(k), var_to_str(key))
print("Test RefCounted-keys dictionary.")
- var d2 := {RefCounted.new(): 1, Resource.new(): 2, ConfigFile.new(): 3}
+ var d2 := { RefCounted.new(): 1, Resource.new(): 2, ConfigFile.new(): 3 }
for k: RefCounted in d2:
var key := k
prints(k.get_class(), key.get_class())
+
+ print("Test implicitly typed dictionary literal.")
+ for k: StringName in { x = 123, y = 456, z = 789 }:
+ var key := k
+ prints(var_to_str(k), var_to_str(key))
diff --git a/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.out b/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.out
index eeebdc4be5..89cc1b76fe 100644
--- a/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.out
+++ b/modules/gdscript/tests/scripts/runtime/features/for_loop_iterator_specified_types.out
@@ -27,3 +27,7 @@ Test RefCounted-keys dictionary.
RefCounted RefCounted
Resource Resource
ConfigFile ConfigFile
+Test implicitly typed dictionary literal.
+&"x" &"x"
+&"y" &"y"
+&"z" &"z"
diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.gd b/modules/gdscript/tests/scripts/runtime/features/member_info.gd
index 91d5a501c8..4ce53aa395 100644
--- a/modules/gdscript/tests/scripts/runtime/features/member_info.gd
+++ b/modules/gdscript/tests/scripts/runtime/features/member_info.gd
@@ -31,6 +31,16 @@ var test_var_hard_array_my_enum: Array[MyEnum]
var test_var_hard_array_resource: Array[Resource]
var test_var_hard_array_this: Array[TestMemberInfo]
var test_var_hard_array_my_class: Array[MyClass]
+var test_var_hard_dictionary: Dictionary
+var test_var_hard_dictionary_int_variant: Dictionary[int, Variant]
+var test_var_hard_dictionary_variant_int: Dictionary[Variant, int]
+var test_var_hard_dictionary_int_int: Dictionary[int, int]
+var test_var_hard_dictionary_variant_type: Dictionary[Variant.Type, Variant.Type]
+var test_var_hard_dictionary_node_process_mode: Dictionary[Node.ProcessMode, Node.ProcessMode]
+var test_var_hard_dictionary_my_enum: Dictionary[MyEnum, MyEnum]
+var test_var_hard_dictionary_resource: Dictionary[Resource, Resource]
+var test_var_hard_dictionary_this: Dictionary[TestMemberInfo, TestMemberInfo]
+var test_var_hard_dictionary_my_class: Dictionary[MyClass, MyClass]
var test_var_hard_resource: Resource
var test_var_hard_this: TestMemberInfo
var test_var_hard_my_class: MyClass
@@ -43,17 +53,17 @@ func test_func_weak_null(): return null
func test_func_weak_int(): return 1
func test_func_hard_variant() -> Variant: return null
func test_func_hard_int() -> int: return 1
-func test_func_args_1(_a: int, _b: Array[int], _c: int = 1, _d = 2): pass
+func test_func_args_1(_a: int, _b: Array[int], _c: Dictionary[int, int], _d: int = 1, _e = 2): pass
func test_func_args_2(_a = 1, _b = _a, _c = [2], _d = 3): pass
signal test_signal_1()
signal test_signal_2(a: Variant, b)
-signal test_signal_3(a: int, b: Array[int])
-signal test_signal_4(a: Variant.Type, b: Array[Variant.Type])
-signal test_signal_5(a: MyEnum, b: Array[MyEnum])
-signal test_signal_6(a: Resource, b: Array[Resource])
-signal test_signal_7(a: TestMemberInfo, b: Array[TestMemberInfo])
-signal test_signal_8(a: MyClass, b: Array[MyClass])
+signal test_signal_3(a: int, b: Array[int], c: Dictionary[int, int])
+signal test_signal_4(a: Variant.Type, b: Array[Variant.Type], c: Dictionary[Variant.Type, Variant.Type])
+signal test_signal_5(a: MyEnum, b: Array[MyEnum], c: Dictionary[MyEnum, MyEnum])
+signal test_signal_6(a: Resource, b: Array[Resource], c: Dictionary[Resource, Resource])
+signal test_signal_7(a: TestMemberInfo, b: Array[TestMemberInfo], c: Dictionary[TestMemberInfo, TestMemberInfo])
+signal test_signal_8(a: MyClass, b: Array[MyClass], c: Dictionary[MyClass, MyClass])
func no_exec():
test_signal_1.emit()
diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.out b/modules/gdscript/tests/scripts/runtime/features/member_info.out
index 7c826ac05a..2baf451aa5 100644
--- a/modules/gdscript/tests/scripts/runtime/features/member_info.out
+++ b/modules/gdscript/tests/scripts/runtime/features/member_info.out
@@ -23,6 +23,16 @@ var test_var_hard_array_my_enum: Array[TestMemberInfo.MyEnum]
var test_var_hard_array_resource: Array[Resource]
var test_var_hard_array_this: Array[TestMemberInfo]
var test_var_hard_array_my_class: Array[RefCounted]
+var test_var_hard_dictionary: Dictionary
+var test_var_hard_dictionary_int_variant: Dictionary[int, Variant]
+var test_var_hard_dictionary_variant_int: Dictionary[Variant, int]
+var test_var_hard_dictionary_int_int: Dictionary[int, int]
+var test_var_hard_dictionary_variant_type: Dictionary[Variant.Type, Variant.Type]
+var test_var_hard_dictionary_node_process_mode: Dictionary[Node.ProcessMode, Node.ProcessMode]
+var test_var_hard_dictionary_my_enum: Dictionary[TestMemberInfo.MyEnum, TestMemberInfo.MyEnum]
+var test_var_hard_dictionary_resource: Dictionary[Resource, Resource]
+var test_var_hard_dictionary_this: Dictionary[TestMemberInfo, TestMemberInfo]
+var test_var_hard_dictionary_my_class: Dictionary[RefCounted, RefCounted]
var test_var_hard_resource: Resource
var test_var_hard_this: TestMemberInfo
var test_var_hard_my_class: RefCounted
@@ -33,13 +43,13 @@ func test_func_weak_null() -> Variant
func test_func_weak_int() -> Variant
func test_func_hard_variant() -> Variant
func test_func_hard_int() -> int
-func test_func_args_1(_a: int, _b: Array[int], _c: int = 1, _d: Variant = 2) -> void
+func test_func_args_1(_a: int, _b: Array[int], _c: Dictionary[int, int], _d: int = 1, _e: Variant = 2) -> void
func test_func_args_2(_a: Variant = 1, _b: Variant = null, _c: Variant = null, _d: Variant = 3) -> void
signal test_signal_1()
signal test_signal_2(a: Variant, b: Variant)
-signal test_signal_3(a: int, b: Array[int])
-signal test_signal_4(a: Variant.Type, b: Array[Variant.Type])
-signal test_signal_5(a: TestMemberInfo.MyEnum, b: Array[TestMemberInfo.MyEnum])
-signal test_signal_6(a: Resource, b: Array[Resource])
-signal test_signal_7(a: TestMemberInfo, b: Array[TestMemberInfo])
-signal test_signal_8(a: RefCounted, b: Array[RefCounted])
+signal test_signal_3(a: int, b: Array[int], c: Dictionary[int, int])
+signal test_signal_4(a: Variant.Type, b: Array[Variant.Type], c: Dictionary[Variant.Type, Variant.Type])
+signal test_signal_5(a: TestMemberInfo.MyEnum, b: Array[TestMemberInfo.MyEnum], c: Dictionary[TestMemberInfo.MyEnum, TestMemberInfo.MyEnum])
+signal test_signal_6(a: Resource, b: Array[Resource], c: Dictionary[Resource, Resource])
+signal test_signal_7(a: TestMemberInfo, b: Array[TestMemberInfo], c: Dictionary[TestMemberInfo, TestMemberInfo])
+signal test_signal_8(a: RefCounted, b: Array[RefCounted], c: Dictionary[RefCounted, RefCounted])
diff --git a/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.gd b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.gd
new file mode 100644
index 0000000000..0371ee5630
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.gd
@@ -0,0 +1,6 @@
+func test_param(dictionary: Dictionary[int, String]) -> void:
+ print(dictionary.get_typed_key_builtin() == TYPE_INT)
+ print(dictionary.get_typed_value_builtin() == TYPE_STRING)
+
+func test() -> void:
+ test_param({ 123: "some_string" })
diff --git a/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.out b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.out
new file mode 100644
index 0000000000..9d111a8322
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_implicit_cast_param.out
@@ -0,0 +1,3 @@
+GDTEST_OK
+true
+true
diff --git a/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.gd b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.gd
new file mode 100644
index 0000000000..ee51440d80
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.gd
@@ -0,0 +1,7 @@
+func test():
+ var untyped: Variant = 32
+ var typed: Dictionary[int, int] = { untyped: untyped }
+ Utils.check(typed.get_typed_key_builtin() == TYPE_INT)
+ Utils.check(typed.get_typed_value_builtin() == TYPE_INT)
+ Utils.check(str(typed) == '{ 32: 32 }')
+ print('ok')
diff --git a/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.out b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/typed_dictionary_init_with_untyped_in_literal.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok
diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd
index 5d615d8557..1e2788f765 100644
--- a/modules/gdscript/tests/scripts/utils.notest.gd
+++ b/modules/gdscript/tests/scripts/utils.notest.gd
@@ -24,6 +24,11 @@ static func get_type(property: Dictionary, is_return: bool = false) -> String:
if str(property.hint_string).is_empty():
return "Array[<unknown type>]"
return "Array[%s]" % property.hint_string
+ TYPE_DICTIONARY:
+ if property.hint == PROPERTY_HINT_DICTIONARY_TYPE:
+ if str(property.hint_string).is_empty():
+ return "Dictionary[<unknown type>, <unknown type>]"
+ return "Dictionary[%s]" % str(property.hint_string).replace(";", ", ")
TYPE_OBJECT:
if not str(property.class_name).is_empty():
return property.class_name
@@ -188,6 +193,8 @@ static func get_property_hint_name(hint: PropertyHint) -> String:
return "PROPERTY_HINT_INT_IS_POINTER"
PROPERTY_HINT_ARRAY_TYPE:
return "PROPERTY_HINT_ARRAY_TYPE"
+ PROPERTY_HINT_DICTIONARY_TYPE:
+ return "PROPERTY_HINT_DICTIONARY_TYPE"
PROPERTY_HINT_LOCALE_ID:
return "PROPERTY_HINT_LOCALE_ID"
PROPERTY_HINT_LOCALIZABLE_STRING:
diff --git a/modules/interactive_music/audio_stream_interactive.cpp b/modules/interactive_music/audio_stream_interactive.cpp
index 8472c4e352..8656be988d 100644
--- a/modules/interactive_music/audio_stream_interactive.cpp
+++ b/modules/interactive_music/audio_stream_interactive.cpp
@@ -777,7 +777,7 @@ void AudioStreamPlaybackInteractive::_queue(int p_to_clip_index, bool p_is_auto_
if (stream->clips[p_to_clip_index].auto_advance == AudioStreamInteractive::AUTO_ADVANCE_ENABLED) {
int next_clip = stream->clips[p_to_clip_index].auto_advance_next_clip;
- if (next_clip >= 0 && next_clip < (int)stream->clip_count && states[next_clip].playback.is_valid() && next_clip != p_to_clip_index && next_clip != playback_current && (!transition.use_filler_clip || next_clip != transition.filler_clip)) {
+ if (next_clip >= 0 && next_clip < (int)stream->clip_count && states[next_clip].playback.is_valid() && next_clip != p_to_clip_index && (!transition.use_filler_clip || next_clip != transition.filler_clip)) {
auto_advance_to = next_clip;
}
}
@@ -905,7 +905,9 @@ void AudioStreamPlaybackInteractive::_mix_internal_state(int p_state_idx, int p_
// time to start!
from_frame = state.fade_wait * mix_rate;
state.fade_wait = 0;
- queue_next = state.auto_advance;
+ if (state.fade_speed == 0.0) {
+ queue_next = state.auto_advance;
+ }
playback_current = p_state_idx;
state.first_mix = false;
} else {
@@ -919,7 +921,6 @@ void AudioStreamPlaybackInteractive::_mix_internal_state(int p_state_idx, int p_
state.playback->mix(temp_buffer + from_frame, 1.0, p_frames - from_frame);
double frame_fade_inc = state.fade_speed * frame_inc;
-
for (int i = from_frame; i < p_frames; i++) {
if (state.fade_wait) {
// This is for fade out of existing stream;
@@ -933,6 +934,7 @@ void AudioStreamPlaybackInteractive::_mix_internal_state(int p_state_idx, int p_
state.fade_speed = 0.0;
frame_fade_inc = 0.0;
state.fade_volume = 1.0;
+ queue_next = state.auto_advance;
}
} else if (frame_fade_inc < 0.0) {
state.fade_volume += frame_fade_inc;
diff --git a/modules/interactive_music/doc_classes/AudioStreamInteractive.xml b/modules/interactive_music/doc_classes/AudioStreamInteractive.xml
index e8f8e7b760..17448724d1 100644
--- a/modules/interactive_music/doc_classes/AudioStreamInteractive.xml
+++ b/modules/interactive_music/doc_classes/AudioStreamInteractive.xml
@@ -4,7 +4,7 @@
Audio stream that can playback music interactively, combining clips and a transition table.
</brief_description>
<description>
- This is an audio stream that can playback music interactively, combining clips and a transition table. Clips must be added first, and the transition rules via the [method add_transition]. Additionally, this stream export a property parameter to control the playback via [AudioStreamPlayer], [AudioStreamPlayer2D], or [AudioStreamPlayer3D].
+ This is an audio stream that can playback music interactively, combining clips and a transition table. Clips must be added first, and then the transition rules via the [method add_transition]. Additionally, this stream exports a property parameter to control the playback via [AudioStreamPlayer], [AudioStreamPlayer2D], or [AudioStreamPlayer3D].
The way this is used is by filling a number of clips, then configuring the transition table. From there, clips are selected for playback and the music will smoothly go from the current to the new one while using the corresponding transition rule defined in the transition table.
</description>
<tutorials>
diff --git a/modules/mono/build_scripts/build_assemblies.py b/modules/mono/build_scripts/build_assemblies.py
index efbf298f99..9f88b0575e 100755
--- a/modules/mono/build_scripts/build_assemblies.py
+++ b/modules/mono/build_scripts/build_assemblies.py
@@ -194,7 +194,7 @@ def run_msbuild(tools: ToolsLocation, sln: str, chdir_to: str, msbuild_args: Opt
return subprocess.call(args, env=msbuild_env, cwd=chdir_to)
-def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision):
+def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision, no_deprecated):
target_filenames = [
"GodotSharp.dll",
"GodotSharp.pdb",
@@ -217,6 +217,8 @@ def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, pre
args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
if precision == "double":
args += ["/p:GodotFloat64=true"]
+ if no_deprecated:
+ args += ["/p:GodotNoDeprecated=true"]
sln = os.path.join(module_dir, "glue/GodotSharp/GodotSharp.sln")
exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
@@ -336,12 +338,14 @@ def generate_sdk_package_versions():
f.write(constants)
-def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision):
+def build_all(
+ msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, precision, no_deprecated
+):
# Generate SdkPackageVersions.props and VersionDocsUrl constant
generate_sdk_package_versions()
# Godot API
- exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision)
+ exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, precision, no_deprecated)
if exit_code != 0:
return exit_code
@@ -364,6 +368,8 @@ def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, p
args += ["/p:ClearNuGetLocalCache=true", "/p:PushNuGetToLocalSource=" + push_nupkgs_local]
if precision == "double":
args += ["/p:GodotFloat64=true"]
+ if no_deprecated:
+ args += ["/p:GodotNoDeprecated=true"]
sln = os.path.join(module_dir, "editor/Godot.NET.Sdk/Godot.NET.Sdk.sln")
exit_code = run_msbuild(msbuild_tool, sln=sln, chdir_to=module_dir, msbuild_args=args)
if exit_code != 0:
@@ -390,6 +396,12 @@ def main():
parser.add_argument(
"--precision", type=str, default="single", choices=["single", "double"], help="Floating-point precision level"
)
+ parser.add_argument(
+ "--no-deprecated",
+ action="store_true",
+ default=False,
+ help="Build GodotSharp without using deprecated features. This is required, if the engine was built with 'deprecated=no'.",
+ )
args = parser.parse_args()
@@ -414,6 +426,7 @@ def main():
args.dev_debug,
push_nupkgs_local,
args.precision,
+ args.no_deprecated,
)
sys.exit(exit_code)
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
index 6a3884dabf..c734dc7be1 100644
--- 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
@@ -807,7 +807,7 @@ partial class ExportedFields
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)27, name: PropertyName.@_fieldGodotGenericDictionary, hint: (global::Godot.PropertyHint)38, hintString: "4/0:;1/0:", 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;
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
index 3c48740773..0de840aa34 100644
--- 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
@@ -925,7 +925,7 @@ partial class ExportedProperties
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)27, name: PropertyName.@PropertyGodotGenericDictionary, hint: (global::Godot.PropertyHint)38, hintString: "4/0:;1/0:", 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;
}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs
index 4cf6a9f431..bb4c4824e7 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs
@@ -88,7 +88,8 @@ namespace Godot.SourceGenerators
HideQuaternionEdit = 35,
Password = 36,
LayersAvoidance = 37,
- Max = 38
+ DictionaryType = 38,
+ Max = 39
}
[Flags]
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs
index d272832950..f5f51722b4 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs
@@ -274,6 +274,14 @@ namespace Godot.SourceGenerators
return null;
}
+ public static ITypeSymbol[]? GetGenericElementTypes(ITypeSymbol typeSymbol)
+ {
+ if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType)
+ return genericType.TypeArguments.ToArray();
+
+ return null;
+ }
+
private static StringBuilder Append(this StringBuilder source, string a, string b)
=> source.Append(a).Append(b);
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 29bae6413d..0f86b3b91c 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
@@ -728,8 +729,72 @@ namespace Godot.SourceGenerators
if (!isTypeArgument && variantType == VariantType.Dictionary)
{
- // TODO: Dictionaries are not supported in the inspector
- return false;
+ var elementTypes = MarshalUtils.GetGenericElementTypes(type);
+
+ if (elementTypes == null)
+ return false; // Non-generic Dictionary, so there's no hint to add
+ Debug.Assert(elementTypes.Length == 2);
+
+ var keyElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[0], typeCache)!.Value;
+ var keyElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(keyElementMarshalType)!.Value;
+ var keyIsPresetHint = false;
+ var keyHintString = (string?)null;
+
+ if (keyElementVariantType == VariantType.String || keyElementVariantType == VariantType.StringName)
+ keyIsPresetHint = GetStringArrayEnumHint(keyElementVariantType, exportAttr, out keyHintString);
+
+ if (!keyIsPresetHint)
+ {
+ bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[0],
+ exportAttr, keyElementVariantType, isTypeArgument: true,
+ out var keyElementHint, out var keyElementHintString);
+
+ // Format: type/hint:hint_string
+ if (hintRes)
+ {
+ keyHintString = (int)keyElementVariantType + "/" + (int)keyElementHint + ":";
+
+ if (keyElementHintString != null)
+ keyHintString += keyElementHintString;
+ }
+ else
+ {
+ keyHintString = (int)keyElementVariantType + "/" + (int)PropertyHint.None + ":";
+ }
+ }
+
+ var valueElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[1], typeCache)!.Value;
+ var valueElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(valueElementMarshalType)!.Value;
+ var valueIsPresetHint = false;
+ var valueHintString = (string?)null;
+
+ if (valueElementVariantType == VariantType.String || valueElementVariantType == VariantType.StringName)
+ valueIsPresetHint = GetStringArrayEnumHint(valueElementVariantType, exportAttr, out valueHintString);
+
+ if (!valueIsPresetHint)
+ {
+ bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[1],
+ exportAttr, valueElementVariantType, isTypeArgument: true,
+ out var valueElementHint, out var valueElementHintString);
+
+ // Format: type/hint:hint_string
+ if (hintRes)
+ {
+ valueHintString = (int)valueElementVariantType + "/" + (int)valueElementHint + ":";
+
+ if (valueElementHintString != null)
+ valueHintString += valueElementHintString;
+ }
+ else
+ {
+ valueHintString = (int)valueElementVariantType + "/" + (int)PropertyHint.None + ":";
+ }
+ }
+
+ hint = PropertyHint.DictionaryType;
+
+ hintString = keyHintString != null && valueHintString != null ? $"{keyHintString};{valueHintString}" : null;
+ return hintString != null;
}
return false;
diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp
index b26f6d1bbf..2ec073e4fa 100644
--- a/modules/mono/editor/bindings_generator.cpp
+++ b/modules/mono/editor/bindings_generator.cpp
@@ -3809,6 +3809,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
} else if (return_info.type == Variant::ARRAY && return_info.hint == PROPERTY_HINT_ARRAY_TYPE) {
imethod.return_type.cname = Variant::get_type_name(return_info.type) + "_@generic";
imethod.return_type.generic_type_parameters.push_back(TypeReference(return_info.hint_string));
+ } else if (return_info.type == Variant::DICTIONARY && return_info.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ imethod.return_type.cname = Variant::get_type_name(return_info.type) + "_@generic";
+ Vector<String> split = return_info.hint_string.split(";");
+ imethod.return_type.generic_type_parameters.push_back(TypeReference(split.get(0)));
+ imethod.return_type.generic_type_parameters.push_back(TypeReference(split.get(1)));
} else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) {
imethod.return_type.cname = return_info.hint_string;
} else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
@@ -3836,6 +3841,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
} else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) {
iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic";
iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string));
+ } else if (arginfo.type == Variant::DICTIONARY && arginfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic";
+ Vector<String> split = arginfo.hint_string.split(";");
+ iarg.type.generic_type_parameters.push_back(TypeReference(split.get(0)));
+ iarg.type.generic_type_parameters.push_back(TypeReference(split.get(1)));
} else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) {
iarg.type.cname = arginfo.hint_string;
} else if (arginfo.type == Variant::NIL) {
@@ -3963,6 +3973,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() {
} else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) {
iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic";
iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string));
+ } else if (arginfo.type == Variant::DICTIONARY && arginfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) {
+ iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic";
+ Vector<String> split = arginfo.hint_string.split(";");
+ iarg.type.generic_type_parameters.push_back(TypeReference(split.get(0)));
+ iarg.type.generic_type_parameters.push_back(TypeReference(split.get(1)));
} else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) {
iarg.type.cname = arginfo.hint_string;
} else if (arginfo.type == Variant::NIL) {
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
index b838f8eac7..3a3134d160 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
+++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
@@ -135,7 +135,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<!-- Compat Sources -->
- <ItemGroup>
+ <ItemGroup Condition=" '$(GodotNoDeprecated)' == '' ">
<Compile Include="Compat.cs" />
</ItemGroup>
<!--
diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
index 65b4824f94..715c1a4d51 100644
--- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
+++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
@@ -36,7 +36,7 @@
</ProjectReference>
</ItemGroup>
<!-- Compat Sources -->
- <ItemGroup>
+ <ItemGroup Condition=" '$(GodotNoDeprecated)' == '' ">
<Compile Include="Compat.cs" />
</ItemGroup>
<!--
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
index d3daa1dbbc..2e1de9a607 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
@@ -48,7 +48,12 @@ enum class LaunchPolicy {
/**
* Adjacent launches are enabled.
*/
- ADJACENT
+ ADJACENT,
+
+ /**
+ * Launches happen in the same window but start in PiP mode.
+ */
+ SAME_AND_LAUNCH_IN_PIP_MODE
}
/**
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 1995a38c2a..405b2fb57f 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -101,6 +101,7 @@ open class GodotEditor : GodotActivity() {
private const val ANDROID_WINDOW_AUTO = 0
private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
+ private const val ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE = 3
/**
* Sets of constants to specify the Play window PiP mode.
@@ -244,25 +245,30 @@ open class GodotEditor : GodotActivity() {
val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) {
val pipMode = getPlayWindowPiPMode()
pipMode == PLAY_WINDOW_PIP_ENABLED ||
- (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME)
+ (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR &&
+ (launchPolicy == LaunchPolicy.SAME || launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE))
} else {
false
}
newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable)
+ var launchInPiP = false
if (launchPolicy == LaunchPolicy.ADJACENT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Log.v(TAG, "Adding flag for adjacent launch")
newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
}
} else if (launchPolicy == LaunchPolicy.SAME) {
- if (isPiPAvailable &&
- (updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))) {
- Log.v(TAG, "Launching in PiP mode because of breakpoints")
- newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true)
- }
+ launchInPiP = isPiPAvailable &&
+ (updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))
+ } else if (launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE) {
+ launchInPiP = isPiPAvailable
}
+ if (launchInPiP) {
+ Log.v(TAG, "Launching in PiP mode")
+ newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, launchInPiP)
+ }
return newInstance
}
@@ -403,6 +409,7 @@ open class GodotEditor : GodotActivity() {
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
+ ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE
else -> {
// ANDROID_WINDOW_AUTO
defaultLaunchPolicy
diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
index 34ad52bbf6..c261c252ba 100644
--- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
+++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml
@@ -38,8 +38,11 @@
<member name="application/icon_interpolation" type="int" setter="" getter="">
Interpolation method used to resize application icon.
</member>
- <member name="application/min_macos_version" type="String" setter="" getter="">
- Minimum version of macOS required for this application to run in the [code]major.minor.patch[/code] or [code]major.minor[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]).
+ <member name="application/min_macos_version_arm64" type="String" setter="" getter="">
+ Minimum version of macOS required for this application to run on Apple Silicon Macs, in the [code]major.minor.patch[/code] or [code]major.minor[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]).
+ </member>
+ <member name="application/min_macos_version_x86_64" type="String" setter="" getter="">
+ Minimum version of macOS required for this application to run on Intel Macs, in the [code]major.minor.patch[/code] or [code]major.minor[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]).
</member>
<member name="application/short_version" type="String" setter="" getter="">
Application version visible to the user, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). Falls back to [member ProjectSettings.application/config/version] if left empty.
diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp
index 8372600ae9..770871ae19 100644
--- a/platform/macos/export/export_plugin.cpp
+++ b/platform/macos/export/export_plugin.cpp
@@ -451,7 +451,8 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary()));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version"), "10.12"));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version_x86_64"), "10.12"));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version_arm64"), "11.00"));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_angle", PROPERTY_HINT_ENUM, "Auto,Yes,No"), 0, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), true));
@@ -823,8 +824,12 @@ void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_pres
strnew += lines[i].replace("$app_category", cat.to_lower()) + "\n";
} else if (lines[i].contains("$copyright")) {
strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
+ } else if (lines[i].contains("$min_version_arm64")) {
+ strnew += lines[i].replace("$min_version_arm64", p_preset->get("application/min_macos_version_arm64")) + "\n";
+ } else if (lines[i].contains("$min_version_x86_64")) {
+ strnew += lines[i].replace("$min_version_x86_64", p_preset->get("application/min_macos_version_x86_64")) + "\n";
} else if (lines[i].contains("$min_version")) {
- strnew += lines[i].replace("$min_version", p_preset->get("application/min_macos_version")) + "\n";
+ strnew += lines[i].replace("$min_version", p_preset->get("application/min_macos_version_x86_64")) + "\n"; // Old template, use x86-64 version for both.
} else if (lines[i].contains("$highres")) {
strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
} else if (lines[i].contains("$additional_plist_content")) {
diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp
index ef8f90421b..51facbaa84 100644
--- a/platform/web/os_web.cpp
+++ b/platform/web/os_web.cpp
@@ -105,7 +105,7 @@ Error OS_Web::execute(const String &p_path, const List<String> &p_arguments, Str
return create_process(p_path, p_arguments);
}
-Dictionary OS_Web::execute_with_pipe(const String &p_path, const List<String> &p_arguments) {
+Dictionary OS_Web::execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking) {
ERR_FAIL_V_MSG(Dictionary(), "OS::execute_with_pipe is not available on the Web platform.");
}
diff --git a/platform/web/os_web.h b/platform/web/os_web.h
index 55a5fcc6c6..1ddb745965 100644
--- a/platform/web/os_web.h
+++ b/platform/web/os_web.h
@@ -80,7 +80,7 @@ public:
bool main_loop_iterate();
Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override;
- Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override;
+ Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking = true) override;
Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
Error kill(const ProcessID &p_pid) override;
int get_process_id() const override;
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 602ca5f52e..50ebe7077f 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -522,7 +522,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
if (!item.has("name") || !item.has("values") || !item.has("default")) {
continue;
}
- event_handler->add_option(pfdc, item["name"], item["values"], item["default_idx"]);
+ event_handler->add_option(pfdc, item["name"], item["values"], item["default"]);
}
event_handler->set_root(fd->root);
@@ -625,62 +625,41 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
}
}
if (fd->callback.is_valid()) {
- if (fd->options_in_cb) {
- Variant v_result = true;
- Variant v_files = file_names;
- Variant v_index = index;
- Variant v_opt = options;
- const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
-
- fd->callback.call_deferredp(cb_args, 4);
- } else {
- Variant v_result = true;
- Variant v_files = file_names;
- Variant v_index = index;
- const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
-
- fd->callback.call_deferredp(cb_args, 3);
- }
+ MutexLock lock(ds->file_dialog_mutex);
+ FileDialogCallback cb;
+ cb.callback = fd->callback;
+ cb.status = true;
+ cb.files = file_names;
+ cb.index = index;
+ cb.options = options;
+ cb.opt_in_cb = fd->options_in_cb;
+ ds->pending_cbs.push_back(cb);
}
} else {
if (fd->callback.is_valid()) {
- if (fd->options_in_cb) {
- Variant v_result = false;
- Variant v_files = Vector<String>();
- Variant v_index = 0;
- Variant v_opt = Dictionary();
- const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
-
- fd->callback.call_deferredp(cb_args, 4);
- } else {
- Variant v_result = false;
- Variant v_files = Vector<String>();
- Variant v_index = 0;
- const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
-
- fd->callback.call_deferredp(cb_args, 3);
- }
+ MutexLock lock(ds->file_dialog_mutex);
+ FileDialogCallback cb;
+ cb.callback = fd->callback;
+ cb.status = false;
+ cb.files = Vector<String>();
+ cb.index = index;
+ cb.options = options;
+ cb.opt_in_cb = fd->options_in_cb;
+ ds->pending_cbs.push_back(cb);
}
}
pfd->Release();
} else {
if (fd->callback.is_valid()) {
- if (fd->options_in_cb) {
- Variant v_result = false;
- Variant v_files = Vector<String>();
- Variant v_index = 0;
- Variant v_opt = Dictionary();
- const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt };
-
- fd->callback.call_deferredp(cb_args, 4);
- } else {
- Variant v_result = false;
- Variant v_files = Vector<String>();
- Variant v_index = 0;
- const Variant *cb_args[3] = { &v_result, &v_files, &v_index };
-
- fd->callback.call_deferredp(cb_args, 3);
- }
+ MutexLock lock(ds->file_dialog_mutex);
+ FileDialogCallback cb;
+ cb.callback = fd->callback;
+ cb.status = false;
+ cb.files = Vector<String>();
+ cb.index = 0;
+ cb.options = Dictionary();
+ cb.opt_in_cb = fd->options_in_cb;
+ ds->pending_cbs.push_back(cb);
}
}
{
@@ -768,6 +747,34 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title
return OK;
}
+void DisplayServerWindows::process_file_dialog_callbacks() {
+ MutexLock lock(file_dialog_mutex);
+ while (!pending_cbs.is_empty()) {
+ FileDialogCallback cb = pending_cbs.front()->get();
+ pending_cbs.pop_front();
+
+ if (cb.opt_in_cb) {
+ Variant ret;
+ Callable::CallError ce;
+ const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options };
+
+ cb.callback.callp(args, 4, ret, ce);
+ if (ce.error != Callable::CallError::CALL_OK) {
+ ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce)));
+ }
+ } else {
+ Variant ret;
+ Callable::CallError ce;
+ const Variant *args[3] = { &cb.status, &cb.files, &cb.index };
+
+ cb.callback.callp(args, 3, ret, ce);
+ if (ce.error != Callable::CallError::CALL_OK) {
+ ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce)));
+ }
+ }
+ }
+}
+
void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) {
_THREAD_SAFE_METHOD_
@@ -3190,6 +3197,7 @@ void DisplayServerWindows::process_events() {
memdelete(E->get());
E->erase();
}
+ process_file_dialog_callbacks();
}
void DisplayServerWindows::force_process_and_drop_events() {
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index 3deb7ac8b0..54e1c9681d 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -572,6 +572,16 @@ class DisplayServerWindows : public DisplayServer {
Mutex file_dialog_mutex;
List<FileDialogData *> file_dialogs;
HashMap<HWND, FileDialogData *> file_dialog_wnd;
+ struct FileDialogCallback {
+ Callable callback;
+ Variant status;
+ Variant files;
+ Variant index;
+ Variant options;
+ bool opt_in_cb = false;
+ };
+ List<FileDialogCallback> pending_cbs;
+ void process_file_dialog_callbacks();
static void _thread_fd_monitor(void *p_ud);
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index 47836788e1..f9c636a4a6 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -878,7 +878,7 @@ Dictionary OS_Windows::get_memory_info() const {
return meminfo;
}
-Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String> &p_arguments) {
+Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking) {
#define CLEAN_PIPES \
if (pipe_in[0] != 0) { \
CloseHandle(pipe_in[0]); \
@@ -977,11 +977,11 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String
Ref<FileAccessWindowsPipe> main_pipe;
main_pipe.instantiate();
- main_pipe->open_existing(pipe_out[0], pipe_in[1]);
+ main_pipe->open_existing(pipe_out[0], pipe_in[1], p_blocking);
Ref<FileAccessWindowsPipe> err_pipe;
err_pipe.instantiate();
- err_pipe->open_existing(pipe_err[0], 0);
+ err_pipe->open_existing(pipe_err[0], 0, p_blocking);
ret["stdio"] = main_pipe;
ret["stderr"] = err_pipe;
@@ -1614,16 +1614,7 @@ String OS_Windows::get_executable_path() const {
}
bool OS_Windows::has_environment(const String &p_var) const {
-#ifdef MINGW_ENABLED
- return _wgetenv((LPCWSTR)(p_var.utf16().get_data())) != nullptr;
-#else
- WCHAR *env;
- size_t len;
- _wdupenv_s(&env, &len, (LPCWSTR)(p_var.utf16().get_data()));
- const bool has_env = env != nullptr;
- free(env);
- return has_env;
-#endif
+ return GetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), nullptr, 0) > 0;
}
String OS_Windows::get_environment(const String &p_var) const {
diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h
index 9c7b98d7fd..4f9bc049ee 100644
--- a/platform/windows/os_windows.h
+++ b/platform/windows/os_windows.h
@@ -188,7 +188,7 @@ public:
virtual Dictionary get_memory_info() const override;
virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override;
- virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override;
+ virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking = true) override;
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
virtual Error kill(const ProcessID &p_pid) override;
virtual int get_process_id() const override;
diff --git a/scene/2d/navigation_region_2d.cpp b/scene/2d/navigation_region_2d.cpp
index 9b3c7bb9ea..f65a3c0ecc 100644
--- a/scene/2d/navigation_region_2d.cpp
+++ b/scene/2d/navigation_region_2d.cpp
@@ -452,6 +452,7 @@ void NavigationRegion2D::_update_debug_mesh() {
const Transform2D region_gt = get_global_transform();
rs->canvas_item_set_parent(debug_instance_rid, get_world_2d()->get_canvas());
+ rs->canvas_item_set_z_index(debug_instance_rid, RS::CANVAS_ITEM_Z_MAX - 2);
rs->canvas_item_set_transform(debug_instance_rid, region_gt);
if (!debug_mesh_dirty) {
diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp
index 282d14da5d..c3768386e9 100644
--- a/scene/2d/path_2d.cpp
+++ b/scene/2d/path_2d.cpp
@@ -378,9 +378,10 @@ real_t PathFollow2D::get_progress() const {
}
void PathFollow2D::set_progress_ratio(real_t p_ratio) {
- if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) {
- set_progress(p_ratio * path->get_curve()->get_baked_length());
- }
+ ERR_FAIL_NULL_MSG(path, "Can only set progress ratio on a PathFollow2D that is the child of a Path2D which is itself part of the scene tree.");
+ ERR_FAIL_COND_MSG(path->get_curve().is_null(), "Can't set progress ratio on a PathFollow2D that does not have a Curve.");
+ ERR_FAIL_COND_MSG(!path->get_curve()->get_baked_length(), "Can't set progress ratio on a PathFollow2D that has a 0 length curve.");
+ set_progress(p_ratio * path->get_curve()->get_baked_length());
}
real_t PathFollow2D::get_progress_ratio() const {
diff --git a/scene/3d/mesh_instance_3d.cpp b/scene/3d/mesh_instance_3d.cpp
index 987117a7a0..f551cb401c 100644
--- a/scene/3d/mesh_instance_3d.cpp
+++ b/scene/3d/mesh_instance_3d.cpp
@@ -671,6 +671,172 @@ Ref<ArrayMesh> MeshInstance3D::bake_mesh_from_current_blend_shape_mix(Ref<ArrayM
return bake_mesh;
}
+Ref<ArrayMesh> MeshInstance3D::bake_mesh_from_current_skeleton_pose(Ref<ArrayMesh> p_existing) {
+ Ref<ArrayMesh> source_mesh = get_mesh();
+ ERR_FAIL_COND_V_MSG(source_mesh.is_null(), Ref<ArrayMesh>(), "The source mesh must be a valid ArrayMesh.");
+
+ Ref<ArrayMesh> bake_mesh;
+
+ if (p_existing.is_valid()) {
+ ERR_FAIL_COND_V_MSG(source_mesh == p_existing, Ref<ArrayMesh>(), "The source mesh can not be the same mesh as the existing mesh.");
+
+ bake_mesh = p_existing;
+ } else {
+ bake_mesh.instantiate();
+ }
+
+ ERR_FAIL_COND_V_MSG(skin_ref.is_null(), Ref<ArrayMesh>(), "The source mesh must have a valid skin.");
+ ERR_FAIL_COND_V_MSG(skin_internal.is_null(), Ref<ArrayMesh>(), "The source mesh must have a valid skin.");
+ RID skeleton = skin_ref->get_skeleton();
+ ERR_FAIL_COND_V_MSG(!skeleton.is_valid(), Ref<ArrayMesh>(), "The source mesh must have its skin registered with a valid skeleton.");
+
+ const int bone_count = RenderingServer::get_singleton()->skeleton_get_bone_count(skeleton);
+ ERR_FAIL_COND_V(bone_count <= 0, Ref<ArrayMesh>());
+ ERR_FAIL_COND_V(bone_count < skin_internal->get_bind_count(), Ref<ArrayMesh>());
+
+ LocalVector<Transform3D> bone_transforms;
+ bone_transforms.resize(bone_count);
+ for (int bone_index = 0; bone_index < bone_count; bone_index++) {
+ bone_transforms[bone_index] = RenderingServer::get_singleton()->skeleton_bone_get_transform(skeleton, bone_index);
+ }
+
+ bake_mesh->clear_surfaces();
+
+ int mesh_surface_count = source_mesh->get_surface_count();
+
+ for (int surface_index = 0; surface_index < mesh_surface_count; surface_index++) {
+ ERR_CONTINUE(source_mesh->surface_get_primitive_type(surface_index) != Mesh::PRIMITIVE_TRIANGLES);
+
+ uint32_t surface_format = source_mesh->surface_get_format(surface_index);
+
+ ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_VERTEX));
+ ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_BONES));
+ ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_WEIGHTS));
+
+ unsigned int bones_per_vertex = surface_format & Mesh::ARRAY_FLAG_USE_8_BONE_WEIGHTS ? 8 : 4;
+
+ surface_format &= ~Mesh::ARRAY_FORMAT_BONES;
+ surface_format &= ~Mesh::ARRAY_FORMAT_WEIGHTS;
+
+ const Array &source_mesh_arrays = source_mesh->surface_get_arrays(surface_index);
+
+ ERR_FAIL_COND_V(source_mesh_arrays.size() != RS::ARRAY_MAX, Ref<ArrayMesh>());
+
+ const Vector<Vector3> &source_mesh_vertex_array = source_mesh_arrays[Mesh::ARRAY_VERTEX];
+ const Vector<Vector3> &source_mesh_normal_array = source_mesh_arrays[Mesh::ARRAY_NORMAL];
+ const Vector<float> &source_mesh_tangent_array = source_mesh_arrays[Mesh::ARRAY_TANGENT];
+ const Vector<int> &source_mesh_bones_array = source_mesh_arrays[Mesh::ARRAY_BONES];
+ const Vector<float> &source_mesh_weights_array = source_mesh_arrays[Mesh::ARRAY_WEIGHTS];
+
+ unsigned int vertex_count = source_mesh_vertex_array.size();
+ int expected_bone_array_size = vertex_count * bones_per_vertex;
+ ERR_CONTINUE(source_mesh_bones_array.size() != expected_bone_array_size);
+ ERR_CONTINUE(source_mesh_weights_array.size() != expected_bone_array_size);
+
+ Array new_mesh_arrays;
+ new_mesh_arrays.resize(Mesh::ARRAY_MAX);
+ for (int i = 0; i < source_mesh_arrays.size(); i++) {
+ if (i == Mesh::ARRAY_VERTEX || i == Mesh::ARRAY_NORMAL || i == Mesh::ARRAY_TANGENT || i == Mesh::ARRAY_BONES || i == Mesh::ARRAY_WEIGHTS) {
+ continue;
+ }
+ new_mesh_arrays[i] = source_mesh_arrays[i];
+ }
+
+ bool use_normal_array = source_mesh_normal_array.size() == source_mesh_vertex_array.size();
+ bool use_tangent_array = source_mesh_tangent_array.size() / 4 == source_mesh_vertex_array.size();
+
+ Vector<Vector3> lerped_vertex_array = source_mesh_vertex_array;
+ Vector<Vector3> lerped_normal_array = source_mesh_normal_array;
+ Vector<float> lerped_tangent_array = source_mesh_tangent_array;
+
+ const Vector3 *source_vertices_ptr = source_mesh_vertex_array.ptr();
+ const Vector3 *source_normals_ptr = source_mesh_normal_array.ptr();
+ const float *source_tangents_ptr = source_mesh_tangent_array.ptr();
+ const int *source_bones_ptr = source_mesh_bones_array.ptr();
+ const float *source_weights_ptr = source_mesh_weights_array.ptr();
+
+ Vector3 *lerped_vertices_ptrw = lerped_vertex_array.ptrw();
+ Vector3 *lerped_normals_ptrw = lerped_normal_array.ptrw();
+ float *lerped_tangents_ptrw = lerped_tangent_array.ptrw();
+
+ for (unsigned int vertex_index = 0; vertex_index < vertex_count; vertex_index++) {
+ Vector3 lerped_vertex;
+ Vector3 lerped_normal;
+ Vector3 lerped_tangent;
+
+ const Vector3 &source_vertex = source_vertices_ptr[vertex_index];
+
+ Vector3 source_normal;
+ if (use_normal_array) {
+ source_normal = source_normals_ptr[vertex_index];
+ }
+
+ int tangent_index = vertex_index * 4;
+ Vector4 source_tangent;
+ Vector3 source_tangent_vec3;
+ if (use_tangent_array) {
+ source_tangent = Vector4(
+ source_tangents_ptr[tangent_index],
+ source_tangents_ptr[tangent_index + 1],
+ source_tangents_ptr[tangent_index + 2],
+ source_tangents_ptr[tangent_index + 3]);
+
+ DEV_ASSERT(source_tangent.w == 1.0 || source_tangent.w == -1.0);
+
+ source_tangent_vec3 = Vector3(source_tangent.x, source_tangent.y, source_tangent.z);
+ }
+
+ for (unsigned int weight_index = 0; weight_index < bones_per_vertex; weight_index++) {
+ float bone_weight = source_weights_ptr[vertex_index * bones_per_vertex + weight_index];
+ if (bone_weight < FLT_EPSILON) {
+ continue;
+ }
+ int vertex_bone_index = source_bones_ptr[vertex_index * bones_per_vertex + weight_index];
+ const Transform3D &bone_transform = bone_transforms[vertex_bone_index];
+ const Basis bone_basis = bone_transform.basis.orthonormalized();
+
+ ERR_FAIL_INDEX_V(vertex_bone_index, static_cast<int>(bone_transforms.size()), Ref<ArrayMesh>());
+
+ lerped_vertex += source_vertex.lerp(bone_transform.xform(source_vertex), bone_weight) - source_vertex;
+ ;
+
+ if (use_normal_array) {
+ lerped_normal += source_normal.lerp(bone_basis.xform(source_normal), bone_weight) - source_normal;
+ }
+
+ if (use_tangent_array) {
+ lerped_tangent += source_tangent_vec3.lerp(bone_basis.xform(source_tangent_vec3), bone_weight) - source_tangent_vec3;
+ }
+ }
+
+ lerped_vertices_ptrw[vertex_index] += lerped_vertex;
+
+ if (use_normal_array) {
+ lerped_normals_ptrw[vertex_index] = (source_normal + lerped_normal).normalized();
+ }
+
+ if (use_tangent_array) {
+ lerped_tangent = (source_tangent_vec3 + lerped_tangent).normalized();
+ lerped_tangents_ptrw[tangent_index] = lerped_tangent.x;
+ lerped_tangents_ptrw[tangent_index + 1] = lerped_tangent.y;
+ lerped_tangents_ptrw[tangent_index + 2] = lerped_tangent.z;
+ }
+ }
+
+ new_mesh_arrays[Mesh::ARRAY_VERTEX] = lerped_vertex_array;
+ if (use_normal_array) {
+ new_mesh_arrays[Mesh::ARRAY_NORMAL] = lerped_normal_array;
+ }
+ if (use_tangent_array) {
+ new_mesh_arrays[Mesh::ARRAY_TANGENT] = lerped_tangent_array;
+ }
+
+ bake_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, new_mesh_arrays, Array(), Dictionary(), surface_format);
+ }
+
+ return bake_mesh;
+}
+
void MeshInstance3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_mesh", "mesh"), &MeshInstance3D::set_mesh);
ClassDB::bind_method(D_METHOD("get_mesh"), &MeshInstance3D::get_mesh);
@@ -700,6 +866,7 @@ void MeshInstance3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("create_debug_tangents"), &MeshInstance3D::create_debug_tangents);
ClassDB::bind_method(D_METHOD("bake_mesh_from_current_blend_shape_mix", "existing"), &MeshInstance3D::bake_mesh_from_current_blend_shape_mix, DEFVAL(Ref<ArrayMesh>()));
+ ClassDB::bind_method(D_METHOD("bake_mesh_from_current_skeleton_pose", "existing"), &MeshInstance3D::bake_mesh_from_current_skeleton_pose, DEFVAL(Ref<ArrayMesh>()));
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh"), "set_mesh", "get_mesh");
ADD_GROUP("Skeleton", "");
diff --git a/scene/3d/mesh_instance_3d.h b/scene/3d/mesh_instance_3d.h
index 8a7e03c5b3..0eff12762d 100644
--- a/scene/3d/mesh_instance_3d.h
+++ b/scene/3d/mesh_instance_3d.h
@@ -102,6 +102,7 @@ public:
virtual AABB get_aabb() const override;
Ref<ArrayMesh> bake_mesh_from_current_blend_shape_mix(Ref<ArrayMesh> p_existing = Ref<ArrayMesh>());
+ Ref<ArrayMesh> bake_mesh_from_current_skeleton_pose(Ref<ArrayMesh> p_existing = Ref<ArrayMesh>());
MeshInstance3D();
~MeshInstance3D();
diff --git a/scene/3d/path_3d.cpp b/scene/3d/path_3d.cpp
index 1f8f7cd54c..dc030b6a0f 100644
--- a/scene/3d/path_3d.cpp
+++ b/scene/3d/path_3d.cpp
@@ -461,9 +461,10 @@ real_t PathFollow3D::get_progress() const {
}
void PathFollow3D::set_progress_ratio(real_t p_ratio) {
- if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) {
- set_progress(p_ratio * path->get_curve()->get_baked_length());
- }
+ ERR_FAIL_NULL_MSG(path, "Can only set progress ratio on a PathFollow3D that is the child of a Path3D which is itself part of the scene tree.");
+ ERR_FAIL_COND_MSG(path->get_curve().is_null(), "Can't set progress ratio on a PathFollow3D that does not have a Curve.");
+ ERR_FAIL_COND_MSG(!path->get_curve()->get_baked_length(), "Can't set progress ratio on a PathFollow3D that has a 0 length curve.");
+ set_progress(p_ratio * path->get_curve()->get_baked_length());
}
real_t PathFollow3D::get_progress_ratio() const {
diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp
index aa63fb623f..3b533da701 100644
--- a/scene/3d/xr_hand_modifier_3d.cpp
+++ b/scene/3d/xr_hand_modifier_3d.cpp
@@ -30,6 +30,7 @@
#include "xr_hand_modifier_3d.h"
+#include "core/config/project_settings.h"
#include "servers/xr/xr_pose.h"
#include "servers/xr_server.h"
@@ -283,6 +284,17 @@ void XRHandModifier3D::_skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) {
_get_joint_data();
}
+PackedStringArray XRHandModifier3D::get_configuration_warnings() const {
+ PackedStringArray warnings = SkeletonModifier3D::get_configuration_warnings();
+
+ // Detect OpenXR without the Hand Tracking extension.
+ if (GLOBAL_GET("xr/openxr/enabled") && !GLOBAL_GET("xr/openxr/extensions/hand_tracking")) {
+ warnings.push_back("XRHandModifier3D requires the OpenXR Hand Tracking extension to be enabled.");
+ }
+
+ return warnings;
+}
+
void XRHandModifier3D::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
diff --git a/scene/3d/xr_hand_modifier_3d.h b/scene/3d/xr_hand_modifier_3d.h
index 3d78f32b64..d58ccd0adf 100644
--- a/scene/3d/xr_hand_modifier_3d.h
+++ b/scene/3d/xr_hand_modifier_3d.h
@@ -55,6 +55,8 @@ public:
void set_bone_update(BoneUpdate p_bone_update);
BoneUpdate get_bone_update() const;
+ PackedStringArray get_configuration_warnings() const override;
+
void _notification(int p_what);
protected:
diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp
index 0ab4dbe470..1c5f40f56e 100644
--- a/scene/animation/animation_mixer.cpp
+++ b/scene/animation/animation_mixer.cpp
@@ -81,9 +81,8 @@ bool AnimationMixer::_set(const StringName &p_name, const Variant &p_value) {
List<Variant> keys;
d.get_key_list(&keys);
for (const Variant &K : keys) {
- StringName lib_name = K;
- Ref<AnimationLibrary> lib = d[lib_name];
- add_animation_library(lib_name, lib);
+ Ref<AnimationLibrary> lib = d[K];
+ add_animation_library(K, lib);
}
emit_signal(SNAME("animation_libraries_updated"));
diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp
index e8be38e680..c3287035ff 100644
--- a/scene/gui/code_edit.cpp
+++ b/scene/gui/code_edit.cpp
@@ -40,8 +40,18 @@ void CodeEdit::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
set_gutter_width(main_gutter, get_line_height());
- set_gutter_width(line_number_gutter, (line_number_digits + 1) * theme_cache.font->get_char_size('0', theme_cache.font_size).width);
+ _update_line_number_gutter_width();
set_gutter_width(fold_gutter, get_line_height() / 1.2);
+ _clear_line_number_text_cache();
+ } break;
+
+ case NOTIFICATION_TRANSLATION_CHANGED:
+ [[fallthrough]];
+ case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
+ [[fallthrough]];
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ // Avoid having many hidden text editors with unused cache filling up memory.
+ _clear_line_number_text_cache();
} break;
case NOTIFICATION_DRAW: {
@@ -1287,9 +1297,9 @@ bool CodeEdit::is_drawing_executing_lines_gutter() const {
}
void CodeEdit::_main_gutter_draw_callback(int p_line, int p_gutter, const Rect2 &p_region) {
+ bool hovering = get_hovered_gutter() == Vector2i(main_gutter, p_line);
if (draw_breakpoints && theme_cache.breakpoint_icon.is_valid()) {
bool breakpointed = is_line_breakpointed(p_line);
- bool hovering = p_region.has_point(get_local_mouse_pos());
bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
if (breakpointed || (hovering && !is_dragging_cursor() && !shift_pressed)) {
@@ -1308,7 +1318,6 @@ void CodeEdit::_main_gutter_draw_callback(int p_line, int p_gutter, const Rect2
if (draw_bookmarks && theme_cache.bookmark_icon.is_valid()) {
bool bookmarked = is_line_bookmarked(p_line);
- bool hovering = p_region.has_point(get_local_mouse_pos());
bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
if (bookmarked || (hovering && !is_dragging_cursor() && shift_pressed)) {
@@ -1442,7 +1451,13 @@ bool CodeEdit::is_draw_line_numbers_enabled() const {
}
void CodeEdit::set_line_numbers_zero_padded(bool p_zero_padded) {
- p_zero_padded ? line_number_padding = "0" : line_number_padding = " ";
+ String new_line_number_padding = p_zero_padded ? "0" : " ";
+ if (line_number_padding == new_line_number_padding) {
+ return;
+ }
+
+ line_number_padding = new_line_number_padding;
+ _clear_line_number_text_cache();
queue_redraw();
}
@@ -1451,19 +1466,55 @@ bool CodeEdit::is_line_numbers_zero_padded() const {
}
void CodeEdit::_line_number_draw_callback(int p_line, int p_gutter, const Rect2 &p_region) {
- String fc = String::num(p_line + 1).lpad(line_number_digits, line_number_padding);
- if (is_localizing_numeral_system()) {
- fc = TS->format_number(fc);
- }
- Ref<TextLine> tl;
- tl.instantiate();
- tl->add_string(fc, theme_cache.font, theme_cache.font_size);
- int yofs = p_region.position.y + (get_line_height() - tl->get_size().y) / 2;
+ if (!Rect2(Vector2(0, 0), get_size()).intersects(p_region)) {
+ return;
+ }
+
+ bool rtl = is_layout_rtl();
+ HashMap<int, RID>::Iterator E = line_number_text_cache.find(p_line);
+ RID text_rid;
+ if (E) {
+ text_rid = E->value;
+ } else {
+ String fc = String::num(p_line + 1).lpad(line_number_digits, line_number_padding);
+ if (is_localizing_numeral_system()) {
+ fc = TS->format_number(fc);
+ }
+
+ text_rid = TS->create_shaped_text();
+ if (theme_cache.font.is_valid()) {
+ TS->shaped_text_add_string(text_rid, fc, theme_cache.font->get_rids(), theme_cache.font_size, theme_cache.font->get_opentype_features());
+ }
+ line_number_text_cache.insert(p_line, text_rid);
+ }
+
+ Size2 text_size = TS->shaped_text_get_size(text_rid);
+ Point2 ofs = p_region.get_center() - text_size / 2;
+ ofs.y += TS->shaped_text_get_ascent(text_rid);
+
+ if (rtl) {
+ ofs.x = p_region.position.x;
+ } else {
+ ofs.x = p_region.get_end().x - text_size.width;
+ }
+
Color number_color = get_line_gutter_item_color(p_line, line_number_gutter);
if (number_color == Color(1, 1, 1)) {
number_color = theme_cache.line_number_color;
}
- tl->draw(get_canvas_item(), Point2(p_region.position.x, yofs), number_color);
+
+ TS->shaped_text_draw(text_rid, get_canvas_item(), ofs, -1, -1, number_color);
+}
+
+void CodeEdit::_clear_line_number_text_cache() {
+ for (const KeyValue<int, RID> &KV : line_number_text_cache) {
+ TS->free_rid(KV.value);
+ }
+ line_number_text_cache.clear();
+}
+
+void CodeEdit::_update_line_number_gutter_width() {
+ set_gutter_width(line_number_gutter, (line_number_digits + 1) * theme_cache.font->get_char_size('0', theme_cache.font_size).width);
}
/* Fold Gutter */
@@ -1983,12 +2034,18 @@ Point2 CodeEdit::get_delimiter_end_position(int p_line, int p_column) const {
/* Code hint */
void CodeEdit::set_code_hint(const String &p_hint) {
+ if (code_hint == p_hint) {
+ return;
+ }
code_hint = p_hint;
code_hint_xpos = -0xFFFF;
queue_redraw();
}
void CodeEdit::set_code_hint_draw_below(bool p_below) {
+ if (code_hint_draw_below == p_below) {
+ return;
+ }
code_hint_draw_below = p_below;
queue_redraw();
}
@@ -3609,16 +3666,13 @@ void CodeEdit::_text_changed() {
}
int lc = get_line_count();
- line_number_digits = 1;
- while (lc /= 10) {
- line_number_digits++;
- }
-
- if (theme_cache.font.is_valid()) {
- set_gutter_width(line_number_gutter, (line_number_digits + 1) * theme_cache.font->get_char_size('0', theme_cache.font_size).width);
+ int new_line_number_digits = log10l(lc) + 1;
+ if (line_number_digits != new_line_number_digits) {
+ _clear_line_number_text_cache();
}
+ line_number_digits = new_line_number_digits;
+ _update_line_number_gutter_width();
- lc = get_line_count();
List<int> breakpoints;
for (const KeyValue<int, bool> &E : breakpointed_lines) {
breakpoints.push_back(E.key);
@@ -3705,6 +3759,7 @@ CodeEdit::CodeEdit() {
}
CodeEdit::~CodeEdit() {
+ _clear_line_number_text_cache();
}
// Return true if l should come before r
diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h
index 09340be035..ab443e95e1 100644
--- a/scene/gui/code_edit.h
+++ b/scene/gui/code_edit.h
@@ -113,6 +113,9 @@ private:
int line_number_gutter = -1;
int line_number_digits = 1;
String line_number_padding = " ";
+ HashMap<int, RID> line_number_text_cache;
+ void _clear_line_number_text_cache();
+ void _update_line_number_gutter_width();
void _line_number_draw_callback(int p_line, int p_gutter, const Rect2 &p_region);
/* Fold Gutter */
diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp
index 1a1aa5ccb0..373488b0fc 100644
--- a/scene/gui/file_dialog.cpp
+++ b/scene/gui/file_dialog.cpp
@@ -124,6 +124,8 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int
file_name = ProjectSettings::get_singleton()->localize_path(file_name);
}
}
+ selected_options = p_selected_options;
+
String f = files[0];
if (mode == FILE_MODE_OPEN_FILES) {
emit_signal(SNAME("files_selected"), files);
@@ -155,7 +157,6 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int
}
file->set_text(f);
dir->set_text(f.get_base_dir());
- selected_options = p_selected_options;
filter->select(p_filter);
}
diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp
index df668aa496..c2818edd9c 100644
--- a/scene/gui/line_edit.cpp
+++ b/scene/gui/line_edit.cpp
@@ -68,10 +68,15 @@ void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
int cc = caret_column;
PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid);
- for (int i = words.size() - 2; i >= 0; i = i - 2) {
- if (words[i] < cc) {
- cc = words[i];
- break;
+ if (words.is_empty() || cc <= words[0]) {
+ // Move to the start when there are no more words.
+ cc = 0;
+ } else {
+ for (int i = words.size() - 2; i >= 0; i = i - 2) {
+ if (words[i] < cc) {
+ cc = words[i];
+ break;
+ }
}
}
@@ -101,10 +106,15 @@ void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
int cc = caret_column;
PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid);
- for (int i = 1; i < words.size(); i = i + 2) {
- if (words[i] > cc) {
- cc = words[i];
- break;
+ if (words.is_empty() || cc >= words[words.size() - 1]) {
+ // Move to the end when there are no more words.
+ cc = text.length();
+ } else {
+ for (int i = 1; i < words.size(); i = i + 2) {
+ if (words[i] > cc) {
+ cc = words[i];
+ break;
+ }
}
}
@@ -159,10 +169,15 @@ void LineEdit::_backspace(bool p_word, bool p_all_to_left) {
int cc = caret_column;
PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid);
- for (int i = words.size() - 2; i >= 0; i = i - 2) {
- if (words[i] < cc) {
- cc = words[i];
- break;
+ if (words.is_empty() || cc <= words[0]) {
+ // Delete to the start when there are no more words.
+ cc = 0;
+ } else {
+ for (int i = words.size() - 2; i >= 0; i = i - 2) {
+ if (words[i] < cc) {
+ cc = words[i];
+ break;
+ }
}
}
@@ -198,10 +213,15 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) {
if (p_word) {
int cc = caret_column;
PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid);
- for (int i = 1; i < words.size(); i = i + 2) {
- if (words[i] > cc) {
- cc = words[i];
- break;
+ if (words.is_empty() || cc >= words[words.size() - 1]) {
+ // Delete to the end when there are no more words.
+ cc = text.length();
+ } else {
+ for (int i = 1; i < words.size(); i = i + 2) {
+ if (words[i] > cc) {
+ cc = words[i];
+ break;
+ }
}
}
diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp
index bd4128384f..715b682342 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -912,84 +912,6 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o
double uth = TS->shaped_text_get_underline_thickness(rid);
off.y += l_ascent;
- // Draw inlined objects.
- Array objects = TS->shaped_text_get_objects(rid);
- for (int i = 0; i < objects.size(); i++) {
- Item *it = items.get_or_null(objects[i]);
- if (it != nullptr) {
- Vector2i obj_range = TS->shaped_text_get_object_range(rid, objects[i]);
- if (trim_chars && l.char_offset + obj_range.y > visible_characters) {
- continue;
- }
- if (trim_glyphs_ltr || trim_glyphs_rtl) {
- int obj_glyph = r_processed_glyphs + TS->shaped_text_get_object_glyph(rid, objects[i]);
- if ((trim_glyphs_ltr && (obj_glyph >= visible_glyphs)) || (trim_glyphs_rtl && (obj_glyph < total_glyphs - visible_glyphs))) {
- continue;
- }
- }
- Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]);
- switch (it->type) {
- case ITEM_IMAGE: {
- ItemImage *img = static_cast<ItemImage *>(it);
- if (img->pad) {
- Size2 pad_size = rect.size.min(img->image->get_size());
- Vector2 pad_off = (rect.size - pad_size) / 2;
- img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off + pad_off, pad_size), false, img->color);
- } else {
- img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off, rect.size), false, img->color);
- }
- } break;
- case ITEM_TABLE: {
- ItemTable *table = static_cast<ItemTable *>(it);
- Color odd_row_bg = theme_cache.table_odd_row_bg;
- Color even_row_bg = theme_cache.table_even_row_bg;
- Color border = theme_cache.table_border;
- float h_separation = theme_cache.table_h_separation;
- float v_separation = theme_cache.table_v_separation;
-
- int col_count = table->columns.size();
- int row_count = table->rows.size();
-
- int idx = 0;
- for (Item *E : table->subitems) {
- ItemFrame *frame = static_cast<ItemFrame *>(E);
-
- int col = idx % col_count;
- int row = idx / col_count;
-
- if (frame->lines.size() != 0 && row < row_count) {
- Vector2 coff = frame->lines[0].offset;
- if (rtl) {
- coff.x = rect.size.width - table->columns[col].width - coff.x;
- }
- if (row % 2 == 0) {
- Color c = frame->odd_row_bg != Color(0, 0, 0, 0) ? frame->odd_row_bg : odd_row_bg;
- if (c.a > 0.0) {
- draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), c, true);
- }
- } else {
- Color c = frame->even_row_bg != Color(0, 0, 0, 0) ? frame->even_row_bg : even_row_bg;
- if (c.a > 0.0) {
- draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), c, true);
- }
- }
- Color bc = frame->border != Color(0, 0, 0, 0) ? frame->border : border;
- if (bc.a > 0.0) {
- draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), bc, false);
- }
- }
-
- for (int j = 0; j < (int)frame->lines.size(); j++) {
- _draw_line(frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_base_color, p_outline_size, p_outline_color, p_font_shadow_color, p_shadow_outline_size, p_shadow_ofs, r_processed_glyphs);
- }
- idx++;
- }
- } break;
- default:
- break;
- }
- }
- }
const Glyph *glyphs = TS->shaped_text_get_glyphs(rid);
int gl_size = TS->shaped_text_get_glyph_count(rid);
@@ -1005,6 +927,86 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o
int processed_glyphs_step = 0;
for (int step = DRAW_STEP_BACKGROUND; step < DRAW_STEP_MAX; step++) {
+ if (step == DRAW_STEP_TEXT) {
+ // Draw inlined objects.
+ Array objects = TS->shaped_text_get_objects(rid);
+ for (int i = 0; i < objects.size(); i++) {
+ Item *it = items.get_or_null(objects[i]);
+ if (it != nullptr) {
+ Vector2i obj_range = TS->shaped_text_get_object_range(rid, objects[i]);
+ if (trim_chars && l.char_offset + obj_range.y > visible_characters) {
+ continue;
+ }
+ if (trim_glyphs_ltr || trim_glyphs_rtl) {
+ int obj_glyph = r_processed_glyphs + TS->shaped_text_get_object_glyph(rid, objects[i]);
+ if ((trim_glyphs_ltr && (obj_glyph >= visible_glyphs)) || (trim_glyphs_rtl && (obj_glyph < total_glyphs - visible_glyphs))) {
+ continue;
+ }
+ }
+ Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]);
+ switch (it->type) {
+ case ITEM_IMAGE: {
+ ItemImage *img = static_cast<ItemImage *>(it);
+ if (img->pad) {
+ Size2 pad_size = rect.size.min(img->image->get_size());
+ Vector2 pad_off = (rect.size - pad_size) / 2;
+ img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off + pad_off, pad_size), false, img->color);
+ } else {
+ img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off, rect.size), false, img->color);
+ }
+ } break;
+ case ITEM_TABLE: {
+ ItemTable *table = static_cast<ItemTable *>(it);
+ Color odd_row_bg = theme_cache.table_odd_row_bg;
+ Color even_row_bg = theme_cache.table_even_row_bg;
+ Color border = theme_cache.table_border;
+ float h_separation = theme_cache.table_h_separation;
+ float v_separation = theme_cache.table_v_separation;
+
+ int col_count = table->columns.size();
+ int row_count = table->rows.size();
+
+ int idx = 0;
+ for (Item *E : table->subitems) {
+ ItemFrame *frame = static_cast<ItemFrame *>(E);
+
+ int col = idx % col_count;
+ int row = idx / col_count;
+
+ if (frame->lines.size() != 0 && row < row_count) {
+ Vector2 coff = frame->lines[0].offset;
+ if (rtl) {
+ coff.x = rect.size.width - table->columns[col].width - coff.x;
+ }
+ if (row % 2 == 0) {
+ Color c = frame->odd_row_bg != Color(0, 0, 0, 0) ? frame->odd_row_bg : odd_row_bg;
+ if (c.a > 0.0) {
+ draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), c, true);
+ }
+ } else {
+ Color c = frame->even_row_bg != Color(0, 0, 0, 0) ? frame->even_row_bg : even_row_bg;
+ if (c.a > 0.0) {
+ draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), c, true);
+ }
+ }
+ Color bc = frame->border != Color(0, 0, 0, 0) ? frame->border : border;
+ if (bc.a > 0.0) {
+ draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position - Vector2(h_separation * 0.5, v_separation * 0.5).floor(), Size2(table->columns[col].width + h_separation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), bc, false);
+ }
+ }
+
+ for (int j = 0; j < (int)frame->lines.size(); j++) {
+ _draw_line(frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_base_color, p_outline_size, p_outline_color, p_font_shadow_color, p_shadow_outline_size, p_shadow_ofs, r_processed_glyphs);
+ }
+ idx++;
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+ }
+ }
Vector2 off_step = off;
processed_glyphs_step = r_processed_glyphs;
@@ -3043,7 +3045,7 @@ void RichTextLabel::add_text(const String &p_text) {
int pos = 0;
while (pos < p_text.length()) {
- int end = p_text.find("\n", pos);
+ int end = p_text.find_char('\n', pos);
String line;
bool eol = false;
if (end == -1) {
@@ -4092,6 +4094,74 @@ void RichTextLabel::parse_bbcode(const String &p_bbcode) {
append_text(p_bbcode);
}
+String RichTextLabel::_get_tag_value(const String &p_tag) {
+ return p_tag.substr(p_tag.find_char('=') + 1);
+}
+
+int RichTextLabel::_find_unquoted(const String &p_src, char32_t p_chr, int p_from) {
+ if (p_from < 0) {
+ return -1;
+ }
+
+ const int len = p_src.length();
+ if (len == 0) {
+ return -1;
+ }
+
+ const char32_t *src = p_src.get_data();
+ bool in_single_quote = false;
+ bool in_double_quote = false;
+ for (int i = p_from; i < len; i++) {
+ if (in_double_quote) {
+ if (src[i] == '"') {
+ in_double_quote = false;
+ }
+ } else if (in_single_quote) {
+ if (src[i] == '\'') {
+ in_single_quote = false;
+ }
+ } else {
+ if (src[i] == '"') {
+ in_double_quote = true;
+ } else if (src[i] == '\'') {
+ in_single_quote = true;
+ } else if (src[i] == p_chr) {
+ return i;
+ }
+ }
+ }
+
+ return -1;
+}
+
+Vector<String> RichTextLabel::_split_unquoted(const String &p_src, char32_t p_splitter) {
+ Vector<String> ret;
+
+ if (p_src.is_empty()) {
+ return ret;
+ }
+
+ int from = 0;
+ int len = p_src.length();
+
+ while (true) {
+ int end = _find_unquoted(p_src, p_splitter, from);
+ if (end < 0) {
+ end = len;
+ }
+ if (end > from) {
+ ret.push_back(p_src.substr(from, end - from));
+ }
+ if (end == len) {
+ break;
+ }
+
+ from = end + 1;
+ }
+
+ return ret;
+}
+
void RichTextLabel::append_text(const String &p_bbcode) {
_stop_thread();
MutexLock data_lock(data_mutex);
@@ -4110,7 +4180,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
set_process_internal(false);
while (pos <= p_bbcode.length()) {
- int brk_pos = p_bbcode.find("[", pos);
+ int brk_pos = p_bbcode.find_char('[', pos);
if (brk_pos < 0) {
brk_pos = p_bbcode.length();
@@ -4135,7 +4205,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
break; //nothing else to add
}
- int brk_end = p_bbcode.find("]", brk_pos + 1);
+ int brk_end = _find_unquoted(p_bbcode, ']', brk_pos + 1);
if (brk_end == -1) {
//no close, add the rest
@@ -4145,7 +4215,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
}
String tag = p_bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
- Vector<String> split_tag_block = tag.split(" ", false);
+ Vector<String> split_tag_block = _split_unquoted(tag, ' ');
// Find optional parameters.
String bbcode_name;
@@ -4155,7 +4225,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
bbcode_name = split_tag_block[0];
for (int i = 1; i < split_tag_block.size(); i++) {
const String &expr = split_tag_block[i];
- int value_pos = expr.find("=");
+ int value_pos = expr.find_char('=');
if (value_pos > -1) {
bbcode_options[expr.substr(0, value_pos)] = expr.substr(value_pos + 1).unquote();
}
@@ -4166,7 +4236,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
// Find main parameter.
String bbcode_value;
- int main_value_pos = bbcode_name.find("=");
+ int main_value_pos = bbcode_name.find_char('=');
if (main_value_pos > -1) {
bbcode_value = bbcode_name.substr(main_value_pos + 1);
bbcode_name = bbcode_name.substr(0, main_value_pos);
@@ -4265,10 +4335,10 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("table=")) {
- Vector<String> subtag = tag.substr(6, tag.length()).split(",");
+ Vector<String> subtag = _split_unquoted(_get_tag_value(tag), U',');
_normalize_subtags(subtag);
- int columns = subtag[0].to_int();
+ int columns = (subtag.is_empty()) ? 1 : subtag[0].to_int();
if (columns < 1) {
columns = 1;
}
@@ -4315,7 +4385,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("cell=")) {
- int ratio = tag.substr(5, tag.length()).to_int();
+ int ratio = _get_tag_value(tag).to_int();
if (ratio < 1) {
ratio = 1;
}
@@ -4326,54 +4396,45 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front("cell");
} else if (tag.begins_with("cell ")) {
- Vector<String> subtag = tag.substr(5, tag.length()).split(" ");
- _normalize_subtags(subtag);
-
- for (int i = 0; i < subtag.size(); i++) {
- Vector<String> subtag_a = subtag[i].split("=");
- _normalize_subtags(subtag_a);
-
- if (subtag_a.size() == 2) {
- if (subtag_a[0] == "expand") {
- int ratio = subtag_a[1].to_int();
- if (ratio < 1) {
- ratio = 1;
- }
- set_table_column_expand(get_current_table_column(), true, ratio);
- }
+ OptionMap::Iterator expand_option = bbcode_options.find("expand");
+ if (expand_option) {
+ int ratio = expand_option->value.to_int();
+ if (ratio < 1) {
+ ratio = 1;
}
+ set_table_column_expand(get_current_table_column(), true, ratio);
}
+
push_cell();
const Color fallback_color = Color(0, 0, 0, 0);
- for (int i = 0; i < subtag.size(); i++) {
- Vector<String> subtag_a = subtag[i].split("=");
- _normalize_subtags(subtag_a);
-
- if (subtag_a.size() == 2) {
- if (subtag_a[0] == "border") {
- Color color = Color::from_string(subtag_a[1], fallback_color);
- set_cell_border_color(color);
- } else if (subtag_a[0] == "bg") {
- Vector<String> subtag_b = subtag_a[1].split(",");
- _normalize_subtags(subtag_b);
- if (subtag_b.size() == 2) {
- Color color1 = Color::from_string(subtag_b[0], fallback_color);
- Color color2 = Color::from_string(subtag_b[1], fallback_color);
- set_cell_row_background_color(color1, color2);
- }
- if (subtag_b.size() == 1) {
- Color color1 = Color::from_string(subtag_a[1], fallback_color);
- set_cell_row_background_color(color1, color1);
- }
- } else if (subtag_a[0] == "padding") {
- Vector<String> subtag_b = subtag_a[1].split(",");
- _normalize_subtags(subtag_b);
+ OptionMap::Iterator border_option = bbcode_options.find("border");
+ if (border_option) {
+ Color color = Color::from_string(border_option->value, fallback_color);
+ set_cell_border_color(color);
+ }
+ OptionMap::Iterator bg_option = bbcode_options.find("bg");
+ if (bg_option) {
+ Vector<String> subtag_b = _split_unquoted(bg_option->value, U',');
+ _normalize_subtags(subtag_b);
- if (subtag_b.size() == 4) {
- set_cell_padding(Rect2(subtag_b[0].to_float(), subtag_b[1].to_float(), subtag_b[2].to_float(), subtag_b[3].to_float()));
- }
- }
+ if (subtag_b.size() == 2) {
+ Color color1 = Color::from_string(subtag_b[0], fallback_color);
+ Color color2 = Color::from_string(subtag_b[1], fallback_color);
+ set_cell_row_background_color(color1, color2);
+ }
+ if (subtag_b.size() == 1) {
+ Color color1 = Color::from_string(bg_option->value, fallback_color);
+ set_cell_row_background_color(color1, color1);
+ }
+ }
+ OptionMap::Iterator padding_option = bbcode_options.find("padding");
+ if (padding_option) {
+ Vector<String> subtag_b = _split_unquoted(padding_option->value, U',');
+ _normalize_subtags(subtag_b);
+
+ if (subtag_b.size() == 4) {
+ set_cell_padding(Rect2(subtag_b[0].to_float(), subtag_b[1].to_float(), subtag_b[2].to_float(), subtag_b[3].to_float()));
}
}
@@ -4390,7 +4451,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("char=")) {
- int32_t char_code = tag.substr(5, tag.length()).hex_to_int();
+ int32_t char_code = _get_tag_value(tag).hex_to_int();
add_text(String::chr(char_code));
pos = brk_end + 1;
} else if (tag == "lb") {
@@ -4469,7 +4530,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("ul bullet=")) {
- String bullet = tag.substr(10, 1);
+ String bullet = _get_tag_value(tag);
indent_level++;
push_list(indent_level, LIST_DOTS, false, bullet);
pos = brk_end + 1;
@@ -4505,7 +4566,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag.begins_with("lang=")) {
- String lang = tag.substr(5, tag.length()).unquote();
+ String lang = _get_tag_value(tag).unquote();
push_language(lang);
pos = brk_end + 1;
tag_stack.push_front("lang");
@@ -4514,89 +4575,104 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = brk_end + 1;
tag_stack.push_front("p");
} else if (tag.begins_with("p ")) {
- Vector<String> subtag = tag.substr(2, tag.length()).split(" ");
- _normalize_subtags(subtag);
-
HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT;
Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED;
String lang = language;
PackedFloat32Array tab_stops = default_tab_stops;
TextServer::StructuredTextParser st_parser_type = TextServer::STRUCTURED_TEXT_DEFAULT;
BitField<TextServer::JustificationFlag> jst_flags = default_jst_flags;
- for (int i = 0; i < subtag.size(); i++) {
- Vector<String> subtag_a = subtag[i].split("=");
- _normalize_subtags(subtag_a);
-
- if (subtag_a.size() == 2) {
- if (subtag_a[0] == "justification_flags" || subtag_a[0] == "jst") {
- Vector<String> subtag_b = subtag_a[1].split(",");
- jst_flags = 0; // Clear flags.
- for (const String &E : subtag_b) {
- if (E == "kashida" || E == "k") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_KASHIDA);
- } else if (E == "word" || E == "w") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_WORD_BOUND);
- } else if (E == "trim" || E == "tr") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_TRIM_EDGE_SPACES);
- } else if (E == "after_last_tab" || E == "lt") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_AFTER_LAST_TAB);
- } else if (E == "skip_last" || E == "sl") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE);
- } else if (E == "skip_last_with_chars" || E == "sv") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS);
- } else if (E == "do_not_skip_single" || E == "ns") {
- jst_flags.set_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE);
- }
- }
- } else if (subtag_a[0] == "tab_stops") {
- Vector<String> splitters;
- splitters.push_back(",");
- splitters.push_back(";");
- tab_stops = subtag_a[1].split_floats_mk(splitters);
- } else if (subtag_a[0] == "align") {
- if (subtag_a[1] == "l" || subtag_a[1] == "left") {
- alignment = HORIZONTAL_ALIGNMENT_LEFT;
- } else if (subtag_a[1] == "c" || subtag_a[1] == "center") {
- alignment = HORIZONTAL_ALIGNMENT_CENTER;
- } else if (subtag_a[1] == "r" || subtag_a[1] == "right") {
- alignment = HORIZONTAL_ALIGNMENT_RIGHT;
- } else if (subtag_a[1] == "f" || subtag_a[1] == "fill") {
- alignment = HORIZONTAL_ALIGNMENT_FILL;
- }
- } else if (subtag_a[0] == "dir" || subtag_a[0] == "direction") {
- if (subtag_a[1] == "a" || subtag_a[1] == "auto") {
- dir = Control::TEXT_DIRECTION_AUTO;
- } else if (subtag_a[1] == "l" || subtag_a[1] == "ltr") {
- dir = Control::TEXT_DIRECTION_LTR;
- } else if (subtag_a[1] == "r" || subtag_a[1] == "rtl") {
- dir = Control::TEXT_DIRECTION_RTL;
- }
- } else if (subtag_a[0] == "lang" || subtag_a[0] == "language") {
- lang = subtag_a[1];
- } else if (subtag_a[0] == "st" || subtag_a[0] == "bidi_override") {
- if (subtag_a[1] == "d" || subtag_a[1] == "default") {
- st_parser_type = TextServer::STRUCTURED_TEXT_DEFAULT;
- } else if (subtag_a[1] == "u" || subtag_a[1] == "uri") {
- st_parser_type = TextServer::STRUCTURED_TEXT_URI;
- } else if (subtag_a[1] == "f" || subtag_a[1] == "file") {
- st_parser_type = TextServer::STRUCTURED_TEXT_FILE;
- } else if (subtag_a[1] == "e" || subtag_a[1] == "email") {
- st_parser_type = TextServer::STRUCTURED_TEXT_EMAIL;
- } else if (subtag_a[1] == "l" || subtag_a[1] == "list") {
- st_parser_type = TextServer::STRUCTURED_TEXT_LIST;
- } else if (subtag_a[1] == "n" || subtag_a[1] == "gdscript") {
- st_parser_type = TextServer::STRUCTURED_TEXT_GDSCRIPT;
- } else if (subtag_a[1] == "c" || subtag_a[1] == "custom") {
- st_parser_type = TextServer::STRUCTURED_TEXT_CUSTOM;
- }
+
+ OptionMap::Iterator justification_flags_option = bbcode_options.find("justification_flags");
+ if (!justification_flags_option) {
+ justification_flags_option = bbcode_options.find("jst");
+ }
+ if (justification_flags_option) {
+ Vector<String> subtag_b = _split_unquoted(justification_flags_option->value, U',');
+ jst_flags = 0; // Clear flags.
+ for (const String &E : subtag_b) {
+ if (E == "kashida" || E == "k") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_KASHIDA);
+ } else if (E == "word" || E == "w") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_WORD_BOUND);
+ } else if (E == "trim" || E == "tr") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_TRIM_EDGE_SPACES);
+ } else if (E == "after_last_tab" || E == "lt") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_AFTER_LAST_TAB);
+ } else if (E == "skip_last" || E == "sl") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE);
+ } else if (E == "skip_last_with_chars" || E == "sv") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS);
+ } else if (E == "do_not_skip_single" || E == "ns") {
+ jst_flags.set_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE);
}
}
}
+ OptionMap::Iterator tab_stops_option = bbcode_options.find("tab_stops");
+ if (tab_stops_option) {
+ Vector<String> splitters;
+ splitters.push_back(",");
+ splitters.push_back(";");
+ tab_stops = tab_stops_option->value.split_floats_mk(splitters);
+ }
+ OptionMap::Iterator align_option = bbcode_options.find("align");
+ if (align_option) {
+ if (align_option->value == "l" || align_option->value == "left") {
+ alignment = HORIZONTAL_ALIGNMENT_LEFT;
+ } else if (align_option->value == "c" || align_option->value == "center") {
+ alignment = HORIZONTAL_ALIGNMENT_CENTER;
+ } else if (align_option->value == "r" || align_option->value == "right") {
+ alignment = HORIZONTAL_ALIGNMENT_RIGHT;
+ } else if (align_option->value == "f" || align_option->value == "fill") {
+ alignment = HORIZONTAL_ALIGNMENT_FILL;
+ }
+ }
+ OptionMap::Iterator direction_option = bbcode_options.find("direction");
+ if (!direction_option) {
+ direction_option = bbcode_options.find("dir");
+ }
+ if (direction_option) {
+ if (direction_option->value == "a" || direction_option->value == "auto") {
+ dir = Control::TEXT_DIRECTION_AUTO;
+ } else if (direction_option->value == "l" || direction_option->value == "ltr") {
+ dir = Control::TEXT_DIRECTION_LTR;
+ } else if (direction_option->value == "r" || direction_option->value == "rtl") {
+ dir = Control::TEXT_DIRECTION_RTL;
+ }
+ }
+ OptionMap::Iterator language_option = bbcode_options.find("language");
+ if (!language_option) {
+ language_option = bbcode_options.find("lang");
+ }
+ if (language_option) {
+ lang = language_option->value;
+ }
+ OptionMap::Iterator bidi_override_option = bbcode_options.find("bidi_override");
+ if (!bidi_override_option) {
+ bidi_override_option = bbcode_options.find("st");
+ }
+ if (bidi_override_option) {
+ if (bidi_override_option->value == "d" || bidi_override_option->value == "default") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_DEFAULT;
+ } else if (bidi_override_option->value == "u" || bidi_override_option->value == "uri") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_URI;
+ } else if (bidi_override_option->value == "f" || bidi_override_option->value == "file") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_FILE;
+ } else if (bidi_override_option->value == "e" || bidi_override_option->value == "email") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_EMAIL;
+ } else if (bidi_override_option->value == "l" || bidi_override_option->value == "list") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_LIST;
+ } else if (bidi_override_option->value == "n" || bidi_override_option->value == "gdscript") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_GDSCRIPT;
+ } else if (bidi_override_option->value == "c" || bidi_override_option->value == "custom") {
+ st_parser_type = TextServer::STRUCTURED_TEXT_CUSTOM;
+ }
+ }
+
push_paragraph(alignment, dir, lang, st_parser_type, jst_flags, tab_stops);
pos = brk_end + 1;
tag_stack.push_front("p");
} else if (tag == "url") {
- int end = p_bbcode.find("[", brk_end);
+ int end = p_bbcode.find_char('[', brk_end);
if (end == -1) {
end = p_bbcode.length();
}
@@ -4607,19 +4683,16 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front(tag);
} else if (tag.begins_with("url=")) {
- String url = tag.substr(4, tag.length()).unquote();
+ String url = _get_tag_value(tag).unquote();
push_meta(url, META_UNDERLINE_ALWAYS);
pos = brk_end + 1;
tag_stack.push_front("url");
} else if (tag.begins_with("hint=")) {
- String description = tag.substr(5, tag.length()).unquote();
+ String description = _get_tag_value(tag).unquote();
push_hint(description);
pos = brk_end + 1;
tag_stack.push_front("hint");
} else if (tag.begins_with("dropcap")) {
- Vector<String> subtag = tag.substr(5, tag.length()).split(" ");
- _normalize_subtags(subtag);
-
int fs = theme_cache.normal_font_size * 3;
Ref<Font> f = theme_cache.normal_font;
Color color = theme_cache.default_color;
@@ -4627,39 +4700,47 @@ void RichTextLabel::append_text(const String &p_bbcode) {
int outline_size = theme_cache.outline_size;
Rect2 dropcap_margins;
- for (int i = 0; i < subtag.size(); i++) {
- Vector<String> subtag_a = subtag[i].split("=");
- _normalize_subtags(subtag_a);
-
- if (subtag_a.size() == 2) {
- if (subtag_a[0] == "font" || subtag_a[0] == "f") {
- const String &fnt = subtag_a[1];
- Ref<Font> font = ResourceLoader::load(fnt, "Font");
- if (font.is_valid()) {
- f = font;
- }
- } else if (subtag_a[0] == "font_size") {
- fs = subtag_a[1].to_int();
- } else if (subtag_a[0] == "margins") {
- Vector<String> subtag_b = subtag_a[1].split(",");
- _normalize_subtags(subtag_b);
+ OptionMap::Iterator font_option = bbcode_options.find("font");
+ if (!font_option) {
+ font_option = bbcode_options.find("f");
+ }
+ if (font_option) {
+ const String &fnt = font_option->value;
+ Ref<Font> font = ResourceLoader::load(fnt, "Font");
+ if (font.is_valid()) {
+ f = font;
+ }
+ }
+ OptionMap::Iterator font_size_option = bbcode_options.find("font_size");
+ if (font_size_option) {
+ fs = font_size_option->value.to_int();
+ }
+ OptionMap::Iterator margins_option = bbcode_options.find("margins");
+ if (margins_option) {
+ Vector<String> subtag_b = _split_unquoted(margins_option->value, U',');
+ _normalize_subtags(subtag_b);
- if (subtag_b.size() == 4) {
- dropcap_margins.position.x = subtag_b[0].to_float();
- dropcap_margins.position.y = subtag_b[1].to_float();
- dropcap_margins.size.x = subtag_b[2].to_float();
- dropcap_margins.size.y = subtag_b[3].to_float();
- }
- } else if (subtag_a[0] == "outline_size") {
- outline_size = subtag_a[1].to_int();
- } else if (subtag_a[0] == "color") {
- color = Color::from_string(subtag_a[1], color);
- } else if (subtag_a[0] == "outline_color") {
- outline_color = Color::from_string(subtag_a[1], outline_color);
- }
+ if (subtag_b.size() == 4) {
+ dropcap_margins.position.x = subtag_b[0].to_float();
+ dropcap_margins.position.y = subtag_b[1].to_float();
+ dropcap_margins.size.x = subtag_b[2].to_float();
+ dropcap_margins.size.y = subtag_b[3].to_float();
}
}
- int end = p_bbcode.find("[", brk_end);
+ OptionMap::Iterator outline_size_option = bbcode_options.find("outline_size");
+ if (outline_size_option) {
+ outline_size = outline_size_option->value.to_int();
+ }
+ OptionMap::Iterator color_option = bbcode_options.find("color");
+ if (color_option) {
+ color = Color::from_string(color_option->value, color);
+ }
+ OptionMap::Iterator outline_color_option = bbcode_options.find("outline_color");
+ if (outline_color_option) {
+ outline_color = Color::from_string(outline_color_option->value, outline_color);
+ }
+
+ int end = p_bbcode.find_char('[', brk_end);
if (end == -1) {
end = p_bbcode.length();
}
@@ -4673,7 +4754,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
} else if (tag.begins_with("img")) {
int alignment = INLINE_ALIGNMENT_CENTER;
if (tag.begins_with("img=")) {
- Vector<String> subtag = tag.substr(4, tag.length()).split(",");
+ Vector<String> subtag = _split_unquoted(_get_tag_value(tag), U',');
_normalize_subtags(subtag);
if (subtag.size() > 1) {
@@ -4704,7 +4785,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
}
}
- int end = p_bbcode.find("[", brk_end);
+ int end = p_bbcode.find_char('[', brk_end);
if (end == -1) {
end = p_bbcode.length();
}
@@ -4716,7 +4797,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
Rect2 region;
OptionMap::Iterator region_option = bbcode_options.find("region");
if (region_option) {
- Vector<String> region_values = region_option->value.split(",", false);
+ Vector<String> region_values = _split_unquoted(region_option->value, U',');
if (region_values.size() == 4) {
region.position.x = region_values[0].to_float();
region.position.y = region_values[1].to_float();
@@ -4778,27 +4859,27 @@ void RichTextLabel::append_text(const String &p_bbcode) {
pos = end;
tag_stack.push_front(bbcode_name);
} else if (tag.begins_with("color=")) {
- String color_str = tag.substr(6, tag.length()).unquote();
+ String color_str = _get_tag_value(tag).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_color(color);
pos = brk_end + 1;
tag_stack.push_front("color");
} else if (tag.begins_with("outline_color=")) {
- String color_str = tag.substr(14, tag.length()).unquote();
+ String color_str = _get_tag_value(tag).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_outline_color(color);
pos = brk_end + 1;
tag_stack.push_front("outline_color");
} else if (tag.begins_with("font_size=")) {
- int fnt_size = tag.substr(10, tag.length()).to_int();
+ int fnt_size = _get_tag_value(tag).to_int();
push_font_size(fnt_size);
pos = brk_end + 1;
tag_stack.push_front("font_size");
} else if (tag.begins_with("opentype_features=") || tag.begins_with("otf=")) {
- int value_pos = tag.find("=");
+ int value_pos = tag.find_char('=');
String fnt_ftr = tag.substr(value_pos + 1);
Vector<String> subtag = fnt_ftr.split(",");
_normalize_subtags(subtag);
@@ -4842,7 +4923,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front(tag.substr(0, value_pos));
} else if (tag.begins_with("font=")) {
- String fnt = tag.substr(5, tag.length()).unquote();
+ String fnt = _get_tag_value(tag).unquote();
Ref<Font> fc = ResourceLoader::load(fnt, "Font");
if (fc.is_valid()) {
@@ -4853,11 +4934,9 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("font");
} else if (tag.begins_with("font ")) {
- Vector<String> subtag = tag.substr(2, tag.length()).split(" ");
- _normalize_subtags(subtag);
-
Ref<Font> font = theme_cache.normal_font;
DefaultFont def_font = NORMAL_FONT;
+ int fnt_size = -1;
ItemFont *font_it = _find_font(current);
if (font_it) {
@@ -4870,75 +4949,122 @@ void RichTextLabel::append_text(const String &p_bbcode) {
Ref<FontVariation> fc;
fc.instantiate();
- int fnt_size = -1;
- for (int i = 1; i < subtag.size(); i++) {
- Vector<String> subtag_a = subtag[i].split("=", true, 1);
- _normalize_subtags(subtag_a);
-
- if (subtag_a.size() == 2) {
- if (subtag_a[0] == "name" || subtag_a[0] == "n") {
- const String &fnt = subtag_a[1];
- Ref<Font> font_data = ResourceLoader::load(fnt, "Font");
- if (font_data.is_valid()) {
- font = font_data;
- def_font = CUSTOM_FONT;
- }
- } else if (subtag_a[0] == "size" || subtag_a[0] == "s") {
- fnt_size = subtag_a[1].to_int();
- } else if (subtag_a[0] == "glyph_spacing" || subtag_a[0] == "gl") {
- int spacing = subtag_a[1].to_int();
- fc->set_spacing(TextServer::SPACING_GLYPH, spacing);
- } else if (subtag_a[0] == "space_spacing" || subtag_a[0] == "sp") {
- int spacing = subtag_a[1].to_int();
- fc->set_spacing(TextServer::SPACING_SPACE, spacing);
- } else if (subtag_a[0] == "top_spacing" || subtag_a[0] == "top") {
- int spacing = subtag_a[1].to_int();
- fc->set_spacing(TextServer::SPACING_TOP, spacing);
- } else if (subtag_a[0] == "bottom_spacing" || subtag_a[0] == "bt") {
- int spacing = subtag_a[1].to_int();
- fc->set_spacing(TextServer::SPACING_BOTTOM, spacing);
- } else if (subtag_a[0] == "embolden" || subtag_a[0] == "emb") {
- float emb = subtag_a[1].to_float();
- fc->set_variation_embolden(emb);
- } else if (subtag_a[0] == "face_index" || subtag_a[0] == "fi") {
- int fi = subtag_a[1].to_int();
- fc->set_variation_face_index(fi);
- } else if (subtag_a[0] == "slant" || subtag_a[0] == "sln") {
- float slant = subtag_a[1].to_float();
- fc->set_variation_transform(Transform2D(1.0, slant, 0.0, 1.0, 0.0, 0.0));
- } else if (subtag_a[0] == "opentype_variation" || subtag_a[0] == "otv") {
- Dictionary variations;
- if (!subtag_a[1].is_empty()) {
- Vector<String> variation_tags = subtag_a[1].split(",");
- for (int j = 0; j < variation_tags.size(); j++) {
- Vector<String> subtag_b = variation_tags[j].split("=");
- _normalize_subtags(subtag_b);
-
- if (subtag_b.size() == 2) {
- variations[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
- }
- }
- fc->set_variation_opentype(variations);
+ OptionMap::Iterator name_option = bbcode_options.find("name");
+ if (!name_option) {
+ name_option = bbcode_options.find("n");
+ }
+ if (name_option) {
+ const String &fnt = name_option->value;
+ Ref<Font> font_data = ResourceLoader::load(fnt, "Font");
+ if (font_data.is_valid()) {
+ font = font_data;
+ def_font = CUSTOM_FONT;
+ }
+ }
+ OptionMap::Iterator size_option = bbcode_options.find("size");
+ if (!size_option) {
+ size_option = bbcode_options.find("s");
+ }
+ if (size_option) {
+ fnt_size = size_option->value.to_int();
+ }
+ OptionMap::Iterator glyph_spacing_option = bbcode_options.find("glyph_spacing");
+ if (!glyph_spacing_option) {
+ glyph_spacing_option = bbcode_options.find("gl");
+ }
+ if (glyph_spacing_option) {
+ int spacing = glyph_spacing_option->value.to_int();
+ fc->set_spacing(TextServer::SPACING_GLYPH, spacing);
+ }
+ OptionMap::Iterator space_spacing_option = bbcode_options.find("space_spacing");
+ if (!space_spacing_option) {
+ space_spacing_option = bbcode_options.find("sp");
+ }
+ if (space_spacing_option) {
+ int spacing = space_spacing_option->value.to_int();
+ fc->set_spacing(TextServer::SPACING_SPACE, spacing);
+ }
+ OptionMap::Iterator top_spacing_option = bbcode_options.find("top_spacing");
+ if (!top_spacing_option) {
+ top_spacing_option = bbcode_options.find("top");
+ }
+ if (top_spacing_option) {
+ int spacing = top_spacing_option->value.to_int();
+ fc->set_spacing(TextServer::SPACING_TOP, spacing);
+ }
+ OptionMap::Iterator bottom_spacing_option = bbcode_options.find("bottom_spacing");
+ if (!bottom_spacing_option) {
+ bottom_spacing_option = bbcode_options.find("bt");
+ }
+ if (bottom_spacing_option) {
+ int spacing = bottom_spacing_option->value.to_int();
+ fc->set_spacing(TextServer::SPACING_BOTTOM, spacing);
+ }
+ OptionMap::Iterator embolden_option = bbcode_options.find("embolden");
+ if (!embolden_option) {
+ embolden_option = bbcode_options.find("emb");
+ }
+ if (embolden_option) {
+ float emb = embolden_option->value.to_float();
+ fc->set_variation_embolden(emb);
+ }
+ OptionMap::Iterator face_index_option = bbcode_options.find("face_index");
+ if (!face_index_option) {
+ face_index_option = bbcode_options.find("fi");
+ }
+ if (face_index_option) {
+ int fi = face_index_option->value.to_int();
+ fc->set_variation_face_index(fi);
+ }
+ OptionMap::Iterator slant_option = bbcode_options.find("slant");
+ if (!slant_option) {
+ slant_option = bbcode_options.find("sln");
+ }
+ if (slant_option) {
+ float slant = slant_option->value.to_float();
+ fc->set_variation_transform(Transform2D(1.0, slant, 0.0, 1.0, 0.0, 0.0));
+ }
+ OptionMap::Iterator opentype_variation_option = bbcode_options.find("opentype_variation");
+ if (!opentype_variation_option) {
+ opentype_variation_option = bbcode_options.find("otv");
+ }
+ if (opentype_variation_option) {
+ Dictionary variations;
+ if (!opentype_variation_option->value.is_empty()) {
+ Vector<String> variation_tags = opentype_variation_option->value.split(",");
+ for (int j = 0; j < variation_tags.size(); j++) {
+ Vector<String> subtag_b = variation_tags[j].split("=");
+ _normalize_subtags(subtag_b);
+
+ if (subtag_b.size() == 2) {
+ variations[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
}
- } else if (subtag_a[0] == "opentype_features" || subtag_a[0] == "otf") {
- Dictionary features;
- if (!subtag_a[1].is_empty()) {
- Vector<String> feature_tags = subtag_a[1].split(",");
- for (int j = 0; j < feature_tags.size(); j++) {
- Vector<String> subtag_b = feature_tags[j].split("=");
- _normalize_subtags(subtag_b);
-
- if (subtag_b.size() == 2) {
- features[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
- } else if (subtag_b.size() == 1) {
- features[TS->name_to_tag(subtag_b[0])] = 1;
- }
- }
- fc->set_opentype_features(features);
+ }
+ fc->set_variation_opentype(variations);
+ }
+ }
+ OptionMap::Iterator opentype_features_option = bbcode_options.find("opentype_features");
+ if (!opentype_features_option) {
+ opentype_features_option = bbcode_options.find("otf");
+ }
+ if (opentype_features_option) {
+ Dictionary features;
+ if (!opentype_features_option->value.is_empty()) {
+ Vector<String> feature_tags = opentype_features_option->value.split(",");
+ for (int j = 0; j < feature_tags.size(); j++) {
+ Vector<String> subtag_b = feature_tags[j].split("=");
+ _normalize_subtags(subtag_b);
+
+ if (subtag_b.size() == 2) {
+ features[TS->name_to_tag(subtag_b[0])] = subtag_b[1].to_float();
+ } else if (subtag_b.size() == 1) {
+ features[TS->name_to_tag(subtag_b[0])] = 1;
}
}
+ fc->set_opentype_features(features);
}
}
+
fc->set_base_font(font);
if (def_font != CUSTOM_FONT) {
@@ -4951,7 +5077,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("font");
} else if (tag.begins_with("outline_size=")) {
- int fnt_size = tag.substr(13, tag.length()).to_int();
+ int fnt_size = _get_tag_value(tag).to_int();
if (fnt_size > 0) {
push_outline_size(fnt_size);
}
@@ -5090,7 +5216,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("pulse");
set_process_internal(true);
} else if (tag.begins_with("bgcolor=")) {
- String color_str = tag.substr(8, tag.length()).unquote();
+ String color_str = _get_tag_value(tag).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_bgcolor(color);
@@ -5098,7 +5224,7 @@ void RichTextLabel::append_text(const String &p_bbcode) {
tag_stack.push_front("bgcolor");
} else if (tag.begins_with("fgcolor=")) {
- String color_str = tag.substr(8, tag.length()).unquote();
+ String color_str = _get_tag_value(tag).unquote();
Color color = Color::from_string(color_str, theme_cache.default_color);
push_fgcolor(color);
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index 6da13e7b2d..a01da02b27 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -614,6 +614,10 @@ private:
String _get_prefix(Item *p_item, const Vector<int> &p_list_index, const Vector<ItemList *> &p_list_items);
+ static int _find_unquoted(const String &p_src, char32_t p_chr, int p_from);
+ static Vector<String> _split_unquoted(const String &p_src, char32_t p_splitter);
+ static String _get_tag_value(const String &p_tag);
+
#ifndef DISABLE_DEPRECATED
// Kept for compatibility from 3.x to 4.0.
bool _set(const StringName &p_name, const Variant &p_value);
diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp
index 4212cd709f..ac81f0de56 100644
--- a/scene/gui/spin_box.cpp
+++ b/scene/gui/spin_box.cpp
@@ -287,7 +287,12 @@ inline void SpinBox::_compute_sizes() {
int buttons_block_wanted_width = theme_cache.buttons_width + theme_cache.field_and_buttons_separation;
int buttons_block_icon_enforced_width = _get_widest_button_icon_width() + theme_cache.field_and_buttons_separation;
- int w = theme_cache.set_min_buttons_width_from_icons != 0 ? MAX(buttons_block_icon_enforced_width, buttons_block_wanted_width) : buttons_block_wanted_width;
+#ifndef DISABLE_DEPRECATED
+ const bool min_width_from_icons = theme_cache.set_min_buttons_width_from_icons || (theme_cache.buttons_width < 0);
+#else
+ const bool min_width_from_icons = theme_cache.buttons_width < 0;
+#endif
+ int w = min_width_from_icons != 0 ? MAX(buttons_block_icon_enforced_width, buttons_block_wanted_width) : buttons_block_wanted_width;
if (w != sizing_cache.buttons_block_width) {
line_edit->set_offset(SIDE_LEFT, 0);
@@ -551,7 +556,9 @@ void SpinBox::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_vertical_separation);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, field_and_buttons_separation);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_width);
+#ifndef DISABLE_DEPRECATED
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, set_min_buttons_width_from_icons);
+#endif
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, updown_icon, "updown");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_icon, "up");
diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h
index 7c6974f6a8..592805f43a 100644
--- a/scene/gui/spin_box.h
+++ b/scene/gui/spin_box.h
@@ -127,8 +127,9 @@ class SpinBox : public Range {
int buttons_vertical_separation = 0;
int field_and_buttons_separation = 0;
int buttons_width = 0;
- int set_min_buttons_width_from_icons = 0;
-
+#ifndef DISABLE_DEPRECATED
+ bool set_min_buttons_width_from_icons = false;
+#endif
} theme_cache;
void _mouse_exited();
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index a2f39af858..ab0ad2f4b7 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -112,8 +112,34 @@ int TextEdit::Text::get_line_width(int p_line, int p_wrap_index) const {
return text[p_line].data_buf->get_size().x;
}
+int TextEdit::Text::get_max_width() const {
+ if (max_line_width_dirty) {
+ int new_max_line_width = 0;
+ for (const Line &l : text) {
+ if (l.hidden) {
+ continue;
+ }
+ new_max_line_width = MAX(new_max_line_width, l.width);
+ }
+ max_line_width = new_max_line_width;
+ }
+
+ return max_line_width;
+}
+
int TextEdit::Text::get_line_height() const {
- return line_height;
+ if (max_line_height_dirty) {
+ int new_max_line_height = 0;
+ for (const Line &l : text) {
+ if (l.hidden) {
+ continue;
+ }
+ new_max_line_height = MAX(new_max_line_height, l.height);
+ }
+ max_line_height = new_max_line_height;
+ }
+
+ return max_line_height;
}
void TextEdit::Text::set_width(float p_width) {
@@ -135,15 +161,17 @@ BitField<TextServer::LineBreakFlag> TextEdit::Text::get_brk_flags() const {
int TextEdit::Text::get_line_wrap_amount(int p_line) const {
ERR_FAIL_INDEX_V(p_line, text.size(), 0);
- return text[p_line].data_buf->get_line_count() - 1;
+ return text[p_line].line_count - 1;
}
Vector<Vector2i> TextEdit::Text::get_line_wrap_ranges(int p_line) const {
Vector<Vector2i> ret;
ERR_FAIL_INDEX_V(p_line, text.size(), ret);
- for (int i = 0; i < text[p_line].data_buf->get_line_count(); i++) {
- ret.push_back(text[p_line].data_buf->get_line_range(i));
+ Ref<TextParagraph> data_buf = text[p_line].data_buf;
+ int line_count = data_buf->get_line_count();
+ for (int i = 0; i < line_count; i++) {
+ ret.push_back(data_buf->get_line_range(i));
}
return ret;
}
@@ -153,40 +181,11 @@ const Ref<TextParagraph> TextEdit::Text::get_line_data(int p_line) const {
return text[p_line].data_buf;
}
-_FORCE_INLINE_ const String &TextEdit::Text::operator[](int p_line) const {
+_FORCE_INLINE_ String TextEdit::Text::operator[](int p_line) const {
+ ERR_FAIL_INDEX_V(p_line, text.size(), "");
return text[p_line].data;
}
-void TextEdit::Text::_calculate_line_height() {
- int height = 0;
- for (const Line &l : text) {
- // Found another line with the same height...nothing to update.
- if (l.height == line_height) {
- height = line_height;
- break;
- }
- height = MAX(height, l.height);
- }
- line_height = height;
-}
-
-void TextEdit::Text::_calculate_max_line_width() {
- int line_width = 0;
- for (const Line &l : text) {
- if (l.hidden) {
- continue;
- }
-
- // Found another line with the same width...nothing to update.
- if (l.width == max_width) {
- line_width = max_width;
- break;
- }
- line_width = MAX(line_width, l.width);
- }
- max_width = line_width;
-}
-
void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_changed, const String &p_ime_text, const Array &p_bidi_override) {
ERR_FAIL_INDEX(p_line, text.size());
@@ -194,8 +193,9 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan
return; // Not in tree?
}
+ Line &text_line = text.write[p_line];
if (p_text_changed) {
- text.write[p_line].data_buf->clear();
+ text_line.data_buf->clear();
}
BitField<TextServer::LineBreakFlag> flags = brk_flags;
@@ -203,30 +203,30 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan
flags.set_flag(TextServer::BREAK_TRIM_INDENT);
}
- text.write[p_line].data_buf->set_width(width);
- text.write[p_line].data_buf->set_direction((TextServer::Direction)direction);
- text.write[p_line].data_buf->set_break_flags(flags);
- text.write[p_line].data_buf->set_preserve_control(draw_control_chars);
- text.write[p_line].data_buf->set_custom_punctuation(get_enabled_word_separators());
+ text_line.data_buf->set_width(width);
+ text_line.data_buf->set_direction((TextServer::Direction)direction);
+ text_line.data_buf->set_break_flags(flags);
+ text_line.data_buf->set_preserve_control(draw_control_chars);
+ text_line.data_buf->set_custom_punctuation(get_enabled_word_separators());
if (p_ime_text.length() > 0) {
if (p_text_changed) {
- text.write[p_line].data_buf->add_string(p_ime_text, font, font_size, language);
+ text_line.data_buf->add_string(p_ime_text, font, font_size, language);
}
if (!p_bidi_override.is_empty()) {
- TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), p_bidi_override);
+ TS->shaped_text_set_bidi_override(text_line.data_buf->get_rid(), p_bidi_override);
}
} else {
if (p_text_changed) {
- text.write[p_line].data_buf->add_string(text[p_line].data, font, font_size, language);
+ text_line.data_buf->add_string(text_line.data, font, font_size, language);
}
- if (!text[p_line].bidi_override.is_empty()) {
- TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), text[p_line].bidi_override);
+ if (!text_line.bidi_override.is_empty()) {
+ TS->shaped_text_set_bidi_override(text_line.data_buf->get_rid(), text_line.bidi_override);
}
}
if (!p_text_changed) {
- RID r = text.write[p_line].data_buf->get_rid();
+ RID r = text_line.data_buf->get_rid();
int spans = TS->shaped_get_span_count(r);
for (int i = 0; i < spans; i++) {
TS->shaped_set_span_update_font(r, i, font->get_rids(), font_size, font->get_opentype_features());
@@ -237,61 +237,58 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_chan
if (tab_size > 0) {
Vector<float> tabs;
tabs.push_back(font->get_char_size(' ', font_size).width * tab_size);
- text.write[p_line].data_buf->tab_align(tabs);
+ text_line.data_buf->tab_align(tabs);
+ }
+
+ // Update wrap amount.
+ const int old_line_count = text_line.line_count;
+ text_line.line_count = text_line.data_buf->get_line_count();
+ if (!text_line.hidden && text_line.line_count != old_line_count) {
+ total_visible_line_count += text_line.line_count - old_line_count;
}
// Update height.
- const int old_height = text.write[p_line].height;
- const int wrap_amount = get_line_wrap_amount(p_line);
- int height = font_height;
- for (int i = 0; i <= wrap_amount; i++) {
- height = MAX(height, text[p_line].data_buf->get_line_size(i).y);
+ const int old_height = text_line.height;
+ text_line.height = font_height;
+ for (int i = 0; i < text_line.line_count; i++) {
+ text_line.height = MAX(text_line.height, text_line.data_buf->get_line_size(i).y);
}
- text.write[p_line].height = height;
- // If this line has shrunk, this may no longer the tallest line.
- if (old_height == line_height && height < line_height) {
- _calculate_line_height();
- } else {
- line_height = MAX(height, line_height);
+ // If this line has shrunk, this may no longer be the tallest line.
+ if (!text_line.hidden) {
+ if (old_height == max_line_height && text_line.height < old_height) {
+ max_line_height_dirty = true;
+ } else {
+ max_line_height = MAX(text_line.height, max_line_height);
+ }
}
// Update width.
- const int old_width = text.write[p_line].width;
- int line_width = get_line_width(p_line);
- text.write[p_line].width = line_width;
+ const int old_width = text_line.width;
+ text_line.width = get_line_width(p_line);
- // If this line has shrunk, this may no longer the longest line.
- if (old_width == max_width && line_width < max_width) {
- _calculate_max_line_width();
- } else if (!is_hidden(p_line)) {
- max_width = MAX(line_width, max_width);
+ if (!text_line.hidden) {
+ // If this line has shrunk, this may no longer be the longest line.
+ if (old_width == max_line_width && text_line.width < old_width) {
+ max_line_width_dirty = true;
+ } else {
+ max_line_width = MAX(text_line.width, max_line_width);
+ }
}
}
void TextEdit::Text::invalidate_all_lines() {
for (int i = 0; i < text.size(); i++) {
- BitField<TextServer::LineBreakFlag> flags = brk_flags;
- if (indent_wrapped_lines) {
- flags.set_flag(TextServer::BREAK_TRIM_INDENT);
- }
- text.write[i].data_buf->set_width(width);
- text.write[i].data_buf->set_break_flags(flags);
- text.write[i].data_buf->set_custom_punctuation(get_enabled_word_separators());
-
if (tab_size_dirty) {
if (tab_size > 0) {
Vector<float> tabs;
tabs.push_back(font->get_char_size(' ', font_size).width * tab_size);
- text.write[i].data_buf->tab_align(tabs);
+ text[i].data_buf->tab_align(tabs);
}
}
- text.write[i].width = get_line_width(i);
+ invalidate_cache(i, -1, false);
}
tab_size_dirty = false;
-
- max_width = -1;
- _calculate_max_line_width();
}
void TextEdit::Text::invalidate_font() {
@@ -299,8 +296,8 @@ void TextEdit::Text::invalidate_font() {
return;
}
- max_width = -1;
- line_height = -1;
+ max_line_width_dirty = true;
+ max_line_height_dirty = true;
if (font.is_valid() && font_size > 0) {
font_height = font->get_height(font_size);
@@ -317,8 +314,8 @@ void TextEdit::Text::invalidate_all() {
return;
}
- max_width = -1;
- line_height = -1;
+ max_line_width_dirty = true;
+ max_line_height_dirty = true;
if (font.is_valid() && font_size > 0) {
font_height = font->get_height(font_size);
@@ -333,8 +330,8 @@ void TextEdit::Text::invalidate_all() {
void TextEdit::Text::clear() {
text.clear();
- max_width = -1;
- line_height = -1;
+ max_line_width_dirty = true;
+ max_line_height_dirty = true;
Line line;
line.gutters.resize(gutter_count);
@@ -343,8 +340,8 @@ void TextEdit::Text::clear() {
invalidate_cache(0, -1, true);
}
-int TextEdit::Text::get_max_width() const {
- return max_width;
+int TextEdit::Text::get_total_visible_line_count() const {
+ return total_visible_line_count;
}
void TextEdit::Text::set(int p_line, const String &p_text, const Array &p_bidi_override) {
@@ -355,7 +352,37 @@ void TextEdit::Text::set(int p_line, const String &p_text, const Array &p_bidi_o
invalidate_cache(p_line, -1, true);
}
+void TextEdit::Text::set_hidden(int p_line, bool p_hidden) {
+ ERR_FAIL_INDEX(p_line, text.size());
+
+ Line &text_line = text.write[p_line];
+ if (text_line.hidden == p_hidden) {
+ return;
+ }
+ text_line.hidden = p_hidden;
+ if (p_hidden) {
+ total_visible_line_count -= text_line.line_count;
+ if (text_line.width == max_line_width) {
+ max_line_width_dirty = true;
+ }
+ if (text_line.height == max_line_height) {
+ max_line_height_dirty = true;
+ }
+ } else {
+ total_visible_line_count += text_line.line_count;
+ max_line_width = MAX(text_line.width, max_line_width);
+ max_line_height = MAX(text_line.height, max_line_height);
+ }
+}
+
+bool TextEdit::Text::is_hidden(int p_line) const {
+ ERR_FAIL_INDEX_V(p_line, text.size(), true);
+ return text[p_line].hidden;
+}
+
void TextEdit::Text::insert(int p_at, const Vector<String> &p_text, const Vector<Array> &p_bidi_override) {
+ ERR_FAIL_INDEX(p_at, text.size() + 1);
+
int new_line_count = p_text.size() - 1;
if (new_line_count > 0) {
text.resize(text.size() + new_line_count);
@@ -382,24 +409,25 @@ void TextEdit::Text::insert(int p_at, const Vector<String> &p_text, const Vector
}
void TextEdit::Text::remove_range(int p_from_line, int p_to_line) {
+ p_from_line = MAX(p_from_line, 0);
+ ERR_FAIL_INDEX(p_from_line, text.size());
+
+ p_to_line = MIN(p_to_line, text.size());
+ ERR_FAIL_COND(p_to_line < p_from_line);
+
if (p_from_line == p_to_line) {
return;
}
- bool dirty_height = false;
- bool dirty_width = false;
for (int i = p_from_line; i < p_to_line; i++) {
- if (!dirty_height && text[i].height == line_height) {
- dirty_height = true;
+ const Line &text_line = text[i];
+ if (text_line.height == max_line_height) {
+ max_line_height_dirty = true;
}
-
- if (!dirty_width && text[i].width == max_width) {
- dirty_width = true;
- }
-
- if (dirty_height && dirty_width) {
- break;
+ if (text_line.width == max_line_width) {
+ max_line_width_dirty = true;
}
+ total_visible_line_count -= text_line.line_count;
}
int diff = (p_to_line - p_from_line);
@@ -407,16 +435,6 @@ void TextEdit::Text::remove_range(int p_from_line, int p_to_line) {
text.write[(i - diff) + 1] = text[i + 1];
}
text.resize(text.size() - diff);
-
- if (dirty_height) {
- line_height = -1;
- _calculate_line_height();
- }
-
- if (dirty_width) {
- max_width = -1;
- _calculate_max_line_width();
- }
}
void TextEdit::Text::add_gutter(int p_at) {
@@ -431,6 +449,8 @@ void TextEdit::Text::add_gutter(int p_at) {
}
void TextEdit::Text::remove_gutter(int p_gutter) {
+ ERR_FAIL_INDEX(p_gutter, text.size());
+
for (int i = 0; i < text.size(); i++) {
text.write[i].gutters.remove_at(p_gutter);
}
@@ -438,6 +458,9 @@ void TextEdit::Text::remove_gutter(int p_gutter) {
}
void TextEdit::Text::move_gutters(int p_from_line, int p_to_line) {
+ ERR_FAIL_INDEX(p_from_line, text.size());
+ ERR_FAIL_INDEX(p_to_line, text.size());
+
text.write[p_to_line].gutters = text[p_from_line].gutters;
text.write[p_from_line].gutters.clear();
text.write[p_from_line].gutters.resize(gutter_count);
@@ -625,6 +648,8 @@ void TextEdit::_notification(int p_what) {
brace_matching.resize(get_caret_count());
for (int caret = 0; caret < get_caret_count(); caret++) {
+ BraceMatchingData &brace_match = brace_matching.write[caret];
+
if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) {
continue;
}
@@ -678,20 +703,20 @@ void TextEdit::_notification(int p_what) {
}
if (stack == 0) {
- brace_matching.write[caret].open_match_line = i;
- brace_matching.write[caret].open_match_column = j;
- brace_matching.write[caret].open_matching = true;
+ brace_match.open_match_line = i;
+ brace_match.open_match_column = j;
+ brace_match.open_matching = true;
break;
}
}
- if (brace_matching.write[caret].open_match_line != -1) {
+ if (brace_match.open_match_line != -1) {
break;
}
}
- if (!brace_matching.write[caret].open_matching) {
- brace_matching.write[caret].open_mismatch = true;
+ if (!brace_match.open_matching) {
+ brace_match.open_mismatch = true;
}
}
}
@@ -744,20 +769,20 @@ void TextEdit::_notification(int p_what) {
}
if (stack == 0) {
- brace_matching.write[caret].close_match_line = i;
- brace_matching.write[caret].close_match_column = j;
- brace_matching.write[caret].close_matching = true;
+ brace_match.close_match_line = i;
+ brace_match.close_match_column = j;
+ brace_match.close_matching = true;
break;
}
}
- if (brace_matching.write[caret].close_match_line != -1) {
+ if (brace_match.close_match_line != -1) {
break;
}
}
- if (!brace_matching.write[caret].close_matching) {
- brace_matching.write[caret].close_mismatch = true;
+ if (!brace_match.close_matching) {
+ brace_match.close_mismatch = true;
}
}
}
@@ -772,13 +797,14 @@ void TextEdit::_notification(int p_what) {
// Check if highlighted words contain only whitespaces (tabs or spaces).
bool only_whitespaces_highlighted = highlighted_text.strip_edges().is_empty();
- HashMap<int, HashSet<int>> caret_line_wrap_index_map;
+ Vector<Pair<int, int>> highlighted_lines;
+ highlighted_lines.resize(carets.size());
Vector<int> carets_wrap_index;
carets_wrap_index.resize(carets.size());
for (int i = 0; i < carets.size(); i++) {
carets.write[i].visible = false;
int wrap_index = get_caret_wrap_index(i);
- caret_line_wrap_index_map[get_caret_line(i)].insert(wrap_index);
+ highlighted_lines.write[i] = Pair<int, int>(get_caret_line(i), wrap_index);
carets_wrap_index.write[i] = wrap_index;
}
@@ -842,7 +868,7 @@ void TextEdit::_notification(int p_what) {
break;
}
- Dictionary color_map = _get_line_syntax_highlighting(minimap_line);
+ const Vector<Pair<int64_t, Color>> color_map = _get_line_syntax_highlighting(minimap_line);
Color line_background_color = text.get_line_background_color(minimap_line);
@@ -853,12 +879,9 @@ void TextEdit::_notification(int p_what) {
line_background_color.a *= 0.6;
}
- Color current_color = theme_cache.font_color;
- if (!editable) {
- current_color = theme_cache.font_readonly_color;
- }
+ Color current_color = editable ? theme_cache.font_color : theme_cache.font_readonly_color;
- Vector<String> wrap_rows = get_line_wrapped_text(minimap_line);
+ const Vector<String> wrap_rows = get_line_wrapped_text(minimap_line);
int line_wrap_amount = get_line_wrap_count(minimap_line);
int last_wrap_column = 0;
@@ -881,13 +904,13 @@ void TextEdit::_notification(int p_what) {
last_wrap_column += wrap_rows[line_wrap_index - 1].length();
}
- if (caret_line_wrap_index_map.has(minimap_line) && caret_line_wrap_index_map[minimap_line].has(line_wrap_index) && highlight_current_line) {
+ if (highlight_current_line && highlighted_lines.has(Pair<int, int>(minimap_line, line_wrap_index))) {
if (rtl) {
RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, i * 3, minimap_width, 2), theme_cache.current_line_color);
} else {
RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, minimap_width, 2), theme_cache.current_line_color);
}
- } else if (line_background_color != Color(0, 0, 0, 0)) {
+ } else if (line_background_color.a > 0) {
if (rtl) {
RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, i * 3, minimap_width, 2), line_background_color);
} else {
@@ -905,13 +928,17 @@ void TextEdit::_notification(int p_what) {
// Get the number of characters to draw together.
for (characters = 0; j + characters < str.length(); characters++) {
int next_char_index = j + characters;
- const Variant *color_data = color_map.getptr(last_wrap_column + next_char_index);
- if (color_data != nullptr) {
- next_color = (color_data->operator Dictionary()).get("color", theme_cache.font_color);
- if (!editable) {
- next_color.a = theme_cache.font_readonly_color.a;
+
+ for (const Pair<int64_t, Color> &color_data : color_map) {
+ if (last_wrap_column + next_char_index >= color_data.first) {
+ next_color = color_data.second;
+ if (!editable) {
+ next_color.a = theme_cache.font_readonly_color.a;
+ }
+ next_color.a *= 0.6;
+ } else {
+ break;
}
- next_color.a *= 0.6;
}
if (characters == 0) {
current_color = next_color;
@@ -1002,7 +1029,7 @@ void TextEdit::_notification(int p_what) {
LineDrawingCache cache_entry;
- Dictionary color_map = _get_line_syntax_highlighting(line);
+ const Vector<Pair<int64_t, Color>> color_map = _get_line_syntax_highlighting(line);
// Ensure we at least use the font color.
Color current_color = !editable ? theme_cache.font_readonly_color : theme_cache.font_color;
@@ -1012,7 +1039,7 @@ void TextEdit::_notification(int p_what) {
const Ref<TextParagraph> ldata = draw_placeholder ? placeholder_data_buf : text.get_line_data(line);
- Vector<String> wrap_rows = draw_placeholder ? placeholder_wraped_rows : get_line_wrapped_text(line);
+ const Vector<String> wrap_rows = draw_placeholder ? placeholder_wraped_rows : get_line_wrapped_text(line);
int line_wrap_amount = draw_placeholder ? placeholder_wraped_rows.size() - 1 : get_line_wrap_count(line);
for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) {
@@ -1053,20 +1080,20 @@ void TextEdit::_notification(int p_what) {
break;
}
- if (text.get_line_background_color(line) != Color(0, 0, 0, 0)) {
+ if (text.get_line_background_color(line).a > 0.0) {
if (rtl) {
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line));
+ RS::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line));
} else {
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line));
+ RS::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line));
}
}
// Draw current line highlight.
- if (highlight_current_line && caret_line_wrap_index_map.has(line) && caret_line_wrap_index_map[line].has(line_wrap_index)) {
+ if (highlight_current_line && highlighted_lines.has(Pair<int, int>(line, line_wrap_index))) {
if (rtl) {
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), theme_cache.current_line_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), theme_cache.current_line_color);
} else {
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), theme_cache.current_line_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), theme_cache.current_line_color);
}
}
@@ -1077,7 +1104,7 @@ void TextEdit::_notification(int p_what) {
int gutter_offset = theme_cache.style_normal->get_margin(SIDE_LEFT);
for (int g = 0; g < gutters.size(); g++) {
- const GutterInfo gutter = gutters[g];
+ const GutterInfo &gutter = gutters[g];
if (!gutter.draw || gutter.width <= 0) {
continue;
@@ -1184,7 +1211,7 @@ void TextEdit::_notification(int p_what) {
if (rect.position.x + rect.size.x > xmargin_end) {
rect.size.x = xmargin_end - rect.position.x;
}
- draw_rect(rect, theme_cache.selection_color, true);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, theme_cache.selection_color);
}
}
}
@@ -1194,7 +1221,7 @@ void TextEdit::_notification(int p_what) {
int search_text_col = _get_column_pos_of_word(search_text, str, search_flags, 0);
int search_text_len = search_text.length();
while (search_text_col != -1) {
- Vector<Vector2> sel = TS->shaped_text_get_selection(rid, search_text_col + start, search_text_col + search_text_len + start);
+ const Vector<Vector2> sel = TS->shaped_text_get_selection(rid, search_text_col + start, search_text_col + search_text_len + start);
for (int j = 0; j < sel.size(); j++) {
Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height);
if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) {
@@ -1206,7 +1233,7 @@ void TextEdit::_notification(int p_what) {
} else if (rect.position.x + rect.size.x > xmargin_end) {
rect.size.x = xmargin_end - rect.position.x;
}
- draw_rect(rect, theme_cache.search_result_color, true);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, theme_cache.search_result_color);
draw_rect(rect, theme_cache.search_result_border_color, false);
}
@@ -1218,7 +1245,7 @@ void TextEdit::_notification(int p_what) {
int highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0);
int highlighted_text_len = highlighted_text.length();
while (highlighted_text_col != -1) {
- Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_text_col + start, highlighted_text_col + highlighted_text_len + start);
+ const Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_text_col + start, highlighted_text_col + highlighted_text_len + start);
for (int j = 0; j < sel.size(); j++) {
Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height);
if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) {
@@ -1230,7 +1257,7 @@ void TextEdit::_notification(int p_what) {
} else if (rect.position.x + rect.size.x > xmargin_end) {
rect.size.x = xmargin_end - rect.position.x;
}
- draw_rect(rect, theme_cache.word_highlighted_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, theme_cache.word_highlighted_color);
}
highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_text_col + highlighted_text_len);
@@ -1243,7 +1270,7 @@ void TextEdit::_notification(int p_what) {
int lookup_symbol_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0);
int lookup_symbol_word_len = lookup_symbol_word.length();
while (lookup_symbol_word_col != -1) {
- Vector<Vector2> sel = TS->shaped_text_get_selection(rid, lookup_symbol_word_col + start, lookup_symbol_word_col + lookup_symbol_word_len + start);
+ const Vector<Vector2> sel = TS->shaped_text_get_selection(rid, lookup_symbol_word_col + start, lookup_symbol_word_col + lookup_symbol_word_len + start);
for (int j = 0; j < sel.size(); j++) {
Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y + (theme_cache.line_spacing / 2), sel[j].y - sel[j].x, row_height);
if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) {
@@ -1257,7 +1284,7 @@ void TextEdit::_notification(int p_what) {
}
rect.position.y += ceil(TS->shaped_text_get_ascent(rid)) + ceil(theme_cache.font->get_underline_position(theme_cache.font_size));
rect.size.y = MAX(1, theme_cache.font->get_underline_thickness(theme_cache.font_size));
- draw_rect(rect, highlight_underline_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, highlight_underline_color);
}
lookup_symbol_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, lookup_symbol_word_col + lookup_symbol_word_len);
@@ -1293,21 +1320,16 @@ void TextEdit::_notification(int p_what) {
char_ofs = 0;
}
for (int j = 0; j < gl_size; j++) {
- int64_t color_start = -1;
- for (const Variant *key = color_map.next(nullptr); key; key = color_map.next(key)) {
- if (int64_t(*key) <= glyphs[j].start) {
- color_start = *key;
+ for (const Pair<int64_t, Color> &color_data : color_map) {
+ if (color_data.first <= glyphs[j].start) {
+ current_color = color_data.second;
+ if (!editable && current_color.a > theme_cache.font_readonly_color.a) {
+ current_color.a = theme_cache.font_readonly_color.a;
+ }
} else {
break;
}
}
- const Variant *color_data = (color_start >= 0) ? color_map.getptr(color_start) : nullptr;
- if (color_data != nullptr) {
- current_color = (color_data->operator Dictionary()).get("color", theme_cache.font_color);
- if (!editable && current_color.a > theme_cache.font_readonly_color.a) {
- current_color.a = theme_cache.font_readonly_color.a;
- }
- }
Color gl_color = current_color;
for (int c = 0; c < get_caret_count(); c++) {
@@ -1325,22 +1347,23 @@ void TextEdit::_notification(int p_what) {
if (char_pos >= xmargin_beg) {
if (highlight_matching_braces_enabled) {
for (int c = 0; c < get_caret_count(); c++) {
- if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) ||
- (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) {
- if (brace_matching[c].open_mismatch) {
+ const BraceMatchingData &brace_match = brace_matching[c];
+ if ((brace_match.open_match_line == line && brace_match.open_match_column == glyphs[j].start) ||
+ (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_match.open_matching || brace_match.open_mismatch))) {
+ if (brace_match.open_mismatch) {
gl_color = _get_brace_mismatch_color();
}
Rect2 rect = Rect2(char_pos, ofs_y + theme_cache.font->get_underline_position(theme_cache.font_size), glyphs[j].advance * glyphs[j].repeat, MAX(theme_cache.font->get_underline_thickness(theme_cache.font_size) * theme_cache.base_scale, 1));
- draw_rect(rect, gl_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, gl_color);
}
- if ((brace_matching[c].close_match_line == line && brace_matching[c].close_match_column == glyphs[j].start) ||
- (get_caret_column(c) == glyphs[j].start + 1 && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].close_matching || brace_matching[c].close_mismatch))) {
- if (brace_matching[c].close_mismatch) {
+ if ((brace_match.close_match_line == line && brace_match.close_match_column == glyphs[j].start) ||
+ (get_caret_column(c) == glyphs[j].start + 1 && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_match.close_matching || brace_match.close_mismatch))) {
+ if (brace_match.close_mismatch) {
gl_color = _get_brace_mismatch_color();
}
Rect2 rect = Rect2(char_pos, ofs_y + theme_cache.font->get_underline_position(theme_cache.font_size), glyphs[j].advance * glyphs[j].repeat, MAX(theme_cache.font->get_underline_thickness(theme_cache.font_size) * theme_cache.base_scale, 1));
- draw_rect(rect, gl_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, rect, gl_color);
}
}
}
@@ -1451,11 +1474,11 @@ void TextEdit::_notification(int p_what) {
// Draw split caret (leading part).
ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y);
ts_caret.l_caret.size.x = caret_width;
- draw_rect(ts_caret.l_caret, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, ts_caret.l_caret, theme_cache.caret_color);
// Draw extra direction marker on top of split caret.
float d = (ts_caret.l_dir == TextServer::DIRECTION_LTR) ? 0.5 : -3;
Rect2 trect = Rect2(ts_caret.l_caret.position.x + d * caret_width, ts_caret.l_caret.position.y + ts_caret.l_caret.size.y - caret_width, 3 * caret_width, caret_width);
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
}
} else { // End of the line.
if (gl_size > 0) {
@@ -1488,28 +1511,28 @@ void TextEdit::_notification(int p_what) {
// Draw extra marker on top of mid caret.
Rect2 trect = Rect2(ts_caret.l_caret.position.x - 2.5 * caret_width, ts_caret.l_caret.position.y, 6 * caret_width, caret_width);
trect.position += Vector2(char_margin + ofs_x, ofs_y);
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
} else if (ts_caret.l_caret != Rect2() && ts_caret.t_caret != Rect2() && ts_caret.l_dir != ts_caret.t_dir) {
// Draw extra direction marker on top of split caret.
float d = (ts_caret.l_dir == TextServer::DIRECTION_LTR) ? 0.5 : -3;
Rect2 trect = Rect2(ts_caret.l_caret.position.x + d * caret_width, ts_caret.l_caret.position.y + ts_caret.l_caret.size.y - caret_width, 3 * caret_width, caret_width);
trect.position += Vector2(char_margin + ofs_x, ofs_y);
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
d = (ts_caret.t_dir == TextServer::DIRECTION_LTR) ? 0.5 : -3;
trect = Rect2(ts_caret.t_caret.position.x + d * caret_width, ts_caret.t_caret.position.y, 3 * caret_width, caret_width);
trect.position += Vector2(char_margin + ofs_x, ofs_y);
- RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, trect, theme_cache.caret_color);
}
ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y);
ts_caret.l_caret.size.x = caret_width;
- draw_rect(ts_caret.l_caret, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, ts_caret.l_caret, theme_cache.caret_color);
ts_caret.t_caret.position += Vector2(char_margin + ofs_x, ofs_y);
ts_caret.t_caret.size.x = caret_width;
- draw_rect(ts_caret.t_caret, theme_cache.caret_color);
+ RS::get_singleton()->canvas_item_add_rect(ci, ts_caret.t_caret, theme_cache.caret_color);
}
}
}
@@ -1517,7 +1540,7 @@ void TextEdit::_notification(int p_what) {
if (!ime_text.is_empty()) {
{
// IME Intermediate text range.
- Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c), get_caret_column(c) + ime_text.length());
+ const Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c), get_caret_column(c) + ime_text.length());
for (int j = 0; j < sel.size(); j++) {
Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height);
if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) {
@@ -1536,7 +1559,7 @@ void TextEdit::_notification(int p_what) {
}
{
// IME caret.
- Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c) + ime_selection.x, get_caret_column(c) + ime_selection.x + ime_selection.y);
+ const Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c) + ime_selection.x, get_caret_column(c) + ime_selection.x + ime_selection.y);
for (int j = 0; j < sel.size(); j++) {
Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height);
if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) {
@@ -1575,26 +1598,7 @@ void TextEdit::_notification(int p_what) {
draw_caret = true;
}
- _update_ime_window_position();
-
- if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) {
- int caret_start = -1;
- int caret_end = -1;
-
- if (!has_selection(0)) {
- String full_text = _base_get_text(0, 0, get_caret_line(), get_caret_column());
-
- caret_start = full_text.length();
- } else {
- String pre_text = _base_get_text(0, 0, get_selection_from_line(), get_selection_from_column());
- String post_text = get_selected_text(0);
-
- caret_start = pre_text.length();
- caret_end = caret_start + post_text.length();
- }
-
- DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), DisplayServer::KEYBOARD_TYPE_MULTILINE, -1, caret_start, caret_end);
- }
+ _show_virtual_keyboard();
} break;
case NOTIFICATION_FOCUS_EXIT: {
@@ -1943,8 +1947,7 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
}
}
- // Notify to show soft keyboard.
- notification(NOTIFICATION_FOCUS_ENTER);
+ _show_virtual_keyboard();
}
}
@@ -2393,7 +2396,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) {
} else {
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid());
if (words.is_empty() || cc <= words[0]) {
- // This solves the scenario where there are no words but glyfs that can be ignored.
+ // Move to the start when there are no more words.
cc = 0;
} else {
for (int j = words.size() - 2; j >= 0; j = j - 2) {
@@ -2450,7 +2453,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) {
} else {
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid());
if (words.is_empty() || cc >= words[words.size() - 1]) {
- // This solves the scenario where there are no words but glyfs that can be ignored.
+ // Move to the end when there are no more words.
cc = text[get_caret_line(i)].length();
} else {
for (int j = 1; j < words.size(); j = j + 2) {
@@ -2666,7 +2669,7 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) {
// Get a list with the indices of the word bounds of the given text line.
const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid());
if (words.is_empty() || column <= words[0]) {
- // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line.
+ // Delete to the start when there are no more words.
column = 0;
} else {
// Otherwise search for the first word break that is smaller than the index from we're currently deleting.
@@ -2731,10 +2734,15 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
int column = get_caret_column(caret_index);
PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid());
- for (int j = 1; j < words.size(); j = j + 2) {
- if (words[j] > column) {
- column = words[j];
- break;
+ if (words.is_empty() || column >= words[words.size() - 1]) {
+ // Delete to the end when there are no more words.
+ column = text[get_caret_line(i)].length();
+ } else {
+ for (int j = 1; j < words.size(); j = j + 2) {
+ if (words[j] > column) {
+ column = words[j];
+ break;
+ }
}
}
@@ -2924,6 +2932,29 @@ void TextEdit::_update_ime_text() {
queue_redraw();
}
+void TextEdit::_show_virtual_keyboard() {
+ _update_ime_window_position();
+
+ if (virtual_keyboard_enabled && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD)) {
+ int caret_start = -1;
+ int caret_end = -1;
+
+ if (!has_selection(0)) {
+ String full_text = _base_get_text(0, 0, get_caret_line(), get_caret_column());
+
+ caret_start = full_text.length();
+ } else {
+ String pre_text = _base_get_text(0, 0, get_selection_from_line(), get_selection_from_column());
+ String post_text = get_selected_text(0);
+
+ caret_start = pre_text.length();
+ caret_end = caret_start + post_text.length();
+ }
+
+ DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), DisplayServer::KEYBOARD_TYPE_MULTILINE, -1, caret_start, caret_end);
+ }
+}
+
/* General overrides. */
Size2 TextEdit::get_minimum_size() const {
Size2 size = theme_cache.style_normal->get_minimum_size();
@@ -5883,7 +5914,7 @@ int TextEdit::get_visible_line_count_in_range(int p_from_line, int p_to_line) co
}
int TextEdit::get_total_visible_line_count() const {
- return get_visible_line_count_in_range(0, text.size() - 1);
+ return text.get_total_visible_line_count();
}
// Auto adjust.
@@ -8157,8 +8188,36 @@ void TextEdit::_update_gutter_width() {
}
/* Syntax highlighting. */
-Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) {
- return (syntax_highlighter.is_null() || setting_text) ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line);
+Vector<Pair<int64_t, Color>> TextEdit::_get_line_syntax_highlighting(int p_line) {
+ if (syntax_highlighter.is_null() || setting_text) {
+ return Vector<Pair<int64_t, Color>>();
+ }
+
+ HashMap<int, Vector<Pair<int64_t, Color>>>::Iterator E = syntax_highlighting_cache.find(p_line);
+ if (E) {
+ return E->value;
+ }
+
+ Dictionary color_map = syntax_highlighter->get_line_syntax_highlighting(p_line);
+ Vector<Pair<int64_t, Color>> result;
+ result.resize(color_map.size());
+ int i = 0;
+ for (const Variant *key = color_map.next(nullptr); key; key = color_map.next(key), i++) {
+ int64_t key_data = *key;
+ const Variant *color_data = color_map.getptr(*key);
+ Color color_value = editable ? theme_cache.font_color : theme_cache.font_readonly_color;
+ if (color_data != nullptr) {
+ color_value = (color_data->operator Dictionary()).get("color", color_value);
+ }
+ result.write[i] = Pair<int64_t, Color>(key_data, color_value);
+ }
+ syntax_highlighting_cache.insert(p_line, result);
+
+ return result;
+}
+
+void TextEdit::_clear_syntax_highlighting_cache() {
+ syntax_highlighting_cache.clear();
}
/* Deprecated. */
@@ -8184,6 +8243,7 @@ int TextEdit::get_selection_column(int p_caret) const {
/*** Super internal Core API. Everything builds on it. ***/
void TextEdit::_text_changed() {
+ _clear_syntax_highlighting_cache();
_cancel_drag_and_drop_text();
queue_redraw();
diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h
index 1f2fd6619a..a448e185b1 100644
--- a/scene/gui/text_edit.h
+++ b/scene/gui/text_edit.h
@@ -153,6 +153,7 @@ private:
Color background_color = Color(0, 0, 0, 0);
bool hidden = false;
+ int line_count = 0;
int height = 0;
int width = 0;
@@ -178,16 +179,19 @@ private:
bool use_default_word_separators = true;
bool use_custom_word_separators = false;
- int line_height = -1;
- int max_width = -1;
+ mutable bool max_line_width_dirty = true;
+ mutable bool max_line_height_dirty = true;
+ mutable int max_line_width = 0;
+ mutable int max_line_height = 0;
+ mutable int total_visible_line_count = 0;
int width = -1;
int tab_size = 4;
int gutter_count = 0;
bool indent_wrapped_lines = false;
- void _calculate_line_height();
- void _calculate_max_line_width();
+ void _calculate_line_height() const;
+ void _calculate_max_line_width() const;
public:
void set_tab_size(int p_tab_size);
@@ -203,6 +207,7 @@ private:
int get_line_height() const;
int get_line_width(int p_line, int p_wrap_index = -1) const;
int get_max_width() const;
+ int get_total_visible_line_count() const;
void set_use_default_word_separators(bool p_enabled);
bool is_default_word_separators_enabled() const;
@@ -226,18 +231,8 @@ private:
const Ref<TextParagraph> get_line_data(int p_line) const;
void set(int p_line, const String &p_text, const Array &p_bidi_override);
- void set_hidden(int p_line, bool p_hidden) {
- if (text[p_line].hidden == p_hidden) {
- return;
- }
- text.write[p_line].hidden = p_hidden;
- if (!p_hidden && text[p_line].width > max_width) {
- max_width = text[p_line].width;
- } else if (p_hidden && text[p_line].width == max_width) {
- _calculate_max_line_width();
- }
- }
- bool is_hidden(int p_line) const { return text[p_line].hidden; }
+ void set_hidden(int p_line, bool p_hidden);
+ bool is_hidden(int p_line) const;
void insert(int p_at, const Vector<String> &p_text, const Vector<Array> &p_bidi_override);
void remove_range(int p_from_line, int p_to_line);
int size() const { return text.size(); }
@@ -248,7 +243,7 @@ private:
void invalidate_all();
void invalidate_all_lines();
- _FORCE_INLINE_ const String &operator[](int p_line) const;
+ _FORCE_INLINE_ String operator[](int p_line) const;
/* Gutters. */
void add_gutter(int p_at);
@@ -453,6 +448,7 @@ private:
void _caret_changed(int p_caret = -1);
void _emit_caret_changed();
+ void _show_virtual_keyboard();
void _reset_caret_blink_timer();
void _toggle_draw_caret();
@@ -568,8 +564,10 @@ private:
/* Syntax highlighting. */
Ref<SyntaxHighlighter> syntax_highlighter;
+ HashMap<int, Vector<Pair<int64_t, Color>>> syntax_highlighting_cache;
- Dictionary _get_line_syntax_highlighting(int p_line);
+ Vector<Pair<int64_t, Color>> _get_line_syntax_highlighting(int p_line);
+ void _clear_syntax_highlighting_cache();
/* Visual. */
struct ThemeCache {
@@ -1023,6 +1021,7 @@ public:
void add_gutter(int p_at = -1);
void remove_gutter(int p_gutter);
int get_gutter_count() const;
+ Vector2i get_hovered_gutter() const { return hovered_gutter; }
void set_gutter_name(int p_gutter, const String &p_name);
String get_gutter_name(int p_gutter) const;
diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp
index 27db65bb1a..62ea749465 100644
--- a/scene/resources/packed_scene.cpp
+++ b/scene/resources/packed_scene.cpp
@@ -369,6 +369,7 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
value = make_local_resource(value, n, resources_local_to_sub_scene, node, snames[nprops[j].name], resources_local_to_scene, i, ret_nodes, p_edit_state);
}
}
+
if (value.get_type() == Variant::ARRAY) {
Array set_array = value;
value = setup_resources_in_array(set_array, n, resources_local_to_sub_scene, node, snames[nprops[j].name], resources_local_to_scene, i, ret_nodes, p_edit_state);
@@ -383,25 +384,20 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
}
}
}
- if (value.get_type() == Variant::DICTIONARY) {
- Dictionary dictionary = value;
- const Array keys = dictionary.keys();
- const Array values = dictionary.values();
- if (has_local_resource(values) || has_local_resource(keys)) {
- Array duplicated_keys = keys.duplicate(true);
- Array duplicated_values = values.duplicate(true);
-
- duplicated_keys = setup_resources_in_array(duplicated_keys, n, resources_local_to_sub_scene, node, snames[nprops[j].name], resources_local_to_scene, i, ret_nodes, p_edit_state);
- duplicated_values = setup_resources_in_array(duplicated_values, n, resources_local_to_sub_scene, node, snames[nprops[j].name], resources_local_to_scene, i, ret_nodes, p_edit_state);
+ if (value.get_type() == Variant::DICTIONARY) {
+ Dictionary set_dict = value;
+ value = setup_resources_in_dictionary(set_dict, n, resources_local_to_sub_scene, node, snames[nprops[j].name], resources_local_to_scene, i, ret_nodes, p_edit_state);
- dictionary.clear();
+ bool is_get_valid = false;
+ Variant get_value = node->get(snames[nprops[j].name], &is_get_valid);
- for (int dictionary_index = 0; dictionary_index < keys.size(); dictionary_index++) {
- dictionary[duplicated_keys[dictionary_index]] = duplicated_values[dictionary_index];
+ if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) {
+ Dictionary get_dict = get_value;
+ if (!set_dict.is_same_typed(get_dict)) {
+ value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(),
+ get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script());
}
-
- value = dictionary;
}
}
@@ -539,6 +535,30 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
array.set(i, dnp.base->get_node_or_null(paths[i]));
}
dnp.base->set(dnp.property, array);
+ } else if (dnp.value.get_type() == Variant::DICTIONARY) {
+ Dictionary paths = dnp.value;
+
+ bool valid;
+ Dictionary dict = dnp.base->get(dnp.property, &valid);
+ ERR_CONTINUE(!valid);
+ dict = dict.duplicate();
+ bool convert_key = dict.get_typed_key_builtin() == Variant::OBJECT &&
+ ClassDB::is_parent_class(dict.get_typed_key_class_name(), "Node");
+ bool convert_value = dict.get_typed_value_builtin() == Variant::OBJECT &&
+ ClassDB::is_parent_class(dict.get_typed_value_class_name(), "Node");
+
+ for (int i = 0; i < paths.size(); i++) {
+ Variant key = paths.get_key_at_index(i);
+ if (convert_key) {
+ key = dnp.base->get_node_or_null(key);
+ }
+ Variant value = paths.get_value_at_index(i);
+ if (convert_value) {
+ value = dnp.base->get_node_or_null(value);
+ }
+ dict[key] = value;
+ }
+ dnp.base->set(dnp.property, dict);
} else {
dnp.base->set(dnp.property, dnp.base->get_node_or_null(dnp.value));
}
@@ -641,6 +661,26 @@ Array SceneState::setup_resources_in_array(Array &p_array_to_scan, const SceneSt
return p_array_to_scan;
}
+Dictionary SceneState::setup_resources_in_dictionary(Dictionary &p_dictionary_to_scan, const SceneState::NodeData &p_n, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_sub_scene, Node *p_node, const StringName p_sname, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_scene, int p_i, Node **p_ret_nodes, SceneState::GenEditState p_edit_state) const {
+ const Array keys = p_dictionary_to_scan.keys();
+ const Array values = p_dictionary_to_scan.values();
+
+ if (has_local_resource(values) || has_local_resource(keys)) {
+ Array duplicated_keys = keys.duplicate(true);
+ Array duplicated_values = values.duplicate(true);
+
+ duplicated_keys = setup_resources_in_array(duplicated_keys, p_n, p_resources_local_to_sub_scene, p_node, p_sname, p_resources_local_to_scene, p_i, p_ret_nodes, p_edit_state);
+ duplicated_values = setup_resources_in_array(duplicated_values, p_n, p_resources_local_to_sub_scene, p_node, p_sname, p_resources_local_to_scene, p_i, p_ret_nodes, p_edit_state);
+ p_dictionary_to_scan.clear();
+
+ for (int i = 0; i < keys.size(); i++) {
+ p_dictionary_to_scan[duplicated_keys[i]] = duplicated_values[i];
+ }
+ }
+
+ return p_dictionary_to_scan;
+}
+
bool SceneState::has_local_resource(const Array &p_array) const {
for (int i = 0; i < p_array.size(); i++) {
Ref<Resource> res = p_array[i];
@@ -810,6 +850,53 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
value = new_array;
}
}
+ } else if (E.type == Variant::DICTIONARY && E.hint == PROPERTY_HINT_TYPE_STRING) {
+ int key_value_separator = E.hint_string.find(";");
+ if (key_value_separator >= 0) {
+ int key_subtype_separator = E.hint_string.find(":");
+ String key_subtype_string = E.hint_string.substr(0, key_subtype_separator);
+ int key_slash_pos = key_subtype_string.find("/");
+ PropertyHint key_subtype_hint = PropertyHint::PROPERTY_HINT_NONE;
+ if (key_slash_pos >= 0) {
+ key_subtype_hint = PropertyHint(key_subtype_string.get_slice("/", 1).to_int());
+ key_subtype_string = key_subtype_string.substr(0, key_slash_pos);
+ }
+ Variant::Type key_subtype = Variant::Type(key_subtype_string.to_int());
+ bool convert_key = key_subtype == Variant::OBJECT && key_subtype_hint == PROPERTY_HINT_NODE_TYPE;
+
+ int value_subtype_separator = E.hint_string.find(":", key_value_separator) - (key_value_separator + 1);
+ String value_subtype_string = E.hint_string.substr(key_value_separator + 1, value_subtype_separator);
+ int value_slash_pos = value_subtype_string.find("/");
+ PropertyHint value_subtype_hint = PropertyHint::PROPERTY_HINT_NONE;
+ if (value_slash_pos >= 0) {
+ value_subtype_hint = PropertyHint(value_subtype_string.get_slice("/", 1).to_int());
+ value_subtype_string = value_subtype_string.substr(0, value_slash_pos);
+ }
+ Variant::Type value_subtype = Variant::Type(value_subtype_string.to_int());
+ bool convert_value = value_subtype == Variant::OBJECT && value_subtype_hint == PROPERTY_HINT_NODE_TYPE;
+
+ if (convert_key || convert_value) {
+ use_deferred_node_path_bit = true;
+ Dictionary dict = value;
+ Dictionary new_dict;
+ for (int i = 0; i < dict.size(); i++) {
+ Variant new_key = dict.get_key_at_index(i);
+ if (convert_key && new_key.get_type() == Variant::OBJECT) {
+ if (Node *n = Object::cast_to<Node>(new_key)) {
+ new_key = p_node->get_path_to(n);
+ }
+ }
+ Variant new_value = dict.get_value_at_index(i);
+ if (convert_value && new_value.get_type() == Variant::OBJECT) {
+ if (Node *n = Object::cast_to<Node>(new_value)) {
+ new_value = p_node->get_path_to(n);
+ }
+ }
+ new_dict[new_key] = new_value;
+ }
+ value = new_dict;
+ }
+ }
}
if (!pinned_props.has(name)) {
diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h
index d27def1760..9f8088910f 100644
--- a/scene/resources/packed_scene.h
+++ b/scene/resources/packed_scene.h
@@ -158,6 +158,7 @@ public:
Node *instantiate(GenEditState p_edit_state) const;
Array setup_resources_in_array(Array &array_to_scan, const SceneState::NodeData &n, HashMap<Ref<Resource>, Ref<Resource>> &resources_local_to_sub_scene, Node *node, const StringName sname, HashMap<Ref<Resource>, Ref<Resource>> &resources_local_to_scene, int i, Node **ret_nodes, SceneState::GenEditState p_edit_state) const;
+ Dictionary setup_resources_in_dictionary(Dictionary &p_dictionary_to_scan, const SceneState::NodeData &p_n, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_sub_scene, Node *p_node, const StringName p_sname, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_scene, int p_i, Node **p_ret_nodes, SceneState::GenEditState p_edit_state) const;
Variant make_local_resource(Variant &value, const SceneState::NodeData &p_node_data, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_sub_scene, Node *p_node, const StringName p_sname, HashMap<Ref<Resource>, Ref<Resource>> &p_resources_local_to_scene, int p_i, Node **p_ret_nodes, SceneState::GenEditState p_edit_state) const;
bool has_local_resource(const Array &p_array) const;
diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp
index e9df20d9db..29f9835ba9 100644
--- a/scene/resources/resource_format_text.cpp
+++ b/scene/resources/resource_format_text.cpp
@@ -624,6 +624,19 @@ Error ResourceLoaderText::load() {
}
}
+ if (value.get_type() == Variant::DICTIONARY) {
+ Dictionary set_dict = value;
+ bool is_get_valid = false;
+ Variant get_value = res->get(assign, &is_get_valid);
+ if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) {
+ Dictionary get_dict = get_value;
+ if (!set_dict.is_same_typed(get_dict)) {
+ value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(),
+ get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script());
+ }
+ }
+ }
+
if (set_valid) {
res->set(assign, value);
}
@@ -751,6 +764,19 @@ Error ResourceLoaderText::load() {
}
}
+ if (value.get_type() == Variant::DICTIONARY) {
+ Dictionary set_dict = value;
+ bool is_get_valid = false;
+ Variant get_value = resource->get(assign, &is_get_valid);
+ if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) {
+ Dictionary get_dict = get_value;
+ if (!set_dict.is_same_typed(get_dict)) {
+ value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(),
+ get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script());
+ }
+ }
+ }
+
if (set_valid) {
resource->set(assign, value);
}
@@ -1642,6 +1668,8 @@ void ResourceFormatSaverTextInstance::_find_resources(const Variant &p_variant,
} break;
case Variant::DICTIONARY: {
Dictionary d = p_variant;
+ _find_resources(d.get_typed_key_script());
+ _find_resources(d.get_typed_value_script());
List<Variant> keys;
d.get_key_list(&keys);
for (const Variant &E : keys) {
diff --git a/scene/resources/style_box_flat.cpp b/scene/resources/style_box_flat.cpp
index 52d02e92cb..60b91ef0cb 100644
--- a/scene/resources/style_box_flat.cpp
+++ b/scene/resources/style_box_flat.cpp
@@ -300,8 +300,8 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices,
const real_t x = radius * (real_t)cos((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.x;
const real_t y = radius * (real_t)sin((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.y;
- const float x_skew = -skew.x * (y - ring_rect.get_center().y);
- const float y_skew = -skew.y * (x - ring_rect.get_center().x);
+ const float x_skew = -skew.x * (y - style_rect.get_center().y);
+ const float y_skew = -skew.y * (x - style_rect.get_center().x);
verts.push_back(Vector2(x + x_skew, y + y_skew));
colors.push_back(color);
}
diff --git a/scene/resources/text_paragraph.cpp b/scene/resources/text_paragraph.cpp
index 5da47966dd..29a8541cb0 100644
--- a/scene/resources/text_paragraph.cpp
+++ b/scene/resources/text_paragraph.cpp
@@ -173,6 +173,7 @@ void TextParagraph::_shape_lines() {
v_offset = TS->shaped_text_get_size(dropcap_rid).x + dropcap_margins.size.x + dropcap_margins.position.x;
}
+ Size2i range = TS->shaped_text_get_range(rid);
if (h_offset > 0) {
// Dropcap, flow around.
PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(rid, width - h_offset, 0, brk_flags);
@@ -182,7 +183,7 @@ void TextParagraph::_shape_lines() {
if (!tab_stops.is_empty()) {
TS->shaped_text_tab_align(line, tab_stops);
}
- start = line_breaks[i + 1];
+ start = (i < line_breaks.size() - 2) ? line_breaks[i + 2] : range.y;
lines_rid.push_back(line);
if (v_offset < h) {
break;
@@ -192,13 +193,15 @@ void TextParagraph::_shape_lines() {
}
}
// Use fixed for the rest of lines.
- PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(rid, width, start, brk_flags);
- for (int i = 0; i < line_breaks.size(); i = i + 2) {
- RID line = TS->shaped_text_substr(rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]);
- if (!tab_stops.is_empty()) {
- TS->shaped_text_tab_align(line, tab_stops);
+ if (start == 0 || start < range.y) {
+ PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(rid, width, start, brk_flags);
+ for (int i = 0; i < line_breaks.size(); i = i + 2) {
+ RID line = TS->shaped_text_substr(rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]);
+ if (!tab_stops.is_empty()) {
+ TS->shaped_text_tab_align(line, tab_stops);
+ }
+ lines_rid.push_back(line);
}
- lines_rid.push_back(line);
}
BitField<TextServer::TextOverrunFlag> overrun_flags = TextServer::OVERRUN_NO_TRIM;
@@ -550,18 +553,42 @@ Size2 TextParagraph::get_size() const {
_THREAD_SAFE_METHOD_
const_cast<TextParagraph *>(this)->_shape_lines();
+
+ float h_offset = 0.f;
+ float v_offset = 0.f;
+ if (TS->shaped_text_get_orientation(dropcap_rid) == TextServer::ORIENTATION_HORIZONTAL) {
+ h_offset = TS->shaped_text_get_size(dropcap_rid).x + dropcap_margins.size.x + dropcap_margins.position.x;
+ v_offset = TS->shaped_text_get_size(dropcap_rid).y + dropcap_margins.size.y + dropcap_margins.position.y;
+ } else {
+ h_offset = TS->shaped_text_get_size(dropcap_rid).y + dropcap_margins.size.y + dropcap_margins.position.y;
+ v_offset = TS->shaped_text_get_size(dropcap_rid).x + dropcap_margins.size.x + dropcap_margins.position.x;
+ }
+
Size2 size;
int visible_lines = (max_lines_visible >= 0) ? MIN(max_lines_visible, (int)lines_rid.size()) : (int)lines_rid.size();
for (int i = 0; i < visible_lines; i++) {
Size2 lsize = TS->shaped_text_get_size(lines_rid[i]);
if (TS->shaped_text_get_orientation(lines_rid[i]) == TextServer::ORIENTATION_HORIZONTAL) {
+ if (h_offset > 0 && i <= dropcap_lines) {
+ lsize.x += h_offset;
+ }
size.x = MAX(size.x, lsize.x);
size.y += lsize.y;
} else {
+ if (h_offset > 0 && i <= dropcap_lines) {
+ lsize.y += h_offset;
+ }
size.x += lsize.x;
size.y = MAX(size.y, lsize.y);
}
}
+ if (h_offset > 0) {
+ if (TS->shaped_text_get_orientation(dropcap_rid) == TextServer::ORIENTATION_HORIZONTAL) {
+ size.y = MAX(size.y, v_offset);
+ } else {
+ size.x = MAX(size.x, v_offset);
+ }
+ }
return size;
}
@@ -624,7 +651,7 @@ Rect2 TextParagraph::get_line_object_rect(int p_line, Variant p_key) const {
ofs.x += TS->shaped_text_get_ascent(lines_rid[i]);
if (i <= dropcap_lines) {
if (TS->shaped_text_get_inferred_direction(dropcap_rid) == TextServer::DIRECTION_LTR) {
- ofs.x -= h_offset;
+ ofs.y -= h_offset;
}
l_width -= h_offset;
}
@@ -793,7 +820,7 @@ void TextParagraph::draw(RID p_canvas, const Vector2 &p_pos, const Color &p_colo
ofs.x += TS->shaped_text_get_ascent(lines_rid[i]);
if (i <= dropcap_lines) {
if (TS->shaped_text_get_inferred_direction(dropcap_rid) == TextServer::DIRECTION_LTR) {
- ofs.x -= h_offset;
+ ofs.y -= h_offset;
}
l_width -= h_offset;
}
@@ -895,7 +922,7 @@ void TextParagraph::draw_outline(RID p_canvas, const Vector2 &p_pos, int p_outli
ofs.x += TS->shaped_text_get_ascent(lines_rid[i]);
if (i <= dropcap_lines) {
if (TS->shaped_text_get_inferred_direction(dropcap_rid) == TextServer::DIRECTION_LTR) {
- ofs.x -= h_offset;
+ ofs.y -= h_offset;
}
l_width -= h_offset;
}
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 8a9e784c47..749d4e3530 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -648,7 +648,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
theme->set_constant("buttons_vertical_separation", "SpinBox", 0);
theme->set_constant("field_and_buttons_separation", "SpinBox", 2);
theme->set_constant("buttons_width", "SpinBox", 16);
+#ifndef DISABLE_DEPRECATED
theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1);
+#endif
// ScrollContainer
diff --git a/servers/rendering/renderer_rd/environment/sky.cpp b/servers/rendering/renderer_rd/environment/sky.cpp
index 7d4ce6888f..2087989102 100644
--- a/servers/rendering/renderer_rd/environment/sky.cpp
+++ b/servers/rendering/renderer_rd/environment/sky.cpp
@@ -1250,7 +1250,7 @@ void SkyRD::update_radiance_buffers(Ref<RenderSceneBuffersRD> p_render_buffers,
RS::SkyMode sky_mode = sky->mode;
if (sky_mode == RS::SKY_MODE_AUTOMATIC) {
- if (shader_data->uses_time || shader_data->uses_position) {
+ if ((shader_data->uses_time || shader_data->uses_position) && sky->radiance_size == 256) {
update_single_frame = true;
sky_mode = RS::SKY_MODE_REALTIME;
} else if (shader_data->uses_light || shader_data->ubo_size > 0) {
diff --git a/tests/core/variant/test_dictionary.h b/tests/core/variant/test_dictionary.h
index 7061bc66dc..48a48f6ca6 100644
--- a/tests/core/variant/test_dictionary.h
+++ b/tests/core/variant/test_dictionary.h
@@ -31,7 +31,7 @@
#ifndef TEST_DICTIONARY_H
#define TEST_DICTIONARY_H
-#include "core/variant/dictionary.h"
+#include "core/variant/typed_dictionary.h"
#include "tests/test_macros.h"
namespace TestDictionary {
@@ -536,6 +536,43 @@ TEST_CASE("[Dictionary] Order and find") {
CHECK_EQ(d.find_key("does not exist"), Variant());
}
+TEST_CASE("[Dictionary] Typed copying") {
+ TypedDictionary<int, int> d1;
+ d1[0] = 1;
+
+ TypedDictionary<double, double> d2;
+ d2[0] = 1.0;
+
+ Dictionary d3 = d1;
+ TypedDictionary<int, int> d4 = d3;
+
+ Dictionary d5 = d2;
+ TypedDictionary<int, int> d6 = d5;
+
+ d3[0] = 2;
+ d4[0] = 3;
+
+ // Same typed TypedDictionary should be shared.
+ CHECK_EQ(d1[0], Variant(3));
+ CHECK_EQ(d3[0], Variant(3));
+ CHECK_EQ(d4[0], Variant(3));
+
+ d5[0] = 2.0;
+ d6[0] = 3.0;
+
+ // Different typed TypedDictionary should not be shared.
+ CHECK_EQ(d2[0], Variant(2.0));
+ CHECK_EQ(d5[0], Variant(2.0));
+ CHECK_EQ(d6[0], Variant(3.0));
+
+ d1.clear();
+ d2.clear();
+ d3.clear();
+ d4.clear();
+ d5.clear();
+ d6.clear();
+}
+
} // namespace TestDictionary
#endif // TEST_DICTIONARY_H
diff --git a/tests/create_test.py b/tests/create_test.py
index deb53aca20..ad6d6b882f 100755
--- a/tests/create_test.py
+++ b/tests/create_test.py
@@ -13,12 +13,16 @@ def main():
os.chdir(os.path.dirname(os.path.realpath(__file__)))
parser = argparse.ArgumentParser(description="Creates a new unit test file.")
- parser.add_argument("name", type=str, help="The unit test name in PascalCase notation")
+ parser.add_argument(
+ "name",
+ type=str,
+ help="Specifies the class or component name to be tested, in PascalCase (e.g., MeshInstance3D). The name will be prefixed with 'test_' for the header file and 'Test' for the namespace.",
+ )
parser.add_argument(
"path",
type=str,
nargs="?",
- help="The path to the unit test file relative to the tests folder (default: .)",
+ help="The path to the unit test file relative to the tests folder (e.g. core). This should correspond to the relative path of the class or component being tested. (default: .)",
default=".",
)
parser.add_argument(
@@ -29,9 +33,10 @@ def main():
)
args = parser.parse_args()
- snake_case_regex = re.compile(r"(?<!^)(?=[A-Z])")
- name_snake_case = snake_case_regex.sub("_", args.name).lower()
-
+ snake_case_regex = re.compile(r"(?<!^)(?=[A-Z, 0-9])")
+ # Replace 2D, 3D, and 4D with 2d, 3d, and 4d, respectively. This avoids undesired splits like node_3_d.
+ prefiltered_name = re.sub(r"([234])D", lambda match: match.group(1).lower() + "d", args.name)
+ name_snake_case = snake_case_regex.sub("_", prefiltered_name).lower()
file_path = os.path.normpath(os.path.join(args.path, f"test_{name_snake_case}.h"))
print(file_path)
diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h
index 5166cd3c13..d3d5cc8a30 100644
--- a/tests/scene/test_audio_stream_wav.h
+++ b/tests/scene/test_audio_stream_wav.h
@@ -181,27 +181,27 @@ void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo,
}
}
-TEST_CASE("[AudioStreamWAV] Mono PCM8 format") {
+TEST_CASE("[Audio][AudioStreamWAV] Mono PCM8 format") {
run_test("test_pcm8_mono.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, WAV_COUNT);
}
-TEST_CASE("[AudioStreamWAV] Mono PCM16 format") {
+TEST_CASE("[Audio][AudioStreamWAV] Mono PCM16 format") {
run_test("test_pcm16_mono.wav", AudioStreamWAV::FORMAT_16_BITS, false, WAV_RATE, WAV_COUNT);
}
-TEST_CASE("[AudioStreamWAV] Stereo PCM8 format") {
+TEST_CASE("[Audio][AudioStreamWAV] Stereo PCM8 format") {
run_test("test_pcm8_stereo.wav", AudioStreamWAV::FORMAT_8_BITS, true, WAV_RATE, WAV_COUNT);
}
-TEST_CASE("[AudioStreamWAV] Stereo PCM16 format") {
+TEST_CASE("[Audio][AudioStreamWAV] Stereo PCM16 format") {
run_test("test_pcm16_stereo.wav", AudioStreamWAV::FORMAT_16_BITS, true, WAV_RATE, WAV_COUNT);
}
-TEST_CASE("[AudioStreamWAV] Alternate mix rate") {
+TEST_CASE("[Audio][AudioStreamWAV] Alternate mix rate") {
run_test("test_pcm16_stereo_38000Hz.wav", AudioStreamWAV::FORMAT_16_BITS, true, 38000, 38000);
}
-TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") {
+TEST_CASE("[Audio][AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") {
String save_path = TestUtils::get_temp_path("test_wav_extension");
Vector<uint8_t> test_data = gen_pcm8_test(WAV_RATE, WAV_COUNT, false);
Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
@@ -213,7 +213,7 @@ TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatical
CHECK(error == OK);
}
-TEST_CASE("[AudioStreamWAV] Default values") {
+TEST_CASE("[Audio][AudioStreamWAV] Default values") {
Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
CHECK(stream->get_format() == AudioStreamWAV::FORMAT_8_BITS);
CHECK(stream->get_loop_mode() == AudioStreamWAV::LOOP_DISABLED);
@@ -227,11 +227,11 @@ TEST_CASE("[AudioStreamWAV] Default values") {
CHECK(stream->get_stream_name() == "");
}
-TEST_CASE("[AudioStreamWAV] Save empty file") {
+TEST_CASE("[Audio][AudioStreamWAV] Save empty file") {
run_test("test_empty.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, 0);
}
-TEST_CASE("[AudioStreamWAV] Saving IMA ADPCM is not supported") {
+TEST_CASE("[Audio][AudioStreamWAV] Saving IMA ADPCM is not supported") {
String save_path = TestUtils::get_temp_path("test_adpcm.wav");
Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV);
stream->set_format(AudioStreamWAV::FORMAT_IMA_ADPCM);
diff --git a/tests/scene/test_height_map_shape_3d.h b/tests/scene/test_height_map_shape_3d.h
new file mode 100644
index 0000000000..60a8db4e58
--- /dev/null
+++ b/tests/scene/test_height_map_shape_3d.h
@@ -0,0 +1,122 @@
+/**************************************************************************/
+/* test_height_map_shape_3d.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_HEIGHT_MAP_SHAPE_3D_H
+#define TEST_HEIGHT_MAP_SHAPE_3D_H
+
+#include "scene/resources/3d/height_map_shape_3d.h"
+#include "scene/resources/image_texture.h"
+
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+namespace TestHeightMapShape3D {
+
+TEST_CASE("[SceneTree][HeightMapShape3D] Constructor") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ CHECK(height_map_shape->get_map_width() == 2);
+ CHECK(height_map_shape->get_map_depth() == 2);
+ CHECK(height_map_shape->get_map_data().size() == 4);
+ CHECK(height_map_shape->get_min_height() == 0.0);
+ CHECK(height_map_shape->get_max_height() == 0.0);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] set_map_width and get_map_width") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ height_map_shape->set_map_width(10);
+ CHECK(height_map_shape->get_map_width() == 10);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] set_map_depth and get_map_depth") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ height_map_shape->set_map_depth(15);
+ CHECK(height_map_shape->get_map_depth() == 15);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] set_map_data and get_map_data") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ Vector<real_t> map_data;
+ map_data.push_back(1.0);
+ map_data.push_back(2.0);
+ height_map_shape->set_map_data(map_data);
+ CHECK(height_map_shape->get_map_data().size() == 4.0);
+ CHECK(height_map_shape->get_map_data()[0] == 0.0);
+ CHECK(height_map_shape->get_map_data()[1] == 0.0);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] get_min_height") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ height_map_shape->set_map_width(3);
+ height_map_shape->set_map_depth(1);
+ height_map_shape->set_map_data(Vector<real_t>{ 1.0, 2.0, 0.5 });
+ CHECK(height_map_shape->get_min_height() == 0.5);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] get_max_height") {
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+ height_map_shape->set_map_width(3);
+ height_map_shape->set_map_depth(1);
+ height_map_shape->set_map_data(Vector<real_t>{ 1.0, 2.0, 0.5 });
+ CHECK(height_map_shape->get_max_height() == 2.0);
+}
+
+TEST_CASE("[SceneTree][HeightMapShape3D] update_map_data_from_image") {
+ // Create a HeightMapShape3D instance.
+ Ref<HeightMapShape3D> height_map_shape = memnew(HeightMapShape3D);
+
+ // Create a mock image with FORMAT_R8 and set its data.
+ Vector<uint8_t> image_data;
+ image_data.push_back(0);
+ image_data.push_back(128);
+ image_data.push_back(255);
+ image_data.push_back(64);
+
+ Ref<Image> image = memnew(Image);
+ image->set_data(2, 2, false, Image::FORMAT_R8, image_data);
+
+ height_map_shape->update_map_data_from_image(image, 0.0, 10.0);
+
+ // Check the map data.
+ Vector<real_t> expected_map_data = { 0.0, 5.0, 10.0, 2.5 };
+ Vector<real_t> actual_map_data = height_map_shape->get_map_data();
+ real_t tolerance = 0.1;
+
+ for (int i = 0; i < expected_map_data.size(); ++i) {
+ CHECK(Math::abs(actual_map_data[i] - expected_map_data[i]) < tolerance);
+ }
+
+ // Check the min and max heights.
+ CHECK(height_map_shape->get_min_height() == 0.0);
+ CHECK(height_map_shape->get_max_height() == 10.0);
+}
+
+} // namespace TestHeightMapShape3D
+
+#endif // TEST_HEIGHT_MAP_SHAPE_3D_H
diff --git a/tests/scene/test_parallax_2d.h b/tests/scene/test_parallax_2d.h
new file mode 100644
index 0000000000..2fdbf09e09
--- /dev/null
+++ b/tests/scene/test_parallax_2d.h
@@ -0,0 +1,131 @@
+/**************************************************************************/
+/* test_parallax_2d.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_PARALLAX_2D_H
+#define TEST_PARALLAX_2D_H
+
+#include "scene/2d/parallax_2d.h"
+#include "tests/test_macros.h"
+
+namespace TestParallax2D {
+
+// Test cases for the Parallax2D class to ensure its properties are set and retrieved correctly.
+
+TEST_CASE("[SceneTree][Parallax2D] Scroll Scale") {
+ // Test setting and getting the scroll scale.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Size2 scale(2, 2);
+ parallax->set_scroll_scale(scale);
+ CHECK(parallax->get_scroll_scale() == scale);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Repeat Size") {
+ // Test setting and getting the repeat size.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Size2 size(100, 100);
+ parallax->set_repeat_size(size);
+ CHECK(parallax->get_repeat_size() == size);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Repeat Times") {
+ // Test setting and getting the repeat times.
+ Parallax2D *parallax = memnew(Parallax2D);
+ int times = 5;
+ parallax->set_repeat_times(times);
+ CHECK(parallax->get_repeat_times() == times);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Autoscroll") {
+ // Test setting and getting the autoscroll values.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Point2 autoscroll(1, 1);
+ parallax->set_autoscroll(autoscroll);
+ CHECK(parallax->get_autoscroll() == autoscroll);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Scroll Offset") {
+ // Test setting and getting the scroll offset.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Point2 offset(10, 10);
+ parallax->set_scroll_offset(offset);
+ CHECK(parallax->get_scroll_offset() == offset);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Screen Offset") {
+ // Test setting and getting the screen offset.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Point2 offset(20, 20);
+ parallax->set_screen_offset(offset);
+ CHECK(parallax->get_screen_offset() == offset);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Limit Begin") {
+ // Test setting and getting the limit begin values.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Point2 limit_begin(-100, -100);
+ parallax->set_limit_begin(limit_begin);
+ CHECK(parallax->get_limit_begin() == limit_begin);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Limit End") {
+ // Test setting and getting the limit end values.
+ Parallax2D *parallax = memnew(Parallax2D);
+ Point2 limit_end(100, 100);
+ parallax->set_limit_end(limit_end);
+ CHECK(parallax->get_limit_end() == limit_end);
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Follow Viewport") {
+ // Test setting and getting the follow viewport flag.
+ Parallax2D *parallax = memnew(Parallax2D);
+ parallax->set_follow_viewport(false);
+ CHECK_FALSE(parallax->get_follow_viewport());
+ memdelete(parallax);
+}
+
+TEST_CASE("[SceneTree][Parallax2D] Ignore Camera Scroll") {
+ // Test setting and getting the ignore camera scroll flag.
+ Parallax2D *parallax = memnew(Parallax2D);
+ parallax->set_ignore_camera_scroll(true);
+ CHECK(parallax->is_ignore_camera_scroll());
+ memdelete(parallax);
+}
+
+} // namespace TestParallax2D
+
+#endif // TEST_PARALLAX_2D_H
diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h
index 69e27fe7a0..46a5046b21 100644
--- a/tests/scene/test_text_edit.h
+++ b/tests/scene/test_text_edit.h
@@ -4232,6 +4232,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_line(0) == 0);
CHECK(text_edit->get_caret_column(0) == 4);
text_edit->remove_secondary_carets();
+
+ // Remove when there are no words, only symbols.
+ text_edit->set_text("#{}");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(3);
+
+ SEND_GUI_ACTION("ui_text_backspace_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
}
SUBCASE("[TextEdit] ui_text_backspace_word same line") {
@@ -4891,6 +4903,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
CHECK(text_edit->get_caret_line(0) == 0);
CHECK(text_edit->get_caret_column(0) == 2);
text_edit->remove_secondary_carets();
+
+ // Remove when there are no words, only symbols.
+ text_edit->set_text("#{}");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ SEND_GUI_ACTION("ui_text_delete_word");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK_FALSE(text_edit->has_selection());
+ CHECK(text_edit->get_text() == "");
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
}
SUBCASE("[TextEdit] ui_text_delete_word same line") {
@@ -5301,6 +5325,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Move when there are no words, only symbols.
+ text_edit->set_text("#{}");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(3);
+
+ SEND_GUI_ACTION("ui_text_caret_word_left");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 0);
}
SUBCASE("[TextEdit] ui_text_caret_left") {
@@ -5563,6 +5597,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") {
SIGNAL_CHECK("caret_changed", empty_signal_args);
SIGNAL_CHECK_FALSE("text_changed");
SIGNAL_CHECK_FALSE("lines_edited_from");
+
+ // Move when there are no words, only symbols.
+ text_edit->set_text("#{}");
+ text_edit->set_caret_line(0);
+ text_edit->set_caret_column(0);
+
+ SEND_GUI_ACTION("ui_text_caret_word_right");
+ CHECK(text_edit->get_viewport()->is_input_handled());
+ CHECK(text_edit->get_caret_line(0) == 0);
+ CHECK(text_edit->get_caret_column(0) == 3);
}
SUBCASE("[TextEdit] ui_text_caret_right") {
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index 7e1c431a3c..2b6461e9ca 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -119,6 +119,7 @@
#include "tests/scene/test_node.h"
#include "tests/scene/test_node_2d.h"
#include "tests/scene/test_packed_scene.h"
+#include "tests/scene/test_parallax_2d.h"
#include "tests/scene/test_path_2d.h"
#include "tests/scene/test_path_follow_2d.h"
#include "tests/scene/test_sprite_frames.h"
@@ -155,6 +156,7 @@
#include "tests/scene/test_arraymesh.h"
#include "tests/scene/test_camera_3d.h"
+#include "tests/scene/test_height_map_shape_3d.h"
#include "tests/scene/test_path_3d.h"
#include "tests/scene/test_path_follow_3d.h"
#include "tests/scene/test_primitives.h"
@@ -320,7 +322,7 @@ struct GodotTestCaseListener : public doctest::IReporter {
return;
}
- if (name.contains("Audio")) {
+ if (name.contains("[Audio]")) {
// The last driver index should always be the dummy driver.
int dummy_idx = AudioDriverManager::get_driver_count() - 1;
AudioDriverManager::initialize(dummy_idx);