diff options
21 files changed, 384 insertions, 181 deletions
diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index fc3547261b..22bf8571bf 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "core/io/file_access.h" #include "core/io/resource_importer.h" +#include "core/os/condition_variable.h" #include "core/os/os.h" #include "core/string/print_string.h" #include "core/string/translation.h" @@ -233,7 +234,7 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { ThreadLoadTask &load_task = *(ThreadLoadTask *)p_userdata; load_task.loader_id = Thread::get_caller_id(); - if (load_task.semaphore) { + if (load_task.cond_var) { //this is an actual thread, so wait for Ok from semaphore thread_load_semaphore->wait(); //wait until its ok to start loading } @@ -247,7 +248,7 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { } else { load_task.status = THREAD_LOAD_LOADED; } - if (load_task.semaphore) { + if (load_task.cond_var) { if (load_task.start_next && thread_waiting_count > 0) { thread_waiting_count--; //thread loading count remains constant, this ends but another one begins @@ -258,11 +259,9 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { print_lt("END: load count: " + itos(thread_loading_count) + " / wait count: " + itos(thread_waiting_count) + " / suspended count: " + itos(thread_suspended_count) + " / active: " + itos(thread_loading_count - thread_suspended_count)); - for (int i = 0; i < load_task.poll_requests; i++) { - load_task.semaphore->post(); - } - memdelete(load_task.semaphore); - load_task.semaphore = nullptr; + load_task.cond_var->notify_all(); + memdelete(load_task.cond_var); + load_task.cond_var = nullptr; } if (load_task.resource.is_valid()) { @@ -373,7 +372,7 @@ Error ResourceLoader::load_threaded_request(const String &p_path, const String & if (load_task.resource.is_null()) { //needs to be loaded in thread - load_task.semaphore = memnew(Semaphore); + load_task.cond_var = memnew(ConditionVariable); if (thread_loading_count < thread_load_max) { thread_loading_count++; thread_load_semaphore->post(); //we have free threads, so allow one @@ -438,9 +437,8 @@ ResourceLoader::ThreadLoadStatus ResourceLoader::load_threaded_get_status(const Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_error) { String local_path = _validate_local_path(p_path); - thread_load_mutex->lock(); + MutexLock thread_load_lock(*thread_load_mutex); if (!thread_load_tasks.has(local_path)) { - thread_load_mutex->unlock(); if (r_error) { *r_error = ERR_INVALID_PARAMETER; } @@ -449,13 +447,21 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e ThreadLoadTask &load_task = thread_load_tasks[local_path]; - //semaphore still exists, meaning it's still loading, request poll - Semaphore *semaphore = load_task.semaphore; - if (semaphore) { - load_task.poll_requests++; + if (!load_task.cond_var && load_task.status == THREAD_LOAD_IN_PROGRESS) { + // A condition variable was never created for this task. + // That happens when a load has been initiated with subthreads disabled, + // but now another load thread needs to interact with this one (either + // because of subthreads being used this time, or because it's simply a + // threaded load running on a different thread). + // Since we want to be notified when the load ends, we must create the + // condition variable now. + load_task.cond_var = memnew(ConditionVariable); + } + //cond var still exists, meaning it's still loading, request poll + if (load_task.cond_var) { { - // As we got a semaphore, this means we are going to have to wait + // As we got a cond var, this means we are going to have to wait // until the sub-resource is done loading // // As this thread will become 'blocked' we should "exchange" its @@ -477,14 +483,13 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e print_lt("GET: load count: " + itos(thread_loading_count) + " / wait count: " + itos(thread_waiting_count) + " / suspended count: " + itos(thread_suspended_count) + " / active: " + itos(thread_loading_count - thread_suspended_count)); } - thread_load_mutex->unlock(); - semaphore->wait(); - thread_load_mutex->lock(); + do { + load_task.cond_var->wait(thread_load_lock); + } while (load_task.cond_var); // In case of spurious wakeup. thread_suspended_count--; if (!thread_load_tasks.has(local_path)) { //may have been erased during unlock and this was always an invalid call - thread_load_mutex->unlock(); if (r_error) { *r_error = ERR_INVALID_PARAMETER; } @@ -507,8 +512,6 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e thread_load_tasks.erase(local_path); } - thread_load_mutex->unlock(); - return resource; } @@ -1067,7 +1070,7 @@ void ResourceLoader::remove_custom_loaders() { } void ResourceLoader::initialize() { - thread_load_mutex = memnew(Mutex); + thread_load_mutex = memnew(SafeBinaryMutex<BINARY_MUTEX_TAG>); thread_load_max = OS::get_singleton()->get_processor_count(); thread_loading_count = 0; thread_waiting_count = 0; @@ -1090,7 +1093,9 @@ bool ResourceLoader::create_missing_resources_if_class_unavailable = false; bool ResourceLoader::abort_on_missing_resource = true; bool ResourceLoader::timestamp_on_load = false; -Mutex *ResourceLoader::thread_load_mutex = nullptr; +template <> +thread_local uint32_t SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::count = 0; +SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG> *ResourceLoader::thread_load_mutex = nullptr; HashMap<String, ResourceLoader::ThreadLoadTask> ResourceLoader::thread_load_tasks; Semaphore *ResourceLoader::thread_load_semaphore = nullptr; diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index c47b6c950a..72c1f90653 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -37,6 +37,8 @@ #include "core/os/semaphore.h" #include "core/os/thread.h" +class ConditionVariable; + class ResourceFormatLoader : public RefCounted { GDCLASS(ResourceFormatLoader, RefCounted); @@ -105,6 +107,8 @@ public: THREAD_LOAD_LOADED }; + static const int BINARY_MUTEX_TAG = 1; + private: static Ref<ResourceFormatLoader> loader[MAX_LOADERS]; static int loader_count; @@ -136,7 +140,7 @@ private: struct ThreadLoadTask { Thread *thread = nullptr; Thread::ID loader_id = 0; - Semaphore *semaphore = nullptr; + ConditionVariable *cond_var = nullptr; String local_path; String remapped_path; String type_hint; @@ -149,12 +153,11 @@ private: bool use_sub_threads = false; bool start_next = true; int requests = 0; - int poll_requests = 0; HashSet<String> sub_tasks; }; static void _thread_load_function(void *p_userdata); - static Mutex *thread_load_mutex; + static SafeBinaryMutex<BINARY_MUTEX_TAG> *thread_load_mutex; static HashMap<String, ThreadLoadTask> thread_load_tasks; static Semaphore *thread_load_semaphore; static int thread_waiting_count; diff --git a/core/os/condition_variable.h b/core/os/condition_variable.h new file mode 100644 index 0000000000..6037ff327d --- /dev/null +++ b/core/os/condition_variable.h @@ -0,0 +1,60 @@ +/**************************************************************************/ +/* condition_variable.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 CONDITION_VARIABLE_H +#define CONDITION_VARIABLE_H + +#include <condition_variable> + +// An object one or multiple threads can wait on a be notified by some other. +// Normally, you want to use a semaphore for such scenarios, but when the +// condition is something different than a count being greater than zero +// (which is the built-in logic in a semaphore) or you want to provide your +// own mutex to tie the wait-notify to some other behavior, you need to use this. + +class ConditionVariable { + mutable std::condition_variable condition; + +public: + template <class BinaryMutexT> + _ALWAYS_INLINE_ void wait(const MutexLock<BinaryMutexT> &p_lock) const { + condition.wait(const_cast<std::unique_lock<std::mutex> &>(p_lock.lock)); + } + + _ALWAYS_INLINE_ void notify_one() const { + condition.notify_one(); + } + + _ALWAYS_INLINE_ void notify_all() const { + condition.notify_all(); + } +}; + +#endif // CONDITION_VARIABLE_H diff --git a/core/os/mutex.h b/core/os/mutex.h index ceedcb235a..90cc1632e8 100644 --- a/core/os/mutex.h +++ b/core/os/mutex.h @@ -31,12 +31,20 @@ #ifndef MUTEX_H #define MUTEX_H +#include "core/error/error_macros.h" #include "core/typedefs.h" #include <mutex> +template <class MutexT> +class MutexLock; + template <class StdMutexT> class MutexImpl { + friend class MutexLock<MutexImpl<StdMutexT>>; + + using StdMutexType = StdMutexT; + mutable StdMutexT mutex; public: @@ -53,18 +61,65 @@ public: } }; +// A very special kind of mutex, used in scenarios where these +// requirements hold at the same time: +// - Must be used with a condition variable (only binary mutexes are suitable). +// - Must have recursive semnantics (or simulate, as this one does). +// The implementation keeps the lock count in TS. Therefore, only +// one object of each version of the template can exists; hence the Tag argument. +// Tags must be unique across the Godot codebase. +// Also, don't forget to declare the thread_local variable on each use. +template <int Tag> +class SafeBinaryMutex { + friend class MutexLock<SafeBinaryMutex>; + + using StdMutexType = std::mutex; + + mutable std::mutex mutex; + static thread_local uint32_t count; + +public: + _ALWAYS_INLINE_ void lock() const { + if (++count == 1) { + mutex.lock(); + } + } + + _ALWAYS_INLINE_ void unlock() const { + DEV_ASSERT(count); + if (--count == 0) { + mutex.unlock(); + } + } + + _ALWAYS_INLINE_ bool try_lock() const { + if (count) { + count++; + return true; + } else { + if (mutex.try_lock()) { + count++; + return true; + } else { + return false; + } + } + } + + ~SafeBinaryMutex() { + DEV_ASSERT(!count); + } +}; + template <class MutexT> class MutexLock { - const MutexT &mutex; + friend class ConditionVariable; + + std::unique_lock<typename MutexT::StdMutexType> lock; public: _ALWAYS_INLINE_ explicit MutexLock(const MutexT &p_mutex) : - mutex(p_mutex) { - mutex.lock(); - } - - _ALWAYS_INLINE_ ~MutexLock() { - mutex.unlock(); + lock(p_mutex.mutex) { } }; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 5e8a4e230e..6fcf092834 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -3984,6 +3984,15 @@ HashMap<StringName, Variant> EditorNode::get_modified_properties_for_node(Node * return modified_property_map; } +void EditorNode::update_ownership_table_for_addition_node_ancestors(Node *p_current_node, HashMap<Node *, Node *> &p_ownership_table) { + p_ownership_table.insert(p_current_node, p_current_node->get_owner()); + + for (int i = 0; i < p_current_node->get_child_count(); i++) { + Node *child = p_current_node->get_child(i); + update_ownership_table_for_addition_node_ancestors(child, p_ownership_table); + } +} + void EditorNode::update_diff_data_for_node( Node *p_edited_scene, Node *p_root, @@ -4079,6 +4088,16 @@ void EditorNode::update_diff_data_for_node( if (node_3d) { new_additive_node_entry.transform_3d = node_3d->get_relative_transform(node_3d->get_parent()); } + + // Gathers the ownership of all ancestor nodes for later use. + HashMap<Node *, Node *> ownership_table; + for (int i = 0; i < p_node->get_child_count(); i++) { + Node *child = p_node->get_child(i); + update_ownership_table_for_addition_node_ancestors(child, ownership_table); + } + + new_additive_node_entry.ownership_table = ownership_table; + p_addition_list.push_back(new_additive_node_entry); return; @@ -6203,6 +6222,18 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins node_3d->set_transform(additive_node_entry.transform_3d); } } + + // Restore the ownership of its ancestors + for (KeyValue<Node *, Node *> &E : additive_node_entry.ownership_table) { + Node *current_ancestor = E.key; + Node *ancestor_owner = E.value; + + if (ancestor_owner == original_node) { + ancestor_owner = instantiated_node; + } + + current_ancestor->set_owner(ancestor_owner); + } } // Restore the selection. diff --git a/editor/editor_node.h b/editor/editor_node.h index eefe45ca1f..8ad5969249 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -832,6 +832,8 @@ public: // Used if the original parent node is lost Transform2D transform_2d; Transform3D transform_3d; + // Used to keep track of the ownership of all ancestor nodes so they can be restored later. + HashMap<Node *, Node *> ownership_table; }; struct ConnectionWithNodePath { @@ -846,6 +848,8 @@ public: List<Node::GroupInfo> groups; }; + void update_ownership_table_for_addition_node_ancestors(Node *p_current_node, HashMap<Node *, Node *> &p_ownership_table); + void update_diff_data_for_node( Node *p_edited_scene, Node *p_root, diff --git a/editor/import/resource_importer_scene.cpp b/editor/import/resource_importer_scene.cpp index f24759ace6..0ee86c8ed0 100644 --- a/editor/import/resource_importer_scene.cpp +++ b/editor/import/resource_importer_scene.cpp @@ -1612,7 +1612,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "decomposition/normalize_mesh", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), decomposition_default.normalize_mesh)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "decomposition/mode", PROPERTY_HINT_ENUM, "Voxel,Tetrahedron", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), static_cast<int>(decomposition_default.mode))); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "decomposition/convexhull_approximation", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), decomposition_default.convexhull_approximation)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "decomposition/max_convex_hulls", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), decomposition_default.max_convex_hulls)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "decomposition/max_convex_hulls", PROPERTY_HINT_RANGE, "1,100,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), decomposition_default.max_convex_hulls)); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "decomposition/project_hull_vertices", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), decomposition_default.project_hull_vertices)); // Primitives: Box, Sphere, Cylinder, Capsule. diff --git a/editor/import/resource_importer_scene.h b/editor/import/resource_importer_scene.h index 7a1fd72f68..520ccd1070 100644 --- a/editor/import/resource_importer_scene.h +++ b/editor/import/resource_importer_scene.h @@ -375,7 +375,7 @@ Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<Impor } if (p_options.has(SNAME("decomposition/max_convex_hulls"))) { - decomposition_settings.max_convex_hulls = p_options[SNAME("decomposition/max_convex_hulls")]; + decomposition_settings.max_convex_hulls = MAX(1, (int)p_options[SNAME("decomposition/max_convex_hulls")]); } if (p_options.has(SNAME("decomposition/project_hull_vertices"))) { @@ -412,6 +412,8 @@ Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<Impor box.instantiate(); if (p_options.has(SNAME("primitive/size"))) { box->set_size(p_options[SNAME("primitive/size")].operator Vector3() * p_applied_root_scale); + } else { + box->set_size(Vector3(2, 2, 2) * p_applied_root_scale); } Vector<Ref<Shape3D>> shapes; @@ -423,6 +425,8 @@ Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<Impor sphere.instantiate(); if (p_options.has(SNAME("primitive/radius"))) { sphere->set_radius(p_options[SNAME("primitive/radius")].operator float() * p_applied_root_scale); + } else { + sphere->set_radius(1.0f * p_applied_root_scale); } Vector<Ref<Shape3D>> shapes; @@ -433,9 +437,13 @@ Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<Impor cylinder.instantiate(); if (p_options.has(SNAME("primitive/height"))) { cylinder->set_height(p_options[SNAME("primitive/height")].operator float() * p_applied_root_scale); + } else { + cylinder->set_height(1.0f * p_applied_root_scale); } if (p_options.has(SNAME("primitive/radius"))) { cylinder->set_radius(p_options[SNAME("primitive/radius")].operator float() * p_applied_root_scale); + } else { + cylinder->set_radius(1.0f * p_applied_root_scale); } Vector<Ref<Shape3D>> shapes; @@ -446,9 +454,13 @@ Vector<Ref<Shape3D>> ResourceImporterScene::get_collision_shapes(const Ref<Impor capsule.instantiate(); if (p_options.has(SNAME("primitive/height"))) { capsule->set_height(p_options[SNAME("primitive/height")].operator float() * p_applied_root_scale); + } else { + capsule->set_height(1.0f * p_applied_root_scale); } if (p_options.has(SNAME("primitive/radius"))) { capsule->set_radius(p_options[SNAME("primitive/radius")].operator float() * p_applied_root_scale); + } else { + capsule->set_radius(1.0f * p_applied_root_scale); } Vector<Ref<Shape3D>> shapes; diff --git a/main/main.cpp b/main/main.cpp index 5bb37df3ca..b15588e700 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1131,6 +1131,9 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph cmdline_tool = true; main_args.push_back(I->get()); #ifndef DISABLE_DEPRECATED + } else if (I->get() == "--export") { // For users used to 3.x syntax. + OS::get_singleton()->print("The Godot 3 --export option was changed to more explicit --export-release / --export-debug / --export-pack options.\nSee the --help output for details.\n"); + goto error; } else if (I->get() == "--convert-3to4") { // Actually handling is done in start(). cmdline_tool = true; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 5ce01a08bf..1161403c0c 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1861,24 +1861,20 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { Vector<Variant> args; args.resize(call->arguments.size()); for (int i = 0; i < call->arguments.size(); i++) { - reduce_expression(call->arguments[i]); + GDScriptParser::ExpressionNode *argument = call->arguments[i]; + reduce_expression(argument); - if (!call->arguments[i]->is_constant) { + if (!argument->is_constant) { all_is_constant = false; - } else if (all_is_constant) { - args.write[i] = call->arguments[i]->reduced_value; + break; } - - GDScriptParser::DataType arg_type = call->arguments[i]->get_datatype(); - if (!arg_type.is_variant()) { - if (arg_type.kind != GDScriptParser::DataType::BUILTIN) { - all_is_constant = false; - push_error(vformat(R"*(Invalid argument for "range()" call. Argument %d should be int or float but "%s" was given.)*", i + 1, arg_type.to_string()), call->arguments[i]); - } else if (arg_type.builtin_type != Variant::INT && arg_type.builtin_type != Variant::FLOAT) { - all_is_constant = false; - push_error(vformat(R"*(Invalid argument for "range()" call. Argument %d should be int or float but "%s" was given.)*", i + 1, arg_type.to_string()), call->arguments[i]); - } + if (argument->reduced_value.get_type() != Variant::INT && argument->reduced_value.get_type() != Variant::FLOAT) { + push_error(vformat(R"*(Invalid argument for "range()" call. Argument %d should be int or float but "%s" was given.)*", i + 1, Variant::get_type_name(argument->reduced_value.get_type())), argument); + all_is_constant = false; + break; } + + args.write[i] = argument->reduced_value; } Variant reduced; diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index b32313dad4..c402b63f7b 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3138,6 +3138,14 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p bool previous_in_lambda = in_lambda; in_lambda = true; + // Save break/continue state. + bool could_break = can_break; + bool could_continue = can_continue; + + // Disallow break/continue. + can_break = false; + can_continue = false; + function->body = parse_suite("lambda declaration", body, true); complete_extents(function); complete_extents(lambda); @@ -3155,6 +3163,11 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_lambda(ExpressionNode *p_p current_function = previous_function; in_lambda = previous_in_lambda; lambda->function = function; + + // Reset break/continue state. + can_break = could_break; + can_continue = could_continue; + return lambda; } diff --git a/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd new file mode 100644 index 0000000000..d2d9d04508 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd @@ -0,0 +1,9 @@ +enum E { E0 = 0, E3 = 3 } + +func test(): + var total := 0 + for value in range(E.E0, E.E3): + var inferable := value + total += inferable + assert(total == 0 + 1 + 2) + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.out b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.gd b/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.gd new file mode 100644 index 0000000000..319c1801c0 --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.gd @@ -0,0 +1,5 @@ +func test(): + for index in range(0, 1): + var lambda := func(): + continue + print('not ok') diff --git a/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.out b/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.out new file mode 100644 index 0000000000..262dfbc09b --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/errors/bad_continue_in_lambda.out @@ -0,0 +1,2 @@ +GDTEST_PARSER_ERROR +Cannot use "continue" outside of a loop. diff --git a/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd new file mode 100644 index 0000000000..2fa45c1d7d --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd @@ -0,0 +1,13 @@ +func test(): + var i_string := '' + for i in 3: + if i == 1: continue + var lambda := func(): + var j_string := '' + for j in 3: + if j == 1: continue + j_string += str(j) + return j_string + i_string += lambda.call() + assert(i_string == '0202') + print('ok') diff --git a/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.out b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/multiplayer/scene_replication_config.cpp b/modules/multiplayer/scene_replication_config.cpp index c8621e7357..f8006228de 100644 --- a/modules/multiplayer/scene_replication_config.cpp +++ b/modules/multiplayer/scene_replication_config.cpp @@ -114,6 +114,8 @@ void SceneReplicationConfig::add_property(const NodePath &p_path, int p_index) { if (p_index < 0 || p_index == properties.size()) { properties.push_back(ReplicationProperty(p_path)); + sync_props.push_back(p_path); + spawn_props.push_back(p_path); return; } @@ -126,6 +128,16 @@ void SceneReplicationConfig::add_property(const NodePath &p_path, int p_index) { c++; } properties.insert_before(I, ReplicationProperty(p_path)); + sync_props.clear(); + spawn_props.clear(); + for (const ReplicationProperty &prop : properties) { + if (prop.sync) { + sync_props.push_back(p_path); + } + if (prop.spawn) { + spawn_props.push_back(p_path); + } + } } void SceneReplicationConfig::remove_property(const NodePath &p_path) { diff --git a/modules/websocket/websocket_multiplayer_peer.cpp b/modules/websocket/websocket_multiplayer_peer.cpp index c12fc5e834..9e706dbeef 100644 --- a/modules/websocket/websocket_multiplayer_peer.cpp +++ b/modules/websocket/websocket_multiplayer_peer.cpp @@ -340,6 +340,7 @@ void WebSocketMultiplayerPeer::_poll_server() { to_remove.insert(id); continue; } + peer.connection = tls; } Ref<StreamPeerTLS> tls = static_cast<Ref<StreamPeerTLS>>(peer.connection); tls->poll(); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index b861d7af01..59f7511894 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -86,7 +86,7 @@ int Label::get_line_height(int p_line) const { } } -bool Label::_shape() { +void Label::_shape() { Ref<StyleBox> style = theme_cache.normal_style; int width = (get_size().width - style->get_minimum_size().width); @@ -101,7 +101,7 @@ bool Label::_shape() { } const Ref<Font> &font = (settings.is_valid() && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font; int font_size = settings.is_valid() ? settings->get_font_size() : theme_cache.font_size; - ERR_FAIL_COND_V(font.is_null(), true); + ERR_FAIL_COND(font.is_null()); String txt = (uppercase) ? TS->string_to_upper(xl_text, language) : xl_text; if (visible_chars >= 0 && visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { txt = txt.substr(0, visible_chars); @@ -121,7 +121,6 @@ bool Label::_shape() { dirty = false; font_dirty = false; lines_dirty = true; - // Note for future maintainers: forgetting stable width here (e.g., setting it to -1) may fix still undiscovered bugs. } if (lines_dirty) { @@ -129,143 +128,126 @@ bool Label::_shape() { TS->free_rid(lines_rid[i]); } lines_rid.clear(); - } - - Size2i prev_minsize = minsize; - minsize = Size2(); - bool can_process_lines = false; - if (xl_text.length() == 0) { - can_process_lines = true; - lines_dirty = false; - } else { - // With autowrap on or off with trimming enabled, we won't compute the minimum size until width is stable - // (two shape requests in a row with the same width.) This avoids situations in which the initial width is - // very narrow and the label would break text into many very short lines, causing a very tall label that can - // leave a deformed container. In the remaining case (namely, autowrap off and no trimming), the label is - // free to dictate its own width, something that will be taken advtantage of. - bool can_dictate_width = autowrap_mode == TextServer::AUTOWRAP_OFF && overrun_behavior == TextServer::OVERRUN_NO_TRIMMING; - bool is_width_stable = get_size().width == stable_width; - can_process_lines = can_dictate_width || is_width_stable; - stable_width = get_size().width; - - if (can_process_lines) { - if (lines_dirty) { - BitField<TextServer::LineBreakFlag> autowrap_flags = TextServer::BREAK_MANDATORY; - switch (autowrap_mode) { - case TextServer::AUTOWRAP_WORD_SMART: - autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_ADAPTIVE | TextServer::BREAK_MANDATORY; - break; - case TextServer::AUTOWRAP_WORD: - autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY; - break; - case TextServer::AUTOWRAP_ARBITRARY: - autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY; - break; - case TextServer::AUTOWRAP_OFF: - break; - } - autowrap_flags = autowrap_flags | TextServer::BREAK_TRIM_EDGE_SPACES; + BitField<TextServer::LineBreakFlag> autowrap_flags = TextServer::BREAK_MANDATORY; + switch (autowrap_mode) { + case TextServer::AUTOWRAP_WORD_SMART: + autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_ADAPTIVE | TextServer::BREAK_MANDATORY; + break; + case TextServer::AUTOWRAP_WORD: + autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY; + break; + case TextServer::AUTOWRAP_ARBITRARY: + autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY; + break; + case TextServer::AUTOWRAP_OFF: + break; + } + autowrap_flags = autowrap_flags | TextServer::BREAK_TRIM_EDGE_SPACES; - PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(text_rid, width, 0, autowrap_flags); - for (int i = 0; i < line_breaks.size(); i = i + 2) { - RID line = TS->shaped_text_substr(text_rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]); - lines_rid.push_back(line); - } - } + PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(text_rid, width, 0, autowrap_flags); + for (int i = 0; i < line_breaks.size(); i = i + 2) { + RID line = TS->shaped_text_substr(text_rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]); + lines_rid.push_back(line); + } + } - if (can_dictate_width) { - for (int i = 0; i < lines_rid.size(); i++) { - if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) { - minsize.width = TS->shaped_text_get_size(lines_rid[i]).x; - } - } + if (xl_text.length() == 0) { + minsize = Size2(1, get_line_height()); + return; + } - width = (minsize.width - style->get_minimum_size().width); + if (autowrap_mode == TextServer::AUTOWRAP_OFF) { + minsize.width = 0.0f; + for (int i = 0; i < lines_rid.size(); i++) { + if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) { + minsize.width = TS->shaped_text_get_size(lines_rid[i]).x; } + } + } - if (lines_dirty) { - BitField<TextServer::TextOverrunFlag> overrun_flags = TextServer::OVERRUN_NO_TRIM; - switch (overrun_behavior) { - case TextServer::OVERRUN_TRIM_WORD_ELLIPSIS: - overrun_flags.set_flag(TextServer::OVERRUN_TRIM); - overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY); - overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS); - break; - case TextServer::OVERRUN_TRIM_ELLIPSIS: - overrun_flags.set_flag(TextServer::OVERRUN_TRIM); - overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS); - break; - case TextServer::OVERRUN_TRIM_WORD: - overrun_flags.set_flag(TextServer::OVERRUN_TRIM); - overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY); - break; - case TextServer::OVERRUN_TRIM_CHAR: - overrun_flags.set_flag(TextServer::OVERRUN_TRIM); - break; - case TextServer::OVERRUN_NO_TRIMMING: - break; - } + if (lines_dirty) { + BitField<TextServer::TextOverrunFlag> overrun_flags = TextServer::OVERRUN_NO_TRIM; + switch (overrun_behavior) { + case TextServer::OVERRUN_TRIM_WORD_ELLIPSIS: + overrun_flags.set_flag(TextServer::OVERRUN_TRIM); + overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY); + overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS); + break; + case TextServer::OVERRUN_TRIM_ELLIPSIS: + overrun_flags.set_flag(TextServer::OVERRUN_TRIM); + overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS); + break; + case TextServer::OVERRUN_TRIM_WORD: + overrun_flags.set_flag(TextServer::OVERRUN_TRIM); + overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY); + break; + case TextServer::OVERRUN_TRIM_CHAR: + overrun_flags.set_flag(TextServer::OVERRUN_TRIM); + break; + case TextServer::OVERRUN_NO_TRIMMING: + break; + } - // Fill after min_size calculation. + // Fill after min_size calculation. - int visible_lines = lines_rid.size(); - if (max_lines_visible >= 0 && visible_lines > max_lines_visible) { - visible_lines = max_lines_visible; - } - if (autowrap_mode != TextServer::AUTOWRAP_OFF) { - bool lines_hidden = visible_lines > 0 && visible_lines < lines_rid.size(); - if (lines_hidden) { - overrun_flags.set_flag(TextServer::OVERRUN_ENFORCE_ELLIPSIS); - } - if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) { - for (int i = 0; i < lines_rid.size(); i++) { - if (i < visible_lines - 1 || lines_rid.size() == 1) { - TS->shaped_text_fit_to_width(lines_rid[i], width); - } else if (i == (visible_lines - 1)) { - TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags); - } - } - } else if (lines_hidden) { + if (autowrap_mode != TextServer::AUTOWRAP_OFF) { + int visible_lines = get_visible_line_count(); + bool lines_hidden = visible_lines > 0 && visible_lines < lines_rid.size(); + if (lines_hidden) { + overrun_flags.set_flag(TextServer::OVERRUN_ENFORCE_ELLIPSIS); + } + if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) { + for (int i = 0; i < lines_rid.size(); i++) { + if (i < visible_lines - 1 || lines_rid.size() == 1) { + TS->shaped_text_fit_to_width(lines_rid[i], width); + } else if (i == (visible_lines - 1)) { TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags); } - } else { - // Autowrap disabled. - for (int i = 0; i < lines_rid.size(); i++) { - if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) { - TS->shaped_text_fit_to_width(lines_rid[i], width); - overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE); - TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); - TS->shaped_text_fit_to_width(lines_rid[i], width, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS); - } else { - TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); - } - } - } - - int last_line = MIN(lines_rid.size(), visible_lines + lines_skipped); - int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing; - for (int64_t i = lines_skipped; i < last_line; i++) { - minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; } - - lines_dirty = false; + } else if (lines_hidden) { + TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags); } } else { - callable_mp(this, &Label::_shape).call_deferred(); + // Autowrap disabled. + for (int i = 0; i < lines_rid.size(); i++) { + if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) { + TS->shaped_text_fit_to_width(lines_rid[i], width); + overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE); + TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); + TS->shaped_text_fit_to_width(lines_rid[i], width, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS); + } else { + TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); + } + } } + lines_dirty = false; } - if (draw_pending) { - queue_redraw(); - draw_pending = false; - } + _update_visible(); - if (minsize != prev_minsize) { + if (autowrap_mode == TextServer::AUTOWRAP_OFF || !clip || overrun_behavior == TextServer::OVERRUN_NO_TRIMMING) { update_minimum_size(); } +} + +void Label::_update_visible() { + int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing; + Ref<StyleBox> style = theme_cache.normal_style; + int lines_visible = lines_rid.size(); - return can_process_lines; + if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { + lines_visible = max_lines_visible; + } + + minsize.height = 0; + int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + for (int64_t i = lines_skipped; i < last_line; i++) { + minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; + if (minsize.height > (get_size().height - style->get_minimum_size().height + line_spacing)) { + break; + } + } } inline void draw_glyph(const Glyph &p_gl, const RID &p_canvas, const Color &p_font_color, const Vector2 &p_ofs) { @@ -375,11 +357,7 @@ void Label::_notification(int p_what) { } if (dirty || font_dirty || lines_dirty) { - if (!_shape()) { - // There will be another pass. - draw_pending = true; - break; - } + _shape(); } RID ci = get_canvas_item(); @@ -622,8 +600,6 @@ void Label::_notification(int p_what) { } ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing; } - - draw_pending = false; } break; case NOTIFICATION_THEME_CHANGED: { @@ -915,7 +891,7 @@ void Label::set_lines_skipped(int p_lines) { } lines_skipped = p_lines; - lines_dirty = true; + _update_visible(); queue_redraw(); } @@ -929,7 +905,7 @@ void Label::set_max_lines_visible(int p_lines) { } max_lines_visible = p_lines; - lines_dirty = true; + _update_visible(); queue_redraw(); } diff --git a/scene/gui/label.h b/scene/gui/label.h index 36b85f7af8..2350525236 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -46,7 +46,6 @@ private: bool clip = false; TextServer::OverrunBehavior overrun_behavior = TextServer::OVERRUN_NO_TRIMMING; Size2 minsize; - real_t stable_width = -1; bool uppercase = false; bool lines_dirty = true; @@ -65,7 +64,6 @@ private: float visible_ratio = 1.0; int lines_skipped = 0; int max_lines_visible = -1; - bool draw_pending = false; Ref<LabelSettings> settings; @@ -83,7 +81,8 @@ private: int font_shadow_outline_size; } theme_cache; - bool _shape(); + void _update_visible(); + void _shape(); void _invalidate(); protected: |