summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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/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.cpp162
-rw-r--r--core/io/resource_loader.h16
-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/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--doc/classes/@GlobalScope.xml5
-rw-r--r--doc/classes/Dictionary.xml95
-rw-r--r--doc/classes/EditorSettings.xml5
-rw-r--r--doc/classes/MenuBar.xml8
-rw-r--r--doc/classes/OS.xml2
-rw-r--r--doc/classes/Object.xml49
-rw-r--r--doc/classes/SpinBox.xml4
-rw-r--r--doc/classes/Viewport.xml6
-rwxr-xr-xdoc/tools/make_rst.py35
-rw-r--r--drivers/gles3/shaders/scene.glsl18
-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/connections_dialog.cpp16
-rw-r--r--editor/doc_tools.cpp2
-rw-r--r--editor/editor_file_system.cpp29
-rw-r--r--editor/editor_file_system.h1
-rw-r--r--editor/editor_help.cpp22
-rw-r--r--editor/editor_node.cpp46
-rw-r--r--editor/editor_node.h2
-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.cpp1
-rw-r--r--editor/export/project_export.cpp2
-rw-r--r--editor/filesystem_dock.cpp2
-rw-r--r--editor/scene_tree_dock.cpp75
-rw-r--r--editor/scene_tree_dock.h3
-rw-r--r--editor/themes/editor_theme_manager.cpp2
-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/gdscript/editor/gdscript_docgen.cpp91
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp308
-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.cpp4
-rw-r--r--modules/gdscript/gdscript_function.h43
-rw-r--r--modules/gdscript/gdscript_parser.cpp218
-rw-r--r--modules/gdscript/gdscript_parser.h2
-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/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
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.cpp37
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.h1
-rw-r--r--modules/lightmapper_rd/lm_compute.glsl27
-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--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/os_windows.cpp6
-rw-r--r--platform/windows/os_windows.h2
-rw-r--r--scene/3d/lightmap_gi.cpp19
-rw-r--r--scene/3d/lightmap_gi.h5
-rw-r--r--scene/gui/code_edit.cpp97
-rw-r--r--scene/gui/code_edit.h3
-rw-r--r--scene/gui/menu_bar.cpp17
-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.cpp467
-rw-r--r--scene/gui/text_edit.h35
-rw-r--r--scene/main/viewport.cpp1
-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/shaders/forward_clustered/scene_forward_clustered.glsl19
-rw-r--r--servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl18
-rw-r--r--tests/core/variant/test_dictionary.h39
-rw-r--r--tests/scene/test_height_map_shape_3d.h122
-rw-r--r--tests/scene/test_parallax_2d.h131
-rw-r--r--tests/test_main.cpp2
150 files changed, 4780 insertions, 967 deletions
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/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..f29f9eef98 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.
}
}
@@ -313,6 +319,7 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin
}
// This implementation must allow re-entrancy for a task that started awaiting in a deeper stack frame.
+// The load task token must be manually re-referenced before this is called, which includes threaded runs.
void ResourceLoader::_run_load_task(void *p_userdata) {
ThreadLoadTask &load_task = *(ThreadLoadTask *)p_userdata;
@@ -324,6 +331,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) {
@@ -440,6 +450,9 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
}
}
+ // It's safe now to let the task go in case no one else was grabbing the token.
+ load_task.load_token->unreference();
+
if (unlock_pending) {
thread_load_mutex.unlock();
}
@@ -451,6 +464,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 +536,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 +591,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 +603,12 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
}
}
- run_on_current_thread = must_not_register || p_thread_mode == LOAD_THREAD_FROM_CURRENT;
+ // It's important to keep the token alive because until the load completes,
+ // which includes before the thread start, it may happen that no one is grabbing
+ // the token anymore so it's released.
+ load_task_ptr->load_token->reference();
- 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 +621,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 +750,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;
@@ -777,6 +792,7 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
// resource loading that means that the task to wait for can be restarted here to break the
// cycle, with as much recursion into this process as needed.
// When the stack is eventually unrolled, the original load will have been notified to go on.
+ load_task.load_token->reference();
_run_load_task(&load_task);
}
@@ -809,22 +825,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 +883,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 +1457,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/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/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/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index ba448534b1..f222cbc969 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -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/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 7f017f39de..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].
diff --git a/doc/classes/MenuBar.xml b/doc/classes/MenuBar.xml
index 9e4287331c..bcf63f0610 100644
--- a/doc/classes/MenuBar.xml
+++ b/doc/classes/MenuBar.xml
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="MenuBar" inherits="Control" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
- A horizontal menu bar that creates a [MenuButton] for each [PopupMenu] child.
+ A horizontal menu bar that creates a menu for each [PopupMenu] child.
</brief_description>
<description>
- A horizontal menu bar that creates a [MenuButton] for each [PopupMenu] child. New items are created by adding [PopupMenu]s to this node.
+ A horizontal menu bar that creates a menu for each [PopupMenu] child. New items are created by adding [PopupMenu]s to this node.
</description>
<tutorials>
</tutorials>
@@ -105,9 +105,11 @@
</member>
<member name="prefer_global_menu" type="bool" setter="set_prefer_global_menu" getter="is_prefer_global_menu" default="true">
If [code]true[/code], [MenuBar] will use system global menu when supported.
+ [b]Note:[/b] If [code]true[/code] and global menu is supported, this node is not displayed, has zero size, and all its child nodes except [PopupMenu]s are inaccessible.
+ [b]Note:[/b] This property overrides the value of the [member PopupMenu.prefer_native_menu] property of the child nodes.
</member>
<member name="start_index" type="int" setter="set_start_index" getter="get_start_index" default="-1">
- Position in the global menu to insert first [MenuBar] item at.
+ Position order in the global menu to insert [MenuBar] items at. All menu items in the [MenuBar] are always inserted as a continuous range. Menus with lower [member start_index] are inserted first. Menus with [member start_index] equal to [code]-1[/code] are inserted last.
</member>
<member name="switch_on_hover" type="bool" setter="set_switch_on_hover" getter="is_switch_on_hover" default="true">
If [code]true[/code], when the cursor hovers above menu item, it will close the current [PopupMenu] and open the other one.
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/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/classes/Viewport.xml b/doc/classes/Viewport.xml
index b24f26a764..350fd65197 100644
--- a/doc/classes/Viewport.xml
+++ b/doc/classes/Viewport.xml
@@ -117,6 +117,12 @@
Returns the visible rectangle in global screen coordinates.
</description>
</method>
+ <method name="gui_cancel_drag">
+ <return type="void" />
+ <description>
+ Cancels the drag operation that was previously started through [method Control._get_drag_data] or forced with [method Control.force_drag].
+ </description>
+ </method>
<method name="gui_get_drag_data" qualifiers="const">
<return type="Variant" />
<description>
diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py
index 29e10ea490..101660881b 100755
--- a/doc/tools/make_rst.py
+++ b/doc/tools/make_rst.py
@@ -1509,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
-
- if link_type.endswith("[]"): # Typed array, strip [] to link to contained type.
- link_type = link_type[:-2]
- is_array = True
-
- 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
-
- 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
+ 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 klass.endswith("[]"): # Typed array, strip [] to link to contained type.
+ return f":ref:`Array<class_Array>`\\[{resolve_type(klass[:-len('[]')])}\\]"
+
+ 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)}\\]"
+
+ return resolve_type(klass)
def make_enum(t: str, is_bitfield: bool, state: State) -> str:
diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl
index 6143ce2167..393ba014c6 100644
--- a/drivers/gles3/shaders/scene.glsl
+++ b/drivers/gles3/shaders/scene.glsl
@@ -1803,22 +1803,22 @@ void main() {
#ifdef LIGHTMAP_BICUBIC_FILTER
vec3 lm_light_l0 = textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 0.0), lightmap_texture_size).rgb;
- vec3 lm_light_l1n1 = textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 1.0), lightmap_texture_size).rgb;
- vec3 lm_light_l1_0 = textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 2.0), lightmap_texture_size).rgb;
- vec3 lm_light_l1p1 = textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 3.0), lightmap_texture_size).rgb;
+ vec3 lm_light_l1n1 = (textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 1.0), lightmap_texture_size).rgb - vec3(0.5)) * 2.0;
+ vec3 lm_light_l1_0 = (textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 2.0), lightmap_texture_size).rgb - vec3(0.5)) * 2.0;
+ vec3 lm_light_l1p1 = (textureArray_bicubic(lightmap_textures, uvw + vec3(0.0, 0.0, 3.0), lightmap_texture_size).rgb - vec3(0.5)) * 2.0;
#else
vec3 lm_light_l0 = textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 0.0), 0.0).rgb;
- vec3 lm_light_l1n1 = textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb;
- vec3 lm_light_l1_0 = textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb;
- vec3 lm_light_l1p1 = textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb;
+ vec3 lm_light_l1n1 = (textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ vec3 lm_light_l1_0 = (textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ vec3 lm_light_l1p1 = (textureLod(lightmap_textures, uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb - vec3(0.5)) * 2.0;
#endif
vec3 n = normalize(lightmap_normal_xform * normal);
ambient_light += lm_light_l0 * lightmap_exposure_normalization;
- ambient_light += lm_light_l1n1 * n.y * lightmap_exposure_normalization;
- ambient_light += lm_light_l1_0 * n.z * lightmap_exposure_normalization;
- ambient_light += lm_light_l1p1 * n.x * lightmap_exposure_normalization;
+ ambient_light += lm_light_l1n1 * n.y * (lm_light_l0 * lightmap_exposure_normalization * 4.0);
+ ambient_light += lm_light_l1_0 * n.z * (lm_light_l0 * lightmap_exposure_normalization * 4.0);
+ ambient_light += lm_light_l1p1 * n.x * (lm_light_l0 * lightmap_exposure_normalization * 4.0);
#else
#ifdef LIGHTMAP_BICUBIC_FILTER
ambient_light += textureArray_bicubic(lightmap_textures, uvw, lightmap_texture_size).rgb * lightmap_exposure_normalization;
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/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 76934c9ec6..79e0c7ebd1 100644
--- a/editor/doc_tools.cpp
+++ b/editor/doc_tools.cpp
@@ -575,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) {
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index e13a984213..b1b64b5d60 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -1075,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;
@@ -1118,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);
@@ -1263,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);
{
@@ -2118,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);
@@ -2137,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);
@@ -2161,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)) {
@@ -2419,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)) {
@@ -3119,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);
@@ -3134,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 7848ede8a7..9adab1ed24 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -235,6 +235,7 @@ 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;
diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp
index 8bba3493e5..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));
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index f154cbd1e2..6899a35ded 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -825,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) {
@@ -833,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();
}
}
@@ -1326,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);
@@ -5240,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();
@@ -5297,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) {
@@ -6980,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 e24bae73f0..e9d2c28528 100644
--- a/editor/editor_node.h
+++ b/editor/editor_node.h
@@ -462,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;
@@ -488,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 36fbd91313..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.
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 c58b24e078..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;
}
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index 9978c55d7b..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);
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/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/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/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_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 2c5e6d46e7..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;
}
@@ -3457,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) {
@@ -3601,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()) {
@@ -4625,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
@@ -4735,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;
}
@@ -4825,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;
@@ -4840,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;
@@ -5019,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];
@@ -5106,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();
@@ -5121,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);
}
@@ -5149,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;
@@ -5281,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()) {
@@ -5701,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 db7bef2f07..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";
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 92f9c5fa11..65aa150be3 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -3568,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.
@@ -4385,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")) {
@@ -4412,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) {
@@ -4473,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;
@@ -4794,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:
@@ -4883,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:
@@ -4967,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 2c7e730772..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.
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/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/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index 89495e2c83..8ba6f9e2ba 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -793,6 +793,35 @@ LightmapperRD::BakeError LightmapperRD::_dilate(RenderingDevice *rd, Ref<RDShade
return BAKE_OK;
}
+LightmapperRD::BakeError LightmapperRD::_pack_l1(RenderingDevice *rd, Ref<RDShaderFile> &compute_shader, RID &compute_base_uniform_set, PushConstant &push_constant, RID &source_light_tex, RID &dest_light_tex, const Size2i &atlas_size, int atlas_slices) {
+ Vector<RD::Uniform> uniforms = dilate_or_denoise_common_uniforms(source_light_tex, dest_light_tex);
+
+ RID compute_shader_pack = rd->shader_create_from_spirv(compute_shader->get_spirv_stages("pack_coeffs"));
+ ERR_FAIL_COND_V(compute_shader_pack.is_null(), BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES); //internal check, should not happen
+ RID compute_shader_pack_pipeline = rd->compute_pipeline_create(compute_shader_pack);
+
+ RID dilate_uniform_set = rd->uniform_set_create(uniforms, compute_shader_pack, 1);
+
+ RD::ComputeListID compute_list = rd->compute_list_begin();
+ rd->compute_list_bind_compute_pipeline(compute_list, compute_shader_pack_pipeline);
+ rd->compute_list_bind_uniform_set(compute_list, compute_base_uniform_set, 0);
+ rd->compute_list_bind_uniform_set(compute_list, dilate_uniform_set, 1);
+ push_constant.region_ofs[0] = 0;
+ push_constant.region_ofs[1] = 0;
+ Vector3i group_size(Math::division_round_up(atlas_size.x, 8), Math::division_round_up(atlas_size.y, 8), 1); //restore group size
+
+ for (int i = 0; i < atlas_slices; i++) {
+ push_constant.atlas_slice = i;
+ rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(PushConstant));
+ rd->compute_list_dispatch(compute_list, group_size.x, group_size.y, group_size.z);
+ //no barrier, let them run all together
+ }
+ rd->compute_list_end();
+ rd->free(compute_shader_pack);
+
+ return BAKE_OK;
+}
+
Error LightmapperRD::_store_pfm(RenderingDevice *p_rd, RID p_atlas_tex, int p_index, const Size2i &p_atlas_size, const String &p_name) {
Vector<uint8_t> data = p_rd->texture_get_data(p_atlas_tex, p_index);
Ref<Image> img = Image::create_from_data(p_atlas_size.width, p_atlas_size.height, false, Image::FORMAT_RGBAH, data);
@@ -2002,6 +2031,14 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
}
}
+ if (p_bake_sh) {
+ SWAP(light_accum_tex, light_accum_tex2);
+ BakeError error = _pack_l1(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, light_accum_tex, atlas_size, atlas_slices);
+ if (unlikely(error != BAKE_OK)) {
+ return error;
+ }
+ }
+
#ifdef DEBUG_TEXTURES
for (int i = 0; i < atlas_slices * (p_bake_sh ? 4 : 1); i++) {
diff --git a/modules/lightmapper_rd/lightmapper_rd.h b/modules/lightmapper_rd/lightmapper_rd.h
index 59c2d52e69..f43da39670 100644
--- a/modules/lightmapper_rd/lightmapper_rd.h
+++ b/modules/lightmapper_rd/lightmapper_rd.h
@@ -273,6 +273,7 @@ class LightmapperRD : public Lightmapper {
BakeError _dilate(RenderingDevice *rd, Ref<RDShaderFile> &compute_shader, RID &compute_base_uniform_set, PushConstant &push_constant, RID &source_light_tex, RID &dest_light_tex, const Size2i &atlas_size, int atlas_slices);
BakeError _denoise(RenderingDevice *p_rd, Ref<RDShaderFile> &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function, void *p_bake_userdata);
+ BakeError _pack_l1(RenderingDevice *rd, Ref<RDShaderFile> &compute_shader, RID &compute_base_uniform_set, PushConstant &push_constant, RID &source_light_tex, RID &dest_light_tex, const Size2i &atlas_size, int atlas_slices);
Error _store_pfm(RenderingDevice *p_rd, RID p_atlas_tex, int p_index, const Size2i &p_atlas_size, const String &p_name);
Ref<Image> _read_pfm(const String &p_name);
diff --git a/modules/lightmapper_rd/lm_compute.glsl b/modules/lightmapper_rd/lm_compute.glsl
index 88fc316679..2c85fff6f3 100644
--- a/modules/lightmapper_rd/lm_compute.glsl
+++ b/modules/lightmapper_rd/lm_compute.glsl
@@ -6,6 +6,7 @@ dilate = "#define MODE_DILATE";
unocclude = "#define MODE_UNOCCLUDE";
light_probes = "#define MODE_LIGHT_PROBES";
denoise = "#define MODE_DENOISE";
+pack_coeffs = "#define MODE_PACK_L1_COEFFS";
#[compute]
@@ -63,7 +64,7 @@ layout(rgba16f, set = 1, binding = 4) uniform restrict image2DArray accum_light;
layout(set = 1, binding = 5) uniform texture2D environment;
#endif
-#if defined(MODE_DILATE) || defined(MODE_DENOISE)
+#if defined(MODE_DILATE) || defined(MODE_DENOISE) || defined(MODE_PACK_L1_COEFFS)
layout(rgba16f, set = 1, binding = 0) uniform restrict writeonly image2DArray dest_light;
layout(set = 1, binding = 1) uniform texture2DArray source_light;
#endif
@@ -1037,4 +1038,28 @@ void main() {
imageStore(dest_light, ivec3(atlas_pos, lightmap_slice), vec4(denoised_rgb, input_light.a));
}
#endif
+
+#ifdef MODE_PACK_L1_COEFFS
+ vec4 base_coeff = texelFetch(sampler2DArray(source_light, linear_sampler), ivec3(atlas_pos, params.atlas_slice * 4), 0);
+
+ for (int i = 1; i < 4; i++) {
+ vec4 c = texelFetch(sampler2DArray(source_light, linear_sampler), ivec3(atlas_pos, params.atlas_slice * 4 + i), 0);
+
+ if (abs(base_coeff.r) > 0.0) {
+ c.r /= (base_coeff.r * 8);
+ }
+
+ if (abs(base_coeff.g) > 0.0) {
+ c.g /= (base_coeff.g * 8);
+ }
+
+ if (abs(base_coeff.b) > 0.0) {
+ c.b /= (base_coeff.b * 8);
+ }
+
+ c.rgb += vec3(0.5);
+ c.rgb = clamp(c.rgb, vec3(0.0), vec3(1.0));
+ imageStore(dest_light, ivec3(atlas_pos, params.atlas_slice * 4 + i), c);
+ }
+#endif
}
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/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/os_windows.cpp b/platform/windows/os_windows.cpp
index bcc6a64671..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;
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/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp
index 3f8b0dfb8e..4048a8bd62 100644
--- a/scene/3d/lightmap_gi.cpp
+++ b/scene/3d/lightmap_gi.cpp
@@ -151,6 +151,14 @@ bool LightmapGIData::is_using_spherical_harmonics() const {
return uses_spherical_harmonics;
}
+void LightmapGIData::_set_uses_packed_directional(bool p_enable) {
+ _uses_packed_directional = p_enable;
+}
+
+bool LightmapGIData::_is_using_packed_directional() const {
+ return _uses_packed_directional;
+}
+
void LightmapGIData::set_capture_data(const AABB &p_bounds, bool p_interior, const PackedVector3Array &p_points, const PackedColorArray &p_point_sh, const PackedInt32Array &p_tetrahedra, const PackedInt32Array &p_bsp_tree, float p_baked_exposure) {
if (p_points.size()) {
int pc = p_points.size();
@@ -255,6 +263,9 @@ void LightmapGIData::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_uses_spherical_harmonics", "uses_spherical_harmonics"), &LightmapGIData::set_uses_spherical_harmonics);
ClassDB::bind_method(D_METHOD("is_using_spherical_harmonics"), &LightmapGIData::is_using_spherical_harmonics);
+ ClassDB::bind_method(D_METHOD("_set_uses_packed_directional", "_uses_packed_directional"), &LightmapGIData::_set_uses_packed_directional);
+ ClassDB::bind_method(D_METHOD("_is_using_packed_directional"), &LightmapGIData::_is_using_packed_directional);
+
ClassDB::bind_method(D_METHOD("add_user", "path", "uv_scale", "slice_index", "sub_instance"), &LightmapGIData::add_user);
ClassDB::bind_method(D_METHOD("get_user_count"), &LightmapGIData::get_user_count);
ClassDB::bind_method(D_METHOD("get_user_path", "user_idx"), &LightmapGIData::get_user_path);
@@ -267,6 +278,7 @@ void LightmapGIData::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "uses_spherical_harmonics", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_uses_spherical_harmonics", "is_using_spherical_harmonics");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "user_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_user_data", "_get_user_data");
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "probe_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_probe_data", "_get_probe_data");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "_uses_packed_directional", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_uses_packed_directional", "_is_using_packed_directional");
#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_light_texture", "light_texture"), &LightmapGIData::set_light_texture);
@@ -1187,6 +1199,7 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa
}
gi_data->set_lightmap_textures(textures);
+ gi_data->_set_uses_packed_directional(directional); // New SH lightmaps are packed automatically.
gi_data->set_uses_spherical_harmonics(directional);
for (int i = 0; i < lightmapper->get_bake_mesh_count(); i++) {
@@ -1352,6 +1365,12 @@ void LightmapGI::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POST_ENTER_TREE: {
if (light_data.is_valid()) {
+ ERR_FAIL_COND_MSG(
+ light_data->is_using_spherical_harmonics() && !light_data->_is_using_packed_directional(),
+ vformat(
+ "%s (%s): The directional lightmap textures are stored in a format that isn't supported anymore. Please bake lightmaps again to make lightmaps display from this node again.",
+ get_light_data()->get_path(), get_name()));
+
_assign_lightmaps();
}
} break;
diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h
index 67480132b6..6377c420d1 100644
--- a/scene/3d/lightmap_gi.h
+++ b/scene/3d/lightmap_gi.h
@@ -49,6 +49,8 @@ class LightmapGIData : public Resource {
bool uses_spherical_harmonics = false;
bool interior = false;
+ bool _uses_packed_directional = false;
+
RID lightmap;
AABB bounds;
float baked_exposure = 1.0;
@@ -92,6 +94,9 @@ public:
void set_uses_spherical_harmonics(bool p_enable);
bool is_using_spherical_harmonics() const;
+ void _set_uses_packed_directional(bool p_enable);
+ bool _is_using_packed_directional() const;
+
bool is_interior() const;
float get_baked_exposure() const;
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/menu_bar.cpp b/scene/gui/menu_bar.cpp
index c8b022d622..46c9c7cccc 100644
--- a/scene/gui/menu_bar.cpp
+++ b/scene/gui/menu_bar.cpp
@@ -218,15 +218,18 @@ void MenuBar::bind_global_menu() {
int global_start_idx = -1;
int count = nmenu->get_item_count(main_menu);
String prev_tag;
- for (int i = 0; i < count; i++) {
- String tag = nmenu->get_item_tag(main_menu, i).operator String().get_slice("#", 1);
- if (!tag.is_empty() && tag != prev_tag) {
- if (i >= start_index) {
- global_start_idx = i;
- break;
+ if (start_index >= 0) {
+ for (int i = 0; i < count; i++) {
+ String tag = nmenu->get_item_tag(main_menu, i).operator String().get_slice("#", 1);
+ if (!tag.is_empty() && tag != prev_tag) {
+ MenuBar *mb = Object::cast_to<MenuBar>(ObjectDB::get_instance(ObjectID(static_cast<uint64_t>(tag.to_int()))));
+ if (mb && mb->get_start_index() >= start_index) {
+ global_start_idx = i;
+ break;
+ }
}
+ prev_tag = tag;
}
- prev_tag = tag;
}
if (global_start_idx == -1) {
global_start_idx = count;
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 c4b33fd889..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();
}
}
@@ -2929,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();
@@ -5888,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.
@@ -8162,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. */
@@ -8189,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/main/viewport.cpp b/scene/main/viewport.cpp
index d4d60545bd..de2332125f 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -4695,6 +4695,7 @@ void Viewport::_bind_methods() {
ClassDB::bind_method(D_METHOD("warp_mouse", "position"), &Viewport::warp_mouse);
ClassDB::bind_method(D_METHOD("update_mouse_cursor_state"), &Viewport::update_mouse_cursor_state);
+ ClassDB::bind_method(D_METHOD("gui_cancel_drag"), &Viewport::gui_cancel_drag);
ClassDB::bind_method(D_METHOD("gui_get_drag_data"), &Viewport::gui_get_drag_data);
ClassDB::bind_method(D_METHOD("gui_is_dragging"), &Viewport::gui_is_dragging);
ClassDB::bind_method(D_METHOD("gui_is_drag_successful"), &Viewport::gui_is_drag_successful);
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/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
index aafb9b4764..cfde97af95 100644
--- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
+++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl
@@ -1513,7 +1513,6 @@ void fragment_shader(in SceneData scene_data) {
if (uses_sh) {
uvw.z *= 4.0; //SH textures use 4 times more data
-
vec3 lm_light_l0;
vec3 lm_light_l1n1;
vec3 lm_light_l1_0;
@@ -1521,23 +1520,23 @@ void fragment_shader(in SceneData scene_data) {
if (sc_use_lightmap_bicubic_filter) {
lm_light_l0 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 0.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1n1 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 1.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1_0 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 2.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1p1 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 3.0), lightmaps.data[ofs].light_texture_size).rgb;
+ lm_light_l1n1 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 1.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1_0 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 2.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1p1 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 3.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
} else {
lm_light_l0 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 0.0), 0.0).rgb;
- lm_light_l1n1 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb;
- lm_light_l1_0 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb;
- lm_light_l1p1 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb;
+ lm_light_l1n1 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1_0 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1p1 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb - vec3(0.5)) * 2.0;
}
vec3 n = normalize(lightmaps.data[ofs].normal_xform * normal);
float en = lightmaps.data[ofs].exposure_normalization;
ambient_light += lm_light_l0 * en;
- ambient_light += lm_light_l1n1 * n.y * en;
- ambient_light += lm_light_l1_0 * n.z * en;
- ambient_light += lm_light_l1p1 * n.x * en;
+ ambient_light += lm_light_l1n1 * n.y * (lm_light_l0 * en * 4.0);
+ ambient_light += lm_light_l1_0 * n.z * (lm_light_l0 * en * 4.0);
+ ambient_light += lm_light_l1p1 * n.x * (lm_light_l0 * en * 4.0);
} else {
if (sc_use_lightmap_bicubic_filter) {
diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl
index c266161834..b21769f207 100644
--- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl
+++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl
@@ -1280,23 +1280,23 @@ void main() {
if (sc_use_lightmap_bicubic_filter) {
lm_light_l0 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 0.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1n1 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 1.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1_0 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 2.0), lightmaps.data[ofs].light_texture_size).rgb;
- lm_light_l1p1 = textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 3.0), lightmaps.data[ofs].light_texture_size).rgb;
+ lm_light_l1n1 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 1.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1_0 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 2.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1p1 = (textureArray_bicubic(lightmap_textures[ofs], uvw + vec3(0.0, 0.0, 3.0), lightmaps.data[ofs].light_texture_size).rgb - vec3(0.5)) * 2.0;
} else {
lm_light_l0 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 0.0), 0.0).rgb;
- lm_light_l1n1 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb;
- lm_light_l1_0 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb;
- lm_light_l1p1 = textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb;
+ lm_light_l1n1 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 1.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1_0 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 2.0), 0.0).rgb - vec3(0.5)) * 2.0;
+ lm_light_l1p1 = (textureLod(sampler2DArray(lightmap_textures[ofs], SAMPLER_LINEAR_CLAMP), uvw + vec3(0.0, 0.0, 3.0), 0.0).rgb - vec3(0.5)) * 2.0;
}
vec3 n = normalize(lightmaps.data[ofs].normal_xform * normal);
float exposure_normalization = lightmaps.data[ofs].exposure_normalization;
ambient_light += lm_light_l0 * exposure_normalization;
- ambient_light += lm_light_l1n1 * n.y * exposure_normalization;
- ambient_light += lm_light_l1_0 * n.z * exposure_normalization;
- ambient_light += lm_light_l1p1 * n.x * exposure_normalization;
+ ambient_light += lm_light_l1n1 * n.y * (lm_light_l0 * exposure_normalization * 4.0);
+ ambient_light += lm_light_l1_0 * n.z * (lm_light_l0 * exposure_normalization * 4.0);
+ ambient_light += lm_light_l1p1 * n.x * (lm_light_l0 * exposure_normalization * 4.0);
} else {
if (sc_use_lightmap_bicubic_filter) {
ambient_light += textureArray_bicubic(lightmap_textures[ofs], uvw, lightmaps.data[ofs].light_texture_size).rgb * lightmaps.data[ofs].exposure_normalization;
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/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/test_main.cpp b/tests/test_main.cpp
index 502aed6a6e..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"