diff options
254 files changed, 6530 insertions, 1662 deletions
diff --git a/.github/workflows/android_builds.yml b/.github/workflows/android_builds.yml index ee75d53282..69ab830829 100644 --- a/.github/workflows/android_builds.yml +++ b/.github/workflows/android_builds.yml @@ -85,6 +85,7 @@ jobs: run: | cd platform/android/java ./gradlew generateGodotEditor + ./gradlew generateGodotMetaEditor cd ../../.. ls -l bin/android_editor_builds/ diff --git a/SConstruct b/SConstruct index 5c1ccd2449..178f2d86a2 100644 --- a/SConstruct +++ b/SConstruct @@ -1053,7 +1053,7 @@ if env["ninja"]: SetOption("experimental", "ninja") env["NINJA_FILE_NAME"] = env["ninja_file"] env["NINJA_DISABLE_AUTO_RUN"] = not env["ninja_auto_run"] - env.Tool("ninja") + env.Tool("ninja", "build.ninja") # Threads if env["threads"]: @@ -1116,7 +1116,7 @@ atexit.register(print_elapsed_time) def purge_flaky_files(): - paths_to_keep = ["ninja.build"] + paths_to_keep = ["build.ninja"] for build_failure in GetBuildFailures(): path = build_failure.node.path if os.path.isfile(path) and path not in paths_to_keep: 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/Node2D.xml b/doc/classes/Node2D.xml index 851290de7b..0b2dfcea03 100644 --- a/doc/classes/Node2D.xml +++ b/doc/classes/Node2D.xml @@ -95,43 +95,44 @@ </methods> <members> <member name="global_position" type="Vector2" setter="set_global_position" getter="get_global_position"> - Global position. + Global position. See also [member position]. </member> <member name="global_rotation" type="float" setter="set_global_rotation" getter="get_global_rotation"> - Global rotation in radians. + Global rotation in radians. See also [member rotation]. </member> <member name="global_rotation_degrees" type="float" setter="set_global_rotation_degrees" getter="get_global_rotation_degrees"> - Helper property to access [member global_rotation] in degrees instead of radians. + Helper property to access [member global_rotation] in degrees instead of radians. See also [member rotation_degrees]. </member> <member name="global_scale" type="Vector2" setter="set_global_scale" getter="get_global_scale"> - Global scale. + Global scale. See also [member scale]. </member> <member name="global_skew" type="float" setter="set_global_skew" getter="get_global_skew"> - Global skew in radians. + Global skew in radians. See also [member skew]. </member> <member name="global_transform" type="Transform2D" setter="set_global_transform" getter="get_global_transform"> - Global [Transform2D]. + Global [Transform2D]. See also [member transform]. </member> <member name="position" type="Vector2" setter="set_position" getter="get_position" default="Vector2(0, 0)"> - Position, relative to the node's parent. + Position, relative to the node's parent. See also [member global_position]. </member> <member name="rotation" type="float" setter="set_rotation" getter="get_rotation" default="0.0"> - Rotation in radians, relative to the node's parent. + Rotation in radians, relative to the node's parent. See also [member global_rotation]. [b]Note:[/b] This property is edited in the inspector in degrees. If you want to use degrees in a script, use [member rotation_degrees]. </member> <member name="rotation_degrees" type="float" setter="set_rotation_degrees" getter="get_rotation_degrees"> - Helper property to access [member rotation] in degrees instead of radians. + Helper property to access [member rotation] in degrees instead of radians. See also [member global_rotation_degrees]. </member> <member name="scale" type="Vector2" setter="set_scale" getter="get_scale" default="Vector2(1, 1)"> - The node's scale. Unscaled value: [code](1, 1)[/code]. + The node's scale, relative to the node's parent. Unscaled value: [code](1, 1)[/code]. See also [member global_scale]. [b]Note:[/b] Negative X scales in 2D are not decomposable from the transformation matrix. Due to the way scale is represented with transformation matrices in Godot, negative scales on the X axis will be changed to negative scales on the Y axis and a rotation of 180 degrees when decomposed. </member> <member name="skew" type="float" setter="set_skew" getter="get_skew" default="0.0"> - Slants the node. - [b]Note:[/b] Skew is X axis only. + If set to a non-zero value, slants the node in one direction or another. This can be used for pseudo-3D effects. See also [member global_skew]. + [b]Note:[/b] Skew is performed on the X axis only, and [i]between[/i] rotation and scaling. + [b]Note:[/b] This property is edited in the inspector in degrees. If you want to use degrees in a script, use [code]skew = deg_to_rad(value_in_degrees)[/code]. </member> <member name="transform" type="Transform2D" setter="set_transform" getter="get_transform"> - Local [Transform2D]. + The node's [Transform2D], relative to the node's parent. See also [member global_transform]. </member> </members> </class> 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 <= to) + _from = from + _to = to + + func _iter_init(iter): + iter[0] = _from + return iter[0] < _to + + func _iter_next(iter): + iter[0] += 1 + return iter[0] < _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/RDShaderSource.xml b/doc/classes/RDShaderSource.xml index 062c22f11f..a7b897d56e 100644 --- a/doc/classes/RDShaderSource.xml +++ b/doc/classes/RDShaderSource.xml @@ -23,6 +23,7 @@ <param index="1" name="source" type="String" /> <description> Sets [param source] code for the specified shader [param stage]. Equivalent to setting one of [member source_compute], [member source_fragment], [member source_tesselation_control], [member source_tesselation_evaluation] or [member source_vertex]. + [b]Note:[/b] If you set the compute shader source code using this method directly, remember to remove the Godot-specific hint [code]#[compute][/code]. </description> </method> </methods> 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/TreeItem.xml b/doc/classes/TreeItem.xml index 78a703c213..bd46c54990 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -19,7 +19,7 @@ <param index="3" name="disabled" type="bool" default="false" /> <param index="4" name="tooltip_text" type="String" default="""" /> <description> - Adds a button with [Texture2D] [param button] at column [param column]. The [param id] is used to identify the button in the according [signal Tree.button_clicked] signal and can be different from the buttons index. If not specified, the next available index is used, which may be retrieved by calling [method get_button_count] immediately before this method. Optionally, the button can be [param disabled] and have a [param tooltip_text]. + Adds a button with [Texture2D] [param button] to the end of the cell at column [param column]. The [param id] is used to identify the button in the according [signal Tree.button_clicked] signal and can be different from the buttons index. If not specified, the next available index is used, which may be retrieved by calling [method get_button_count] immediately before this method. Optionally, the button can be [param disabled] and have a [param tooltip_text]. </description> </method> <method name="add_child"> @@ -643,7 +643,7 @@ <param index="0" name="column" type="int" /> <param index="1" name="texture" type="Texture2D" /> <description> - Sets the given cell's icon [Texture2D]. The cell has to be in [constant CELL_MODE_ICON] mode. + Sets the given cell's icon [Texture2D]. If the cell is in [constant CELL_MODE_ICON] mode, the icon is displayed in the center of the cell. Otherwise, the icon is displayed before the cell's text. [constant CELL_MODE_RANGE] does not display an icon. </description> </method> <method name="set_icon_max_width"> @@ -811,17 +811,17 @@ </members> <constants> <constant name="CELL_MODE_STRING" value="0" enum="TreeCellMode"> - Cell shows a string label. When editable, the text can be edited using a [LineEdit], or a [TextEdit] popup if [method set_edit_multiline] is used. + Cell shows a string label, optionally with an icon. When editable, the text can be edited using a [LineEdit], or a [TextEdit] popup if [method set_edit_multiline] is used. </constant> <constant name="CELL_MODE_CHECK" value="1" enum="TreeCellMode"> - Cell shows a checkbox, optionally with text. The checkbox can be pressed, released, or indeterminate (via [method set_indeterminate]). The checkbox can't be clicked unless the cell is editable. + Cell shows a checkbox, optionally with text and an icon. The checkbox can be pressed, released, or indeterminate (via [method set_indeterminate]). The checkbox can't be clicked unless the cell is editable. </constant> <constant name="CELL_MODE_RANGE" value="2" enum="TreeCellMode"> Cell shows a numeric range. When editable, it can be edited using a range slider. Use [method set_range] to set the value and [method set_range_config] to configure the range. This cell can also be used in a text dropdown mode when you assign a text with [method set_text]. Separate options with a comma, e.g. [code]"Option1,Option2,Option3"[/code]. </constant> <constant name="CELL_MODE_ICON" value="3" enum="TreeCellMode"> - Cell shows an icon. It can't be edited nor display text. + Cell shows an icon. It can't be edited nor display text. The icon is always centered within the cell. </constant> <constant name="CELL_MODE_CUSTOM" value="4" enum="TreeCellMode"> Cell shows as a clickable button. It will display an arrow similar to [OptionButton], but doesn't feature a dropdown (for that you can use [constant CELL_MODE_RANGE]). Clicking the button emits the [signal Tree.item_edited] signal. The button is flat by default, you can use [method set_custom_as_button] to display it with a [StyleBox]. 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/gles3/storage/light_storage.cpp b/drivers/gles3/storage/light_storage.cpp index aab1aadf02..9b976c2206 100644 --- a/drivers/gles3/storage/light_storage.cpp +++ b/drivers/gles3/storage/light_storage.cpp @@ -1521,6 +1521,11 @@ bool LightStorage::_shadow_atlas_find_shadow(ShadowAtlas *shadow_atlas, int *p_i uint64_t min_pass = 0; // Pass of the existing one, try to use the least recently used one (LRU fashion). for (int j = 0; j < sc; j++) { + if (sarr[j].owner_is_omni != is_omni) { + // Existing light instance type doesn't match new light instance type skip. + continue; + } + LightInstance *sli = light_instance_owner.get_or_null(sarr[j].owner); if (!sli) { // Found a released light instance. diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index a37eba3b15..7d5af48384 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -348,7 +348,7 @@ static void _fill_std140_variant_ubo_value(ShaderLanguage::DataType type, int p_ } } -_FORCE_INLINE_ static void _fill_std140_ubo_value(ShaderLanguage::DataType type, const Vector<ShaderLanguage::ConstantNode::Value> &value, uint8_t *data) { +_FORCE_INLINE_ static void _fill_std140_ubo_value(ShaderLanguage::DataType type, const Vector<ShaderLanguage::Scalar> &value, uint8_t *data) { switch (type) { case ShaderLanguage::TYPE_BOOL: { uint32_t *gui = (uint32_t *)data; @@ -572,7 +572,7 @@ void ShaderData::set_default_texture_parameter(const StringName &p_name, RID p_t Variant ShaderData::get_default_parameter(const StringName &p_parameter) const { if (uniforms.has(p_parameter)) { ShaderLanguage::ShaderNode::Uniform uniform = uniforms[p_parameter]; - Vector<ShaderLanguage::ConstantNode::Value> default_value = uniform.default_value; + Vector<ShaderLanguage::Scalar> default_value = uniform.default_value; return ShaderLanguage::constant_value_to_variant(default_value, uniform.type, uniform.array_size, uniform.hint); } return Variant(); diff --git a/drivers/unix/dir_access_unix.cpp b/drivers/unix/dir_access_unix.cpp index a4208829b7..489181a025 100644 --- a/drivers/unix/dir_access_unix.cpp +++ b/drivers/unix/dir_access_unix.cpp @@ -397,12 +397,18 @@ Error DirAccessUnix::rename(String p_path, String p_new_path) { } p_path = fix_path(p_path); + if (p_path.ends_with("/")) { + p_path = p_path.left(-1); + } if (p_new_path.is_relative_path()) { p_new_path = get_current_dir().path_join(p_new_path); } p_new_path = fix_path(p_new_path); + if (p_new_path.ends_with("/")) { + p_new_path = p_new_path.left(-1); + } return ::rename(p_path.utf8().get_data(), p_new_path.utf8().get_data()) == 0 ? OK : FAILED; } 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..5273b61f37 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -486,11 +486,6 @@ void ConnectDialog::_notification(int p_what) { type_list->set_item_icon(i, get_editor_theme_icon(type_name)); } - Ref<StyleBox> style = get_theme_stylebox(CoreStringName(normal), "LineEdit")->duplicate(); - if (style.is_valid()) { - style->set_content_margin(SIDE_TOP, style->get_content_margin(SIDE_TOP) + 1.0); - from_signal->add_theme_style_override(CoreStringName(normal), style); - } method_search->set_right_icon(get_editor_theme_icon("Search")); open_method_tree->set_icon(get_editor_theme_icon("Edit")); } break; @@ -576,6 +571,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_dock_manager.cpp b/editor/editor_dock_manager.cpp index 75135532aa..0bdda41f26 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -509,7 +509,7 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str } for (int i = 0; i < hsplits.size(); i++) { - p_layout->set_value(p_section, "dock_hsplit_" + itos(i + 1), hsplits[i]->get_split_offset()); + p_layout->set_value(p_section, "dock_hsplit_" + itos(i + 1), int(hsplits[i]->get_split_offset() / EDSCALE)); } FileSystemDock::get_singleton()->save_layout_to_config(p_layout, p_section); @@ -605,7 +605,7 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S continue; } int ofs = p_layout->get_value(p_section, "dock_hsplit_" + itos(i + 1)); - hsplits[i]->set_split_offset(ofs); + hsplits[i]->set_split_offset(ofs * EDSCALE); } FileSystemDock::get_singleton()->load_layout_from_config(p_layout, p_section); 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..39291138a6 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); @@ -4392,6 +4404,21 @@ bool EditorNode::is_additional_node_in_scene(Node *p_edited_scene, Node *p_reimp return true; } +void EditorNode::get_scene_editor_data_for_node(Node *p_root, Node *p_node, HashMap<NodePath, SceneEditorDataEntry> &p_table) { + SceneEditorDataEntry new_entry; + new_entry.is_display_folded = p_node->is_displayed_folded(); + + if (p_root != p_node) { + new_entry.is_editable = p_root->is_editable_instance(p_node); + } + + p_table.insert(p_root->get_path_to(p_node), new_entry); + + for (int i = 0; i < p_node->get_child_count(); i++) { + get_scene_editor_data_for_node(p_root, p_node->get_child(i), p_table); + } +} + void EditorNode::get_preload_scene_modification_table( Node *p_edited_scene, Node *p_reimported_root, @@ -4498,7 +4525,7 @@ void EditorNode::get_preload_scene_modification_table( void EditorNode::get_preload_modifications_reference_to_nodes( Node *p_root, Node *p_node, - List<Node *> &p_excluded_nodes, + HashSet<Node *> &p_excluded_nodes, List<Node *> &p_instance_list_with_children, HashMap<NodePath, ModificationNodeEntry> &p_modification_table) { if (!p_excluded_nodes.find(p_node)) { @@ -5240,8 +5267,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 +5324,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) { @@ -5972,12 +5999,14 @@ void EditorNode::reload_scene(const String &p_path) { scene_tabs->set_current_tab(current_tab); } -void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node *p_node, const String &p_instance_path, List<Node *> &p_instance_list) { +void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node *p_node, const String &p_instance_path, HashSet<Node *> &p_instance_list) { String scene_file_path = p_node->get_scene_file_path(); - // This is going to get messy... + bool valid_instance_found = false; + + // Attempt to find all the instances matching path we're going to reload. if (p_node->get_scene_file_path() == p_instance_path) { - p_instance_list.push_back(p_node); + valid_instance_found = true; } else { Node *current_node = p_node; @@ -5985,7 +6014,7 @@ void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node * while (inherited_state.is_valid()) { String inherited_path = inherited_state->get_path(); if (inherited_path == p_instance_path) { - p_instance_list.push_back(p_node); + valid_instance_found = true; break; } @@ -5993,6 +6022,19 @@ void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node * } } + // Instead of adding this instance directly, if its not owned by the scene, walk its ancestors + // and find the first node still owned by the scene. This is what we will reloading instead. + if (valid_instance_found) { + Node *current_node = p_node; + while (true) { + if (current_node->get_owner() == p_root || current_node->get_owner() == nullptr) { + p_instance_list.insert(current_node); + break; + } + current_node = current_node->get_parent(); + } + } + for (int i = 0; i < p_node->get_child_count(); i++) { find_all_instances_inheriting_path_in_node(p_root, p_node->get_child(i), p_instance_path, p_instance_list); } @@ -6011,20 +6053,20 @@ void EditorNode::preload_reimporting_with_path_in_edited_scenes(const List<Strin Node *edited_scene_root = editor_data.get_edited_scene_root(current_scene_idx); if (edited_scene_root) { - SceneModificationsEntry scene_motifications; + SceneModificationsEntry scene_modifications; for (const String &instance_path : p_scenes) { if (editor_data.get_scene_path(current_scene_idx) == instance_path) { continue; } - List<Node *> instance_list; - find_all_instances_inheriting_path_in_node(edited_scene_root, edited_scene_root, instance_path, instance_list); - if (instance_list.size() > 0) { + HashSet<Node *> instances_to_reimport; + find_all_instances_inheriting_path_in_node(edited_scene_root, edited_scene_root, instance_path, instances_to_reimport); + if (instances_to_reimport.size() > 0) { editor_data.set_edited_scene(current_scene_idx); List<Node *> instance_list_with_children; - for (Node *original_node : instance_list) { + for (Node *original_node : instances_to_reimport) { InstanceModificationsEntry instance_modifications; // Fetching all the modified properties of the nodes reimported scene. @@ -6032,19 +6074,19 @@ void EditorNode::preload_reimporting_with_path_in_edited_scenes(const List<Strin instance_modifications.original_node = original_node; instance_modifications.instance_path = instance_path; - scene_motifications.instance_list.push_back(instance_modifications); + scene_modifications.instance_list.push_back(instance_modifications); instance_list_with_children.push_back(original_node); get_children_nodes(original_node, instance_list_with_children); } // Search the scene to find nodes that references the nodes will be recreated. - get_preload_modifications_reference_to_nodes(edited_scene_root, edited_scene_root, instance_list, instance_list_with_children, scene_motifications.other_instances_modifications); + get_preload_modifications_reference_to_nodes(edited_scene_root, edited_scene_root, instances_to_reimport, instance_list_with_children, scene_modifications.other_instances_modifications); } } - if (scene_motifications.instance_list.size() > 0) { - scenes_modification_table[current_scene_idx] = scene_motifications; + if (scene_modifications.instance_list.size() > 0) { + scenes_modification_table[current_scene_idx] = scene_modifications; } } } @@ -6167,10 +6209,10 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { // Instantiate early so that caches cleared on load in SceneState can be rebuilt early. Node *instantiated_node = nullptr; - // If we are in a inherit scene, it's easier to create a new base scene and + // If we are in a inherited scene, it's easier to create a new base scene and // grab the node from there. // When scene_path_to_node is '.' and we have scene_inherited_state, it's because - // it's a muli-level inheritance scene. We should use + // it's a multi-level inheritance scene. We should use NodePath scene_path_to_node = current_edited_scene->get_path_to(original_node); Ref<SceneState> scene_state = current_edited_scene->get_scene_inherited_state(); if (scene_path_to_node != "." && scene_state.is_valid() && scene_state->get_path() != instance_modifications.instance_path && scene_state->find_node_by_path(scene_path_to_node) >= 0) { @@ -6194,9 +6236,9 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { if (!instantiated_node) { // If no base scene was found to create the node, we will use the reimported packed scene directly. - // But, when the current edited scene is the reimported scene, it's because it's a inherited scene - // of the reimported scene. In that case, we will not instantiate current_packed_scene, because - // we would reinstanciate ourself. Using the base scene is better. + // But, when the current edited scene is the reimported scene, it's because it's an inherited scene + // derived from the reimported scene. In that case, we will not instantiate current_packed_scene, because + // we would reinstantiate ourself. Using the base scene is better. if (current_edited_scene == original_node) { if (base_packed_scene.is_valid()) { instantiated_node = base_packed_scene->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); @@ -6252,6 +6294,17 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { // crash when reimporting scenes with animations when "Editable children" was enabled. replace_history_reimported_nodes(original_node, instantiated_node, original_node); + // Reset the editable instance state. + HashMap<NodePath, SceneEditorDataEntry> scene_editor_data_table; + Node *owner = original_node->get_owner(); + if (!owner) { + owner = original_node; + } + + get_scene_editor_data_for_node(owner, original_node, scene_editor_data_table); + + bool original_node_scene_instance_load_placeholder = original_node->get_scene_instance_load_placeholder(); + // Delete all the remaining node children. while (original_node->get_child_count()) { Node *child = original_node->get_child(0); @@ -6260,16 +6313,6 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { child->queue_free(); } - // Reset the editable instance state. - bool is_editable = true; - Node *owner = original_node->get_owner(); - if (owner) { - is_editable = owner->is_editable_instance(original_node); - } - - bool original_node_is_displayed_folded = original_node->is_displayed_folded(); - bool original_node_scene_instance_load_placeholder = original_node->get_scene_instance_load_placeholder(); - // Update the name to match instantiated_node->set_name(original_node->get_name()); @@ -6300,19 +6343,9 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { // Mark the old node for deletion. original_node->queue_free(); - // Restore the folded and placeholder state from the original node. - instantiated_node->set_display_folded(original_node_is_displayed_folded); + // Restore the placeholder state from the original node. instantiated_node->set_scene_instance_load_placeholder(original_node_scene_instance_load_placeholder); - if (owner) { - Ref<SceneState> ss_inst = owner->get_scene_instance_state(); - if (ss_inst.is_valid()) { - ss_inst->update_instance_resource(instance_modifications.instance_path, current_packed_scene); - } - - owner->set_editable_instance(instantiated_node, is_editable); - } - // Attempt to re-add all the additional nodes. for (AdditiveNodeEntry additive_node_entry : instance_modifications.addition_list) { Node *parent_node = instantiated_node->get_node_or_null(additive_node_entry.parent); @@ -6344,6 +6377,17 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { } } + // Restore the scene's editable instance and folded states. + for (HashMap<NodePath, SceneEditorDataEntry>::Iterator I = scene_editor_data_table.begin(); I; ++I) { + Node *node = owner->get_node_or_null(I->key); + if (node) { + if (owner != node) { + owner->set_editable_instance(node, I->value.is_editable); + } + node->set_display_folded(I->value.is_display_folded); + } + } + // Restore the selection. if (selected_node_paths.size()) { for (NodePath selected_node_path : selected_node_paths) { @@ -6980,6 +7024,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); @@ -7569,8 +7617,8 @@ EditorNode::EditorNode() { default_layout->set_value(docks_section, "dock_split_" + itos(i + 1), 0); } default_layout->set_value(docks_section, "dock_hsplit_1", 0); - default_layout->set_value(docks_section, "dock_hsplit_2", 270 * EDSCALE); - default_layout->set_value(docks_section, "dock_hsplit_3", -270 * EDSCALE); + default_layout->set_value(docks_section, "dock_hsplit_2", 270); + default_layout->set_value(docks_section, "dock_hsplit_3", -270); default_layout->set_value(docks_section, "dock_hsplit_4", 0); _update_layouts_menu(); diff --git a/editor/editor_node.h b/editor/editor_node.h index e24bae73f0..f1b50cf3fa 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; @@ -850,12 +852,19 @@ public: HashMap<NodePath, ModificationNodeEntry> other_instances_modifications; }; + struct SceneEditorDataEntry { + bool is_editable; + bool is_display_folded; + }; + HashMap<int, SceneModificationsEntry> scenes_modification_table; List<String> scenes_reimported; List<String> resources_reimported; void update_node_from_node_modification_entry(Node *p_node, ModificationNodeEntry &p_node_modification); + void get_scene_editor_data_for_node(Node *p_root, Node *p_node, HashMap<NodePath, SceneEditorDataEntry> &p_table); + void get_preload_scene_modification_table( Node *p_edited_scene, Node *p_reimported_root, @@ -864,7 +873,7 @@ public: void get_preload_modifications_reference_to_nodes( Node *p_root, Node *p_node, - List<Node *> &p_excluded_nodes, + HashSet<Node *> &p_excluded_nodes, List<Node *> &p_instance_list_with_children, HashMap<NodePath, ModificationNodeEntry> &p_modification_table); void get_children_nodes(Node *p_node, List<Node *> &p_nodes); @@ -925,7 +934,7 @@ public: void reload_scene(const String &p_path); - void find_all_instances_inheriting_path_in_node(Node *p_root, Node *p_node, const String &p_instance_path, List<Node *> &p_instance_list); + void find_all_instances_inheriting_path_in_node(Node *p_root, Node *p_node, const String &p_instance_path, HashSet<Node *> &p_instance_list); void preload_reimporting_with_path_in_edited_scenes(const List<String> &p_scenes); void reload_instances_with_path_in_edited_scenes(); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 07d37630cc..6e375dc1fc 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -3775,7 +3775,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/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp index 731f682605..a073a2338b 100644 --- a/editor/gui/editor_spin_slider.cpp +++ b/editor/gui/editor_spin_slider.cpp @@ -185,10 +185,6 @@ void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; - if (is_read_only()) { - return; - } - if (grabbing_grabber) { if (mb.is_valid()) { if (mb->get_button_index() == MouseButton::WHEEL_UP) { @@ -241,7 +237,7 @@ void EditorSpinSlider::_grabber_gui_input(const Ref<InputEvent> &p_event) { void EditorSpinSlider::_value_input_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventKey> k = p_event; - if (k.is_valid() && k->is_pressed() && !is_read_only()) { + if (k.is_valid() && k->is_pressed() && !read_only) { Key code = k->get_keycode(); switch (code) { @@ -315,7 +311,7 @@ void EditorSpinSlider::_draw_spin_slider() { bool rtl = is_layout_rtl(); Vector2 size = get_size(); - Ref<StyleBox> sb = get_theme_stylebox(is_read_only() ? SNAME("read_only") : CoreStringName(normal), SNAME("LineEdit")); + Ref<StyleBox> sb = get_theme_stylebox(read_only ? SNAME("read_only") : CoreStringName(normal), SNAME("LineEdit")); if (!flat) { draw_style_box(sb, Rect2(Vector2(), size)); } @@ -327,14 +323,14 @@ void EditorSpinSlider::_draw_spin_slider() { int label_width = font->get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width; int number_width = size.width - sb->get_minimum_size().width - label_width - sep; - Ref<Texture2D> updown = get_theme_icon(is_read_only() ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox")); + Ref<Texture2D> updown = get_theme_icon(read_only ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox")); String numstr = get_text_value(); int vofs = (size.height - font->get_height(font_size)) / 2 + font->get_ascent(font_size); - Color fc = get_theme_color(is_read_only() ? SNAME("font_uneditable_color") : SceneStringName(font_color), SNAME("LineEdit")); - Color lc = get_theme_color(is_read_only() ? SNAME("read_only_label_color") : SNAME("label_color")); + Color fc = get_theme_color(read_only ? SNAME("font_uneditable_color") : SceneStringName(font_color), SNAME("LineEdit")); + Color lc = get_theme_color(read_only ? SNAME("read_only_label_color") : SNAME("label_color")); if (flat && !label.is_empty()) { Ref<StyleBox> label_bg = get_theme_stylebox(SNAME("label_bg"), SNAME("EditorSpinSlider")); @@ -384,7 +380,7 @@ void EditorSpinSlider::_draw_spin_slider() { if (!hide_slider) { if (get_step() == 1) { - Ref<Texture2D> updown2 = is_read_only() ? theme_cache.updown_disabled_icon : theme_cache.updown_icon; + Ref<Texture2D> updown2 = read_only ? theme_cache.updown_disabled_icon : theme_cache.updown_icon; int updown_vofs = (size.height - updown2->get_height()) / 2; if (rtl) { updown_offset = sb->get_margin(SIDE_LEFT); @@ -604,7 +600,7 @@ void EditorSpinSlider::_value_focus_exited() { return; } - if (is_read_only()) { + if (read_only) { // Spin slider has become read only while it was being edited. return; } @@ -640,7 +636,7 @@ void EditorSpinSlider::_grabber_mouse_exited() { void EditorSpinSlider::set_read_only(bool p_enable) { read_only = p_enable; - if (read_only && value_input) { + if (read_only && value_input && value_input->is_inside_tree()) { value_input->release_focus(); } @@ -665,7 +661,7 @@ bool EditorSpinSlider::is_grabbing() const { } void EditorSpinSlider::_focus_entered() { - if (is_read_only()) { + if (read_only) { return; } diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp index 7ca3cb6c3a..011d0135b4 100644 --- a/editor/import/3d/scene_import_settings.cpp +++ b/editor/import/3d/scene_import_settings.cpp @@ -37,6 +37,7 @@ #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/plugins/skeleton_3d_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/3d/importer_mesh_instance_3d.h" #include "scene/animation/animation_player.h" @@ -419,7 +420,9 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite animation_player->connect(SceneStringName(animation_finished), callable_mp(this, &SceneImportSettingsDialog::_animation_finished)); } else if (Object::cast_to<Skeleton3D>(p_node)) { category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE; - skeletons.push_back(Object::cast_to<Skeleton3D>(p_node)); + Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(p_node); + skeleton->connect(SceneStringName(tree_entered), callable_mp(this, &SceneImportSettingsDialog::_skeleton_tree_entered).bind(skeleton)); + skeletons.push_back(skeleton); } else { category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_NODE; } @@ -480,6 +483,31 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite contents_aabb.merge_with(aabb); } } + + Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(p_node); + if (skeleton) { + Ref<ArrayMesh> bones_mesh = Skeleton3DGizmoPlugin::get_bones_mesh(skeleton, -1, true); + + bones_mesh_preview->set_mesh(bones_mesh); + + Transform3D accum_xform; + Node3D *base = skeleton; + while (base) { + accum_xform = base->get_transform() * accum_xform; + base = Object::cast_to<Node3D>(base->get_parent()); + } + + bones_mesh_preview->set_transform(accum_xform * skeleton->get_transform()); + + AABB aabb = accum_xform.xform(bones_mesh->get_aabb()); + + if (first_aabb) { + contents_aabb = aabb; + first_aabb = false; + } else { + contents_aabb.merge_with(aabb); + } + } } void SceneImportSettingsDialog::_update_scene() { @@ -795,6 +823,7 @@ void SceneImportSettingsDialog::_select(Tree *p_from, const String &p_type, cons selecting = true; scene_import_settings_data->hide_options = false; + bones_mesh_preview->hide(); if (p_type == "Node") { node_selected->hide(); // Always hide just in case. mesh_preview->hide(); @@ -834,6 +863,7 @@ void SceneImportSettingsDialog::_select(Tree *p_from, const String &p_type, cons scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE; } else if (Object::cast_to<Skeleton3D>(nd.node)) { scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE; + bones_mesh_preview->show(); } else { scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_NODE; scene_import_settings_data->hide_options = editing_animation; @@ -853,6 +883,8 @@ void SceneImportSettingsDialog::_select(Tree *p_from, const String &p_type, cons scene_import_settings_data->settings = &ad.settings; scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_ANIMATION; + + _animation_update_skeleton_visibility(); } else if (p_type == "Mesh") { node_selected->hide(); if (Object::cast_to<Node3D>(scene)) { @@ -1055,6 +1087,11 @@ void SceneImportSettingsDialog::_animation_slider_value_changed(double p_value) animation_player->seek(p_value * animation_map[selected_id].animation->get_length(), true); } +void SceneImportSettingsDialog::_skeleton_tree_entered(Skeleton3D *p_skeleton) { + bones_mesh_preview->set_skeleton_path(p_skeleton->get_path()); + bones_mesh_preview->set_skin(p_skeleton->register_skin(p_skeleton->create_skin_from_rest_transforms())); +} + void SceneImportSettingsDialog::_animation_finished(const StringName &p_name) { Animation::LoopMode loop_mode = animation_loop_mode; @@ -1080,6 +1117,14 @@ void SceneImportSettingsDialog::_animation_finished(const StringName &p_name) { } } +void SceneImportSettingsDialog::_animation_update_skeleton_visibility() { + if (animation_toggle_skeleton_visibility->is_pressed()) { + bones_mesh_preview->show(); + } else { + bones_mesh_preview->hide(); + } +} + void SceneImportSettingsDialog::_material_tree_selected() { if (selecting) { return; @@ -1282,6 +1327,8 @@ void SceneImportSettingsDialog::_notification(int p_what) { light_1_switch->set_icon(theme_cache.light_1_icon); light_2_switch->set_icon(theme_cache.light_2_icon); light_rotate_switch->set_icon(theme_cache.rotate_icon); + + animation_toggle_skeleton_visibility->set_icon(get_editor_theme_icon(SNAME("Skeleton3D"))); } break; case NOTIFICATION_PROCESS: { @@ -1661,6 +1708,7 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() { animation_hbox->add_child(animation_stop_button); animation_stop_button->set_flat(true); animation_stop_button->set_focus_mode(Control::FOCUS_NONE); + animation_stop_button->set_tooltip_text(TTR("Selected Animation Stop")); animation_stop_button->connect(SceneStringName(pressed), callable_mp(this, &SceneImportSettingsDialog::_stop_current_animation)); animation_slider = memnew(HSlider); @@ -1673,6 +1721,15 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() { animation_slider->set_focus_mode(Control::FOCUS_NONE); animation_slider->connect(SceneStringName(value_changed), callable_mp(this, &SceneImportSettingsDialog::_animation_slider_value_changed)); + animation_toggle_skeleton_visibility = memnew(Button); + animation_hbox->add_child(animation_toggle_skeleton_visibility); + animation_toggle_skeleton_visibility->set_toggle_mode(true); + animation_toggle_skeleton_visibility->set_flat(true); + animation_toggle_skeleton_visibility->set_focus_mode(Control::FOCUS_NONE); + animation_toggle_skeleton_visibility->set_tooltip_text(TTR("Toggle Animation Skeleton Visibility")); + + animation_toggle_skeleton_visibility->connect(SceneStringName(pressed), callable_mp(this, &SceneImportSettingsDialog::_animation_update_skeleton_visibility)); + base_viewport->set_use_own_world_3d(true); HBoxContainer *viewport_hbox = memnew(HBoxContainer); @@ -1800,6 +1857,13 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() { collider_mat->set_albedo(Color(0.5, 0.5, 1.0)); } + { + bones_mesh_preview = memnew(MeshInstance3D); + bones_mesh_preview->set_cast_shadows_setting(GeometryInstance3D::SHADOW_CASTING_SETTING_OFF); + bones_mesh_preview->set_skeleton_path(NodePath()); + base_viewport->add_child(bones_mesh_preview); + } + inspector = memnew(EditorInspector); inspector->set_custom_minimum_size(Size2(300 * EDSCALE, 0)); inspector->connect(SNAME("property_edited"), callable_mp(this, &SceneImportSettingsDialog::_inspector_property_edited)); diff --git a/editor/import/3d/scene_import_settings.h b/editor/import/3d/scene_import_settings.h index bbd0d2c22d..1088acf772 100644 --- a/editor/import/3d/scene_import_settings.h +++ b/editor/import/3d/scene_import_settings.h @@ -109,10 +109,12 @@ class SceneImportSettingsDialog : public ConfirmationDialog { HSlider *animation_slider = nullptr; Button *animation_play_button = nullptr; Button *animation_stop_button = nullptr; + Button *animation_toggle_skeleton_visibility = nullptr; Animation::LoopMode animation_loop_mode = Animation::LOOP_NONE; bool animation_pingpong = false; bool previous_import_as_skeleton = false; bool previous_rest_as_reset = false; + MeshInstance3D *bones_mesh_preview = nullptr; Ref<StandardMaterial3D> collider_mat; @@ -187,9 +189,11 @@ class SceneImportSettingsDialog : public ConfirmationDialog { void _reset_animation(const String &p_animation_name = ""); void _animation_slider_value_changed(double p_value); void _animation_finished(const StringName &p_name); + void _animation_update_skeleton_visibility(); void _material_tree_selected(); void _mesh_tree_selected(); void _scene_tree_selected(); + void _skeleton_tree_entered(Skeleton3D *p_skeleton); void _cleanup(); void _on_light_1_switch_pressed(); void _on_light_2_switch_pressed(); diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 4b7bf1674f..d1032470f2 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -5384,7 +5384,7 @@ CanvasItemEditor::CanvasItemEditor() { main_menu_hbox->add_child(pivot_button); pivot_button->set_toggle_mode(true); pivot_button->connect(SceneStringName(pressed), callable_mp(this, &CanvasItemEditor::_button_tool_select).bind(TOOL_EDIT_PIVOT)); - pivot_button->set_tooltip_text(TTR("Click to change object's rotation pivot.") + "\n" + TTR("Shift: Set temporary rotation pivot.") + "\n" + TTR("Click this button while holding Shift to put the rotation pivot in the center of the selected nodes.")); + pivot_button->set_tooltip_text(TTR("Click to change object's rotation pivot.") + "\n" + TTR("Shift: Set temporary rotation pivot.") + "\n" + TTR("Click this button while holding Shift to put the temporary rotation pivot in the center of the selected nodes.")); pan_button = memnew(Button); pan_button->set_theme_type_variation("FlatButton"); diff --git a/editor/plugins/path_2d_editor_plugin.cpp b/editor/plugins/path_2d_editor_plugin.cpp index 24cc15d656..0455b266b4 100644 --- a/editor/plugins/path_2d_editor_plugin.cpp +++ b/editor/plugins/path_2d_editor_plugin.cpp @@ -42,7 +42,6 @@ void Path2DEditor::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_ENTER_TREE: case NOTIFICATION_THEME_CHANGED: { curve_edit->set_icon(get_editor_theme_icon(SNAME("CurveEdit"))); curve_edit_curve->set_icon(get_editor_theme_icon(SNAME("CurveCurve"))); @@ -50,6 +49,8 @@ void Path2DEditor::_notification(int p_what) { curve_del->set_icon(get_editor_theme_icon(SNAME("CurveDelete"))); curve_close->set_icon(get_editor_theme_icon(SNAME("CurveClose"))); curve_clear_points->set_icon(get_editor_theme_icon(SNAME("Clear"))); + + create_curve_button->set_icon(get_editor_theme_icon(SNAME("Curve2D"))); } break; } } @@ -450,6 +451,16 @@ void Path2DEditor::_node_visibility_changed() { } canvas_item_editor->update_viewport(); + _update_toolbar(); +} + +void Path2DEditor::_update_toolbar() { + if (!node) { + return; + } + bool has_curve = node->get_curve().is_valid(); + toolbar->set_visible(has_curve); + create_curve_button->set_visible(!has_curve); } void Path2DEditor::edit(Node *p_path2d) { @@ -463,6 +474,7 @@ void Path2DEditor::edit(Node *p_path2d) { if (p_path2d) { node = Object::cast_to<Path2D>(p_path2d); + _update_toolbar(); if (!node->is_connected(SceneStringName(visibility_changed), callable_mp(this, &Path2DEditor::_node_visibility_changed))) { node->connect(SceneStringName(visibility_changed), callable_mp(this, &Path2DEditor::_node_visibility_changed)); @@ -477,7 +489,7 @@ void Path2DEditor::edit(Node *p_path2d) { } void Path2DEditor::_bind_methods() { - //ClassDB::bind_method(D_METHOD("_menu_option"),&Path2DEditor::_menu_option); + ClassDB::bind_method(D_METHOD("_update_toolbar"), &Path2DEditor::_update_toolbar); ClassDB::bind_method(D_METHOD("_clear_curve_points"), &Path2DEditor::_clear_curve_points); ClassDB::bind_method(D_METHOD("_restore_curve_points"), &Path2DEditor::_restore_curve_points); } @@ -601,6 +613,21 @@ void Path2DEditor::_cancel_current_action() { action = ACTION_NONE; } +void Path2DEditor::_create_curve() { + ERR_FAIL_NULL(node); + + Ref<Curve2D> new_curve; + new_curve.instantiate(); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Create Curve in Path2D")); + undo_redo->add_do_property(node, "curve", new_curve); + undo_redo->add_undo_property(node, "curve", Ref<Curve2D>()); + undo_redo->add_do_method(this, "_update_toolbar"); + undo_redo->add_undo_method(this, "_update_toolbar"); + undo_redo->commit_action(); +} + void Path2DEditor::_confirm_clear_points() { if (!node || node->get_curve().is_null()) { return; @@ -648,6 +675,8 @@ void Path2DEditor::_restore_curve_points(Path2D *p_path2d, const PackedVector2Ar } Path2DEditor::Path2DEditor() { + toolbar = memnew(HBoxContainer); + curve_edit = memnew(Button); curve_edit->set_theme_type_variation("FlatButton"); curve_edit->set_toggle_mode(true); @@ -655,7 +684,7 @@ Path2DEditor::Path2DEditor() { curve_edit->set_focus_mode(Control::FOCUS_NONE); curve_edit->set_tooltip_text(TTR("Select Points") + "\n" + TTR("Shift+Drag: Select Control Points") + "\n" + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Click: Add Point") + "\n" + TTR("Left Click: Split Segment (in curve)") + "\n" + TTR("Right Click: Delete Point")); curve_edit->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_EDIT)); - add_child(curve_edit); + toolbar->add_child(curve_edit); curve_edit_curve = memnew(Button); curve_edit_curve->set_theme_type_variation("FlatButton"); @@ -663,7 +692,7 @@ Path2DEditor::Path2DEditor() { curve_edit_curve->set_focus_mode(Control::FOCUS_NONE); curve_edit_curve->set_tooltip_text(TTR("Select Control Points (Shift+Drag)")); curve_edit_curve->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_EDIT_CURVE)); - add_child(curve_edit_curve); + toolbar->add_child(curve_edit_curve); curve_create = memnew(Button); curve_create->set_theme_type_variation("FlatButton"); @@ -671,7 +700,7 @@ Path2DEditor::Path2DEditor() { curve_create->set_focus_mode(Control::FOCUS_NONE); curve_create->set_tooltip_text(TTR("Add Point (in empty space)") + "\n" + TTR("Right Click: Delete Point")); curve_create->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_CREATE)); - add_child(curve_create); + toolbar->add_child(curve_create); curve_del = memnew(Button); curve_del->set_theme_type_variation("FlatButton"); @@ -679,42 +708,48 @@ Path2DEditor::Path2DEditor() { curve_del->set_focus_mode(Control::FOCUS_NONE); curve_del->set_tooltip_text(TTR("Delete Point")); curve_del->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_DELETE)); - add_child(curve_del); + toolbar->add_child(curve_del); curve_close = memnew(Button); curve_close->set_theme_type_variation("FlatButton"); curve_close->set_focus_mode(Control::FOCUS_NONE); curve_close->set_tooltip_text(TTR("Close Curve")); curve_close->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_CLOSE)); - add_child(curve_close); + toolbar->add_child(curve_close); curve_clear_points = memnew(Button); curve_clear_points->set_theme_type_variation("FlatButton"); curve_clear_points->set_focus_mode(Control::FOCUS_NONE); curve_clear_points->set_tooltip_text(TTR("Clear Points")); curve_clear_points->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_confirm_clear_points)); - add_child(curve_clear_points); + toolbar->add_child(curve_clear_points); clear_points_dialog = memnew(ConfirmationDialog); clear_points_dialog->set_title(TTR("Please Confirm...")); clear_points_dialog->set_text(TTR("Remove all curve points?")); clear_points_dialog->connect(SceneStringName(confirmed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_CLEAR_POINTS)); - add_child(clear_points_dialog); - - PopupMenu *menu; + toolbar->add_child(clear_points_dialog); handle_menu = memnew(MenuButton); handle_menu->set_flat(false); handle_menu->set_theme_type_variation("FlatMenuButton"); handle_menu->set_text(TTR("Options")); - add_child(handle_menu); + toolbar->add_child(handle_menu); - menu = handle_menu->get_popup(); + PopupMenu *menu = handle_menu->get_popup(); menu->add_check_item(TTR("Mirror Handle Angles")); menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle); menu->add_check_item(TTR("Mirror Handle Lengths")); menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); menu->connect(SceneStringName(id_pressed), callable_mp(this, &Path2DEditor::_handle_option_pressed)); + + add_child(toolbar); + + create_curve_button = memnew(Button); + create_curve_button->set_text(TTR("Create Curve")); + create_curve_button->hide(); + add_child(create_curve_button); + create_curve_button->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_create_curve)); } void Path2DEditorPlugin::edit(Object *p_object) { diff --git a/editor/plugins/path_2d_editor_plugin.h b/editor/plugins/path_2d_editor_plugin.h index f45b75a968..f2f03f12f1 100644 --- a/editor/plugins/path_2d_editor_plugin.h +++ b/editor/plugins/path_2d_editor_plugin.h @@ -58,6 +58,7 @@ class Path2DEditor : public HBoxContainer { }; Mode mode = MODE_EDIT; + HBoxContainer *toolbar = nullptr; Button *curve_clear_points = nullptr; Button *curve_close = nullptr; Button *curve_create = nullptr; @@ -66,6 +67,7 @@ class Path2DEditor : public HBoxContainer { Button *curve_edit_curve = nullptr; MenuButton *handle_menu = nullptr; + Button *create_curve_button = nullptr; ConfirmationDialog *clear_points_dialog = nullptr; bool mirror_handle_angle = true; @@ -100,7 +102,9 @@ class Path2DEditor : public HBoxContainer { void _cancel_current_action(); void _node_visibility_changed(); + void _update_toolbar(); + void _create_curve(); void _confirm_clear_points(); void _clear_curve_points(Path2D *p_path2d); void _restore_curve_points(Path2D *p_path2d, const PackedVector2Array &p_points); diff --git a/editor/plugins/path_3d_editor_plugin.cpp b/editor/plugins/path_3d_editor_plugin.cpp index 240206e124..890035e6a6 100644 --- a/editor/plugins/path_3d_editor_plugin.cpp +++ b/editor/plugins/path_3d_editor_plugin.cpp @@ -652,6 +652,7 @@ void Path3DEditorPlugin::edit(Object *p_object) { if (path->get_curve().is_valid()) { path->get_curve()->emit_signal(CoreStringName(changed)); } + _update_toolbar(); } } else { Path3D *pre = path; @@ -686,11 +687,11 @@ void Path3DEditorPlugin::make_visible(bool p_visible) { } void Path3DEditorPlugin::_mode_changed(int p_mode) { - curve_create->set_pressed(p_mode == MODE_CREATE); - curve_edit_curve->set_pressed(p_mode == MODE_EDIT_CURVE); - curve_edit_tilt->set_pressed(p_mode == MODE_EDIT_TILT); - curve_edit->set_pressed(p_mode == MODE_EDIT); - curve_del->set_pressed(p_mode == MODE_DELETE); + curve_create->set_pressed_no_signal(p_mode == MODE_CREATE); + curve_edit_curve->set_pressed_no_signal(p_mode == MODE_EDIT_CURVE); + curve_edit_tilt->set_pressed_no_signal(p_mode == MODE_EDIT_TILT); + curve_edit->set_pressed_no_signal(p_mode == MODE_EDIT); + curve_del->set_pressed_no_signal(p_mode == MODE_DELETE); Node3DEditor::get_singleton()->clear_subgizmo_selection(); } @@ -732,6 +733,21 @@ void Path3DEditorPlugin::_handle_option_pressed(int p_option) { } } +void Path3DEditorPlugin::_create_curve() { + ERR_FAIL_NULL(path); + + Ref<Curve3D> new_curve; + new_curve.instantiate(); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Create Curve in Path3D")); + undo_redo->add_do_property(path, "curve", new_curve); + undo_redo->add_undo_property(path, "curve", Ref<Curve3D>()); + undo_redo->add_do_method(this, "_update_toolbar"); + undo_redo->add_undo_method(this, "_update_toolbar"); + undo_redo->commit_action(); +} + void Path3DEditorPlugin::_confirm_clear_points() { if (!path || path->get_curve().is_null() || path->get_curve()->get_point_count() == 0) { return; @@ -774,52 +790,33 @@ void Path3DEditorPlugin::_restore_curve_points(const PackedVector3Array &p_point } void Path3DEditorPlugin::_update_theme() { - // TODO: Split the EditorPlugin instance from the UI instance and connect this properly. - // See the 2D path editor for inspiration. - curve_edit->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveEdit"), EditorStringName(EditorIcons))); - curve_edit_curve->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveCurve"), EditorStringName(EditorIcons))); - curve_edit_tilt->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveTilt"), EditorStringName(EditorIcons))); - curve_create->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveCreate"), EditorStringName(EditorIcons))); - curve_del->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveDelete"), EditorStringName(EditorIcons))); - curve_close->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveClose"), EditorStringName(EditorIcons))); - curve_clear_points->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Clear"), EditorStringName(EditorIcons))); + curve_edit->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveEdit"))); + curve_edit_curve->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveCurve"))); + curve_edit_tilt->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveTilt"))); + curve_create->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveCreate"))); + curve_del->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveDelete"))); + curve_close->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("CurveClose"))); + curve_clear_points->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("Clear"))); + create_curve_button->set_icon(topmenu_bar->get_editor_theme_icon(SNAME("Curve3D"))); } -void Path3DEditorPlugin::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - curve_create->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_CREATE)); - curve_edit_curve->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT_CURVE)); - curve_edit_tilt->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT_TILT)); - curve_edit->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT)); - curve_del->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_DELETE)); - curve_close->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_close_curve)); - - _update_theme(); - } break; - - case NOTIFICATION_READY: { - // FIXME: This can trigger theme updates when the nodes that we want to update are not yet available. - // The toolbar should be extracted to a dedicated control and theme updates should be handled through - // the notification. - Node3DEditor::get_singleton()->connect(SceneStringName(theme_changed), callable_mp(this, &Path3DEditorPlugin::_update_theme)); - } break; +void Path3DEditorPlugin::_update_toolbar() { + if (!path) { + return; } + bool has_curve = path->get_curve().is_valid(); + toolbar->set_visible(has_curve); + create_curve_button->set_visible(!has_curve); } void Path3DEditorPlugin::_bind_methods() { + ClassDB::bind_method(D_METHOD("_update_toolbar"), &Path3DEditorPlugin::_update_toolbar); ClassDB::bind_method(D_METHOD("_clear_curve_points"), &Path3DEditorPlugin::_clear_curve_points); ClassDB::bind_method(D_METHOD("_restore_curve_points"), &Path3DEditorPlugin::_restore_curve_points); } -Path3DEditorPlugin *Path3DEditorPlugin::singleton = nullptr; - Path3DEditorPlugin::Path3DEditorPlugin() { - path = nullptr; singleton = this; - mirror_handle_angle = true; - mirror_handle_length = true; - disk_size = EDITOR_DEF_RST("editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size", 0.8); Ref<Path3DGizmoPlugin> gizmo_plugin = memnew(Path3DGizmoPlugin(disk_size)); @@ -828,80 +825,93 @@ Path3DEditorPlugin::Path3DEditorPlugin() { topmenu_bar = memnew(HBoxContainer); topmenu_bar->hide(); - Node3DEditor::get_singleton()->add_control_to_menu_panel(topmenu_bar); + + toolbar = memnew(HBoxContainer); + topmenu_bar->add_child(toolbar); curve_edit = memnew(Button); curve_edit->set_theme_type_variation("FlatButton"); curve_edit->set_toggle_mode(true); curve_edit->set_focus_mode(Control::FOCUS_NONE); curve_edit->set_tooltip_text(TTR("Select Points") + "\n" + TTR("Shift+Click: Select multiple Points") + "\n" + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Click: Add Point") + "\n" + TTR("Right Click: Delete Point")); - topmenu_bar->add_child(curve_edit); + toolbar->add_child(curve_edit); + curve_edit->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT)); curve_edit_curve = memnew(Button); curve_edit_curve->set_theme_type_variation("FlatButton"); curve_edit_curve->set_toggle_mode(true); curve_edit_curve->set_focus_mode(Control::FOCUS_NONE); curve_edit_curve->set_tooltip_text(TTR("Select Control Points") + "\n" + TTR("Shift+Click: Drag out Control Points")); - topmenu_bar->add_child(curve_edit_curve); + toolbar->add_child(curve_edit_curve); + curve_edit_curve->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT_CURVE)); curve_edit_tilt = memnew(Button); curve_edit_tilt->set_theme_type_variation("FlatButton"); curve_edit_tilt->set_toggle_mode(true); curve_edit_tilt->set_focus_mode(Control::FOCUS_NONE); curve_edit_tilt->set_tooltip_text(TTR("Select Tilt Handles")); - topmenu_bar->add_child(curve_edit_tilt); + toolbar->add_child(curve_edit_tilt); + curve_edit_tilt->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT_TILT)); curve_create = memnew(Button); curve_create->set_theme_type_variation("FlatButton"); curve_create->set_toggle_mode(true); curve_create->set_focus_mode(Control::FOCUS_NONE); curve_create->set_tooltip_text(TTR("Add Point (in empty space)") + "\n" + TTR("Split Segment (in curve)")); - topmenu_bar->add_child(curve_create); + toolbar->add_child(curve_create); + curve_create->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_CREATE)); curve_del = memnew(Button); curve_del->set_theme_type_variation("FlatButton"); curve_del->set_toggle_mode(true); curve_del->set_focus_mode(Control::FOCUS_NONE); curve_del->set_tooltip_text(TTR("Delete Point")); - topmenu_bar->add_child(curve_del); + toolbar->add_child(curve_del); + curve_del->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_DELETE)); curve_close = memnew(Button); curve_close->set_theme_type_variation("FlatButton"); curve_close->set_focus_mode(Control::FOCUS_NONE); curve_close->set_tooltip_text(TTR("Close Curve")); - topmenu_bar->add_child(curve_close); + toolbar->add_child(curve_close); + curve_close->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_close_curve)); curve_clear_points = memnew(Button); curve_clear_points->set_theme_type_variation("FlatButton"); curve_clear_points->set_focus_mode(Control::FOCUS_NONE); curve_clear_points->set_tooltip_text(TTR("Clear Points")); curve_clear_points->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_confirm_clear_points)); - topmenu_bar->add_child(curve_clear_points); + toolbar->add_child(curve_clear_points); clear_points_dialog = memnew(ConfirmationDialog); clear_points_dialog->set_title(TTR("Please Confirm...")); clear_points_dialog->set_text(TTR("Remove all curve points?")); clear_points_dialog->connect(SceneStringName(confirmed), callable_mp(this, &Path3DEditorPlugin::_clear_points)); - topmenu_bar->add_child(clear_points_dialog); + toolbar->add_child(clear_points_dialog); handle_menu = memnew(MenuButton); handle_menu->set_flat(false); handle_menu->set_theme_type_variation("FlatMenuButton"); handle_menu->set_text(TTR("Options")); - topmenu_bar->add_child(handle_menu); + toolbar->add_child(handle_menu); + + create_curve_button = memnew(Button); + create_curve_button->set_text(TTR("Create Curve")); + create_curve_button->hide(); + topmenu_bar->add_child(create_curve_button); + create_curve_button->connect(SceneStringName(pressed), callable_mp(this, &Path3DEditorPlugin::_create_curve)); - PopupMenu *menu; - menu = handle_menu->get_popup(); + PopupMenu *menu = handle_menu->get_popup(); menu->add_check_item(TTR("Mirror Handle Angles")); menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle); menu->add_check_item(TTR("Mirror Handle Lengths")); menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); menu->connect(SceneStringName(id_pressed), callable_mp(this, &Path3DEditorPlugin::_handle_option_pressed)); - curve_edit->set_pressed(true); -} + curve_edit->set_pressed_no_signal(true); -Path3DEditorPlugin::~Path3DEditorPlugin() { + topmenu_bar->connect(SceneStringName(theme_changed), callable_mp(this, &Path3DEditorPlugin::_update_theme)); + Node3DEditor::get_singleton()->add_control_to_menu_panel(topmenu_bar); } Ref<EditorNode3DGizmo> Path3DGizmoPlugin::create_gizmo(Node3D *p_spatial) { diff --git a/editor/plugins/path_3d_editor_plugin.h b/editor/plugins/path_3d_editor_plugin.h index ee73df1617..60cb7f940f 100644 --- a/editor/plugins/path_3d_editor_plugin.h +++ b/editor/plugins/path_3d_editor_plugin.h @@ -113,6 +113,8 @@ class Path3DEditorPlugin : public EditorPlugin { Ref<Path3DGizmoPlugin> path_3d_gizmo_plugin; HBoxContainer *topmenu_bar = nullptr; + + HBoxContainer *toolbar = nullptr; Button *curve_create = nullptr; Button *curve_edit = nullptr; Button *curve_edit_curve = nullptr; @@ -122,6 +124,7 @@ class Path3DEditorPlugin : public EditorPlugin { Button *curve_clear_points = nullptr; MenuButton *handle_menu = nullptr; + Button *create_curve_button = nullptr; ConfirmationDialog *clear_points_dialog = nullptr; float disk_size = 0.8; @@ -138,14 +141,16 @@ class Path3DEditorPlugin : public EditorPlugin { Path3D *path = nullptr; void _update_theme(); + void _update_toolbar(); void _mode_changed(int p_mode); void _close_curve(); void _handle_option_pressed(int p_option); bool handle_clicked = false; - bool mirror_handle_angle; - bool mirror_handle_length; + bool mirror_handle_angle = true; + bool mirror_handle_length = true; + void _create_curve(); void _confirm_clear_points(); void _clear_points(); void _clear_curve_points(); @@ -157,13 +162,12 @@ class Path3DEditorPlugin : public EditorPlugin { }; protected: - void _notification(int p_what); static void _bind_methods(); public: Path3D *get_edited_path() { return path; } - static Path3DEditorPlugin *singleton; + inline static Path3DEditorPlugin *singleton = nullptr; virtual EditorPlugin::AfterGUIInput forward_3d_gui_input(Camera3D *p_camera, const Ref<InputEvent> &p_event) override; virtual String get_name() const override { return "Path3D"; } @@ -178,7 +182,6 @@ public: void set_handle_clicked(bool clicked) { handle_clicked = clicked; } Path3DEditorPlugin(); - ~Path3DEditorPlugin(); }; #endif // PATH_3D_EDITOR_PLUGIN_H diff --git a/editor/plugins/skeleton_3d_editor_plugin.cpp b/editor/plugins/skeleton_3d_editor_plugin.cpp index dc4d4db3f8..9fc88b3a6a 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.cpp +++ b/editor/plugins/skeleton_3d_editor_plugin.cpp @@ -1348,16 +1348,18 @@ int Skeleton3DEditor::get_selected_bone() const { return selected_bone; } +Skeleton3DGizmoPlugin::SelectionMaterials Skeleton3DGizmoPlugin::selection_materials; + Skeleton3DGizmoPlugin::Skeleton3DGizmoPlugin() { - unselected_mat = Ref<StandardMaterial3D>(memnew(StandardMaterial3D)); - unselected_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); - unselected_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA); - unselected_mat->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); - unselected_mat->set_flag(StandardMaterial3D::FLAG_SRGB_VERTEX_COLOR, true); - unselected_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); - - selected_mat = Ref<ShaderMaterial>(memnew(ShaderMaterial)); - selected_sh = Ref<Shader>(memnew(Shader)); + selection_materials.unselected_mat.instantiate(); + selection_materials.unselected_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); + selection_materials.unselected_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA); + selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_SRGB_VERTEX_COLOR, true); + selection_materials.unselected_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); + + selection_materials.selected_mat.instantiate(); + Ref<Shader> selected_sh = Ref<Shader>(memnew(Shader)); selected_sh->set_code(R"( // Skeleton 3D gizmo bones shader. @@ -1376,7 +1378,7 @@ void fragment() { ALPHA = COLOR.a; } )"); - selected_mat->set_shader(selected_sh); + selection_materials.selected_mat->set_shader(selected_sh); // Register properties in editor settings. EDITOR_DEF_RST("editors/3d_gizmos/gizmo_colors/skeleton", Color(1, 0.8, 0.4)); @@ -1386,6 +1388,11 @@ void fragment() { EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "editors/3d_gizmos/gizmo_settings/bone_shape", PROPERTY_HINT_ENUM, "Wire,Octahedron")); } +Skeleton3DGizmoPlugin::~Skeleton3DGizmoPlugin() { + selection_materials.unselected_mat.unref(); + selection_materials.selected_mat.unref(); +} + bool Skeleton3DGizmoPlugin::has_gizmo(Node3D *p_spatial) { return Object::cast_to<Skeleton3D>(p_spatial) != nullptr; } @@ -1526,6 +1533,11 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { selected = se->get_selected_bone(); } + Ref<ArrayMesh> m = get_bones_mesh(skeleton, selected, p_gizmo->is_selected()); + p_gizmo->add_mesh(m, Ref<Material>(), Transform3D(), skeleton->register_skin(skeleton->create_skin_from_rest_transforms())); +} + +Ref<ArrayMesh> Skeleton3DGizmoPlugin::get_bones_mesh(Skeleton3D *p_skeleton, int p_selected, bool p_is_selected) { Color bone_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/skeleton"); Color selected_bone_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/selected_bone"); real_t bone_axis_length = EDITOR_GET("editors/3d_gizmos/gizmo_settings/bone_axis_length"); @@ -1539,11 +1551,11 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { Ref<SurfaceTool> surface_tool(memnew(SurfaceTool)); surface_tool->begin(Mesh::PRIMITIVE_LINES); - if (p_gizmo->is_selected()) { - surface_tool->set_material(selected_mat); + if (p_is_selected) { + surface_tool->set_material(selection_materials.selected_mat); } else { - unselected_mat->set_albedo(bone_color); - surface_tool->set_material(unselected_mat); + selection_materials.unselected_mat->set_albedo(bone_color); + surface_tool->set_material(selection_materials.unselected_mat); } LocalVector<int> bones; @@ -1557,16 +1569,16 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { weights[0] = 1; int current_bone_index = 0; - Vector<int> bones_to_process = skeleton->get_parentless_bones(); + Vector<int> bones_to_process = p_skeleton->get_parentless_bones(); while (bones_to_process.size() > current_bone_index) { int current_bone_idx = bones_to_process[current_bone_index]; current_bone_index++; - Color current_bone_color = (current_bone_idx == selected) ? selected_bone_color : bone_color; + Color current_bone_color = (current_bone_idx == p_selected) ? selected_bone_color : bone_color; Vector<int> child_bones_vector; - child_bones_vector = skeleton->get_bone_children(current_bone_idx); + child_bones_vector = p_skeleton->get_bone_children(current_bone_idx); int child_bones_size = child_bones_vector.size(); for (int i = 0; i < child_bones_size; i++) { @@ -1577,8 +1589,8 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { int child_bone_idx = child_bones_vector[i]; - Vector3 v0 = skeleton->get_bone_global_rest(current_bone_idx).origin; - Vector3 v1 = skeleton->get_bone_global_rest(child_bone_idx).origin; + Vector3 v0 = p_skeleton->get_bone_global_rest(current_bone_idx).origin; + Vector3 v1 = p_skeleton->get_bone_global_rest(child_bone_idx).origin; Vector3 d = (v1 - v0).normalized(); real_t dist = v0.distance_to(v1); @@ -1586,7 +1598,7 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { int closest = -1; real_t closest_d = 0.0; for (int j = 0; j < 3; j++) { - real_t dp = Math::abs(skeleton->get_bone_global_rest(current_bone_idx).basis[j].normalized().dot(d)); + real_t dp = Math::abs(p_skeleton->get_bone_global_rest(current_bone_idx).basis[j].normalized().dot(d)); if (j == 0 || dp > closest_d) { closest = j; } @@ -1613,7 +1625,7 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { for (int j = 0; j < 3; j++) { Vector3 axis; if (first == Vector3()) { - axis = d.cross(d.cross(skeleton->get_bone_global_rest(current_bone_idx).basis[j])).normalized(); + axis = d.cross(d.cross(p_skeleton->get_bone_global_rest(current_bone_idx).basis[j])).normalized(); first = axis; } else { axis = d.cross(first).normalized(); @@ -1668,7 +1680,7 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { surface_tool->add_vertex(v0); surface_tool->set_bones(bones); surface_tool->set_weights(weights); - surface_tool->add_vertex(v0 + (skeleton->get_bone_global_rest(current_bone_idx).basis.inverse())[j].normalized() * dist * bone_axis_length); + surface_tool->add_vertex(v0 + (p_skeleton->get_bone_global_rest(current_bone_idx).basis.inverse())[j].normalized() * dist * bone_axis_length); if (j == closest) { continue; @@ -1685,7 +1697,7 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { surface_tool->add_vertex(v1); surface_tool->set_bones(bones); surface_tool->set_weights(weights); - surface_tool->add_vertex(v1 + (skeleton->get_bone_global_rest(child_bone_idx).basis.inverse())[j].normalized() * dist * bone_axis_length); + surface_tool->add_vertex(v1 + (p_skeleton->get_bone_global_rest(child_bone_idx).basis.inverse())[j].normalized() * dist * bone_axis_length); if (j == closest) { continue; @@ -1698,6 +1710,5 @@ void Skeleton3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { } } - Ref<ArrayMesh> m = surface_tool->commit(); - p_gizmo->add_mesh(m, Ref<Material>(), Transform3D(), skeleton->register_skin(skeleton->create_skin_from_rest_transforms())); + return surface_tool->commit(); } diff --git a/editor/plugins/skeleton_3d_editor_plugin.h b/editor/plugins/skeleton_3d_editor_plugin.h index 0bb58aac23..d4dee1f16f 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.h +++ b/editor/plugins/skeleton_3d_editor_plugin.h @@ -260,11 +260,15 @@ public: class Skeleton3DGizmoPlugin : public EditorNode3DGizmoPlugin { GDCLASS(Skeleton3DGizmoPlugin, EditorNode3DGizmoPlugin); - Ref<StandardMaterial3D> unselected_mat; - Ref<ShaderMaterial> selected_mat; - Ref<Shader> selected_sh; + struct SelectionMaterials { + Ref<StandardMaterial3D> unselected_mat; + Ref<ShaderMaterial> selected_mat; + }; + static SelectionMaterials selection_materials; public: + static Ref<ArrayMesh> get_bones_mesh(Skeleton3D *p_skeleton, int p_selected, bool p_is_selected); + bool has_gizmo(Node3D *p_spatial) override; String get_gizmo_name() const override; int get_priority() const override; @@ -277,6 +281,7 @@ public: void redraw(EditorNode3DGizmo *p_gizmo) override; Skeleton3DGizmoPlugin(); + ~Skeleton3DGizmoPlugin(); }; #endif // SKELETON_3D_EDITOR_PLUGIN_H diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 9978c55d7b..2a39b11815 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1241,6 +1241,10 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { break; } + if (!_validate_no_foreign()) { + break; + } + List<Node *> selection = editor_selection->get_selected_node_list(); List<Node *>::Element *e = selection.front(); if (e) { @@ -2247,8 +2251,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 +2291,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 +2313,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 +2323,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 +2334,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 +2369,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 +2394,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 +2412,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 +2792,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 +2809,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 +2823,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 +2933,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(false); } 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 +2989,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 +4408,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..3041857d83 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -865,7 +865,6 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the // CheckBox. { Ref<StyleBoxFlat> checkbox_style = p_config.panel_container_style->duplicate(); - checkbox_style->set_content_margin_individual((p_config.increased_margin + 2) * EDSCALE, p_config.base_margin * EDSCALE, (p_config.increased_margin + 2) * EDSCALE, p_config.base_margin * EDSCALE); p_theme->set_stylebox(CoreStringName(normal), "CheckBox", checkbox_style); p_theme->set_stylebox(SceneStringName(pressed), "CheckBox", checkbox_style); @@ -1165,9 +1164,6 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the // LineEdit & TextEdit. { Ref<StyleBoxFlat> text_editor_style = p_config.button_style->duplicate(); - // The original button_style style has an extra 1 pixel offset that makes LineEdits not align with Buttons, - // so this compensates for that. - text_editor_style->set_content_margin(SIDE_TOP, text_editor_style->get_content_margin(SIDE_TOP) - 1 * EDSCALE); // Don't round the bottom corners to make the line look sharper. text_editor_style->set_corner_radius(CORNER_BOTTOM_LEFT, 0); @@ -1507,7 +1503,9 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the p_theme->set_constant("buttons_vertical_separation", "SpinBox", 0); p_theme->set_constant("field_and_buttons_separation", "SpinBox", 2); p_theme->set_constant("buttons_width", "SpinBox", 16); +#ifndef DISABLE_DEPRECATED p_theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1); +#endif } // ProgressBar. diff --git a/main/main.cpp b/main/main.cpp index 1af08bc036..18ffedef18 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1037,7 +1037,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph if (arg == "--audio-driver" || arg == "--display-driver" || arg == "--rendering-method" || - arg == "--rendering-driver") { + arg == "--rendering-driver" || + arg == "--xr-mode") { if (N) { forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(arg); forwardable_cli_arguments[CLI_SCOPE_TOOL].push_back(N->get()); 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/minimp3/audio_stream_mp3.cpp b/modules/minimp3/audio_stream_mp3.cpp index 394213963a..e29fe9295a 100644 --- a/modules/minimp3/audio_stream_mp3.cpp +++ b/modules/minimp3/audio_stream_mp3.cpp @@ -159,6 +159,9 @@ Ref<AudioSamplePlayback> AudioStreamPlaybackMP3::get_sample_playback() const { void AudioStreamPlaybackMP3::set_sample_playback(const Ref<AudioSamplePlayback> &p_playback) { sample_playback = p_playback; + if (sample_playback.is_valid()) { + sample_playback->stream_playback = Ref<AudioStreamPlayback>(this); + } } void AudioStreamPlaybackMP3::set_parameter(const StringName &p_name, const Variant &p_value) { diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs index 6a3884dabf..c734dc7be1 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedFields_ScriptProperties.generated.cs @@ -807,7 +807,7 @@ partial class ExportedFields properties.Add(new(type: (global::Godot.Variant.Type)23, name: PropertyName.@_fieldRid, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@_fieldGodotDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.@_fieldGodotArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); - properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@_fieldGodotGenericDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@_fieldGodotGenericDictionary, hint: (global::Godot.PropertyHint)38, hintString: "4/0:;1/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.@_fieldGodotGenericArray, hint: (global::Godot.PropertyHint)23, hintString: "2/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)31, name: PropertyName.@_fieldEmptyInt64Array, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); return properties; diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs index 3c48740773..0de840aa34 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/GeneratedSources/ExportedProperties_ScriptProperties.generated.cs @@ -925,7 +925,7 @@ partial class ExportedProperties properties.Add(new(type: (global::Godot.Variant.Type)23, name: PropertyName.@PropertyRid, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@PropertyGodotDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.@PropertyGodotArray, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); - properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@PropertyGodotGenericDictionary, hint: (global::Godot.PropertyHint)0, hintString: "", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); + properties.Add(new(type: (global::Godot.Variant.Type)27, name: PropertyName.@PropertyGodotGenericDictionary, hint: (global::Godot.PropertyHint)38, hintString: "4/0:;1/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); properties.Add(new(type: (global::Godot.Variant.Type)28, name: PropertyName.@PropertyGodotGenericArray, hint: (global::Godot.PropertyHint)23, hintString: "2/0:", usage: (global::Godot.PropertyUsageFlags)4102, exported: true)); return properties; } diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs index 4cf6a9f431..bb4c4824e7 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/GodotEnums.cs @@ -88,7 +88,8 @@ namespace Godot.SourceGenerators HideQuaternionEdit = 35, Password = 36, LayersAvoidance = 37, - Max = 38 + DictionaryType = 38, + Max = 39 } [Flags] diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs index d272832950..f5f51722b4 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/MarshalUtils.cs @@ -274,6 +274,14 @@ namespace Godot.SourceGenerators return null; } + public static ITypeSymbol[]? GetGenericElementTypes(ITypeSymbol typeSymbol) + { + if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType) + return genericType.TypeArguments.ToArray(); + + return null; + } + private static StringBuilder Append(this StringBuilder source, string a, string b) => source.Append(a).Append(b); diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs index 29bae6413d..0f86b3b91c 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptPropertiesGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -728,8 +729,72 @@ namespace Godot.SourceGenerators if (!isTypeArgument && variantType == VariantType.Dictionary) { - // TODO: Dictionaries are not supported in the inspector - return false; + var elementTypes = MarshalUtils.GetGenericElementTypes(type); + + if (elementTypes == null) + return false; // Non-generic Dictionary, so there's no hint to add + Debug.Assert(elementTypes.Length == 2); + + var keyElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[0], typeCache)!.Value; + var keyElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(keyElementMarshalType)!.Value; + var keyIsPresetHint = false; + var keyHintString = (string?)null; + + if (keyElementVariantType == VariantType.String || keyElementVariantType == VariantType.StringName) + keyIsPresetHint = GetStringArrayEnumHint(keyElementVariantType, exportAttr, out keyHintString); + + if (!keyIsPresetHint) + { + bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[0], + exportAttr, keyElementVariantType, isTypeArgument: true, + out var keyElementHint, out var keyElementHintString); + + // Format: type/hint:hint_string + if (hintRes) + { + keyHintString = (int)keyElementVariantType + "/" + (int)keyElementHint + ":"; + + if (keyElementHintString != null) + keyHintString += keyElementHintString; + } + else + { + keyHintString = (int)keyElementVariantType + "/" + (int)PropertyHint.None + ":"; + } + } + + var valueElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[1], typeCache)!.Value; + var valueElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(valueElementMarshalType)!.Value; + var valueIsPresetHint = false; + var valueHintString = (string?)null; + + if (valueElementVariantType == VariantType.String || valueElementVariantType == VariantType.StringName) + valueIsPresetHint = GetStringArrayEnumHint(valueElementVariantType, exportAttr, out valueHintString); + + if (!valueIsPresetHint) + { + bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[1], + exportAttr, valueElementVariantType, isTypeArgument: true, + out var valueElementHint, out var valueElementHintString); + + // Format: type/hint:hint_string + if (hintRes) + { + valueHintString = (int)valueElementVariantType + "/" + (int)valueElementHint + ":"; + + if (valueElementHintString != null) + valueHintString += valueElementHintString; + } + else + { + valueHintString = (int)valueElementVariantType + "/" + (int)PropertyHint.None + ":"; + } + } + + hint = PropertyHint.DictionaryType; + + hintString = keyHintString != null && valueHintString != null ? $"{keyHintString};{valueHintString}" : null; + return hintString != null; } return false; diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index b26f6d1bbf..2ec073e4fa 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -3809,6 +3809,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (return_info.type == Variant::ARRAY && return_info.hint == PROPERTY_HINT_ARRAY_TYPE) { imethod.return_type.cname = Variant::get_type_name(return_info.type) + "_@generic"; imethod.return_type.generic_type_parameters.push_back(TypeReference(return_info.hint_string)); + } else if (return_info.type == Variant::DICTIONARY && return_info.hint == PROPERTY_HINT_DICTIONARY_TYPE) { + imethod.return_type.cname = Variant::get_type_name(return_info.type) + "_@generic"; + Vector<String> split = return_info.hint_string.split(";"); + imethod.return_type.generic_type_parameters.push_back(TypeReference(split.get(0))); + imethod.return_type.generic_type_parameters.push_back(TypeReference(split.get(1))); } else if (return_info.hint == PROPERTY_HINT_RESOURCE_TYPE) { imethod.return_type.cname = return_info.hint_string; } else if (return_info.type == Variant::NIL && return_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) { @@ -3836,6 +3841,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) { iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string)); + } else if (arginfo.type == Variant::DICTIONARY && arginfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) { + iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; + Vector<String> split = arginfo.hint_string.split(";"); + iarg.type.generic_type_parameters.push_back(TypeReference(split.get(0))); + iarg.type.generic_type_parameters.push_back(TypeReference(split.get(1))); } else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) { iarg.type.cname = arginfo.hint_string; } else if (arginfo.type == Variant::NIL) { @@ -3963,6 +3973,11 @@ bool BindingsGenerator::_populate_object_type_interfaces() { } else if (arginfo.type == Variant::ARRAY && arginfo.hint == PROPERTY_HINT_ARRAY_TYPE) { iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; iarg.type.generic_type_parameters.push_back(TypeReference(arginfo.hint_string)); + } else if (arginfo.type == Variant::DICTIONARY && arginfo.hint == PROPERTY_HINT_DICTIONARY_TYPE) { + iarg.type.cname = Variant::get_type_name(arginfo.type) + "_@generic"; + Vector<String> split = arginfo.hint_string.split(";"); + iarg.type.generic_type_parameters.push_back(TypeReference(split.get(0))); + iarg.type.generic_type_parameters.push_back(TypeReference(split.get(1))); } else if (arginfo.hint == PROPERTY_HINT_RESOURCE_TYPE) { iarg.type.cname = arginfo.hint_string; } else if (arginfo.type == Variant::NIL) { diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index ecf7c05789..a19a75e722 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -271,17 +271,14 @@ OpenXRAPI *OpenXRAPI::singleton = nullptr; Vector<OpenXRExtensionWrapper *> OpenXRAPI::registered_extension_wrappers; bool OpenXRAPI::openxr_is_enabled(bool p_check_run_in_editor) { - // @TODO we need an overrule switch so we can force enable openxr, i.e run "godot --openxr_enabled" - - if (Engine::get_singleton()->is_editor_hint() && p_check_run_in_editor) { - // Disabled for now, using XR inside of the editor we'll be working on during the coming months. - return false; - } else { - if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) { - return GLOBAL_GET("xr/openxr/enabled"); + if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) { + if (Engine::get_singleton()->is_editor_hint() && p_check_run_in_editor) { + return GLOBAL_GET("xr/openxr/enabled.editor"); } else { - return XRServer::get_xr_mode() == XRServer::XRMODE_ON; + return GLOBAL_GET("xr/openxr/enabled"); } + } else { + return XRServer::get_xr_mode() == XRServer::XRMODE_ON; } } @@ -557,14 +554,11 @@ bool OpenXRAPI::create_instance() { extension_ptrs.push_back(enabled_extensions[i].get_data()); } - // Get our project name - String project_name = GLOBAL_GET("application/config/name"); - // Create our OpenXR instance XrApplicationInfo application_info{ - "", // applicationName, we'll set this down below + "Godot Engine", // applicationName, if we're running a game we'll update this down below. 1, // applicationVersion, we don't currently have this - "Godot Game Engine", // engineName + "Godot Engine", // engineName VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH, // engineVersion 4.0 -> 40000, 4.0.1 -> 40001, 4.1 -> 40100, etc. XR_API_VERSION_1_0 // apiVersion }; @@ -588,7 +582,11 @@ bool OpenXRAPI::create_instance() { extension_ptrs.ptr() // enabledExtensionNames }; - copy_string_to_char_buffer(project_name, instance_create_info.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE); + // Get our project name + String project_name = GLOBAL_GET("application/config/name"); + if (!project_name.is_empty()) { + copy_string_to_char_buffer(project_name, instance_create_info.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE); + } XrResult result = xrCreateInstance(&instance_create_info, &instance); ERR_FAIL_COND_V_MSG(XR_FAILED(result), false, "Failed to create XR instance."); @@ -2583,7 +2581,6 @@ OpenXRAPI::OpenXRAPI() { if (Engine::get_singleton()->is_editor_hint()) { // Enabled OpenXR in the editor? Adjust our settings for the editor - } else { // Load settings from project settings int form_factor_setting = GLOBAL_GET("xr/openxr/form_factor"); diff --git a/modules/vorbis/audio_stream_ogg_vorbis.cpp b/modules/vorbis/audio_stream_ogg_vorbis.cpp index ff032c88c6..c89534a60c 100644 --- a/modules/vorbis/audio_stream_ogg_vorbis.cpp +++ b/modules/vorbis/audio_stream_ogg_vorbis.cpp @@ -390,6 +390,9 @@ Ref<AudioSamplePlayback> AudioStreamPlaybackOggVorbis::get_sample_playback() con void AudioStreamPlaybackOggVorbis::set_sample_playback(const Ref<AudioSamplePlayback> &p_playback) { sample_playback = p_playback; + if (sample_playback.is_valid()) { + sample_playback->stream_playback = Ref<AudioStreamPlayback>(this); + } } AudioStreamPlaybackOggVorbis::~AudioStreamPlaybackOggVorbis() { diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 05b4f379b3..b9d15deec9 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -12,6 +12,7 @@ allprojects { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} // Godot user plugins custom maven repos String[] mavenRepos = getGodotPluginsMavenRepos() diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index dcac44e393..e758d4e99a 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -11,6 +11,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 771bda6948..974f072c18 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -18,12 +18,14 @@ allprojects { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedFlavors = ["editor", "template"] + supportedEditorVendors = ["google", "meta"] supportedFlavorsBuildTypes = [ "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] @@ -92,15 +94,20 @@ def templateExcludedBuildTask() { /** * Generates the build tasks for the given flavor * @param flavor Must be one of the supported flavors ('template' / 'editor') + * @param editorVendor Must be one of the supported editor vendors ('google' / 'meta') */ -def generateBuildTasks(String flavor = "template") { +def generateBuildTasks(String flavor = "template", String editorVendor = "google") { if (!supportedFlavors.contains(flavor)) { throw new GradleException("Invalid build flavor: $flavor") } + if (!supportedEditorVendors.contains(editorVendor)) { + throw new GradleException("Invalid editor vendor: $editorVendor") + } + String capitalizedEditorVendor = editorVendor.capitalize() def buildTasks = [] - // Only build the apks and aar files for which we have native shared libraries unless we intend + // Only build the binary files for which we have native shared libraries unless we intend // to run the scons build tasks. boolean excludeSconsBuildTasks = excludeSconsBuildTasks() boolean isTemplate = flavor == "template" @@ -163,28 +170,28 @@ def generateBuildTasks(String flavor = "template") { } } else { // Copy the generated editor apk to the bin directory. - String copyEditorApkTaskName = "copyEditor${capitalizedTarget}ApkToBin" + String copyEditorApkTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}ApkToBin" if (tasks.findByName(copyEditorApkTaskName) != null) { buildTasks += tasks.getByName(copyEditorApkTaskName) } else { buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) { - dependsOn ":editor:assemble${capitalizedTarget}" - from("editor/build/outputs/apk/${target}") + dependsOn ":editor:assemble${capitalizedEditorVendor}${capitalizedTarget}" + from("editor/build/outputs/apk/${editorVendor}/${target}") into(androidEditorBuildsDir) - include("android_editor-${target}*.apk") + include("android_editor-${editorVendor}-${target}*.apk") } } // Copy the generated editor aab to the bin directory. - String copyEditorAabTaskName = "copyEditor${capitalizedTarget}AabToBin" + String copyEditorAabTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}AabToBin" if (tasks.findByName(copyEditorAabTaskName) != null) { buildTasks += tasks.getByName(copyEditorAabTaskName) } else { buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) { - dependsOn ":editor:bundle${capitalizedTarget}" - from("editor/build/outputs/bundle/${target}") + dependsOn ":editor:bundle${capitalizedEditorVendor}${capitalizedTarget}" + from("editor/build/outputs/bundle/${editorVendor}${capitalizedTarget}") into(androidEditorBuildsDir) - include("android_editor-${target}*.aab") + include("android_editor-${editorVendor}-${target}*.aab") } } } @@ -197,15 +204,27 @@ def generateBuildTasks(String flavor = "template") { } /** - * Generate the Godot Editor Android apk. + * Generate the Godot Editor Android binaries. * * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries * must have been generated (via scons) prior to running this gradle task. - * The task will only build the apk(s) for which the shared libraries is available. + * The task will only build the binaries for which the shared libraries is available. */ task generateGodotEditor { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = generateBuildTasks("editor") + dependsOn = generateBuildTasks("editor", "google") +} + +/** + * Generate the Godot Editor Android binaries for Meta devices. + * + * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries + * must have been generated (via scons) prior to running this gradle task. + * The task will only build the binaries for which the shared libraries is available. + */ +task generateGodotMetaEditor { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + dependsOn = generateBuildTasks("editor", "meta") } /** diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index b8b4233636..54d6b9b5f3 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -5,16 +5,6 @@ plugins { id 'base' } -dependencies { - implementation "androidx.fragment:fragment:$versions.fragmentVersion" - implementation project(":lib") - - implementation "androidx.window:window:1.3.0" - implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation "org.bouncycastle:bcprov-jdk15to18:1.77" -} - ext { // Retrieve the build number from the environment variable; default to 0 if none is specified. // The build number is added as a suffix to the version code for upload to the Google Play store. @@ -154,4 +144,37 @@ android { doNotStrip '**/*.so' } } + + flavorDimensions = ["vendor"] + productFlavors { + google { + dimension "vendor" + missingDimensionStrategy 'products', 'editor' + } + meta { + dimension "vendor" + missingDimensionStrategy 'products', 'editor' + ndk { + //noinspection ChromeOsAbiSupport + abiFilters "arm64-v8a" + } + applicationIdSuffix ".meta" + versionNameSuffix "-meta" + minSdkVersion 23 + targetSdkVersion 32 + } + } +} + +dependencies { + implementation "androidx.fragment:fragment:$versions.fragmentVersion" + implementation project(":lib") + + implementation "androidx.window:window:1.3.0" + implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "org.bouncycastle:bcprov-jdk15to18:1.77" + + // Meta dependencies + metaImplementation "org.godotengine:godot-openxr-vendors-meta:3.0.0-stable" } diff --git a/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..f15d9f7768 --- /dev/null +++ b/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,39 @@ +/**************************************************************************/ +/* GodotEditor.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.editor + +/** + * Primary window of the Godot Editor. + * + * This is the implementation of the editor used when running on regular Android devices. + */ +open class GodotEditor : BaseGodotEditor() { +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt index 405b2fb57f..50daf44d2a 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt @@ -1,5 +1,5 @@ /**************************************************************************/ -/* GodotEditor.kt */ +/* BaseGodotEditor.kt */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -52,6 +52,8 @@ import org.godotengine.godot.GodotLib import org.godotengine.godot.error.Error import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix +import org.godotengine.godot.utils.isHorizonOSDevice +import org.godotengine.godot.utils.isNativeXRDevice import java.util.* import kotlin.math.min @@ -61,13 +63,11 @@ import kotlin.math.min * This provides the basic templates for the activities making up this application. * Each derived activity runs in its own process, which enable up to have several instances of * the Godot engine up and running at the same time. - * - * It also plays the role of the primary editor window. */ -open class GodotEditor : GodotActivity() { +abstract class BaseGodotEditor : GodotActivity() { companion object { - private val TAG = GodotEditor::class.java.simpleName + private val TAG = BaseGodotEditor::class.java.simpleName private const val WAIT_FOR_DEBUGGER = false @@ -81,12 +81,13 @@ open class GodotEditor : GodotActivity() { // Command line arguments private const val FULLSCREEN_ARG = "--fullscreen" private const val FULLSCREEN_ARG_SHORT = "-f" - private const val EDITOR_ARG = "--editor" - private const val EDITOR_ARG_SHORT = "-e" - private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" - private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" - private const val BREAKPOINTS_ARG = "--breakpoints" - private const val BREAKPOINTS_ARG_SHORT = "-b" + internal const val EDITOR_ARG = "--editor" + internal const val EDITOR_ARG_SHORT = "-e" + internal const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" + internal const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" + internal const val BREAKPOINTS_ARG = "--breakpoints" + internal const val BREAKPOINTS_ARG_SHORT = "-b" + internal const val XR_MODE_ARG = "--xr-mode" // Info for the various classes used by the editor internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") @@ -122,6 +123,20 @@ open class GodotEditor : GodotActivity() { internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO + /** + * Set of permissions to be excluded when requesting all permissions at startup. + * + * The permissions in this set will be requested on demand based on use cases. + */ + @CallSuper + protected open fun getExcludedPermissions(): MutableSet<String> { + return mutableSetOf( + // The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project + // setting is enabled. + Manifest.permission.RECORD_AUDIO + ) + } + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() @@ -131,8 +146,8 @@ open class GodotEditor : GodotActivity() { } // We exclude certain permissions from the set we request at startup, as they'll be - // requested on demand based on use-cases. - PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) + // requested on demand based on use cases. + PermissionsUtil.requestManifestPermissions(this, getExcludedPermissions()) val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") @@ -152,8 +167,6 @@ open class GodotEditor : GodotActivity() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() - checkForProjectPermissionsToEnable() - runOnUiThread { // Enable long press, panning and scaling gestures godotFragment?.godot?.renderView?.inputHandler?.apply { @@ -171,17 +184,6 @@ open class GodotEditor : GodotActivity() { } } - /** - * Check for project permissions to enable - */ - protected open fun checkForProjectPermissionsToEnable() { - // Check for RECORD_AUDIO permission - val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")) - if (audioInputEnabled) { - PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) - } - } - @CallSuper protected open fun updateCommandLineParams(args: List<String>) { // Update the list of command line params with the new args @@ -196,7 +198,7 @@ open class GodotEditor : GodotActivity() { final override fun getCommandLine() = commandLineParams - protected open fun getEditorWindowInfo(args: Array<String>): EditorWindowInfo { + protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo { var hasEditor = false var i = 0 @@ -273,7 +275,7 @@ open class GodotEditor : GodotActivity() { } override fun onNewGodotInstanceRequested(args: Array<String>): Int { - val editorWindowInfo = getEditorWindowInfo(args) + val editorWindowInfo = retrieveEditorWindowInfo(args) // Launch a new activity val sourceView = godotFragment?.view @@ -405,20 +407,26 @@ open class GodotEditor : GodotActivity() { return when (policy) { LaunchPolicy.AUTO -> { - try { - when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { - ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME - ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT - ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE - else -> { - // ANDROID_WINDOW_AUTO - defaultLaunchPolicy + if (isHorizonOSDevice()) { + // Horizon OS UX is more desktop-like and has support for launching adjacent + // windows. So we always want to launch in adjacent mode when auto is selected. + LaunchPolicy.ADJACENT + } else { + try { + when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { + ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME + ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT + ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE + else -> { + // ANDROID_WINDOW_AUTO + defaultLaunchPolicy + } } + } catch (e: NumberFormatException) { + Log.w(TAG, "Error parsing the Android window placement editor setting", e) + // Fall-back to the default launch policy + defaultLaunchPolicy } - } catch (e: NumberFormatException) { - Log.w(TAG, "Error parsing the Android window placement editor setting", e) - // Fall-back to the default launch policy - defaultLaunchPolicy } } @@ -431,8 +439,16 @@ open class GodotEditor : GodotActivity() { /** * Returns true the if the device supports picture-in-picture (PiP) */ - protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + protected open fun hasPiPSystemFeature(): Boolean { + if (isNativeXRDevice()) { + // Known native XR devices do not support PiP. + // Will need to revisit as they update their OS. + return false + } + + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt index b16e62149a..f5a6ed7dab 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt @@ -42,9 +42,9 @@ import android.util.Log import java.util.concurrent.ConcurrentHashMap /** - * Used by the [GodotEditor] classes to dispatch messages across processes. + * Used by the [BaseGodotEditor] classes to dispatch messages across processes. */ -internal class EditorMessageDispatcher(private val editor: GodotEditor) { +internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) { companion object { private val TAG = EditorMessageDispatcher::class.java.simpleName @@ -173,7 +173,11 @@ internal class EditorMessageDispatcher(private val editor: GodotEditor) { // to the sender. val senderId = messengerBundle.getInt(KEY_EDITOR_ID) val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER) - registerMessenger(senderId, senderMessenger) + registerMessenger(senderId, senderMessenger) { + // Terminate current instance when parent is no longer available. + Log.d(TAG, "Terminating current editor instance because parent is no longer available") + editor.finish() + } // Register ourselves to the sender so that it can communicate with us. registerSelfTo(pm, senderMessenger, editor.getEditorWindowInfo().windowId) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 6b4bf255f2..e52d566347 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -30,6 +30,7 @@ package org.godotengine.editor +import android.Manifest import android.annotation.SuppressLint import android.app.PictureInPictureParams import android.content.Intent @@ -38,12 +39,15 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.view.View +import androidx.annotation.CallSuper import org.godotengine.godot.GodotLib +import org.godotengine.godot.utils.PermissionsUtil +import org.godotengine.godot.utils.ProcessPhoenix /** * Drives the 'run project' window of the Godot Editor. */ -class GodotGame : GodotEditor() { +open class GodotGame : GodotEditor() { companion object { private val TAG = GodotGame::class.java.simpleName @@ -136,8 +140,53 @@ class GodotGame : GodotEditor() { override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - override fun checkForProjectPermissionsToEnable() { - // Nothing to do.. by the time we get here, the project permissions will have already - // been requested by the Editor window. + override fun onGodotSetupCompleted() { + super.onGodotSetupCompleted() + Log.v(TAG, "OnGodotSetupCompleted") + + // Check if we should be running in XR instead (if available) as it's possible we were + // launched from the project manager which doesn't have that information. + val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) + if (launchingArgs != null) { + val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs) + if (editorWindowInfo != getEditorWindowInfo()) { + val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs) + relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true) + .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD)) + + Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}") + val godot = godot + if (godot != null) { + godot.destroyAndKillProcess { + ProcessPhoenix.triggerRebirth(this, relaunchIntent) + } + } else { + ProcessPhoenix.triggerRebirth(this, relaunchIntent) + } + return + } + } + + // Request project runtime permissions if necessary + val permissionsToEnable = getProjectPermissionsToEnable() + if (permissionsToEnable.isNotEmpty()) { + PermissionsUtil.requestPermissions(this, permissionsToEnable) + } + } + + /** + * Check for project permissions to enable + */ + @CallSuper + protected open fun getProjectPermissionsToEnable(): MutableList<String> { + val permissionsToEnable = mutableListOf<String>() + + // Check for RECORD_AUDIO permission + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")) + if (audioInputEnabled) { + permissionsToEnable.add(Manifest.permission.RECORD_AUDIO) + } + + return permissionsToEnable } } diff --git a/platform/android/java/editor/src/meta/AndroidManifest.xml b/platform/android/java/editor/src/meta/AndroidManifest.xml new file mode 100644 index 0000000000..06442ac4e6 --- /dev/null +++ b/platform/android/java/editor/src/meta/AndroidManifest.xml @@ -0,0 +1,99 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:horizonos="http://schemas.horizonos/sdk"> + + <horizonos:uses-horizonos-sdk + horizonos:minSdkVersion="69" + horizonos:targetSdkVersion="69" /> + + <uses-feature + android:name="android.hardware.vr.headtracking" + android:required="true" + android:version="1"/> + + <!-- Oculus Quest hand tracking --> + <uses-permission android:name="com.oculus.permission.HAND_TRACKING" /> + <uses-feature + android:name="oculus.software.handtracking" + android:required="false" /> + + <!-- Passthrough feature flag --> + <uses-feature android:name="com.oculus.feature.PASSTHROUGH" + android:required="false" /> + + <!-- Overlay keyboard support --> + <uses-feature android:name="oculus.software.overlay_keyboard" android:required="false"/> + + <!-- Render model --> + <uses-permission android:name="com.oculus.permission.RENDER_MODEL" /> + <uses-feature android:name="com.oculus.feature.RENDER_MODEL" android:required="false" /> + + <!-- Anchor api --> + <uses-permission android:name="com.oculus.permission.USE_ANCHOR_API" /> + + <!-- Scene api --> + <uses-permission android:name="com.oculus.permission.USE_SCENE" /> + + <application> + + <activity + android:name=".GodotEditor" + android:exported="true" + android:screenOrientation="landscape" + tools:node="merge" + tools:replace="android:screenOrientation"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.LAUNCHER" /> + <category android:name="com.oculus.intent.category.2D" /> + </intent-filter> + + <meta-data android:name="com.oculus.vrshell.free_resizing_lock_aspect_ratio" android:value="true"/> + </activity> + + <activity + android:name=".GodotXRGame" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:process=":GodotXRGame" + android:launchMode="singleTask" + android:icon="@mipmap/ic_play_window" + android:label="@string/godot_game_activity_name" + android:exported="false" + android:screenOrientation="landscape" + android:resizeableActivity="false" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="com.oculus.intent.category.VR" /> + <category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" /> + </intent-filter> + </activity> + + <!-- Supported Meta devices --> + <meta-data + android:name="com.oculus.supportedDevices" + android:value="quest3|questpro" + tools:replace="android:value" /> + + <!-- + We remove this meta-data originating from the vendors plugin as we only need the loader for + now since the project being edited provides its own version of the vendors plugin. + + This needs to be removed once we start implementing the immersive version of the project + manager and editor windows. + --> + <meta-data + android:name="org.godotengine.plugin.v2.GodotOpenXRMeta" + android:value="org.godotengine.openxr.vendors.meta.GodotOpenXRMeta" + tools:node="remove" /> + + <!-- Enable system splash screen --> + <meta-data android:name="com.oculus.ossplash" android:value="true"/> + <!-- Enable passthrough background during the splash screen --> + <meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/> + + </application> + +</manifest> diff --git a/platform/android/java/editor/src/meta/assets/vr_splash.png b/platform/android/java/editor/src/meta/assets/vr_splash.png Binary files differnew file mode 100644 index 0000000000..7bddd4325a --- /dev/null +++ b/platform/android/java/editor/src/meta/assets/vr_splash.png diff --git a/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..9f0440e87d --- /dev/null +++ b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,94 @@ +/**************************************************************************/ +/* GodotEditor.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.editor + +import org.godotengine.godot.GodotLib +import org.godotengine.godot.utils.isNativeXRDevice + +/** + * Primary window of the Godot Editor. + * + * This is the implementation of the editor used when running on Meta devices. + */ +open class GodotEditor : BaseGodotEditor() { + + companion object { + private val TAG = GodotEditor::class.java.simpleName + + internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame") + + internal const val USE_ANCHOR_API_PERMISSION = "com.oculus.permission.USE_ANCHOR_API" + internal const val USE_SCENE_PERMISSION = "com.oculus.permission.USE_SCENE" + } + + override fun getExcludedPermissions(): MutableSet<String> { + val excludedPermissions = super.getExcludedPermissions() + // The USE_ANCHOR_API and USE_SCENE permissions are requested when the "xr/openxr/enabled" + // project setting is enabled. + excludedPermissions.add(USE_ANCHOR_API_PERMISSION) + excludedPermissions.add(USE_SCENE_PERMISSION) + return excludedPermissions + } + + override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo { + var hasEditor = false + var xrModeOn = false + + var i = 0 + while (i < args.size) { + when (args[i++]) { + EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true + XR_MODE_ARG -> { + val argValue = args[i++] + xrModeOn = xrModeOn || ("on" == argValue) + } + } + } + + return if (hasEditor) { + EDITOR_MAIN_INFO + } else { + val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean() + if (openxrEnabled && isNativeXRDevice()) { + XR_RUN_GAME_INFO + } else { + RUN_GAME_INFO + } + } + } + + override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? { + return when (instanceId) { + XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO + else -> super.getEditorWindowInfoForInstanceId(instanceId) + } + } +} diff --git a/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt new file mode 100644 index 0000000000..d71fbb53f2 --- /dev/null +++ b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* GodotXRGame.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +package org.godotengine.editor + +import org.godotengine.godot.GodotLib +import org.godotengine.godot.utils.PermissionsUtil +import org.godotengine.godot.xr.XRMode + +/** + * Provide support for running XR apps / games from the editor window. + */ +open class GodotXRGame: GodotGame() { + + override fun overrideOrientationRequest() = true + + override fun updateCommandLineParams(args: List<String>) { + val updatedArgs = ArrayList<String>() + if (!args.contains(XRMode.OPENXR.cmdLineArg)) { + updatedArgs.add(XRMode.OPENXR.cmdLineArg) + } + if (!args.contains(XR_MODE_ARG)) { + updatedArgs.add(XR_MODE_ARG) + updatedArgs.add("on") + } + updatedArgs.addAll(args) + + super.updateCommandLineParams(updatedArgs) + } + + override fun getEditorWindowInfo() = XR_RUN_GAME_INFO + + override fun getProjectPermissionsToEnable(): MutableList<String> { + val permissionsToEnable = super.getProjectPermissionsToEnable() + + val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean() + if (openxrEnabled) { + permissionsToEnable.add(USE_ANCHOR_API_PERMISSION) + permissionsToEnable.add(USE_SCENE_PERMISSION) + } + + return permissionsToEnable + } +} diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 81ab598b90..f6aee434e5 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -51,7 +51,7 @@ android { } } - flavorDimensions "products" + flavorDimensions = ["products"] productFlavors { editor {} template {} @@ -104,7 +104,7 @@ android { } boolean devBuild = buildType == "dev" - boolean debugSymbols = devBuild || isAndroidStudio() + boolean debugSymbols = devBuild boolean runTests = devBuild boolean productionBuild = !devBuild boolean storeRelease = buildType == "release" diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 30821eaa8e..9db9ef6080 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -31,6 +31,7 @@ package org.godotengine.godot; import org.godotengine.godot.input.GodotInputHandler; +import org.godotengine.godot.utils.DeviceUtils; import android.view.SurfaceView; @@ -63,7 +64,11 @@ public interface GodotRenderView { void setPointerIcon(int pointerType); + /** + * @return true if pointer capture is supported. + */ default boolean canCapturePointer() { - return getInputHandler().canCapturePointer(); + // Pointer capture is not supported on Horizon OS + return !DeviceUtils.isHorizonOSDevice() && getInputHandler().canCapturePointer(); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index d5b05913d8..8b3880d32d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -68,6 +68,7 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); + setClickable(false); } @Override @@ -132,17 +133,17 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { @Override public boolean onKeyUp(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyUp(keyCode, event); + return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyDown(keyCode, event); + return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { - return mInputHandler.onGenericMotionEvent(event); + return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt new file mode 100644 index 0000000000..abe0c5f885 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt @@ -0,0 +1,52 @@ +/**************************************************************************/ +/* DeviceUtils.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +/** + * Contains utility methods for detecting specific devices. + */ +@file:JvmName("DeviceUtils") + +package org.godotengine.godot.utils + +import android.os.Build + +/** + * Returns true if running on Meta's Horizon OS. + */ +fun isHorizonOSDevice(): Boolean { + return "Oculus".equals(Build.BRAND, true) +} + +/** + * Returns true if running on a native Android XR device. + */ +fun isNativeXRDevice(): Boolean { + return isHorizonOSDevice() +} diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index 96b6dfc9f3..a7d2774db5 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(GODOT_ROOT_DIR ../../../..) set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "") +set(OPENXR_INCLUDE_DIR "${GODOT_ROOT_DIR}/thirdparty/openxr/include" CACHE STRING "") # Get sources file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**) @@ -17,6 +18,7 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${GODOT_ROOT_DIR} - ${ANDROID_ROOT_DIR}) + ${ANDROID_ROOT_DIR} + ${OPENXR_INCLUDE_DIR}) add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED) diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 3137e74244..41418a4a45 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -14,6 +14,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index e3cde145cb..75c8dd9528 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -35,7 +35,6 @@ #include "string_android.h" #include "core/config/engine.h" -#include "core/config/project_settings.h" #include "core/error/error_macros.h" static HashMap<String, JNISingleton *> jni_singletons; @@ -43,7 +42,6 @@ static HashMap<String, JNISingleton *> jni_singletons; void unregister_plugins_singletons() { for (const KeyValue<String, JNISingleton *> &E : jni_singletons) { Engine::get_singleton()->remove_singleton(E.key); - ProjectSettings::get_singleton()->set(E.key, Variant()); if (E.value) { memdelete(E.value); @@ -64,7 +62,6 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeR jni_singletons[singname] = s; Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); - ProjectSettings::get_singleton()->set(singname, s); return true; } diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index ab13105d18..66be313ff6 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -169,12 +169,13 @@ Vector<uint8_t> WaylandThread::_wp_primary_selection_offer_read(struct wl_displa int fds[2]; if (pipe(fds) == 0) { - // This function expects to return a string, so we can only ask for a MIME of - // "text/plain" zwp_primary_selection_offer_v1_receive(p_offer, p_mime, fds[1]); - // Wait for the compositor to know about the pipe. - wl_display_roundtrip(p_display); + // NOTE: It's important to just flush and not roundtrip here as we would risk + // running some cleanup event, like for example `wl_data_device::leave`. We're + // going to wait for the message anyways as the read will probably block if + // the compositor doesn't read from the other end of the pipe. + wl_display_flush(p_display); // Close the write end of the pipe, which we don't need and would otherwise // just stall our next `read`s. 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..c99e9cdd0c 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -357,17 +357,13 @@ List<String> EditorExportPlatformMacOS::get_binary_extensions(const Ref<EditorEx list.push_back("dmg"); #endif list.push_back("zip"); -#ifndef WINDOWS_ENABLED list.push_back("app"); -#endif } else if (dist_type == 1) { #ifdef MACOS_ENABLED list.push_back("dmg"); #endif list.push_back("zip"); -#ifndef WINDOWS_ENABLED list.push_back("app"); -#endif } else if (dist_type == 2) { #ifdef MACOS_ENABLED list.push_back("pkg"); @@ -451,7 +447,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 +820,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")) { @@ -1898,6 +1899,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (is_executable(file)) { // chmod with 0755 if the file is executable. FileAccess::set_unix_permissions(file, 0755); +#ifndef UNIX_ENABLED + if (export_format == "app") { + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/MacOS/" + file.get_file())); + } +#endif } } else { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file)); @@ -1923,6 +1929,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if ((con_scr == 1 && p_debug) || (con_scr == 2)) { err = _export_debug_script(p_preset, pkg_name, tmp_app_path_name.get_file() + "/Contents/MacOS/" + pkg_name, scr_path); FileAccess::set_unix_permissions(scr_path, 0755); +#ifndef UNIX_ENABLED + if (export_format == "app") { + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), scr_path.get_file())); + } +#endif if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console wrapper.")); } @@ -2151,6 +2162,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true); } FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); +#ifndef UNIX_ENABLED + if (export_format == "app") { + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Unable to set Unix permissions for executable \"%s\". Use \"chmod +x\" to set it after transferring the exported .app to macOS or Linux."), "Contents/Helpers/" + hlp_path.get_file())); + } +#endif } } 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/2d/canvas_modulate.cpp b/scene/2d/canvas_modulate.cpp index 2c5c6a1a16..dc83775c71 100644 --- a/scene/2d/canvas_modulate.cpp +++ b/scene/2d/canvas_modulate.cpp @@ -114,7 +114,7 @@ Color CanvasModulate::get_color() const { } PackedStringArray CanvasModulate::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (is_in_canvas && is_visible_in_tree()) { List<Node *> nodes; diff --git a/scene/2d/light_2d.cpp b/scene/2d/light_2d.cpp index 5ce26b3ed4..50c5873781 100644 --- a/scene/2d/light_2d.cpp +++ b/scene/2d/light_2d.cpp @@ -417,7 +417,7 @@ Vector2 PointLight2D::get_texture_offset() const { } PackedStringArray PointLight2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!texture.is_valid()) { warnings.push_back(RTR("A texture with the shape of the light must be supplied to the \"Texture\" property.")); diff --git a/scene/2d/light_occluder_2d.cpp b/scene/2d/light_occluder_2d.cpp index 092c987ac0..7c3fb61d04 100644 --- a/scene/2d/light_occluder_2d.cpp +++ b/scene/2d/light_occluder_2d.cpp @@ -263,7 +263,7 @@ int LightOccluder2D::get_occluder_light_mask() const { } PackedStringArray LightOccluder2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!occluder_polygon.is_valid()) { warnings.push_back(RTR("An occluder polygon must be set (or drawn) for this occluder to take effect.")); diff --git a/scene/2d/navigation_link_2d.cpp b/scene/2d/navigation_link_2d.cpp index 111f5a7b78..4961e18dc9 100644 --- a/scene/2d/navigation_link_2d.cpp +++ b/scene/2d/navigation_link_2d.cpp @@ -328,7 +328,7 @@ void NavigationLink2D::set_travel_cost(real_t p_travel_cost) { } PackedStringArray NavigationLink2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (start_position.is_equal_approx(end_position)) { warnings.push_back(RTR("NavigationLink2D start position should be different than the end position to be useful.")); diff --git a/scene/2d/parallax_layer.cpp b/scene/2d/parallax_layer.cpp index 023e9201fc..24f261deb6 100644 --- a/scene/2d/parallax_layer.cpp +++ b/scene/2d/parallax_layer.cpp @@ -133,7 +133,7 @@ void ParallaxLayer::set_base_offset_and_scale(const Point2 &p_offset, real_t p_s } PackedStringArray ParallaxLayer::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!Object::cast_to<ParallaxBackground>(get_parent())) { warnings.push_back(RTR("ParallaxLayer node only works when set as child of a ParallaxBackground node.")); diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp index c3768386e9..5813ab02e3 100644 --- a/scene/2d/path_2d.cpp +++ b/scene/2d/path_2d.cpp @@ -288,7 +288,7 @@ void PathFollow2D::_validate_property(PropertyInfo &p_property) const { } PackedStringArray PathFollow2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (is_visible_in_tree() && is_inside_tree()) { if (!Object::cast_to<Path2D>(get_parent())) { diff --git a/scene/2d/physics/collision_object_2d.cpp b/scene/2d/physics/collision_object_2d.cpp index 00b6085f0c..27ee6b883c 100644 --- a/scene/2d/physics/collision_object_2d.cpp +++ b/scene/2d/physics/collision_object_2d.cpp @@ -582,7 +582,7 @@ void CollisionObject2D::_update_pickable() { } PackedStringArray CollisionObject2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (shapes.is_empty()) { warnings.push_back(RTR("This node has no shape, so it can't collide or interact with other objects.\nConsider adding a CollisionShape2D or CollisionPolygon2D as a child to define its shape.")); diff --git a/scene/2d/physics/collision_polygon_2d.cpp b/scene/2d/physics/collision_polygon_2d.cpp index a9b47ef4d4..b49badac1f 100644 --- a/scene/2d/physics/collision_polygon_2d.cpp +++ b/scene/2d/physics/collision_polygon_2d.cpp @@ -232,7 +232,7 @@ bool CollisionPolygon2D::_edit_is_selected_on_click(const Point2 &p_point, doubl #endif PackedStringArray CollisionPolygon2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!Object::cast_to<CollisionObject2D>(get_parent())) { warnings.push_back(RTR("CollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node. Please only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape.")); diff --git a/scene/2d/physics/collision_shape_2d.cpp b/scene/2d/physics/collision_shape_2d.cpp index 6fc29ffe63..bdd0d06b5e 100644 --- a/scene/2d/physics/collision_shape_2d.cpp +++ b/scene/2d/physics/collision_shape_2d.cpp @@ -174,7 +174,7 @@ bool CollisionShape2D::_edit_is_selected_on_click(const Point2 &p_point, double } PackedStringArray CollisionShape2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); CollisionObject2D *col_object = Object::cast_to<CollisionObject2D>(get_parent()); if (col_object == nullptr) { diff --git a/scene/2d/physics/physical_bone_2d.cpp b/scene/2d/physics/physical_bone_2d.cpp index 77bb8c24b8..19274c8084 100644 --- a/scene/2d/physics/physical_bone_2d.cpp +++ b/scene/2d/physics/physical_bone_2d.cpp @@ -107,7 +107,7 @@ void PhysicalBone2D::_find_joint_child() { } PackedStringArray PhysicalBone2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = RigidBody2D::get_configuration_warnings(); if (!parent_skeleton) { warnings.push_back(RTR("A PhysicalBone2D only works with a Skeleton2D or another PhysicalBone2D as a parent node!")); diff --git a/scene/2d/remote_transform_2d.cpp b/scene/2d/remote_transform_2d.cpp index 920f5720fa..1816a3409b 100644 --- a/scene/2d/remote_transform_2d.cpp +++ b/scene/2d/remote_transform_2d.cpp @@ -211,7 +211,7 @@ void RemoteTransform2D::force_update_cache() { } PackedStringArray RemoteTransform2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!has_node(remote_node) || !Object::cast_to<Node2D>(get_node(remote_node))) { warnings.push_back(RTR("Path property must point to a valid Node2D node to work.")); diff --git a/scene/2d/skeleton_2d.cpp b/scene/2d/skeleton_2d.cpp index f9e8c831d1..90bfb4c84c 100644 --- a/scene/2d/skeleton_2d.cpp +++ b/scene/2d/skeleton_2d.cpp @@ -412,7 +412,7 @@ int Bone2D::get_index_in_skeleton() const { } PackedStringArray Bone2D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); if (!skeleton) { if (parent_bone) { warnings.push_back(RTR("This Bone2D chain should end at a Skeleton2D node.")); diff --git a/scene/2d/tile_map.cpp b/scene/2d/tile_map.cpp index b10f2097da..45cfb8cf33 100644 --- a/scene/2d/tile_map.cpp +++ b/scene/2d/tile_map.cpp @@ -827,7 +827,7 @@ TypedArray<Vector2i> TileMap::get_surrounding_cells(const Vector2i &p_coords) { } PackedStringArray TileMap::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node2D::get_configuration_warnings(); warnings.push_back(RTR("The TileMap node is deprecated as it is superseded by the use of multiple TileMapLayer nodes.\nTo convert a TileMap to a set of TileMapLayer nodes, open the TileMap bottom panel with this node selected, click the toolbox icon in the top-right corner and choose \"Extract TileMap layers as individual TileMapLayer nodes\".")); diff --git a/scene/3d/decal.cpp b/scene/3d/decal.cpp index 485599d0fb..8702b1d3da 100644 --- a/scene/3d/decal.cpp +++ b/scene/3d/decal.cpp @@ -163,7 +163,7 @@ void Decal::_validate_property(PropertyInfo &p_property) const { } PackedStringArray Decal::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { warnings.push_back(RTR("Decals are only available when using the Forward+ or Mobile rendering backends.")); diff --git a/scene/3d/fog_volume.cpp b/scene/3d/fog_volume.cpp index 54631a8dff..195074ba2f 100644 --- a/scene/3d/fog_volume.cpp +++ b/scene/3d/fog_volume.cpp @@ -116,7 +116,7 @@ AABB FogVolume::get_aabb() const { } PackedStringArray FogVolume::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); Ref<Environment> environment = get_viewport()->find_world_3d()->get_environment(); diff --git a/scene/3d/gpu_particles_collision_3d.cpp b/scene/3d/gpu_particles_collision_3d.cpp index 3a05ec9c9e..9791f23bc3 100644 --- a/scene/3d/gpu_particles_collision_3d.cpp +++ b/scene/3d/gpu_particles_collision_3d.cpp @@ -524,7 +524,7 @@ Ref<Image> GPUParticlesCollisionSDF3D::bake() { } PackedStringArray GPUParticlesCollisionSDF3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = GPUParticlesCollision3D::get_configuration_warnings(); if (bake_mask == 0) { warnings.push_back(RTR("The Bake Mask has no bits enabled, which means baking will not produce any collision for this GPUParticlesCollisionSDF3D.\nTo resolve this, enable at least one bit in the Bake Mask property.")); diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp index 3f8b0dfb8e..26a574cd26 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; @@ -1581,7 +1600,7 @@ Ref<CameraAttributes> LightmapGI::get_camera_attributes() const { } PackedStringArray LightmapGI::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { warnings.push_back(RTR("Lightmap can only be baked from a device that supports the RD backends. Lightmap baking may fail.")); 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/3d/navigation_link_3d.cpp b/scene/3d/navigation_link_3d.cpp index bebba9a6c0..0cce21b9d0 100644 --- a/scene/3d/navigation_link_3d.cpp +++ b/scene/3d/navigation_link_3d.cpp @@ -453,7 +453,7 @@ void NavigationLink3D::set_travel_cost(real_t p_travel_cost) { } PackedStringArray NavigationLink3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (start_position.is_equal_approx(end_position)) { warnings.push_back(RTR("NavigationLink3D start position should be different than the end position to be useful.")); diff --git a/scene/3d/navigation_region_3d.cpp b/scene/3d/navigation_region_3d.cpp index d7397a932d..c0c254e7ed 100644 --- a/scene/3d/navigation_region_3d.cpp +++ b/scene/3d/navigation_region_3d.cpp @@ -271,7 +271,7 @@ bool NavigationRegion3D::is_baking() const { } PackedStringArray NavigationRegion3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (is_visible_in_tree() && is_inside_tree()) { if (!navigation_mesh.is_valid()) { diff --git a/scene/3d/occluder_instance_3d.cpp b/scene/3d/occluder_instance_3d.cpp index 6982df12f6..6d88323c76 100644 --- a/scene/3d/occluder_instance_3d.cpp +++ b/scene/3d/occluder_instance_3d.cpp @@ -691,7 +691,7 @@ OccluderInstance3D::BakeError OccluderInstance3D::bake_scene(Node *p_from_node, } PackedStringArray OccluderInstance3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); if (!bool(GLOBAL_GET("rendering/occlusion_culling/use_occlusion_culling"))) { warnings.push_back(RTR("Occlusion culling is disabled in the Project Settings, which means occlusion culling won't be performed in the root viewport.\nTo resolve this, open the Project Settings and enable Rendering > Occlusion Culling > Use Occlusion Culling.")); diff --git a/scene/3d/path_3d.cpp b/scene/3d/path_3d.cpp index dc030b6a0f..20d646fe1e 100644 --- a/scene/3d/path_3d.cpp +++ b/scene/3d/path_3d.cpp @@ -318,7 +318,7 @@ void PathFollow3D::_validate_property(PropertyInfo &p_property) const { } PackedStringArray PathFollow3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (is_visible_in_tree() && is_inside_tree()) { if (!Object::cast_to<Path3D>(get_parent())) { diff --git a/scene/3d/physics/collision_object_3d.cpp b/scene/3d/physics/collision_object_3d.cpp index f11aa7012a..f0a5013ca2 100644 --- a/scene/3d/physics/collision_object_3d.cpp +++ b/scene/3d/physics/collision_object_3d.cpp @@ -731,7 +731,7 @@ bool CollisionObject3D::get_capture_input_on_drag() const { } PackedStringArray CollisionObject3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (shapes.is_empty()) { warnings.push_back(RTR("This node has no shape, so it can't collide or interact with other objects.\nConsider adding a CollisionShape3D or CollisionPolygon3D as a child to define its shape.")); diff --git a/scene/3d/physics/collision_polygon_3d.cpp b/scene/3d/physics/collision_polygon_3d.cpp index 76cd4db779..bf8dec7b54 100644 --- a/scene/3d/physics/collision_polygon_3d.cpp +++ b/scene/3d/physics/collision_polygon_3d.cpp @@ -169,7 +169,7 @@ void CollisionPolygon3D::set_margin(real_t p_margin) { } PackedStringArray CollisionPolygon3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (!Object::cast_to<CollisionObject3D>(get_parent())) { warnings.push_back(RTR("CollisionPolygon3D only serves to provide a collision shape to a CollisionObject3D derived node.\nPlease only use it as a child of Area3D, StaticBody3D, RigidBody3D, CharacterBody3D, etc. to give them a shape.")); diff --git a/scene/3d/physics/collision_shape_3d.cpp b/scene/3d/physics/collision_shape_3d.cpp index f3492a3cf3..304fa74b06 100644 --- a/scene/3d/physics/collision_shape_3d.cpp +++ b/scene/3d/physics/collision_shape_3d.cpp @@ -120,7 +120,7 @@ void CollisionShape3D::resource_changed(Ref<Resource> res) { #endif PackedStringArray CollisionShape3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); CollisionObject3D *col_object = Object::cast_to<CollisionObject3D>(get_parent()); if (col_object == nullptr) { diff --git a/scene/3d/physics/vehicle_body_3d.cpp b/scene/3d/physics/vehicle_body_3d.cpp index b4c321cf5f..5073705145 100644 --- a/scene/3d/physics/vehicle_body_3d.cpp +++ b/scene/3d/physics/vehicle_body_3d.cpp @@ -106,7 +106,7 @@ void VehicleWheel3D::_notification(int p_what) { } PackedStringArray VehicleWheel3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (!Object::cast_to<VehicleBody3D>(get_parent())) { warnings.push_back(RTR("VehicleWheel3D serves to provide a wheel system to a VehicleBody3D. Please use it as a child of a VehicleBody3D.")); diff --git a/scene/3d/remote_transform_3d.cpp b/scene/3d/remote_transform_3d.cpp index e580882c46..f970879aa4 100644 --- a/scene/3d/remote_transform_3d.cpp +++ b/scene/3d/remote_transform_3d.cpp @@ -211,7 +211,7 @@ void RemoteTransform3D::force_update_cache() { } PackedStringArray RemoteTransform3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (!has_node(remote_node) || !Object::cast_to<Node3D>(get_node(remote_node))) { warnings.push_back(RTR("The \"Remote Path\" property must point to a valid Node3D or Node3D-derived node to work.")); diff --git a/scene/3d/soft_body_3d.cpp b/scene/3d/soft_body_3d.cpp index 4fe5dd2385..7f67bde0cf 100644 --- a/scene/3d/soft_body_3d.cpp +++ b/scene/3d/soft_body_3d.cpp @@ -383,7 +383,7 @@ void SoftBody3D::_bind_methods() { } PackedStringArray SoftBody3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = MeshInstance3D::get_configuration_warnings(); if (mesh.is_null()) { warnings.push_back(RTR("This body will be ignored until you set a mesh.")); diff --git a/scene/3d/visual_instance_3d.cpp b/scene/3d/visual_instance_3d.cpp index 79a01450dd..a59754c8cc 100644 --- a/scene/3d/visual_instance_3d.cpp +++ b/scene/3d/visual_instance_3d.cpp @@ -497,7 +497,7 @@ bool GeometryInstance3D::is_ignoring_occlusion_culling() { } PackedStringArray GeometryInstance3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); if (!Math::is_zero_approx(visibility_range_end) && visibility_range_end <= visibility_range_begin) { warnings.push_back(RTR("The GeometryInstance3D visibility range's End distance is set to a non-zero value, but is lower than the Begin distance.\nThis means the GeometryInstance3D will never be visible.\nTo resolve this, set the End distance to 0 or to a value greater than the Begin distance.")); diff --git a/scene/3d/voxel_gi.cpp b/scene/3d/voxel_gi.cpp index ffca856fba..80ff176a98 100644 --- a/scene/3d/voxel_gi.cpp +++ b/scene/3d/voxel_gi.cpp @@ -518,7 +518,7 @@ AABB VoxelGI::get_aabb() const { } PackedStringArray VoxelGI::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = VisualInstance3D::get_configuration_warnings(); if (OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") { warnings.push_back(RTR("VoxelGI nodes are not supported when using the GL Compatibility backend yet. Support will be added in a future release.")); diff --git a/scene/3d/xr_nodes.cpp b/scene/3d/xr_nodes.cpp index b71f9bc0c4..214c1f77ca 100644 --- a/scene/3d/xr_nodes.cpp +++ b/scene/3d/xr_nodes.cpp @@ -77,7 +77,7 @@ void XRCamera3D::_pose_changed(const Ref<XRPose> &p_pose) { } PackedStringArray XRCamera3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Camera3D::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { // Warn if the node has a parent which isn't an XROrigin3D! @@ -461,7 +461,7 @@ XRNode3D::~XRNode3D() { } PackedStringArray XRNode3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { // Warn if the node has a parent which isn't an XROrigin3D! @@ -644,7 +644,7 @@ Plane XRAnchor3D::get_plane() const { Vector<XROrigin3D *> XROrigin3D::origin_nodes; PackedStringArray XROrigin3D::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Node3D::get_configuration_warnings(); if (is_visible() && is_inside_tree()) { bool has_camera = false; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 1c5f40f56e..664302d45e 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1636,6 +1636,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } if (t_obj->call(SNAME("get_is_sample"))) { + if (t->audio_stream_playback->get_sample_playback().is_valid()) { + AudioServer::get_singleton()->stop_sample_playback(t->audio_stream_playback->get_sample_playback()); + } Ref<AudioSamplePlayback> sample_playback; sample_playback.instantiate(); sample_playback->stream = stream; diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index 867bbda4b3..19080e61de 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -681,7 +681,7 @@ uint64_t AnimationTree::get_last_process_pass() const { } PackedStringArray AnimationTree::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = AnimationMixer::get_configuration_warnings(); if (!root_animation_node.is_valid()) { warnings.push_back(RTR("No root AnimationNode for the graph is set.")); } 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/control.cpp b/scene/gui/control.cpp index 15ada0021a..e030828595 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -248,7 +248,7 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List PackedStringArray Control::get_configuration_warnings() const { ERR_READ_THREAD_GUARD_V(PackedStringArray()); - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = CanvasItem::get_configuration_warnings(); if (data.mouse_filter == MOUSE_FILTER_IGNORE && !data.tooltip.is_empty()) { warnings.push_back(RTR("The Hint Tooltip won't be displayed as the control's Mouse Filter is set to \"Ignore\". To solve this, set the Mouse Filter to \"Stop\" or \"Pass\".")); @@ -2356,6 +2356,24 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons points[2] = xform.xform(c->get_size()); points[3] = xform.xform(Point2(0, c->get_size().y)); + // Tie-breaking aims to address situations where a potential focus neighbor's bounding rect + // is right next to the currently focused control (e.g. in BoxContainer with + // separation overridden to 0). This needs specific handling so that the correct + // focus neighbor is selected. + + // Calculate centers of the potential neighbor, currently focused, and closest controls. + Point2 center = xform.xform(0.5 * c->get_size()); + // We only have the points, not an actual reference. + Point2 p_center = 0.25 * (p_points[0] + p_points[1] + p_points[2] + p_points[3]); + Point2 closest_center; + bool should_tiebreak = false; + if (*r_closest != nullptr) { + should_tiebreak = true; + Control *closest = *r_closest; + Transform2D closest_xform = closest->get_global_transform(); + closest_center = closest_xform.xform(0.5 * closest->get_size()); + } + real_t min = 1e7; for (int i = 0; i < 4; i++) { @@ -2376,10 +2394,15 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons Vector2 pa, pb; real_t d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); - //real_t d = Geometry2D::get_closest_distance_between_segments(Vector3(la.x,la.y,0),Vector3(lb.x,lb.y,0),Vector3(fa.x,fa.y,0),Vector3(fb.x,fb.y,0)); if (d < r_closest_dist) { r_closest_dist = d; *r_closest = c; + } else if (should_tiebreak && d == r_closest_dist) { + // Tie-break in favor of the control most aligned with p_dir. + if (p_dir.dot((center - p_center).normalized()) > p_dir.dot((closest_center - p_center).normalized())) { + r_closest_dist = d; + *r_closest = c; + } } } } 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/range.cpp b/scene/gui/range.cpp index 83359653f1..d7b1a4933d 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -31,7 +31,7 @@ #include "range.h" PackedStringArray Range::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Control::get_configuration_warnings(); if (shared->exp_ratio && shared->min <= 0) { warnings.push_back(RTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0.")); diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index bd4128384f..552458245c 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) { @@ -3410,6 +3412,21 @@ bool RichTextLabel::remove_paragraph(int p_paragraph, bool p_no_invalidate) { selection.click_item = nullptr; selection.active = false; + if (is_processing_internal()) { + bool process_enabled = false; + Item *it = main; + while (it) { + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + if (fx_stack.size()) { + process_enabled = true; + break; + } + it = _get_next_item(it, true); + } + set_process_internal(process_enabled); + } + if (p_no_invalidate) { // Do not invalidate cache, only update vertical offsets of the paragraphs after deleted one and scrollbar. int to_line = main->first_invalid_line.load() - 1; @@ -3983,6 +4000,7 @@ void RichTextLabel::pop_all() { void RichTextLabel::clear() { _stop_thread(); + set_process_internal(false); MutexLock data_lock(data_mutex); main->_clear_children(); @@ -4092,6 +4110,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); @@ -4107,10 +4193,8 @@ void RichTextLabel::append_text(const String &p_bbcode) { bool after_list_open_tag = false; bool after_list_close_tag = false; - 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 +4219,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 +4229,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 +4239,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 +4250,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 +4349,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 +4399,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 +4410,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 +4465,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 +4544,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 +4580,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 +4589,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 +4697,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 +4714,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 +4768,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 +4799,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 +4811,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 +4873,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 +4937,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 +4948,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 +4963,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 +5091,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 +5230,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 +5238,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); @@ -5127,17 +5267,6 @@ void RichTextLabel::append_text(const String &p_bbcode) { } } } - - Vector<ItemFX *> fx_items; - for (Item *E : main->subitems) { - Item *subitem = static_cast<Item *>(E); - _fetch_item_fx_stack(subitem, fx_items); - - if (fx_items.size()) { - set_process_internal(true); - break; - } - } } void RichTextLabel::scroll_to_selection() { 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/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index c715aceb0b..a443ae9abf 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -259,7 +259,7 @@ void SubViewportContainer::remove_child_notify(Node *p_child) { } PackedStringArray SubViewportContainer::get_configuration_warnings() const { - PackedStringArray warnings = Node::get_configuration_warnings(); + PackedStringArray warnings = Container::get_configuration_warnings(); bool has_viewport = false; for (int i = 0; i < get_child_count(); i++) { 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/gui/tree.cpp b/scene/gui/tree.cpp index edd25d1d5c..8871af23cb 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -3466,29 +3466,37 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { accept_event(); } - if (!selected_item || select_mode == SELECT_ROW || selected_col > (columns.size() - 1)) { + if (!selected_item || selected_col > (columns.size() - 1)) { return; } + if (k.is_valid() && k->is_shift_pressed()) { selected_item->set_collapsed_recursive(false); - } else { + } else if (select_mode != SELECT_ROW) { _go_right(); + } else if (selected_item->get_first_child() != nullptr && selected_item->is_collapsed()) { + selected_item->set_collapsed(false); + } else { + _go_down(); } } else if (p_event->is_action("ui_left") && p_event->is_pressed()) { if (!cursor_can_exit_tree) { accept_event(); } - if (!selected_item || select_mode == SELECT_ROW || selected_col < 0) { + if (!selected_item || selected_col < 0) { return; } if (k.is_valid() && k->is_shift_pressed()) { selected_item->set_collapsed_recursive(true); - } else { + } else if (select_mode != SELECT_ROW) { _go_left(); + } else if (selected_item->get_first_child() != nullptr && !selected_item->is_collapsed()) { + selected_item->set_collapsed(true); + } else { + _go_up(); } - } else if (p_event->is_action("ui_up") && p_event->is_pressed() && !is_command) { if (!cursor_can_exit_tree) { accept_event(); 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/audio_stream_polyphonic.cpp b/scene/resources/audio_stream_polyphonic.cpp index 999b0c9f0a..c7b8b1c723 100644 --- a/scene/resources/audio_stream_polyphonic.cpp +++ b/scene/resources/audio_stream_polyphonic.cpp @@ -247,6 +247,11 @@ AudioStreamPlaybackPolyphonic::ID AudioStreamPlaybackPolyphonic::play_stream(con sp->volume_vector.write[2] = AudioFrame(linear_volume, linear_volume); sp->volume_vector.write[3] = AudioFrame(linear_volume, linear_volume); sp->bus = p_bus; + + if (streams[i].stream_playback->get_sample_playback().is_valid()) { + AudioServer::get_singleton()->stop_playback_stream(sp); + } + streams[i].stream_playback->set_sample_playback(sp); AudioServer::get_singleton()->start_sample_playback(sp); } @@ -315,6 +320,9 @@ Ref<AudioSamplePlayback> AudioStreamPlaybackPolyphonic::get_sample_playback() co void AudioStreamPlaybackPolyphonic::set_sample_playback(const Ref<AudioSamplePlayback> &p_playback) { sample_playback = p_playback; + if (sample_playback.is_valid()) { + sample_playback->stream_playback = Ref<AudioStreamPlayback>(this); + } } void AudioStreamPlaybackPolyphonic::_bind_methods() { diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index 08ebacc2b3..de67a93bd1 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -123,10 +123,8 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, int16_t nibble, diff, step; p_ima_adpcm[i].last_nibble++; - const uint8_t *src_ptr = (const uint8_t *)base->data; - src_ptr += AudioStreamWAV::DATA_PAD; - uint8_t nbb = src_ptr[(p_ima_adpcm[i].last_nibble >> 1) * (is_stereo ? 2 : 1) + i]; + uint8_t nbb = p_src[(p_ima_adpcm[i].last_nibble >> 1) * (is_stereo ? 2 : 1) + i]; nibble = (p_ima_adpcm[i].last_nibble & 1) ? (nbb >> 4) : (nbb & 0xF); step = _ima_adpcm_step_table[p_ima_adpcm[i].step_index]; @@ -184,9 +182,8 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, if (p_qoa->data_ofs != new_data_ofs) { p_qoa->data_ofs = new_data_ofs; - const uint8_t *src_ptr = (const uint8_t *)base->data; - src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD; - qoa_decode_frame(src_ptr, p_qoa->frame_len, &p_qoa->desc, p_qoa->dec, &p_qoa->dec_len); + const uint8_t *ofs_src = (uint8_t *)p_src + p_qoa->data_ofs; + qoa_decode_frame(ofs_src, p_qoa->frame_len, &p_qoa->desc, p_qoa->dec.ptr(), &p_qoa->dec_len); } uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc.channels; @@ -267,7 +264,7 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, } int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames) { - if (!base->data || !active) { + if (base->data.is_empty() || !active) { for (int i = 0; i < p_frames; i++) { p_buffer[i] = AudioFrame(0, 0); } @@ -324,8 +321,7 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ /* audio data */ - uint8_t *dataptr = (uint8_t *)base->data; - const void *data = dataptr + AudioStreamWAV::DATA_PAD; + const uint8_t *data = base->data.ptr() + AudioStreamWAV::DATA_PAD; AudioFrame *dst_buff = p_buffer; if (format == AudioStreamWAV::FORMAT_IMA_ADPCM) { @@ -479,15 +475,14 @@ Ref<AudioSamplePlayback> AudioStreamPlaybackWAV::get_sample_playback() const { void AudioStreamPlaybackWAV::set_sample_playback(const Ref<AudioSamplePlayback> &p_playback) { sample_playback = p_playback; + if (sample_playback.is_valid()) { + sample_playback->stream_playback = Ref<AudioStreamPlayback>(this); + } } AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {} -AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() { - if (qoa.dec) { - memfree(qoa.dec); - } -} +AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() {} ///////////////////// @@ -554,7 +549,7 @@ double AudioStreamWAV::get_length() const { break; case AudioStreamWAV::FORMAT_QOA: qoa_desc desc = {}; - qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, &desc); + qoa_decode_header(data.ptr() + DATA_PAD, data_bytes, &desc); len = desc.samples * desc.channels; break; } @@ -572,22 +567,16 @@ bool AudioStreamWAV::is_monophonic() const { void AudioStreamWAV::set_data(const Vector<uint8_t> &p_data) { AudioServer::get_singleton()->lock(); - if (data) { - memfree(data); - data = nullptr; - data_bytes = 0; - } - int datalen = p_data.size(); - if (datalen) { - const uint8_t *r = p_data.ptr(); - int alloc_len = datalen + DATA_PAD * 2; - data = memalloc(alloc_len); //alloc with some padding for interpolation - memset(data, 0, alloc_len); - uint8_t *dataptr = (uint8_t *)data; - memcpy(dataptr + DATA_PAD, r, datalen); - data_bytes = datalen; - } + int src_data_len = p_data.size(); + + data.clear(); + + int alloc_len = src_data_len + DATA_PAD * 2; + data.resize(alloc_len); + memset(data.ptr(), 0, alloc_len); + memcpy(data.ptr() + DATA_PAD, p_data.ptr(), src_data_len); + data_bytes = src_data_len; AudioServer::get_singleton()->unlock(); } @@ -595,13 +584,9 @@ void AudioStreamWAV::set_data(const Vector<uint8_t> &p_data) { Vector<uint8_t> AudioStreamWAV::get_data() const { Vector<uint8_t> pv; - if (data) { + if (!data.is_empty()) { pv.resize(data_bytes); - { - uint8_t *w = pv.ptrw(); - uint8_t *dataptr = (uint8_t *)data; - memcpy(w, dataptr + DATA_PAD, data_bytes); - } + memcpy(pv.ptrw(), data.ptr() + DATA_PAD, data_bytes); } return pv; @@ -693,12 +678,12 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() { sample->base = Ref<AudioStreamWAV>(this); if (format == AudioStreamWAV::FORMAT_QOA) { - uint32_t ffp = qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, &sample->qoa.desc); + uint32_t ffp = qoa_decode_header(data.ptr() + DATA_PAD, data_bytes, &sample->qoa.desc); ERR_FAIL_COND_V(ffp != 8, Ref<AudioStreamPlaybackWAV>()); sample->qoa.frame_len = qoa_max_frame_size(&sample->qoa.desc); int samples_len = (sample->qoa.desc.samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc.samples); - int alloc_len = sample->qoa.desc.channels * samples_len * sizeof(int16_t); - sample->qoa.dec = (int16_t *)memalloc(alloc_len); + int dec_len = sample->qoa.desc.channels * samples_len; + sample->qoa.dec.resize(dec_len); } return sample; @@ -780,10 +765,4 @@ void AudioStreamWAV::_bind_methods() { AudioStreamWAV::AudioStreamWAV() {} -AudioStreamWAV::~AudioStreamWAV() { - if (data) { - memfree(data); - data = nullptr; - data_bytes = 0; - } -} +AudioStreamWAV::~AudioStreamWAV() {} diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 47aa10e790..bc62e8883a 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -62,7 +62,7 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { qoa_desc desc = {}; uint32_t data_ofs = 0; uint32_t frame_len = 0; - int16_t *dec = nullptr; + LocalVector<int16_t> dec; uint32_t dec_len = 0; int64_t cache_pos = -1; int16_t cache[2] = { 0, 0 }; @@ -137,7 +137,7 @@ private: int loop_begin = 0; int loop_end = 0; int mix_rate = 44100; - void *data = nullptr; + LocalVector<uint8_t> data; uint32_t data_bytes = 0; protected: diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 27db65bb1a..69dc71e414 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)) { @@ -1267,25 +1354,6 @@ Ref<SceneState> SceneState::get_base_scene_state() const { return Ref<SceneState>(); } -void SceneState::update_instance_resource(String p_path, Ref<PackedScene> p_packed_scene) { - ERR_FAIL_COND(p_packed_scene.is_null()); - - for (const NodeData &nd : nodes) { - if (nd.instance >= 0) { - if (!(nd.instance & FLAG_INSTANCE_IS_PLACEHOLDER)) { - int instance_id = nd.instance & FLAG_MASK; - Ref<PackedScene> original_packed_scene = variants[instance_id]; - if (original_packed_scene.is_valid()) { - if (original_packed_scene->get_path() == p_path) { - variants.remove_at(instance_id); - variants.insert(instance_id, p_packed_scene); - } - } - } - } - } -} - int SceneState::find_node_by_path(const NodePath &p_node) const { ERR_FAIL_COND_V_MSG(node_path_cache.is_empty(), -1, "This operation requires the node cache to have been built."); 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/audio/audio_stream.cpp b/servers/audio/audio_stream.cpp index e2c8686911..ece088a694 100644 --- a/servers/audio/audio_stream.cpp +++ b/servers/audio/audio_stream.cpp @@ -173,12 +173,12 @@ int AudioStreamPlaybackResampled::mix(AudioFrame *p_buffer, float p_rate_scale, } float mu2 = mu * mu; - AudioFrame a0 = 3 * y1 - 3 * y2 + y3 - y0; - AudioFrame a1 = 2 * y0 - 5 * y1 + 4 * y2 - y3; - AudioFrame a2 = y2 - y0; - AudioFrame a3 = 2 * y1; + float h11 = mu2 * (mu - 1); + float z = mu2 - h11; + float h01 = z - h11; + float h10 = mu - z; - p_buffer[i] = (a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3) / 2; + p_buffer[i] = y1 + (y2 - y1) * h01 + ((y2 - y0) * h10 + (y3 - y1) * h11) * 0.5; mix_offset += mix_increment; diff --git a/servers/audio/audio_stream.h b/servers/audio/audio_stream.h index c41545aeba..7f2b68a796 100644 --- a/servers/audio/audio_stream.h +++ b/servers/audio/audio_stream.h @@ -48,6 +48,7 @@ class AudioSamplePlayback : public RefCounted { public: Ref<AudioStream> stream; + Ref<AudioStreamPlayback> stream_playback; float offset = 0.0f; float pitch_scale = 1.0; diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index 332f8984a2..e06079efe8 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -501,12 +501,7 @@ void AudioServer::_mix_step() { switch (playback->state.load()) { case AudioStreamPlaybackListNode::AWAITING_DELETION: case AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION: - playback_list.erase(playback, [](AudioStreamPlaybackListNode *p) { - delete p->prev_bus_details; - delete p->bus_details.load(); - p->stream_playback.unref(); - delete p; - }); + _delete_stream_playback_list_node(playback); break; case AudioStreamPlaybackListNode::FADE_OUT_TO_PAUSE: { // Pause the stream. @@ -697,6 +692,23 @@ AudioServer::AudioStreamPlaybackListNode *AudioServer::_find_playback_list_node( return nullptr; } +void AudioServer::_delete_stream_playback(Ref<AudioStreamPlayback> p_playback) { + ERR_FAIL_COND(p_playback.is_null()); + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); + if (playback_node) { + _delete_stream_playback_list_node(playback_node); + } +} + +void AudioServer::_delete_stream_playback_list_node(AudioStreamPlaybackListNode *p_playback_node) { + playback_list.erase(p_playback_node, [](AudioStreamPlaybackListNode *p) { + delete p->prev_bus_details; + delete p->bus_details.load(); + p->stream_playback.unref(); + delete p; + }); +} + bool AudioServer::thread_has_channel_mix_buffer(int p_bus, int p_buffer) const { if (p_bus < 0 || p_bus >= buses.size()) { return false; @@ -1227,8 +1239,12 @@ void AudioServer::stop_playback_stream(Ref<AudioStreamPlayback> p_playback) { ERR_FAIL_COND(p_playback.is_null()); // Handle sample playback. - if (p_playback->get_is_sample() && p_playback->get_sample_playback().is_valid()) { - AudioServer::get_singleton()->stop_sample_playback(p_playback->get_sample_playback()); + if (p_playback->get_is_sample()) { + if (p_playback->get_sample_playback().is_valid()) { + AudioServer::get_singleton()->stop_sample_playback(p_playback->get_sample_playback()); + } else { + _delete_stream_playback(p_playback); + } return; } @@ -1370,8 +1386,12 @@ void AudioServer::set_playback_highshelf_params(Ref<AudioStreamPlayback> p_playb bool AudioServer::is_playback_active(Ref<AudioStreamPlayback> p_playback) { ERR_FAIL_COND_V(p_playback.is_null(), false); - if (p_playback->get_is_sample() && p_playback->get_sample_playback().is_valid()) { - return sample_playback_list.has(p_playback->get_sample_playback()); + if (p_playback->get_is_sample()) { + if (p_playback->get_sample_playback().is_valid()) { + return sample_playback_list.has(p_playback->get_sample_playback()); + } else { + return false; + } } AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); @@ -1845,8 +1865,12 @@ void AudioServer::start_sample_playback(const Ref<AudioSamplePlayback> &p_playba void AudioServer::stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) { ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); - AudioDriver::get_singleton()->stop_sample_playback(p_playback); - sample_playback_list.erase(p_playback); + if (sample_playback_list.has(p_playback)) { + sample_playback_list.erase(p_playback); + AudioDriver::get_singleton()->stop_sample_playback(p_playback); + p_playback->stream_playback->set_sample_playback(nullptr); + stop_playback_stream(p_playback->stream_playback); + } } void AudioServer::set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) { diff --git a/servers/audio_server.h b/servers/audio_server.h index 2d6fc60860..16fcc029b3 100644 --- a/servers/audio_server.h +++ b/servers/audio_server.h @@ -297,6 +297,8 @@ private: SafeList<AudioStreamPlaybackListNode *> playback_list; SafeList<AudioStreamPlaybackBusDetails *> bus_details_graveyard; + void _delete_stream_playback(Ref<AudioStreamPlayback> p_playback); + void _delete_stream_playback_list_node(AudioStreamPlaybackListNode *p_node); // TODO document if this is necessary. SafeList<AudioStreamPlaybackBusDetails *> bus_details_graveyard_frame_old; 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/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index 63dc54e24c..9f390c99f9 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -342,7 +342,7 @@ static void _fill_std140_variant_ubo_value(ShaderLanguage::DataType type, int p_ } } -_FORCE_INLINE_ static void _fill_std140_ubo_value(ShaderLanguage::DataType type, const Vector<ShaderLanguage::ConstantNode::Value> &value, uint8_t *data) { +_FORCE_INLINE_ static void _fill_std140_ubo_value(ShaderLanguage::DataType type, const Vector<ShaderLanguage::Scalar> &value, uint8_t *data) { switch (type) { case ShaderLanguage::TYPE_BOOL: { uint32_t *gui = (uint32_t *)data; @@ -566,7 +566,7 @@ void MaterialStorage::ShaderData::set_default_texture_parameter(const StringName Variant MaterialStorage::ShaderData::get_default_parameter(const StringName &p_parameter) const { if (uniforms.has(p_parameter)) { ShaderLanguage::ShaderNode::Uniform uniform = uniforms[p_parameter]; - Vector<ShaderLanguage::ConstantNode::Value> default_value = uniform.default_value; + Vector<ShaderLanguage::Scalar> default_value = uniform.default_value; return ShaderLanguage::constant_value_to_variant(default_value, uniform.type, uniform.array_size, uniform.hint); } return Variant(); diff --git a/servers/rendering/shader_compiler.cpp b/servers/rendering/shader_compiler.cpp index 49e005ca96..43703f8656 100644 --- a/servers/rendering/shader_compiler.cpp +++ b/servers/rendering/shader_compiler.cpp @@ -185,7 +185,7 @@ static String f2sp0(float p_float) { return num; } -static String get_constant_text(SL::DataType p_type, const Vector<SL::ConstantNode::Value> &p_values) { +static String get_constant_text(SL::DataType p_type, const Vector<SL::Scalar> &p_values) { switch (p_type) { case SL::TYPE_BOOL: return p_values[0].boolean ? "true" : "false"; diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index 4eaf7fcb55..2249cd2010 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -1362,7 +1362,7 @@ void ShaderLanguage::_parse_used_identifier(const StringName &p_identifier, Iden } #endif // DEBUG_ENABLED -bool ShaderLanguage::_find_identifier(const BlockNode *p_block, bool p_allow_reassign, const FunctionInfo &p_function_info, const StringName &p_identifier, DataType *r_data_type, IdentifierType *r_type, bool *r_is_const, int *r_array_size, StringName *r_struct_name, ConstantNode::Value *r_constant_value) { +bool ShaderLanguage::_find_identifier(const BlockNode *p_block, bool p_allow_reassign, const FunctionInfo &p_function_info, const StringName &p_identifier, DataType *r_data_type, IdentifierType *r_type, bool *r_is_const, int *r_array_size, StringName *r_struct_name, Vector<Scalar> *r_constant_values) { if (is_shader_inc) { for (int i = 0; i < RenderingServer::SHADER_MAX; i++) { for (const KeyValue<StringName, FunctionInfo> &E : ShaderTypes::get_singleton()->get_functions(RenderingServer::ShaderMode(i))) { @@ -1424,8 +1424,8 @@ bool ShaderLanguage::_find_identifier(const BlockNode *p_block, bool p_allow_rea if (r_struct_name) { *r_struct_name = p_block->variables[p_identifier].struct_name; } - if (r_constant_value) { - *r_constant_value = p_block->variables[p_identifier].value; + if (r_constant_values && !p_block->variables[p_identifier].values.is_empty()) { + *r_constant_values = p_block->variables[p_identifier].values; } if (r_type) { *r_type = IDENTIFIER_LOCAL_VAR; @@ -1507,13 +1507,9 @@ bool ShaderLanguage::_find_identifier(const BlockNode *p_block, bool p_allow_rea if (r_struct_name) { *r_struct_name = shader->constants[p_identifier].struct_name; } - if (r_constant_value) { - if (shader->constants[p_identifier].initializer && shader->constants[p_identifier].initializer->type == Node::NODE_TYPE_CONSTANT) { - ConstantNode *cnode = static_cast<ConstantNode *>(shader->constants[p_identifier].initializer); - - if (cnode->values.size() == 1) { - *r_constant_value = cnode->values[0]; - } + if (r_constant_values) { + if (shader->constants[p_identifier].initializer && !shader->constants[p_identifier].initializer->get_values().is_empty()) { + *r_constant_values = shader->constants[p_identifier].initializer->get_values(); } } if (r_type) { @@ -1544,7 +1540,7 @@ bool ShaderLanguage::_find_identifier(const BlockNode *p_block, bool p_allow_rea return false; } -bool ShaderLanguage::_validate_operator(OperatorNode *p_op, DataType *r_ret_type, int *r_ret_size) { +bool ShaderLanguage::_validate_operator(const BlockNode *p_block, OperatorNode *p_op, DataType *r_ret_type, int *r_ret_size) { bool valid = false; DataType ret_type = TYPE_VOID; int ret_size = 0; @@ -2007,9 +2003,384 @@ bool ShaderLanguage::_validate_operator(OperatorNode *p_op, DataType *r_ret_type if (r_ret_size) { *r_ret_size = ret_size; } + + if (valid && (!p_block || p_block->use_op_eval)) { + // Need to be placed here and not in the `_reduce_expression` because otherwise expressions like `1 + 2 / 2` will not work correctly. + valid = _eval_operator(p_block, p_op); + } + return valid; } +Vector<ShaderLanguage::Scalar> ShaderLanguage::_get_node_values(const BlockNode *p_block, Node *p_node) { + Vector<Scalar> result; + + switch (p_node->type) { + case Node::NODE_TYPE_VARIABLE: { + _find_identifier(p_block, false, FunctionInfo(), static_cast<VariableNode *>(p_node)->name, nullptr, nullptr, nullptr, nullptr, nullptr, &result); + } break; + default: { + result = p_node->get_values(); + } break; + } + + return result; +} + +bool ShaderLanguage::_eval_operator(const BlockNode *p_block, OperatorNode *p_op) { + bool is_valid = true; + + switch (p_op->op) { + case OP_EQUAL: + case OP_NOT_EQUAL: + case OP_LESS: + case OP_LESS_EQUAL: + case OP_GREATER: + case OP_GREATER_EQUAL: + case OP_AND: + case OP_OR: + case OP_ADD: + case OP_SUB: + case OP_MUL: + case OP_DIV: + case OP_MOD: + case OP_SHIFT_LEFT: + case OP_SHIFT_RIGHT: + case OP_BIT_AND: + case OP_BIT_OR: + case OP_BIT_XOR: { + DataType a = p_op->arguments[0]->get_datatype(); + DataType b = p_op->arguments[1]->get_datatype(); + + bool is_op_vec_transform = false; + if (p_op->op == OP_MUL) { + DataType ta = a; + DataType tb = b; + + if (ta > tb) { + SWAP(ta, tb); + } + if (ta == TYPE_VEC2 && tb == TYPE_MAT2) { + is_op_vec_transform = true; + } else if (ta == TYPE_VEC3 && tb == TYPE_MAT3) { + is_op_vec_transform = true; + } else if (ta == TYPE_VEC4 && tb == TYPE_MAT4) { + is_op_vec_transform = true; + } + } + + Vector<Scalar> va = _get_node_values(p_block, p_op->arguments[0]); + Vector<Scalar> vb = _get_node_values(p_block, p_op->arguments[1]); + + if (is_op_vec_transform) { + p_op->values = _eval_vector_transform(va, vb, a, b, p_op->get_datatype()); + } else { + p_op->values = _eval_vector(va, vb, a, b, p_op->get_datatype(), p_op->op, is_valid); + } + } break; + case OP_NOT: + case OP_NEGATE: + case OP_BIT_INVERT: { + p_op->values = _eval_unary_vector(_get_node_values(p_block, p_op->arguments[0]), p_op->get_datatype(), p_op->op); + } break; + default: { + } break; + } + + return is_valid; +} + +ShaderLanguage::Scalar ShaderLanguage::_eval_unary_scalar(const Scalar &p_a, Operator p_op, DataType p_ret_type) { + Scalar scalar; + + switch (p_op) { + case OP_NOT: { + scalar.boolean = !p_a.boolean; + } break; + case OP_NEGATE: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = -p_a.sint; + } else if (p_ret_type >= TYPE_UINT && p_ret_type <= TYPE_UVEC4) { + // Intentionally wrap the unsigned int value, because GLSL does. + scalar.uint = 0 - p_a.uint; + } else { // float types + scalar.real = -scalar.real; + } + } break; + case OP_BIT_INVERT: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = ~p_a.sint; + } else { // uint types + scalar.uint = ~p_a.uint; + } + } break; + default: { + } break; + } + + return scalar; +} + +ShaderLanguage::Scalar ShaderLanguage::_eval_scalar(const Scalar &p_a, const Scalar &p_b, Operator p_op, DataType p_ret_type, bool &r_is_valid) { + Scalar scalar; + + switch (p_op) { + case OP_EQUAL: { + scalar.boolean = p_a.boolean == p_b.boolean; + } break; + case OP_NOT_EQUAL: { + scalar.boolean = p_a.boolean != p_b.boolean; + } break; + case OP_LESS: { + if (p_ret_type == TYPE_INT) { + scalar.boolean = p_a.sint < p_b.sint; + } else if (p_ret_type == TYPE_UINT) { + scalar.boolean = p_a.uint < p_b.uint; + } else { // float type + scalar.boolean = p_a.real < p_b.real; + } + } break; + case OP_LESS_EQUAL: { + if (p_ret_type == TYPE_INT) { + scalar.boolean = p_a.sint <= p_b.sint; + } else if (p_ret_type == TYPE_UINT) { + scalar.boolean = p_a.uint <= p_b.uint; + } else { // float type + scalar.boolean = p_a.real <= p_b.real; + } + } break; + case OP_GREATER: { + if (p_ret_type == TYPE_INT) { + scalar.boolean = p_a.sint > p_b.sint; + } else if (p_ret_type == TYPE_UINT) { + scalar.boolean = p_a.uint > p_b.uint; + } else { // float type + scalar.boolean = p_a.real > p_b.real; + } + } break; + case OP_GREATER_EQUAL: { + if (p_ret_type == TYPE_INT) { + scalar.boolean = p_a.sint >= p_b.sint; + } else if (p_ret_type == TYPE_UINT) { + scalar.boolean = p_a.uint >= p_b.uint; + } else { // float type + scalar.boolean = p_a.real >= p_b.real; + } + } break; + case OP_AND: { + scalar.boolean = p_a.boolean && p_b.boolean; + } break; + case OP_OR: { + scalar.boolean = p_a.boolean || p_b.boolean; + } break; + case OP_ADD: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint + p_b.sint; + } else if (p_ret_type >= TYPE_UINT && p_ret_type <= TYPE_UVEC4) { + scalar.uint = p_a.uint + p_b.uint; + } else { // float + matrix types + scalar.real = p_a.real + p_b.real; + } + } break; + case OP_SUB: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint - p_b.sint; + } else if (p_ret_type >= TYPE_UINT && p_ret_type <= TYPE_UVEC4) { + scalar.uint = p_a.uint - p_b.uint; + } else { // float + matrix types + scalar.real = p_a.real - p_b.real; + } + } break; + case OP_MUL: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint * p_b.sint; + } else if (p_ret_type >= TYPE_UINT && p_ret_type <= TYPE_UVEC4) { + scalar.uint = p_a.uint * p_b.uint; + } else { // float + matrix types + scalar.real = p_a.real * p_b.real; + } + } break; + case OP_DIV: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + if (p_b.sint == 0) { + _set_error(RTR("Division by zero error.")); + r_is_valid = false; + break; + } + scalar.sint = p_a.sint / p_b.sint; + } else if (p_ret_type == TYPE_UINT && p_ret_type <= TYPE_UVEC4) { + if (p_b.uint == 0U) { + _set_error(RTR("Division by zero error.")); + r_is_valid = false; + break; + } + scalar.uint = p_a.uint / p_b.uint; + } else { // float + matrix types + scalar.real = p_a.real / p_b.real; + } + } break; + case OP_MOD: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + if (p_b.sint == 0) { + _set_error(RTR("Modulo by zero error.")); + r_is_valid = false; + break; + } + scalar.sint = p_a.sint % p_b.sint; + } else { // uint types + if (p_b.uint == 0U) { + _set_error(RTR("Modulo by zero error.")); + r_is_valid = false; + break; + } + scalar.uint = p_a.uint % p_b.uint; + } + } break; + case OP_SHIFT_LEFT: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint << p_b.sint; + } else { // uint types + scalar.uint = p_a.uint << p_b.uint; + } + } break; + case OP_SHIFT_RIGHT: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint >> p_b.sint; + } else { // uint types + scalar.uint = p_a.uint >> p_b.uint; + } + } break; + case OP_BIT_AND: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint & p_b.sint; + } else { // uint types + scalar.uint = p_a.uint & p_b.uint; + } + } break; + case OP_BIT_OR: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint | p_b.sint; + } else { // uint types + scalar.uint = p_a.uint | p_b.uint; + } + } break; + case OP_BIT_XOR: { + if (p_ret_type >= TYPE_INT && p_ret_type <= TYPE_IVEC4) { + scalar.sint = p_a.sint ^ p_b.sint; + } else { // uint types + scalar.uint = p_a.uint ^ p_b.uint; + } + } break; + default: { + } break; + } + + return scalar; +} + +Vector<ShaderLanguage::Scalar> ShaderLanguage::_eval_unary_vector(const Vector<Scalar> &p_va, DataType p_ret_type, Operator p_op) { + uint32_t size = get_datatype_component_count(p_ret_type); + if (p_va.size() != p_ret_type) { + return Vector<Scalar>(); // Non-evaluable values should not be parsed further. + } + Vector<Scalar> value; + value.resize(size); + + Scalar *w = value.ptrw(); + for (uint32_t i = 0U; i < size; i++) { + w[i] = _eval_unary_scalar(p_va[i], p_op, p_ret_type); + } + return value; +} + +Vector<ShaderLanguage::Scalar> ShaderLanguage::_eval_vector(const Vector<Scalar> &p_va, const Vector<Scalar> &p_vb, DataType p_left_type, DataType p_right_type, DataType p_ret_type, Operator p_op, bool &r_is_valid) { + uint32_t left_size = get_datatype_component_count(p_left_type); + uint32_t right_size = get_datatype_component_count(p_right_type); + + if (p_va.size() != left_size || p_vb.size() != right_size) { + return Vector<Scalar>(); // Non-evaluable values should not be parsed further. + } + + uint32_t ret_size = get_datatype_component_count(p_ret_type); + Vector<Scalar> value; + value.resize(ret_size); + + Scalar *w = value.ptrw(); + for (uint32_t i = 0U; i < ret_size; i++) { + w[i] = _eval_scalar(p_va[MIN(i, left_size - 1)], p_vb[MIN(i, right_size - 1)], p_op, p_ret_type, r_is_valid); + if (!r_is_valid) { + return value; + } + } + return value; +} + +Vector<ShaderLanguage::Scalar> ShaderLanguage::_eval_vector_transform(const Vector<Scalar> &p_va, const Vector<Scalar> &p_vb, DataType p_left_type, DataType p_right_type, DataType p_ret_type) { + uint32_t left_size = get_datatype_component_count(p_left_type); + uint32_t right_size = get_datatype_component_count(p_right_type); + + if (p_va.size() != left_size || p_vb.size() != right_size) { + return Vector<Scalar>(); // Non-evaluable values should not be parsed further. + } + + uint32_t ret_size = get_datatype_component_count(p_ret_type); + Vector<Scalar> value; + value.resize_zeroed(ret_size); + + Scalar *w = value.ptrw(); + switch (p_ret_type) { + case TYPE_VEC2: { + if (left_size == 2) { // v * m + Vector2 v = Vector2(p_va[0].real, p_va[1].real); + + w[0].real = (p_vb[0].real * v.x + p_vb[1].real * v.y); + w[1].real = (p_vb[2].real * v.x + p_vb[3].real * v.y); + } else { // m * v + Vector2 v = Vector2(p_vb[0].real, p_vb[1].real); + + w[0].real = (p_va[0].real * v.x + p_va[2].real * v.y); + w[1].real = (p_va[1].real * v.x + p_va[3].real * v.y); + } + } break; + case TYPE_VEC3: { + if (left_size == 3) { // v * m + Vector3 v = Vector3(p_va[0].real, p_va[1].real, p_va[2].real); + + w[0].real = (p_vb[0].real * v.x + p_vb[1].real * v.y + p_vb[2].real * v.z); + w[1].real = (p_vb[3].real * v.x + p_vb[4].real * v.y + p_vb[5].real * v.z); + w[2].real = (p_vb[6].real * v.x + p_vb[7].real * v.y + p_vb[8].real * v.z); + } else { // m * v + Vector3 v = Vector3(p_vb[0].real, p_vb[1].real, p_vb[2].real); + + w[0].real = (p_va[0].real * v.x + p_va[3].real * v.y + p_va[6].real * v.z); + w[1].real = (p_va[1].real * v.x + p_va[4].real * v.y + p_va[7].real * v.z); + w[2].real = (p_va[2].real * v.x + p_va[5].real * v.y + p_va[8].real * v.z); + } + } break; + case TYPE_VEC4: { + if (left_size == 4) { // v * m + Vector4 v = Vector4(p_va[0].real, p_va[1].real, p_va[2].real, p_va[3].real); + + w[0].real = (p_vb[0].real * v.x + p_vb[1].real * v.y + p_vb[2].real * v.z + p_vb[3].real * v.w); + w[1].real = (p_vb[4].real * v.x + p_vb[5].real * v.y + p_vb[6].real * v.z + p_vb[7].real * v.w); + w[2].real = (p_vb[8].real * v.x + p_vb[9].real * v.y + p_vb[10].real * v.z + p_vb[11].real * v.w); + w[3].real = (p_vb[12].real * v.x + p_vb[13].real * v.y + p_vb[14].real * v.z + p_vb[15].real * v.w); + } else { // m * v + Vector4 v = Vector4(p_vb[0].real, p_vb[1].real, p_vb[2].real, p_vb[3].real); + + w[0].real = (p_vb[0].real * v.x + p_vb[4].real * v.y + p_vb[8].real * v.z + p_vb[12].real * v.w); + w[1].real = (p_vb[1].real * v.x + p_vb[5].real * v.y + p_vb[9].real * v.z + p_vb[13].real * v.w); + w[2].real = (p_vb[2].real * v.x + p_vb[6].real * v.y + p_vb[10].real * v.z + p_vb[14].real * v.w); + w[3].real = (p_vb[3].real * v.x + p_vb[7].real * v.y + p_vb[11].real * v.z + p_vb[15].real * v.w); + } + } break; + default: { + } break; + } + + return value; +} + const ShaderLanguage::BuiltinFuncDef ShaderLanguage::builtin_func_defs[] = { // Constructors. @@ -3271,34 +3642,15 @@ bool ShaderLanguage::_validate_function_call(BlockNode *p_block, const FunctionI int max = builtin_func_const_args[constarg_idx].max; bool error = false; - if (p_func->arguments[arg]->type == Node::NODE_TYPE_VARIABLE) { - const VariableNode *vn = static_cast<VariableNode *>(p_func->arguments[arg]); - - bool is_const = false; - ConstantNode::Value value; - value.sint = -1; - - _find_identifier(p_block, false, p_function_info, vn->name, nullptr, nullptr, &is_const, nullptr, nullptr, &value); - if (!is_const || value.sint < min || value.sint > max) { + Vector<Scalar> values = _get_node_values(p_block, p_func->arguments[arg]); + if (p_func->arguments[arg]->get_datatype() == TYPE_INT && !values.is_empty()) { + if (values[0].sint < min || values[0].sint > max) { error = true; } } else { - if (p_func->arguments[arg]->type == Node::NODE_TYPE_CONSTANT) { - const ConstantNode *cn = static_cast<ConstantNode *>(p_func->arguments[arg]); - - if (cn->get_datatype() == TYPE_INT && cn->values.size() == 1) { - int value = cn->values[0].sint; - - if (value < min || value > max) { - error = true; - } - } else { - error = true; - } - } else { - error = true; - } + error = true; } + if (error) { _set_error(vformat(RTR("Expected integer constant within [%d..%d] range."), min, max)); return false; @@ -3760,7 +4112,7 @@ bool ShaderLanguage::is_token_hint(TokenType p_type) { return int(p_type) > int(TK_RENDER_MODE) && int(p_type) < int(TK_SHADER_TYPE); } -bool ShaderLanguage::convert_constant(ConstantNode *p_constant, DataType p_to_type, ConstantNode::Value *p_value) { +bool ShaderLanguage::convert_constant(ConstantNode *p_constant, DataType p_to_type, Scalar *p_value) { if (p_constant->datatype == p_to_type) { if (p_value) { for (int i = 0; i < p_constant->values.size(); i++) { @@ -3828,7 +4180,7 @@ bool ShaderLanguage::is_sampler_type(DataType p_type) { return p_type > TYPE_MAT4 && p_type < TYPE_STRUCT; } -Variant ShaderLanguage::constant_value_to_variant(const Vector<ShaderLanguage::ConstantNode::Value> &p_value, DataType p_type, int p_array_size, ShaderLanguage::ShaderNode::Uniform::Hint p_hint) { +Variant ShaderLanguage::constant_value_to_variant(const Vector<Scalar> &p_value, DataType p_type, int p_array_size, ShaderLanguage::ShaderNode::Uniform::Hint p_hint) { int array_size = p_array_size; if (p_value.size() > 0) { @@ -4437,6 +4789,52 @@ uint32_t ShaderLanguage::get_datatype_size(ShaderLanguage::DataType p_type) { ERR_FAIL_V(0); } +uint32_t ShaderLanguage::get_datatype_component_count(ShaderLanguage::DataType p_type) { + switch (p_type) { + case TYPE_BOOL: + return 1U; + case TYPE_BVEC2: + return 2U; + case TYPE_BVEC3: + return 3U; + case TYPE_BVEC4: + return 4U; + case TYPE_INT: + return 1U; + case TYPE_IVEC2: + return 2U; + case TYPE_IVEC3: + return 3U; + case TYPE_IVEC4: + return 4U; + case TYPE_UINT: + return 1U; + case TYPE_UVEC2: + return 2U; + case TYPE_UVEC3: + return 3U; + case TYPE_UVEC4: + return 4U; + case TYPE_FLOAT: + return 1U; + case TYPE_VEC2: + return 2U; + case TYPE_VEC3: + return 3U; + case TYPE_VEC4: + return 4U; + case TYPE_MAT2: + return 4U; + case TYPE_MAT3: + return 9U; + case TYPE_MAT4: + return 16U; + default: + break; + } + return 0U; +} + void ShaderLanguage::get_keyword_list(List<String> *r_keywords) { HashSet<String> kws; @@ -4929,45 +5327,30 @@ Error ShaderLanguage::_parse_array_size(BlockNode *p_block, const FunctionInfo & *r_unknown_size = true; } } else { - int array_size = 0; + _set_tkpos(pos); - if (!tk.is_integer_constant() || ((int)tk.constant) <= 0) { - _set_tkpos(pos); - Node *n = _parse_and_reduce_expression(p_block, p_function_info); - if (n) { - if (n->type == Node::NODE_TYPE_VARIABLE) { - VariableNode *vn = static_cast<VariableNode *>(n); - if (vn) { - ConstantNode::Value v; - DataType data_type; - bool is_const = false; + int array_size = 0; + Node *expr = _parse_and_reduce_expression(p_block, p_function_info); - _find_identifier(p_block, false, p_function_info, vn->name, &data_type, nullptr, &is_const, nullptr, nullptr, &v); + if (expr) { + Vector<Scalar> values = _get_node_values(p_block, expr); - if (is_const) { - if (data_type == TYPE_INT) { - int32_t value = v.sint; - if (value > 0) { - array_size = value; - } - } else if (data_type == TYPE_UINT) { - uint32_t value = v.uint; - if (value > 0U) { - array_size = value; - } - } - } - } - } else if (n->type == Node::NODE_TYPE_OPERATOR) { - _set_error(vformat(RTR("Array size expressions are not supported."))); - return ERR_PARSE_ERROR; - } - if (r_size_expression != nullptr) { - *r_size_expression = n; + if (!values.is_empty()) { + switch (expr->get_datatype()) { + case TYPE_INT: { + array_size = values[0].sint; + } break; + case TYPE_UINT: { + array_size = (int)values[0].uint; + } break; + default: { + } break; } } - } else if (((int)tk.constant) > 0) { - array_size = (uint32_t)tk.constant; + + if (r_size_expression != nullptr) { + *r_size_expression = expr; + } } if (array_size <= 0) { @@ -5064,7 +5447,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_array_constructor(BlockNode *p_bloc idx++; } if (!auto_size && !undefined_size && an->initializer.size() != array_size) { - _set_error(RTR("Array size mismatch.")); + _set_error(vformat(RTR("Array size mismatch. Expected %d elements (found %d)."), array_size, an->initializer.size())); return nullptr; } } else { @@ -5185,7 +5568,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_array_constructor(BlockNode *p_bloc } } if (an->initializer.size() != p_array_size) { - _set_error(RTR("Array size mismatch.")); + _set_error(vformat(RTR("Array size mismatch. Expected %d elements (found %d)."), p_array_size, an->initializer.size())); return nullptr; } } else { @@ -5230,7 +5613,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } else if (tk.type == TK_FLOAT_CONSTANT) { ConstantNode *constant = alloc_node<ConstantNode>(); - ConstantNode::Value v; + Scalar v; v.real = tk.constant; constant->values.push_back(v); constant->datatype = TYPE_FLOAT; @@ -5238,7 +5621,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } else if (tk.type == TK_INT_CONSTANT) { ConstantNode *constant = alloc_node<ConstantNode>(); - ConstantNode::Value v; + Scalar v; v.sint = tk.constant; constant->values.push_back(v); constant->datatype = TYPE_INT; @@ -5246,7 +5629,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } else if (tk.type == TK_UINT_CONSTANT) { ConstantNode *constant = alloc_node<ConstantNode>(); - ConstantNode::Value v; + Scalar v; v.uint = tk.constant; constant->values.push_back(v); constant->datatype = TYPE_UINT; @@ -5255,7 +5638,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } else if (tk.type == TK_TRUE) { //handle true constant ConstantNode *constant = alloc_node<ConstantNode>(); - ConstantNode::Value v; + Scalar v; v.boolean = true; constant->values.push_back(v); constant->datatype = TYPE_BOOL; @@ -5264,7 +5647,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } else if (tk.type == TK_FALSE) { //handle false constant ConstantNode *constant = alloc_node<ConstantNode>(); - ConstantNode::Value v; + Scalar v; v.boolean = false; constant->values.push_back(v); constant->datatype = TYPE_BOOL; @@ -6527,7 +6910,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons op->op = tk.type == TK_OP_DECREMENT ? OP_POST_DECREMENT : OP_POST_INCREMENT; op->arguments.push_back(expr); - if (!_validate_operator(op, &op->return_cache, &op->return_array_size)) { + if (!_validate_operator(p_block, op, &op->return_cache, &op->return_array_size)) { _set_error(RTR("Invalid base type for increment/decrement operator.")); return nullptr; } @@ -6876,7 +7259,11 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons expression.write[i].is_op = false; expression.write[i].node = op; - if (!_validate_operator(op, &op->return_cache, &op->return_array_size)) { + if (!_validate_operator(p_block, op, &op->return_cache, &op->return_array_size)) { + if (error_set) { + return nullptr; + } + String at; for (int j = 0; j < op->arguments.size(); j++) { if (j > 0) { @@ -6914,7 +7301,11 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons expression.write[next_op - 1].is_op = false; expression.write[next_op - 1].node = op; - if (!_validate_operator(op, &op->return_cache, &op->return_array_size)) { + if (!_validate_operator(p_block, op, &op->return_cache, &op->return_array_size)) { + if (error_set) { + return nullptr; + } + String at; for (int i = 0; i < op->arguments.size(); i++) { if (i > 0) { @@ -6950,6 +7341,11 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } if (_is_operator_assign(op->op)) { + if (p_block && expression[next_op - 1].node->type == Node::NODE_TYPE_VARIABLE) { + VariableNode *vn = static_cast<VariableNode *>(expression[next_op - 1].node); + p_block->use_op_eval = vn->is_const; + } + String assign_message; if (!_validate_assign(expression[next_op - 1].node, p_function_info, &assign_message)) { _set_error(assign_message); @@ -6972,7 +7368,11 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons //replace all 3 nodes by this operator and make it an expression - if (!_validate_operator(op, &op->return_cache, &op->return_array_size)) { + if (!_validate_operator(p_block, op, &op->return_cache, &op->return_array_size)) { + if (error_set) { + return nullptr; + } + String at; for (int i = 0; i < op->arguments.size(); i++) { if (i > 0) { @@ -6998,6 +7398,10 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } } + if (p_block) { + p_block->use_op_eval = true; + } + if (p_previous_expression_info != nullptr) { p_previous_expression_info->expression->push_back(expression[0]); } @@ -7020,7 +7424,7 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha DataType base = get_scalar_type(type); int cardinality = get_cardinality(type); - Vector<ConstantNode::Value> values; + Vector<Scalar> values; for (int i = 1; i < op->arguments.size(); i++) { op->arguments.write[i] = _reduce_expression(p_block, op->arguments[i]); @@ -7032,7 +7436,7 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha values.push_back(cn->values[j]); } } else if (get_scalar_type(cn->datatype) == cn->datatype) { - ConstantNode::Value v; + Scalar v; if (!convert_constant(cn, base, &v)) { return p_node; } @@ -7048,8 +7452,8 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha if (values.size() == 1) { if (type >= TYPE_MAT2 && type <= TYPE_MAT4) { - ConstantNode::Value value = values[0]; - ConstantNode::Value zero; + Scalar value = values[0]; + Scalar zero; zero.real = 0.0f; int size = 2 + (type - TYPE_MAT2); @@ -7060,7 +7464,7 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha } } } else { - ConstantNode::Value value = values[0]; + Scalar value = values[0]; for (int i = 1; i < cardinality; i++) { values.push_back(value); } @@ -7081,10 +7485,10 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha DataType base = get_scalar_type(cn->datatype); - Vector<ConstantNode::Value> values; + Vector<Scalar> values; for (int i = 0; i < cn->values.size(); i++) { - ConstantNode::Value nv; + Scalar nv; switch (base) { case TYPE_BOOL: { nv.boolean = !cn->values[i].boolean; @@ -7515,7 +7919,7 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun decl.size = decl.initializer.size(); var.array_size = decl.initializer.size(); } else if (decl.initializer.size() != var.array_size) { - _set_error(RTR("Array size mismatch.")); + _set_error(vformat(RTR("Array size mismatch. Expected %d elements (found %d)."), var.array_size, decl.initializer.size())); return ERR_PARSE_ERROR; } tk = _get_token(); @@ -7537,7 +7941,9 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun array_size = var.array_size; } else if (tk.type == TK_OP_ASSIGN) { - //variable created with assignment! must parse an expression + p_block->use_op_eval = is_const; + + // Variable created with assignment! Must parse an expression. Node *n = _parse_and_reduce_expression(p_block, p_function_info); if (!n) { return ERR_PARSE_ERROR; @@ -7552,11 +7958,8 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun } } - if (n->type == Node::NODE_TYPE_CONSTANT) { - ConstantNode *const_node = static_cast<ConstantNode *>(n); - if (const_node && const_node->values.size() == 1) { - var.value = const_node->values[0]; - } + if (is_const) { + var.values = n->get_values(); } if (!_compare_datatypes(var.type, var.struct_name, var.array_size, n->get_datatype(), n->get_datatype_name(), n->get_array_size())) { @@ -7734,13 +8137,13 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun if (!vn) { return ERR_PARSE_ERROR; } - ConstantNode::Value v; + Vector<Scalar> v = { Scalar() }; _find_identifier(p_block, false, p_function_info, vn->name, nullptr, nullptr, nullptr, nullptr, nullptr, &v); - if (constants.has(v.sint)) { - _set_error(vformat(RTR("Duplicated case label: %d."), v.sint)); + if (constants.has(v[0].sint)) { + _set_error(vformat(RTR("Duplicated case label: %d."), v[0].sint)); return ERR_PARSE_ERROR; } - constants.insert(v.sint); + constants.insert(v[0].sint); } } else if (flow->flow_op == FLOW_OP_DEFAULT) { continue; @@ -7801,7 +8204,7 @@ Error ShaderLanguage::_parse_block(BlockNode *p_block, const FunctionInfo &p_fun vn->name = tk.text; n = vn; } else { - ConstantNode::Value v; + Scalar v; if (tk.type == TK_UINT_CONSTANT) { v.uint = (uint32_t)tk.constant; } else { @@ -9678,7 +10081,7 @@ Error ShaderLanguage::_parse_shader(const HashMap<StringName, FunctionInfo> &p_f decl.size = decl.initializer.size(); constant.array_size = decl.initializer.size(); } else if (decl.initializer.size() != constant.array_size) { - _set_error(RTR("Array size mismatch.")); + _set_error(vformat(RTR("Array size mismatch. Expected %d elements (found %d)."), constant.array_size, decl.initializer.size())); return ERR_PARSE_ERROR; } } else { diff --git a/servers/rendering/shader_language.h b/servers/rendering/shader_language.h index 63dca99654..ba02e181b9 100644 --- a/servers/rendering/shader_language.h +++ b/servers/rendering/shader_language.h @@ -355,6 +355,13 @@ public: } }; + union Scalar { + bool boolean = false; + float real; + int32_t sint; + uint32_t uint; + }; + struct Node { Node *next = nullptr; @@ -379,6 +386,7 @@ public: virtual String get_datatype_name() const { return ""; } virtual int get_array_size() const { return 0; } virtual bool is_indexed() const { return false; } + virtual Vector<Scalar> get_values() const { return Vector<Scalar>(); } Node(Type t) : type(t) {} @@ -402,11 +410,13 @@ public: Operator op = OP_EQUAL; StringName struct_name; Vector<Node *> arguments; + Vector<Scalar> values; virtual DataType get_datatype() const override { return return_cache; } virtual String get_datatype_name() const override { return String(struct_name); } virtual int get_array_size() const override { return return_array_size; } virtual bool is_indexed() const override { return op == OP_INDEX; } + virtual Vector<Scalar> get_values() const override { return values; } OperatorNode() : Node(NODE_TYPE_OPERATOR) {} @@ -485,19 +495,15 @@ public: String struct_name = ""; int array_size = 0; - union Value { - bool boolean = false; - float real; - int32_t sint; - uint32_t uint; - }; - - Vector<Value> values; + Vector<Scalar> values; Vector<VariableDeclarationNode::Declaration> array_declarations; virtual DataType get_datatype() const override { return datatype; } virtual String get_datatype_name() const override { return struct_name; } virtual int get_array_size() const override { return array_size; } + virtual Vector<Scalar> get_values() const override { + return values; + } ConstantNode() : Node(NODE_TYPE_CONSTANT) {} @@ -529,13 +535,14 @@ public: int line; //for completion int array_size; bool is_const; - ConstantNode::Value value; + Vector<Scalar> values; }; HashMap<StringName, Variable> variables; List<Node *> statements; bool single_statement = false; bool use_comma_between_statements = false; + bool use_op_eval = true; BlockNode() : Node(NODE_TYPE_BLOCK) {} @@ -657,7 +664,7 @@ public: DataType type = TYPE_VOID; DataPrecision precision = PRECISION_DEFAULT; int array_size = 0; - Vector<ConstantNode::Value> default_value; + Vector<Scalar> default_value; Scope scope = SCOPE_LOCAL; Hint hint = HINT_NONE; bool use_color = false; @@ -803,15 +810,16 @@ public: static bool is_token_operator_assign(TokenType p_type); static bool is_token_hint(TokenType p_type); - static bool convert_constant(ConstantNode *p_constant, DataType p_to_type, ConstantNode::Value *p_value = nullptr); + static bool convert_constant(ConstantNode *p_constant, DataType p_to_type, Scalar *p_value = nullptr); static DataType get_scalar_type(DataType p_type); static int get_cardinality(DataType p_type); static bool is_scalar_type(DataType p_type); static bool is_float_type(DataType p_type); static bool is_sampler_type(DataType p_type); - static Variant constant_value_to_variant(const Vector<ShaderLanguage::ConstantNode::Value> &p_value, DataType p_type, int p_array_size, ShaderLanguage::ShaderNode::Uniform::Hint p_hint = ShaderLanguage::ShaderNode::Uniform::HINT_NONE); + static Variant constant_value_to_variant(const Vector<Scalar> &p_value, DataType p_type, int p_array_size, ShaderLanguage::ShaderNode::Uniform::Hint p_hint = ShaderLanguage::ShaderNode::Uniform::HINT_NONE); static PropertyInfo uniform_to_property_info(const ShaderNode::Uniform &p_uniform); static uint32_t get_datatype_size(DataType p_type); + static uint32_t get_datatype_component_count(DataType p_type); static void get_keyword_list(List<String> *r_keywords); static bool is_control_flow_keyword(String p_keyword); @@ -1070,13 +1078,21 @@ private: IdentifierType last_type = IDENTIFIER_MAX; - bool _find_identifier(const BlockNode *p_block, bool p_allow_reassign, const FunctionInfo &p_function_info, const StringName &p_identifier, DataType *r_data_type = nullptr, IdentifierType *r_type = nullptr, bool *r_is_const = nullptr, int *r_array_size = nullptr, StringName *r_struct_name = nullptr, ConstantNode::Value *r_constant_value = nullptr); + bool _find_identifier(const BlockNode *p_block, bool p_allow_reassign, const FunctionInfo &p_function_info, const StringName &p_identifier, DataType *r_data_type = nullptr, IdentifierType *r_type = nullptr, bool *r_is_const = nullptr, int *r_array_size = nullptr, StringName *r_struct_name = nullptr, Vector<Scalar> *r_constant_values = nullptr); #ifdef DEBUG_ENABLED void _parse_used_identifier(const StringName &p_identifier, IdentifierType p_type, const StringName &p_function); #endif // DEBUG_ENABLED bool _is_operator_assign(Operator p_op) const; bool _validate_assign(Node *p_node, const FunctionInfo &p_function_info, String *r_message = nullptr); - bool _validate_operator(OperatorNode *p_op, DataType *r_ret_type = nullptr, int *r_ret_size = nullptr); + bool _validate_operator(const BlockNode *p_block, OperatorNode *p_op, DataType *r_ret_type = nullptr, int *r_ret_size = nullptr); + + Vector<Scalar> _get_node_values(const BlockNode *p_block, Node *p_node); + bool _eval_operator(const BlockNode *p_block, OperatorNode *p_op); + Scalar _eval_unary_scalar(const Scalar &p_a, Operator p_op, DataType p_ret_type); + Scalar _eval_scalar(const Scalar &p_a, const Scalar &p_b, Operator p_op, DataType p_ret_type, bool &r_is_valid); + Vector<Scalar> _eval_unary_vector(const Vector<Scalar> &p_va, DataType p_ret_type, Operator p_op); + Vector<Scalar> _eval_vector(const Vector<Scalar> &p_va, const Vector<Scalar> &p_vb, DataType p_left_type, DataType p_right_type, DataType p_ret_type, Operator p_op, bool &r_is_valid); + Vector<Scalar> _eval_vector_transform(const Vector<Scalar> &p_va, const Vector<Scalar> &p_vb, DataType p_left_type, DataType p_right_type, DataType p_ret_type); struct BuiltinEntry { const char *name; 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" diff --git a/thirdparty/README.md b/thirdparty/README.md index f8b41a8c52..dbe20ba2a5 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -907,7 +907,7 @@ instead of `miniz.h` as an external dependency. ## thorvg - Upstream: https://github.com/thorvg/thorvg -- Version: 0.14.7 (e3a6bf5229a9671c385ee78bc33e6e6b611a9729, 2024) +- Version: 0.14.9 (81a0fbfd590873b21e53c3af77969c71d3d9b586, 2024) - License: MIT Files extracted from upstream source: diff --git a/thirdparty/thorvg/inc/config.h b/thirdparty/thorvg/inc/config.h index 02fec07448..654d70f918 100644 --- a/thirdparty/thorvg/inc/config.h +++ b/thirdparty/thorvg/inc/config.h @@ -15,5 +15,5 @@ // For internal debugging: //#define THORVG_LOG_ENABLED -#define THORVG_VERSION_STRING "0.14.8" +#define THORVG_VERSION_STRING "0.14.9" #endif diff --git a/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch b/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch deleted file mode 100644 index 69f4a5cf85..0000000000 --- a/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch +++ /dev/null @@ -1,96 +0,0 @@ -From ac7d208ed8e4651c93ce1b2384070fccac9b6cb6 Mon Sep 17 00:00:00 2001 -From: Mira Grudzinska <mira@lottiefiles.com> -Date: Sun, 1 Sep 2024 22:36:18 +0200 -Subject: [PATCH] sw_engine: handle small cubics - -During the stroke's outline calculation, the function -handling small cubics set all angles to zero. Such cases -should be ignored, as further processing caused errors - -when the cubic was small but not zero, setting the angles -to zero resulted in incorrect outlines. - -@Issue: https://github.com/godotengine/godot/issues/96262 ---- - src/renderer/sw_engine/tvgSwCommon.h | 3 ++- - src/renderer/sw_engine/tvgSwMath.cpp | 19 ++++++++++++------- - src/renderer/sw_engine/tvgSwStroke.cpp | 16 +++++++++++----- - 3 files changed, 25 insertions(+), 13 deletions(-) - -diff --git a/src/renderer/sw_engine/tvgSwCommon.h b/src/renderer/sw_engine/tvgSwCommon.h -index 893e9beca..158fe8ecd 100644 ---- a/src/renderer/sw_engine/tvgSwCommon.h -+++ b/src/renderer/sw_engine/tvgSwCommon.h -@@ -491,7 +491,8 @@ SwFixed mathSin(SwFixed angle); - void mathSplitCubic(SwPoint* base); - SwFixed mathDiff(SwFixed angle1, SwFixed angle2); - SwFixed mathLength(const SwPoint& pt); --bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); -+bool mathSmallCubic(const SwPoint* base); -+bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); - SwFixed mathMean(SwFixed angle1, SwFixed angle2); - SwPoint mathTransform(const Point* to, const Matrix& transform); - bool mathUpdateOutlineBBox(const SwOutline* outline, const SwBBox& clipRegion, SwBBox& renderRegion, bool fastTrack); -diff --git a/src/renderer/sw_engine/tvgSwMath.cpp b/src/renderer/sw_engine/tvgSwMath.cpp -index 1093edd62..b311be05f 100644 ---- a/src/renderer/sw_engine/tvgSwMath.cpp -+++ b/src/renderer/sw_engine/tvgSwMath.cpp -@@ -44,7 +44,17 @@ SwFixed mathMean(SwFixed angle1, SwFixed angle2) - } - - --bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) -+bool mathSmallCubic(const SwPoint* base) -+{ -+ auto d1 = base[2] - base[3]; -+ auto d2 = base[1] - base[2]; -+ auto d3 = base[0] - base[1]; -+ -+ return d1.small() && d2.small() && d3.small(); -+} -+ -+ -+bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) - { - auto d1 = base[2] - base[3]; - auto d2 = base[1] - base[2]; -@@ -52,12 +62,7 @@ bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, Sw - - if (d1.small()) { - if (d2.small()) { -- if (d3.small()) { -- angleIn = angleMid = angleOut = 0; -- return true; -- } else { -- angleIn = angleMid = angleOut = mathAtan(d3); -- } -+ angleIn = angleMid = angleOut = mathAtan(d3); - } else { - if (d3.small()) { - angleIn = angleMid = angleOut = mathAtan(d2); -diff --git a/src/renderer/sw_engine/tvgSwStroke.cpp b/src/renderer/sw_engine/tvgSwStroke.cpp -index 575d12951..4679b72cc 100644 ---- a/src/renderer/sw_engine/tvgSwStroke.cpp -+++ b/src/renderer/sw_engine/tvgSwStroke.cpp -@@ -441,11 +441,17 @@ static void _cubicTo(SwStroke& stroke, const SwPoint& ctrl1, const SwPoint& ctrl - //initialize with current direction - angleIn = angleOut = angleMid = stroke.angleIn; - -- if (arc < limit && !mathSmallCubic(arc, angleIn, angleMid, angleOut)) { -- if (stroke.firstPt) stroke.angleIn = angleIn; -- mathSplitCubic(arc); -- arc += 3; -- continue; -+ if (arc < limit) { -+ if (mathSmallCubic(arc)) { -+ arc -= 3; -+ continue; -+ } -+ if (!mathFlatCubic(arc, angleIn, angleMid, angleOut)) { -+ if (stroke.firstPt) stroke.angleIn = angleIn; -+ mathSplitCubic(arc); -+ arc += 3; -+ continue; -+ } - } - - if (firstArc) { diff --git a/thirdparty/thorvg/patches/pr2716-text-drawing-reliability.patch b/thirdparty/thorvg/patches/pr2716-text-drawing-reliability.patch new file mode 100644 index 0000000000..c79f7c63d6 --- /dev/null +++ b/thirdparty/thorvg/patches/pr2716-text-drawing-reliability.patch @@ -0,0 +1,25 @@ +From 41d67213607e7ff20b7a3ca833f1cfde9780da65 Mon Sep 17 00:00:00 2001 +From: Hermet Park <hermet@lottiefiles.com> +Date: Sat, 7 Sep 2024 01:35:09 +0900 +Subject: [PATCH] renderer: ++reliability in text drawing + +Allow the canvas to pass through +even if text elements are not properly supported. + +issue: https://github.com/thorvg/thorvg/issues/2715 +--- + src/renderer/tvgText.h | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/thirdparty/thorvg/src/renderer/tvgText.h b/thirdparty/thorvg/src/renderer/tvgText.h +index 746b85bea6..55d33ffd4b 100644 +--- a/thirdparty/thorvg/src/renderer/tvgText.h ++++ b/thirdparty/thorvg/src/renderer/tvgText.h +@@ -89,6 +89,7 @@ struct Text::Impl + + bool render(RenderMethod* renderer) + { ++ if (!loader) return true; + renderer->blend(paint->blend(), true); + return PP(shape)->render(renderer); + } diff --git a/thirdparty/thorvg/patches/revert-tvgLines-bezier-precision-change.patch b/thirdparty/thorvg/patches/revert-tvgLines-bezier-precision-change.patch new file mode 100644 index 0000000000..dd6c8ba5e7 --- /dev/null +++ b/thirdparty/thorvg/patches/revert-tvgLines-bezier-precision-change.patch @@ -0,0 +1,13 @@ +diff --git a/thirdparty/thorvg/src/common/tvgLines.cpp b/thirdparty/thorvg/src/common/tvgLines.cpp +index 49d992f127..9d704900a5 100644 +--- a/thirdparty/thorvg/src/common/tvgLines.cpp ++++ b/thirdparty/thorvg/src/common/tvgLines.cpp +@@ -79,7 +79,7 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc + Bezier left; + bezSplitLeft(right, t, left); + length = _bezLength(left, lineLengthFunc); +- if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < 1e-3f) { ++ if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < BEZIER_EPSILON) { + break; + } + if (length < at) { diff --git a/thirdparty/thorvg/src/common/tvgLines.cpp b/thirdparty/thorvg/src/common/tvgLines.cpp index 49d992f127..9d704900a5 100644 --- a/thirdparty/thorvg/src/common/tvgLines.cpp +++ b/thirdparty/thorvg/src/common/tvgLines.cpp @@ -79,7 +79,7 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc Bezier left; bezSplitLeft(right, t, left); length = _bezLength(left, lineLengthFunc); - if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < 1e-3f) { + if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < BEZIER_EPSILON) { break; } if (length < at) { diff --git a/thirdparty/thorvg/src/common/tvgMath.cpp b/thirdparty/thorvg/src/common/tvgMath.cpp index 0254cce9b8..c03b54e5f8 100644 --- a/thirdparty/thorvg/src/common/tvgMath.cpp +++ b/thirdparty/thorvg/src/common/tvgMath.cpp @@ -43,9 +43,8 @@ bool mathInverse(const Matrix* m, Matrix* out) m->e12 * (m->e21 * m->e33 - m->e23 * m->e31) + m->e13 * (m->e21 * m->e32 - m->e22 * m->e31); - if (mathZero(det)) return false; - - auto invDet = 1 / det; + auto invDet = 1.0f / det; + if (std::isinf(invDet)) return false; out->e11 = (m->e22 * m->e33 - m->e32 * m->e23) * invDet; out->e12 = (m->e13 * m->e32 - m->e12 * m->e33) * invDet; @@ -137,7 +136,6 @@ Point operator*(const Point& pt, const Matrix& m) uint8_t mathLerp(const uint8_t &start, const uint8_t &end, float t) { auto result = static_cast<int>(start + (end - start) * t); - if (result > 255) result = 255; - else if (result < 0) result = 0; + mathClamp(result, 0, 255); return static_cast<uint8_t>(result); } diff --git a/thirdparty/thorvg/src/common/tvgMath.h b/thirdparty/thorvg/src/common/tvgMath.h index df39e3b9af..50786754a1 100644 --- a/thirdparty/thorvg/src/common/tvgMath.h +++ b/thirdparty/thorvg/src/common/tvgMath.h @@ -26,7 +26,7 @@ #define _USE_MATH_DEFINES #include <float.h> -#include <math.h> +#include <cmath> #include "tvgCommon.h" #define MATH_PI 3.14159265358979323846f @@ -68,6 +68,13 @@ static inline bool mathEqual(float a, float b) } +template <typename T> +static inline void mathClamp(T& v, const T& min, const T& max) +{ + if (v < min) v = min; + else if (v > max) v = max; +} + /************************************************************************/ /* Matrix functions */ /************************************************************************/ diff --git a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp index 5aab4f1b0d..cccd056a13 100644 --- a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp +++ b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp @@ -3288,6 +3288,7 @@ static void _svgLoaderParserXmlClose(SvgLoaderData* loader, const char* content, for (unsigned int i = 0; i < sizeof(graphicsTags) / sizeof(graphicsTags[0]); i++) { if (!strncmp(tagName, graphicsTags[i].tag, sz)) { loader->currentGraphicsNode = nullptr; + if (!strncmp(tagName, "text", 4)) loader->openedTag = OpenedTagType::Other; loader->stack.pop(); break; } @@ -3361,11 +3362,9 @@ static void _svgLoaderParserXmlOpen(SvgLoaderData* loader, const char* content, node = method(loader, parent, attrs, attrsLength, simpleXmlParseAttributes); if (node && !empty) { if (!strcmp(tagName, "text")) loader->openedTag = OpenedTagType::Text; - else { - auto defs = _createDefsNode(loader, nullptr, nullptr, 0, nullptr); - loader->stack.push(defs); - loader->currentGraphicsNode = node; - } + auto defs = _createDefsNode(loader, nullptr, nullptr, 0, nullptr); + loader->stack.push(defs); + loader->currentGraphicsNode = node; } } else if ((gradientMethod = _findGradientFactory(tagName))) { SvgStyleGradient* gradient; @@ -3403,7 +3402,6 @@ static void _svgLoaderParserText(SvgLoaderData* loader, const char* content, uns auto text = &loader->svgParse->node->node.text; if (text->text) free(text->text); text->text = strDuplicate(content, length); - loader->openedTag = OpenedTagType::Other; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h index 09b75d370b..20442c1c20 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h @@ -495,8 +495,7 @@ SwFixed mathSin(SwFixed angle); void mathSplitCubic(SwPoint* base); SwFixed mathDiff(SwFixed angle1, SwFixed angle2); SwFixed mathLength(const SwPoint& pt); -bool mathSmallCubic(const SwPoint* base); -bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); +bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); SwFixed mathMean(SwFixed angle1, SwFixed angle2); SwPoint mathTransform(const Point* to, const Matrix& transform); bool mathUpdateOutlineBBox(const SwOutline* outline, const SwBBox& clipRegion, SwBBox& renderRegion, bool fastTrack); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp index 60dbbc4fbc..fb809c4f7e 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp @@ -44,17 +44,7 @@ SwFixed mathMean(SwFixed angle1, SwFixed angle2) } -bool mathSmallCubic(const SwPoint* base) -{ - auto d1 = base[2] - base[3]; - auto d2 = base[1] - base[2]; - auto d3 = base[0] - base[1]; - - return d1.small() && d2.small() && d3.small(); -} - - -bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) +bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) { auto d1 = base[2] - base[3]; auto d2 = base[1] - base[2]; @@ -62,7 +52,12 @@ bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwF if (d1.small()) { if (d2.small()) { - angleIn = angleMid = angleOut = mathAtan(d3); + if (d3.small()) { + angleIn = angleMid = angleOut = 0; + return true; + } else { + angleIn = angleMid = angleOut = mathAtan(d3); + } } else { if (d3.small()) { angleIn = angleMid = angleOut = mathAtan(d2); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp index 96c3bc28b9..24c4a9e372 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp @@ -210,7 +210,7 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct } _outlineCubicTo(*dash.outline, &cur.ctrl1, &cur.ctrl2, &cur.end, transform); } - if (dash.curLen < 1 && TO_SWCOORD(len) > 1) { + if (dash.curLen < 0.1f && TO_SWCOORD(len) > 1) { //move to next dash dash.curIdx = (dash.curIdx + 1) % dash.cnt; dash.curLen = dash.pattern[dash.curIdx]; diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp index e0e74ce53c..75ac96be04 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp @@ -441,17 +441,11 @@ static void _cubicTo(SwStroke& stroke, const SwPoint& ctrl1, const SwPoint& ctrl //initialize with current direction angleIn = angleOut = angleMid = stroke.angleIn; - if (arc < limit) { - if (mathSmallCubic(arc)) { - arc -= 3; - continue; - } - if (!mathFlatCubic(arc, angleIn, angleMid, angleOut)) { - if (stroke.firstPt) stroke.angleIn = angleIn; - mathSplitCubic(arc); - arc += 3; - continue; - } + if (arc < limit && !mathSmallCubic(arc, angleIn, angleMid, angleOut)) { + if (stroke.firstPt) stroke.angleIn = angleIn; + mathSplitCubic(arc); + arc += 3; + continue; } if (firstArc) { diff --git a/thirdparty/thorvg/src/renderer/tvgText.h b/thirdparty/thorvg/src/renderer/tvgText.h index 746b85bea6..55d33ffd4b 100644 --- a/thirdparty/thorvg/src/renderer/tvgText.h +++ b/thirdparty/thorvg/src/renderer/tvgText.h @@ -89,6 +89,7 @@ struct Text::Impl bool render(RenderMethod* renderer) { + if (!loader) return true; renderer->blend(paint->blend(), true); return PP(shape)->render(renderer); } diff --git a/thirdparty/thorvg/update-thorvg.sh b/thirdparty/thorvg/update-thorvg.sh index 51dc156661..1ec21f3dd8 100755 --- a/thirdparty/thorvg/update-thorvg.sh +++ b/thirdparty/thorvg/update-thorvg.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -VERSION=0.14.8 +VERSION=0.14.9 # Uncomment and set a git hash to use specific commit instead of tag. #GIT_COMMIT= |