diff options
139 files changed, 1710 insertions, 1043 deletions
diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 768540a0fa..eac1a66be7 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1515,6 +1515,7 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/window/stretch/scale_mode", PROPERTY_HINT_ENUM, "fractional,integer"), "fractional"); GLOBAL_DEF(PropertyInfo(Variant::INT, "debug/settings/profiler/max_functions", PROPERTY_HINT_RANGE, "128,65535,1"), 16384); + GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "debug/settings/profiler/max_timestamp_query_elements", PROPERTY_HINT_RANGE, "256,65535,1"), 256); GLOBAL_DEF(PropertyInfo(Variant::BOOL, "compression/formats/zstd/long_distance_matching"), Compression::zstd_long_distance_matching); GLOBAL_DEF(PropertyInfo(Variant::INT, "compression/formats/zstd/compression_level", PROPERTY_HINT_RANGE, "1,22,1"), Compression::zstd_level); diff --git a/core/input/input.cpp b/core/input/input.cpp index 56f616fac4..ec0303df06 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -758,12 +758,13 @@ void Input::_parse_input_event_impl(const Ref<InputEvent> &p_event, bool p_is_em bool was_pressed = action_state.cache.pressed; _update_action_cache(E.key, action_state); + // As input may come in part way through a physics tick, the earliest we can react to it is the next physics tick. if (action_state.cache.pressed && !was_pressed) { - action_state.pressed_physics_frame = Engine::get_singleton()->get_physics_frames(); + action_state.pressed_physics_frame = Engine::get_singleton()->get_physics_frames() + 1; action_state.pressed_process_frame = Engine::get_singleton()->get_process_frames(); } if (!action_state.cache.pressed && was_pressed) { - action_state.released_physics_frame = Engine::get_singleton()->get_physics_frames(); + action_state.released_physics_frame = Engine::get_singleton()->get_physics_frames() + 1; action_state.released_process_frame = Engine::get_singleton()->get_process_frames(); } } diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index 178d02b987..ddeee9d765 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -636,6 +636,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::A | KeyModifierMask::CTRL)); inputs.push_back(InputEventKey::create_reference(Key::LEFT | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::HOME)); default_builtin_cache.insert("ui_text_caret_line_start.macos", inputs); inputs = List<Ref<InputEvent>>(); @@ -645,6 +646,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::E | KeyModifierMask::CTRL)); inputs.push_back(InputEventKey::create_reference(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::END)); default_builtin_cache.insert("ui_text_caret_line_end.macos", inputs); // Text Caret Movement Page Up/Down @@ -665,6 +667,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::UP | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::HOME | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_text_caret_document_start.macos", inputs); inputs = List<Ref<InputEvent>>(); @@ -673,6 +676,7 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() { inputs = List<Ref<InputEvent>>(); inputs.push_back(InputEventKey::create_reference(Key::DOWN | KeyModifierMask::CMD_OR_CTRL)); + inputs.push_back(InputEventKey::create_reference(Key::END | KeyModifierMask::CMD_OR_CTRL)); default_builtin_cache.insert("ui_text_caret_document_end.macos", inputs); // Text Caret Addition Below/Above diff --git a/core/io/logger.cpp b/core/io/logger.cpp index 1476b8ccac..a24277fe72 100644 --- a/core/io/logger.cpp +++ b/core/io/logger.cpp @@ -212,7 +212,7 @@ void RotatedFileLogger::logv(const char *p_format, va_list p_list, bool p_err) { // Strip ANSI escape codes (such as those inserted by `print_rich()`) // before writing to file, as text editors cannot display those // correctly. - file->store_string(strip_ansi_regex->sub(String(buf), "", true)); + file->store_string(strip_ansi_regex->sub(String::utf8(buf), "", true)); #else file->store_buffer((uint8_t *)buf, len); #endif // MODULE_REGEX_ENABLED diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index 58ad61b621..20dd192da1 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -304,9 +304,10 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { thread_load_mutex.unlock(); // Thread-safe either if it's the current thread or a brand new one. - bool mq_override_present = false; + thread_local bool mq_override_present = false; CallQueue *own_mq_override = nullptr; if (load_nesting == 0) { + mq_override_present = false; load_paths_stack = memnew(Vector<String>); if (!load_task.dependent_path.is_empty()) { @@ -326,10 +327,6 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { } // -- - if (!Thread::is_main_thread()) { - set_current_thread_safe_for_nodes(true); - } - Ref<Resource> res = _load(load_task.remapped_path, load_task.remapped_path != load_task.local_path ? load_task.local_path : String(), load_task.type_hint, load_task.cache_mode, &load_task.error, load_task.use_sub_threads, &load_task.progress); if (mq_override_present) { MessageQueue::get_singleton()->flush(); @@ -691,6 +688,7 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro Error wtp_task_err = FAILED; if (loader_is_wtp) { // Loading thread is in the worker pool. + load_task.awaited = true; thread_load_mutex.unlock(); wtp_task_err = WorkerThreadPool::get_singleton()->wait_for_task_completion(load_task.task_id); } @@ -715,7 +713,6 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro } else { DEV_ASSERT(wtp_task_err == OK); thread_load_mutex.lock(); - load_task.awaited = true; } } else { // Loading thread is main or user thread. diff --git a/core/math/a_star_grid_2d.cpp b/core/math/a_star_grid_2d.cpp index f272407869..984bb1c9c1 100644 --- a/core/math/a_star_grid_2d.cpp +++ b/core/math/a_star_grid_2d.cpp @@ -122,6 +122,10 @@ AStarGrid2D::CellShape AStarGrid2D::get_cell_shape() const { } void AStarGrid2D::update() { + if (!dirty) { + return; + } + points.clear(); const int32_t end_x = region.get_end().x; diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 3b7a6e66fe..80fd52c684 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -560,7 +560,7 @@ <param index="0" name="track_idx" type="int" /> <param index="1" name="path" type="NodePath" /> <description> - Sets the path of a track. Paths must be valid scene-tree paths to a node and must be specified starting from the parent node of the node that will reproduce the animation. Tracks that control properties or bones must append their name after the path, separated by [code]":"[/code]. + Sets the path of a track. Paths must be valid scene-tree paths to a node and must be specified starting from the [member AnimationMixer.root_node] that will reproduce the animation. Tracks that control properties or bones must append their name after the path, separated by [code]":"[/code]. For example, [code]"character/skeleton:ankle"[/code] or [code]"character/mesh:transform/local"[/code]. </description> </method> diff --git a/doc/classes/AnimationMixer.xml b/doc/classes/AnimationMixer.xml index 8a493ce91f..58ef118e46 100644 --- a/doc/classes/AnimationMixer.xml +++ b/doc/classes/AnimationMixer.xml @@ -304,8 +304,8 @@ This makes it more convenient to preview and edit animations in the editor, as changes to the scene will not be saved as long as they are set in the reset animation. </member> <member name="root_motion_track" type="NodePath" setter="set_root_motion_track" getter="get_root_motion_track" default="NodePath("")"> - The path to the Animation track used for root motion. Paths must be valid scene-tree paths to a node, and must be specified starting from the parent node of the node that will reproduce the animation. To specify a track that controls properties or bones, append its name after the path, separated by [code]":"[/code]. For example, [code]"character/skeleton:ankle"[/code] or [code]"character/mesh:transform/local"[/code]. - If the track has type [constant Animation.TYPE_POSITION_3D], [constant Animation.TYPE_ROTATION_3D] or [constant Animation.TYPE_SCALE_3D] the transformation will be canceled visually, and the animation will appear to stay in place. See also [method get_root_motion_position], [method get_root_motion_rotation], [method get_root_motion_scale] and [RootMotionView]. + The path to the Animation track used for root motion. Paths must be valid scene-tree paths to a node, and must be specified starting from the parent node of the node that will reproduce the animation. The [member root_motion_track] uses the same format as [method Animation.track_set_path], but note that a bone must be specified. + If the track has type [constant Animation.TYPE_POSITION_3D], [constant Animation.TYPE_ROTATION_3D], or [constant Animation.TYPE_SCALE_3D] the transformation will be canceled visually, and the animation will appear to stay in place. See also [method get_root_motion_position], [method get_root_motion_rotation], [method get_root_motion_scale], and [RootMotionView]. </member> <member name="root_node" type="NodePath" setter="set_root_node" getter="get_root_node" default="NodePath("..")"> The node which node path references will travel from. diff --git a/doc/classes/Array.xml b/doc/classes/Array.xml index 326b71c588..3731b8dcf1 100644 --- a/doc/classes/Array.xml +++ b/doc/classes/Array.xml @@ -4,41 +4,31 @@ A built-in data structure that holds a sequence of elements. </brief_description> <description> - An array data structure that can contain a sequence of elements of any type. Elements are accessed by a numerical index starting at 0. Negative indices are used to count from the back (-1 is the last element, -2 is the second to last, etc.). + An array data structure that can contain a sequence of elements of any [Variant] type. Elements are accessed by a numerical index starting at 0. Negative indices are used to count from the back (-1 is the last element, -2 is the second to last, etc.). [b]Example:[/b] [codeblocks] [gdscript] - var array = ["One", 2, 3, "Four"] - print(array[0]) # One. - print(array[2]) # 3. - print(array[-1]) # Four. - array[2] = "Three" - print(array[-2]) # Three. - [/gdscript] - [csharp] - var array = new Godot.Collections.Array{"One", 2, 3, "Four"}; - GD.Print(array[0]); // One. - GD.Print(array[2]); // 3. - GD.Print(array[array.Count - 1]); // Four. - array[2] = "Three"; - GD.Print(array[array.Count - 2]); // Three. - [/csharp] - [/codeblocks] - Arrays can be concatenated using the [code]+[/code] operator: - [codeblocks] - [gdscript] - var array1 = ["One", 2] - var array2 = [3, "Four"] - print(array1 + array2) # ["One", 2, 3, "Four"] + var array = ["First", 2, 3, "Last"] + print(array[0]) # Prints "First" + print(array[2]) # Prints 3 + print(array[-1]) # Prints "Last" + + array[1] = "Second" + print(array[1]) # Prints "Second" + print(array[-3]) # Prints "Second" [/gdscript] [csharp] - // Array concatenation is not possible with C# arrays, but is with Godot.Collections.Array. - var array1 = new Godot.Collections.Array{"One", 2}; - var array2 = new Godot.Collections.Array{3, "Four"}; - GD.Print(array1 + array2); // Prints [One, 2, 3, Four] + var array = new Godot.Collections.Array{"First", 2, 3, "Last"}; + GD.Print(array[0]); // Prints "First" + GD.Print(array[2]); // Prints 3 + GD.Print(array[array.Count - 1]); // Prints "Last" + + array[2] = "Second"; + GD.Print(array[1]); // Prints "Second" + GD.Print(array[array.Count - 3]); // Prints "Second" [/csharp] [/codeblocks] - [b]Note:[/b] Arrays are always passed by reference. To get a copy of an array that can be modified independently of the original array, use [method duplicate]. + [b]Note:[/b] Arrays are always passed by [b]reference[/b]. To get a copy of an array that can be modified independently of the original array, use [method duplicate]. [b]Note:[/b] Erasing elements while iterating over arrays is [b]not[/b] supported and will result in unpredictable behavior. [b]Differences between packed arrays, typed arrays, and untyped arrays:[/b] Packed arrays are generally faster to iterate on and modify compared to a typed array of the same type (e.g. [PackedInt64Array] versus [code]Array[int][/code]). Also, packed arrays consume less memory. As a downside, packed arrays are less flexible as they don't offer as many convenience methods such as [method Array.map]. Typed arrays are in turn faster to iterate on and modify than untyped arrays. </description> @@ -58,29 +48,32 @@ <param index="2" name="class_name" type="StringName" /> <param index="3" name="script" type="Variant" /> <description> - Creates a typed array from the [param base] array. All arguments are required. - - [param type] is the built-in type as a [enum Variant.Type] constant, for example [constant TYPE_INT]. - - [param class_name] is the [b]native[/b] class name, for example [Node]. If [param type] is not [constant TYPE_OBJECT], must be an empty string. - - [param script] is the associated script. Must be a [Script] instance or [code]null[/code]. - Examples: + Creates a typed array from the [param base] array. A typed array can only contain elements of the given type, or that inherit from the given class, as described by this constructor's parameters: + - [param type] is the built-in [Variant] type, as one the [enum Variant.Type] constants. + - [param class_name] is the built-in class name (see [method Object.get_class]). + - [param script] is the associated script. It must be a [Script] instance or [code]null[/code]. + If [param type] is not [constant TYPE_OBJECT], [param class_name] must be an empty [StringName] and [param script] must be [code]null[/code]. [codeblock] - class_name MyNode + class_name Sword extends Node - class MyClass: + class Stats: pass func _ready(): - var a = Array([], TYPE_INT, &"", null) # Array[int] - var b = Array([], TYPE_OBJECT, &"Node", null) # Array[Node] - var c = Array([], TYPE_OBJECT, &"Node", MyNode) # Array[MyNode] - var d = Array([], TYPE_OBJECT, &"RefCounted", MyClass) # Array[MyClass] + var a = Array([], TYPE_INT, "", null) # Array[int] + var b = Array([], TYPE_OBJECT, "Node", null) # Array[Node] + var c = Array([], TYPE_OBJECT, "Node", Sword) # Array[Sword] + var d = Array([], TYPE_OBJECT, "RefCounted", Stats) # Array[Stats] [/codeblock] - [b]Note:[/b] This constructor can be useful if you want to create a typed array on the fly, but you are not required to use it. In GDScript you can use a temporary variable with the static type you need and then pass it: + The [param base] array's elements are converted when necessary. If this is not possible or [param base] is already typed, this constructor fails and returns an empty [Array]. + In GDScript, this constructor is usually not necessary, as it is possible to create a typed array through static typing: [codeblock] - func _ready(): - var a: Array[int] = [] - some_func(a) + var numbers: Array[float] = [] + var children: Array[Node] = [$Node, $Sprite2D, $RigidBody3D] + + var integers: Array[int] = [0.2, 4.5, -2.0] + print(integers) # Prints [0, 4, -2] [/codeblock] </description> </constructor> @@ -167,20 +160,44 @@ <return type="bool" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]all[/i] elements in the array. If the [Callable] returns [code]false[/code] for one array element or more, this method returns [code]false[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. - [codeblock] + Calls the given [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]all[/i] elements in the array. If the [Callable] returns [code]false[/code] for one array element or more, this method returns [code]false[/code]. + The [param method] should take one [Variant] parameter (the current array element) and return a [bool]. + [codeblocks] + [gdscript] + func greater_than_5(number): + return number > 5 + func _ready(): - print([6, 10, 6].all(greater_than_5)) # Prints True (3/3 elements evaluate to `true`). - print([4, 10, 4].all(greater_than_5)) # Prints False (1/3 elements evaluate to `true`). - print([4, 4, 4].all(greater_than_5)) # Prints False (0/3 elements evaluate to `true`). - print([].all(greater_than_5)) # Prints True (0/0 elements evaluate to `true`). + print([6, 10, 6].all(greater_than_5)) # Prints true (3/3 elements evaluate to true). + print([4, 10, 4].all(greater_than_5)) # Prints false (1/3 elements evaluate to true). + print([4, 4, 4].all(greater_than_5)) # Prints false (0/3 elements evaluate to true). + print([].all(greater_than_5)) # Prints true (0/0 elements evaluate to true). + + # Same as the first line above, but using a lambda function. + print([6, 10, 6].all(func(element): return element > 5)) # Prints true + [/gdscript] + [csharp] + private static bool GreaterThan5(int number) + { + return number > 5; + } - print([6, 10, 6].all(func(number): return number > 5)) # Prints True. Same as the first line above, but using lambda function. + public override void _Ready() + { + // Prints true (3/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 6, 10, 6 }.All(GreaterThan5)); + // Prints false (1/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 4, 10, 4 }.All(GreaterThan5)); + // Prints false (0/3 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { 4, 4, 4 }.All(GreaterThan5)); + // Prints true (0/0 elements evaluate to true). + GD.Print(new Godot.Collections.Array>int< { }.All(GreaterThan5)); - func greater_than_5(number): - return number > 5 - [/codeblock] + // Same as the first line above, but using a lambda function. + GD.Print(new Godot.Collections.Array>int< { 6, 10, 6 }.All(element => element > 5)); // Prints true + } + [/csharp] + [/codeblocks] See also [method any], [method filter], [method map] and [method reduce]. [b]Note:[/b] Unlike relying on the size of an array returned by [method filter], this method will return as early as possible to improve performance (especially with large arrays). [b]Note:[/b] For an empty array, this method [url=https://en.wikipedia.org/wiki/Vacuous_truth]always[/url] returns [code]true[/code]. @@ -190,19 +207,20 @@ <return type="bool" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]one or more[/i] elements in the array. If the [Callable] returns [code]false[/code] for all elements in the array, this method returns [code]false[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. + Calls the given [Callable] on each element in the array and returns [code]true[/code] if the [Callable] returns [code]true[/code] for [i]one or more[/i] elements in the array. If the [Callable] returns [code]false[/code] for all elements in the array, this method returns [code]false[/code]. + The [param method] should take one [Variant] parameter (the current array element) and return a [bool]. [codeblock] - func _ready(): - print([6, 10, 6].any(greater_than_5)) # Prints True (3 elements evaluate to `true`). - print([4, 10, 4].any(greater_than_5)) # Prints True (1 elements evaluate to `true`). - print([4, 4, 4].any(greater_than_5)) # Prints False (0 elements evaluate to `true`). - print([].any(greater_than_5)) # Prints False (0 elements evaluate to `true`). - - print([6, 10, 6].any(func(number): return number > 5)) # Prints True. Same as the first line above, but using lambda function. - func greater_than_5(number): return number > 5 + + func _ready(): + print([6, 10, 6].any(greater_than_5)) # Prints true (3 elements evaluate to true). + print([4, 10, 4].any(greater_than_5)) # Prints true (1 elements evaluate to true). + print([4, 4, 4].any(greater_than_5)) # Prints false (0 elements evaluate to true). + print([].any(greater_than_5)) # Prints false (0 elements evaluate to true). + + # Same as the first line above, but using a lambda function. + print([6, 10, 6].any(func(number): return number > 5)) # Prints true [/codeblock] See also [method all], [method filter], [method map] and [method reduce]. [b]Note:[/b] Unlike relying on the size of an array returned by [method filter], this method will return as early as possible to improve performance (especially with large arrays). @@ -213,19 +231,19 @@ <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Appends an element at the end of the array (alias of [method push_back]). + Appends [param value] at the end of the array (alias of [method push_back]). </description> </method> <method name="append_array"> <return type="void" /> <param index="0" name="array" type="Array" /> <description> - Appends another array at the end of this array. + Appends another [param array] at the end of this array. [codeblock] - var array1 = [1, 2, 3] - var array2 = [4, 5, 6] - array1.append_array(array2) - print(array1) # Prints [1, 2, 3, 4, 5, 6]. + var numbers = [1, 2, 3] + var extra = [4, 5, 6] + numbers.append_array(extra) + print(nums) # Prints [1, 2, 3, 4, 5, 6] [/codeblock] </description> </method> @@ -239,8 +257,8 @@ <method name="back" qualifiers="const"> <return type="Variant" /> <description> - Returns the last element of the array. Prints an error and returns [code]null[/code] if the array is empty. - [b]Note:[/b] Calling this function is not the same as writing [code]array[-1][/code]. If the array is empty, accessing by index will pause project execution when running from the editor. + Returns the last element of the array. If the array is empty, fails and returns [code]null[/code]. See also [method front]. + [b]Note:[/b] Unlike with the [code][][/code] operator ([code]array[-1][/code]), an error is generated without stopping project execution. </description> </method> <method name="bsearch" qualifiers="const"> @@ -248,13 +266,20 @@ <param index="0" name="value" type="Variant" /> <param index="1" name="before" type="bool" default="true" /> <description> - Finds the index of an existing value (or the insertion index that maintains sorting order, if the value is not yet present in the array) using binary search. Optionally, a [param before] specifier can be passed. If [code]false[/code], the returned index comes after all existing entries of the value in the array. + Returns the index of [param value] in the sorted array. If it cannot be found, returns where [param value] should be inserted to keep the array sorted. The algorithm used is [url=https://en.wikipedia.org/wiki/Binary_search_algorithm]binary search[/url]. + If [param before] is [code]true[/code] (as by default), the returned index comes before all existing elements equal to [param value] in the array. [codeblock] - var array = ["a", "b", "c", "c", "d", "e"] - print(array.bsearch("c", true)) # Prints 2, at the first matching element. - print(array.bsearch("c", false)) # Prints 4, after the last matching element, pointing to "d". + var numbers = [2, 4, 8, 10] + var idx = numbers.bsearch(7) + + numbers.insert(idx, 7) + print(numbers) # Prints [2, 4, 7, 8, 10] + + var fruits = ["Apple", "Lemon", "Lemon", "Orange"] + print(fruits.bsearch("Lemon", true)) # Prints 1, points at the first "Lemon". + print(fruits.bsearch("Lemon", false)) # Prints 3, points at "Orange". [/codeblock] - [b]Note:[/b] Calling [method bsearch] on an unsorted array results in unexpected behavior. + [b]Note:[/b] Calling [method bsearch] on an [i]unsorted[/i] array will result in unexpected behavior. Use [method sort] before calling this method. </description> </method> <method name="bsearch_custom" qualifiers="const"> @@ -263,15 +288,36 @@ <param index="1" name="func" type="Callable" /> <param index="2" name="before" type="bool" default="true" /> <description> - Finds the index of an existing value (or the insertion index that maintains sorting order, if the value is not yet present in the array) using binary search and a custom comparison method. Optionally, a [param before] specifier can be passed. If [code]false[/code], the returned index comes after all existing entries of the value in the array. The custom method receives two arguments (an element from the array and the value searched for) and must return [code]true[/code] if the first argument is less than the second, and return [code]false[/code] otherwise. - [b]Note:[/b] The custom method must accept the two arguments in any order, you cannot rely on that the first argument will always be from the array. - [b]Note:[/b] Calling [method bsearch_custom] on an unsorted array results in unexpected behavior. + Returns the index of [param value] in the sorted array. If it cannot be found, returns where [param value] should be inserted to keep the array sorted (using [param func] for the comparisons). The algorithm used is [url=https://en.wikipedia.org/wiki/Binary_search_algorithm]binary search[/url]. + Similar to [method sort_custom], [param func] is called as many times as necessary, receiving one array element and [param value] as arguments. The function should return [code]true[/code] if the array element should be [i]behind[/i] [param value], otherwise it should return [code]false[/code]. + If [param before] is [code]true[/code] (as by default), the returned index comes before all existing elements equal to [param value] in the array. + [codeblock] + func sort_by_amount(a, b): + if a[1] < b[1]: + return true + return false + + func _ready(): + var my_items = [["Tomato", 2], ["Kiwi", 5], ["Rice", 9]] + + var apple = ["Apple", 5] + # "Apple" is inserted before "Kiwi". + my_items.insert(my_items.bsearch_custom(apple, sort_by_amount, true), apple) + + var banana = ["Banana", 5] + # "Banana" is inserted after "Kiwi". + my_items.insert(my_items.bsearch_custom(banana, sort_by_amount, false), banana) + + # Prints [["Tomato", 2], ["Apple", 5], ["Kiwi", 5], ["Banana", 5], ["Rice", 9]] + print(my_items) + [/codeblock] + [b]Note:[/b] Calling [method bsearch_custom] on an [i]unsorted[/i] array will result in unexpected behavior. Use [method sort_custom] with [param func] before calling this method. </description> </method> <method name="clear"> <return type="void" /> <description> - Clears the array. This is equivalent to using [method resize] with a size of [code]0[/code]. + Removes all elements from the array. This is equivalent to using [method resize] with a size of [code]0[/code]. </description> </method> <method name="count" qualifiers="const"> @@ -285,53 +331,57 @@ <return type="Array" /> <param index="0" name="deep" type="bool" default="false" /> <description> - Returns a copy of the array. - If [param deep] is [code]true[/code], a deep copy is performed: all nested arrays and dictionaries are duplicated and will not be shared with the original array. If [code]false[/code], a shallow copy is made and references to the original nested arrays and dictionaries are kept, so that modifying a sub-array or dictionary in the copy will also impact those referenced in the source array. Note that any [Object]-derived elements will be shallow copied regardless of the [param deep] setting. + Returns a new copy of the array. + By default, a [b]shallow[/b] copy is returned: all nested [Array] and [Dictionary] elements are shared with the original array. Modifying them in one array will also affect them in the other.[br]If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). </description> </method> <method name="erase"> <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Removes the first occurrence of a value from the array. If the value does not exist in the array, nothing happens. To remove an element by index, use [method remove_at] instead. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the removed element is close to the beginning of the array (index 0). This is because all elements placed after the removed element have to be reindexed. - [b]Note:[/b] Do not erase entries while iterating over the array. + Finds and removes the first occurrence of [param value] from the array. If [param value] does not exist in the array, nothing happens. To remove an element by index, use [method remove_at] instead. + [b]Note:[/b] This method shifts every element's index after the removed [param value] back, which may have a noticeable performance cost, especially on larger arrays. + [b]Note:[/b] Erasing elements while iterating over arrays is [b]not[/b] supported and will result in unpredictable behavior. </description> </method> <method name="fill"> <return type="void" /> <param index="0" name="value" type="Variant" /> <description> - Assigns the given value to all elements in the array. This can typically be used together with [method resize] to create an array with a given size and initialized elements: + Assigns the given [param value] to all elements in the array. + This method can often be combined with [method resize] to create an array with a given size and initialized elements: [codeblocks] [gdscript] var array = [] - array.resize(10) - array.fill(0) # Initialize the 10 elements to 0. + array.resize(5) + array.fill(2) + print(array) # Prints [2, 2, 2, 2, 2] [/gdscript] [csharp] var array = new Godot.Collections.Array(); - array.Resize(10); - array.Fill(0); // Initialize the 10 elements to 0. + array.Resize(5); + array.Fill(2); + GD.Print(array); // Prints [2, 2, 2, 2, 2] [/csharp] [/codeblocks] - [b]Note:[/b] If [param value] is of a reference type ([Object]-derived, [Array], [Dictionary], etc.) then the array is filled with the references to the same object, i.e. no duplicates are created. + [b]Note:[/b] If [param value] is a [Variant] passed by reference ([Object]-derived, [Array], [Dictionary], etc.), the array will be filled with references to the same [param value], which are not duplicates. </description> </method> <method name="filter" qualifiers="const"> <return type="Array" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] on each element in the array and returns a new array with the elements for which the method returned [code]true[/code]. - The callable's method should take one [Variant] parameter (the current array element) and return a boolean value. + Calls the given [Callable] on each element in the array and returns a new, filtered [Array]. + The [param method] receives one of the array elements as an argument, and should return [code]true[/code] to add the element to the filtered array, or [code]false[/code] to exclude it. [codeblock] + func is_even(number): + return number % 2 == 0 + func _ready(): - print([1, 2, 3].filter(remove_1)) # Prints [2, 3]. - print([1, 2, 3].filter(func(number): return number != 1)) # Same as above, but using lambda function. + print([1, 4, 5, 8].filter(is_even)) # Prints [4, 8] - func remove_1(number): - return number != 1 + # Same as above, but using a lambda function. + print([1, 4, 5, 8].filter(func(number): return number % 2 == 0)) [/codeblock] See also [method any], [method all], [method map] and [method reduce]. </description> @@ -341,78 +391,70 @@ <param index="0" name="what" type="Variant" /> <param index="1" name="from" type="int" default="0" /> <description> - Searches the array for a value and returns its index or [code]-1[/code] if not found. Optionally, the initial search index can be passed. + Returns the index of the [b]first[/b] occurrence of [param what] in this array, or [code]-1[/code] if there are none. The search's start can be specified with [param from], continuing to the end of the array. + [b]Note:[/b] If you just want to know whether the array contains [param what], use [method has] ([code]Contains[/code] in C#). In GDScript, you may also use the [code]in[/code] operator. + [b]Note:[/b] For performance reasons, the search is affected by [param what]'s [enum Variant.Type]. For example, [code]7[/code] ([int]) and [code]7.0[/code] ([float]) are not considered equal for this method. </description> </method> <method name="front" qualifiers="const"> <return type="Variant" /> <description> - Returns the first element of the array. Prints an error and returns [code]null[/code] if the array is empty. - [b]Note:[/b] Calling this function is not the same as writing [code]array[0][/code]. If the array is empty, accessing by index will pause project execution when running from the editor. + Returns the first element of the array. If the array is empty, fails and returns [code]null[/code]. See also [method back]. + [b]Note:[/b] Unlike with the [code][][/code] operator ([code]array[0][/code]), an error is generated without stopping project execution. </description> </method> <method name="get_typed_builtin" qualifiers="const"> <return type="int" /> <description> - Returns the built-in type of the typed array as a [enum Variant.Type] constant. If the array is not typed, returns [constant TYPE_NIL]. + Returns the built-in [Variant] type of the typed array as a [enum Variant.Type] constant. If the array is not typed, returns [constant TYPE_NIL]. See also [method is_typed]. </description> </method> <method name="get_typed_class_name" qualifiers="const"> <return type="StringName" /> <description> - Returns the [b]native[/b] class name of the typed array if the built-in type is [constant TYPE_OBJECT]. Otherwise, this method returns an empty string. + Returns the [b]built-in[/b] class name of the typed array, if the built-in [Variant] type [constant TYPE_OBJECT]. Otherwise, returns an empty [StringName]. See also [method is_typed] and [method Object.get_class]. </description> </method> <method name="get_typed_script" qualifiers="const"> <return type="Variant" /> <description> - Returns the script associated with the typed array. This method returns a [Script] instance or [code]null[/code]. + Returns the [Script] instance associated with this typed array, or [code]null[/code] if it does not exist. See also [method is_typed]. </description> </method> <method name="has" qualifiers="const" keywords="includes, contains"> <return type="bool" /> <param index="0" name="value" type="Variant" /> <description> - Returns [code]true[/code] if the array contains the given value. + Returns [code]true[/code] if the array contains the given [param value]. [codeblocks] [gdscript] - print(["inside", 7].has("inside")) # True - print(["inside", 7].has("outside")) # False - print(["inside", 7].has(7)) # True - print(["inside", 7].has("7")) # False + print(["inside", 7].has("inside")) # Prints true + print(["inside", 7].has("outside")) # Prints false + print(["inside", 7].has(7)) # Prints true + print(["inside", 7].has("7")) # Prints false [/gdscript] [csharp] var arr = new Godot.Collections.Array { "inside", 7 }; - // has is renamed to Contains - GD.Print(arr.Contains("inside")); // True - GD.Print(arr.Contains("outside")); // False - GD.Print(arr.Contains(7)); // True - GD.Print(arr.Contains("7")); // False - [/csharp] - [/codeblocks] - [b]Note:[/b] This is equivalent to using the [code]in[/code] operator as follows: - [codeblocks] - [gdscript] - # Will evaluate to `true`. - if 2 in [2, 4, 6, 8]: - print("Contains!") - [/gdscript] - [csharp] - // As there is no "in" keyword in C#, you have to use Contains - var array = new Godot.Collections.Array { 2, 4, 6, 8 }; - if (array.Contains(2)) - { - GD.Print("Contains!"); - } + // By C# convention, this method is renamed to `Contains`. + GD.Print(arr.Contains("inside")); // Prints true + GD.Print(arr.Contains("outside")); // Prints false + GD.Print(arr.Contains(7)); // Prints true + GD.Print(arr.Contains("7")); // Prints false [/csharp] [/codeblocks] + In GDScript, this is equivalent to the [code]in[/code] operator: + [codeblock] + if 4 in [2, 4, 6, 8]: + print("4 is here!") # Will be printed. + [/codeblock] + [b]Note:[/b] For performance reasons, the search is affected by the [param value]'s [enum Variant.Type]. For example, [code]7[/code] ([int]) and [code]7.0[/code] ([float]) are not considered equal for this method. </description> </method> <method name="hash" qualifiers="const"> <return type="int" /> <description> Returns a hashed 32-bit integer value representing the array and its contents. - [b]Note:[/b] [Array]s with equal content will always produce identical hash values. However, the reverse is not true. Returning identical hash values does [i]not[/i] imply the arrays are equal, because different arrays can have identical hash values due to hash collisions. + [b]Note:[/b] Arrays with equal hash values are [i]not[/i] guaranteed to be the same, as a result of hash collisions. On the countrary, arrays with different hash values are guaranteed to be different. </description> </method> <method name="insert"> @@ -420,55 +462,64 @@ <param index="0" name="position" type="int" /> <param index="1" name="value" type="Variant" /> <description> - Inserts a new element at a given position in the array. The position must be valid, or at the end of the array ([code]pos == size()[/code]). Returns [constant OK] on success, or one of the other [enum Error] values if the operation failed. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the inserted element is close to the beginning of the array (index 0). This is because all elements placed after the newly inserted element have to be reindexed. + Inserts a new element ([param value]) at a given index ([param position]) in the array. [param position] should be between [code]0[/code] and the array's [method size]. + Returns [constant OK] on success, or one of the other [enum Error] constants if this method fails. + [b]Note:[/b] Every element's index after [param position] needs to be shifted forward, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="is_empty" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is empty. + Returns [code]true[/code] if the array is empty ([code][][/code]). See also [method size]. </description> </method> <method name="is_read_only" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is read-only. See [method make_read_only]. Arrays are automatically read-only if declared with [code]const[/code] keyword. + Returns [code]true[/code] if the array is read-only. See [method make_read_only]. + In GDScript, arrays are automatically read-only if declared with the [code]const[/code] keyword. </description> </method> <method name="is_same_typed" qualifiers="const"> <return type="bool" /> <param index="0" name="array" type="Array" /> <description> - Returns [code]true[/code] if the array is typed the same as [param array]. + Returns [code]true[/code] if this array is typed the same as the given [param array]. See also [method is_typed]. </description> </method> <method name="is_typed" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the array is typed. Typed arrays can only store elements of their associated type and provide type safety for the [code][][/code] operator. Methods of typed array still return [Variant]. + Returns [code]true[/code] if the array is typed. Typed arrays can only contain elements of a specific type, as defined by the typed array constructor. The methods of a typed array are still expected to return a generic [Variant]. + In GDScript, it is possible to define a typed array with static typing: + [codeblock] + var numbers: Array[float] = [0.2, 4.2, -2.0] + print(numbers.is_typed()) # Prints true + [/codeblock] </description> </method> <method name="make_read_only"> <return type="void" /> <description> - Makes the array read-only, i.e. disabled modifying of the array's elements. Does not apply to nested content, e.g. content of nested arrays. + Makes the array read-only. The array's elements cannot be overridden with different values, and their order cannot change. Does not apply to nested elements, such as dictionaries. + In GDScript, arrays are automatically read-only if declared with the [code]const[/code] keyword. </description> </method> <method name="map" qualifiers="const"> <return type="Array" /> <param index="0" name="method" type="Callable" /> <description> - Calls the provided [Callable] for each element in the array and returns a new array filled with values returned by the method. - The callable's method should take one [Variant] parameter (the current array element) and can return any [Variant]. + Calls the given [Callable] for each element in the array and returns a new array filled with values returned by the [param method]. + The [param method] should take one [Variant] parameter (the current array element) and can return any [Variant]. [codeblock] + func double(number): + return number * 2 + func _ready(): - print([1, 2, 3].map(negate)) # Prints [-1, -2, -3]. - print([1, 2, 3].map(func(number): return -number)) # Same as above, but using lambda function. + print([1, 2, 3].map(double)) # Prints [2, 4, 6] - func negate(number): - return -number + # Same as above, but using a lambda function. + print([1, 2, 3].map(func(element): return element * 2)) [/codeblock] See also [method filter], [method reduce], [method any] and [method all]. </description> @@ -476,61 +527,52 @@ <method name="max" qualifiers="const"> <return type="Variant" /> <description> - Returns the maximum value contained in the array if all elements are of comparable types. If the elements can't be compared, [code]null[/code] is returned. - To find the maximum value using a custom comparator, you can use [method reduce]. In this example every array element is checked and the first maximum value is returned: - [codeblock] - func _ready(): - var arr = [Vector2(0, 1), Vector2(2, 0), Vector2(1, 1), Vector2(1, 0), Vector2(0, 2)] - # In this example we compare the lengths. - print(arr.reduce(func(max, val): return val if is_length_greater(val, max) else max)) - - func is_length_greater(a, b): - return a.length() > b.length() - [/codeblock] + Returns the maximum value contained in the array, if all elements can be compared. Otherwise, returns [code]null[/code]. See also [method min]. + To find the maximum value using a custom comparator, you can use [method reduce]. </description> </method> <method name="min" qualifiers="const"> <return type="Variant" /> <description> - Returns the minimum value contained in the array if all elements are of comparable types. If the elements can't be compared, [code]null[/code] is returned. - See also [method max] for an example of using a custom comparator. + Returns the minimum value contained in the array, if all elements can be compared. Otherwise, returns [code]null[/code]. See also [method max]. </description> </method> <method name="pick_random" qualifiers="const"> <return type="Variant" /> <description> - Returns a random value from the target array. Prints an error and returns [code]null[/code] if the array is empty. + Returns a random element from the array. Generates an error and returns [code]null[/code] if the array is empty. [codeblocks] [gdscript] - var array: Array[int] = [1, 2, 3, 4] - print(array.pick_random()) # Prints either of the four numbers. + # May print 1, 2, 3.25, or "Hi". + print([1, 2, 3.25, "Hi"].pick_random()) [/gdscript] [csharp] - var array = new Godot.Collections.Array { 1, 2, 3, 4 }; - GD.Print(array.PickRandom()); // Prints either of the four numbers. + var array = new Godot.Collections.Array { 1, 2, 3.25f, "Hi" }; + GD.Print(array.PickRandom()); // May print 1, 2, 3.25, or "Hi". [/csharp] [/codeblocks] + [b]Note:[/b] Like many similar functions in the engine (such as [method @GlobalScope.randi] or [method shuffle]), this method uses a common, global random seed. To get a predictable outcome from this method, see [method @GlobalScope.seed]. </description> </method> <method name="pop_at"> <return type="Variant" /> <param index="0" name="position" type="int" /> <description> - Removes and returns the element of the array at index [param position]. If negative, [param position] is considered relative to the end of the array. Leaves the array unchanged and returns [code]null[/code] if the array is empty or if it's accessed out of bounds. An error message is printed when the array is accessed out of bounds, but not when the array is empty. - [b]Note:[/b] On large arrays, this method can be slower than [method pop_back] as it will reindex the array's elements that are located after the removed element. The larger the array and the lower the index of the removed element, the slower [method pop_at] will be. + Removes and returns the element of the array at index [param position]. If negative, [param position] is considered relative to the end of the array. Returns [code]null[/code] if the array is empty. If [param position] is out of bounds, an error message is also generated. + [b]Note:[/b] This method shifts every element's index after [param position] back, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="pop_back"> <return type="Variant" /> <description> - Removes and returns the last element of the array. Returns [code]null[/code] if the array is empty, without printing an error message. See also [method pop_front]. + Removes and returns the last element of the array. Returns [code]null[/code] if the array is empty, without generating an error. See also [method pop_front]. </description> </method> <method name="pop_front"> <return type="Variant" /> <description> - Removes and returns the first element of the array. Returns [code]null[/code] if the array is empty, without printing an error message. See also [method pop_back]. - [b]Note:[/b] On large arrays, this method is much slower than [method pop_back] as it will reindex all the array's elements every time it's called. The larger the array, the slower [method pop_front] will be. + Removes and returns the first element of the array. Returns [code]null[/code] if the array is empty, without generating an error. See also [method pop_back]. + [b]Note:[/b] This method shifts every other element's index back, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="push_back"> @@ -545,7 +587,7 @@ <param index="0" name="value" type="Variant" /> <description> Adds an element at the beginning of the array. See also [method push_back]. - [b]Note:[/b] On large arrays, this method is much slower than [method push_back] as it will reindex all the array's elements every time it's called. The larger the array, the slower [method push_front] will be. + [b]Note:[/b] This method shifts every other element's index forward, which may have a noticeable performance cost, especially on larger arrays. </description> </method> <method name="reduce" qualifiers="const"> @@ -553,15 +595,29 @@ <param index="0" name="method" type="Callable" /> <param index="1" name="accum" type="Variant" default="null" /> <description> - Calls the provided [Callable] for each element in array and accumulates the result in [param accum]. - The callable's method takes two arguments: the current value of [param accum] and the current array element. If [param accum] is [code]null[/code] (default value), the iteration will start from the second element, with the first one used as initial value of [param accum]. + Calls the given [Callable] for each element in array, accumulates the result in [param accum], then returns it. + The [param method] takes two arguments: the current value of [param accum] and the current array element. If [param accum] is [code]null[/code] (as by default), the iteration will start from the second element, with the first one used as initial value of [param accum]. [codeblock] - func _ready(): - print([1, 2, 3].reduce(sum, 10)) # Prints 16. - print([1, 2, 3].reduce(func(accum, number): return accum + number, 10)) # Same as above, but using lambda function. - func sum(accum, number): return accum + number + + func _ready(): + print([1, 2, 3].reduce(sum, 0)) # Prints 6 + print([1, 2, 3].reduce(sum, 10)) # Prints 16 + + # Same as above, but using a lambda function. + print([1, 2, 3].reduce(func(accum, number): return accum + number, 10)) + [/codeblock] + If [method max] is not desirable, this method may also be used to implement a custom comparator: + [codeblock] + func _ready(): + var arr = [Vector2(5, 0), Vector2(3, 4), Vector2(1, 2)] + + var longest_vec = arr.reduce(func(max, vec): return vec if is_length_greater(vec, max) else max) + print(longest_vec) # Prints Vector2(3, 4). + + func is_length_greater(a, b): + return a.length() > b.length() [/codeblock] See also [method map], [method filter], [method any] and [method all]. </description> @@ -570,25 +626,25 @@ <return type="void" /> <param index="0" name="position" type="int" /> <description> - Removes an element from the array by index. If the index does not exist in the array, nothing happens. To remove an element by searching for its value, use [method erase] instead. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. - [b]Note:[/b] On large arrays, this method will be slower if the removed element is close to the beginning of the array (index 0). This is because all elements placed after the removed element have to be reindexed. - [b]Note:[/b] [param position] cannot be negative. To remove an element relative to the end of the array, use [code]arr.remove_at(arr.size() - (i + 1))[/code]. To remove the last element from the array without returning the value, use [code]arr.resize(arr.size() - 1)[/code]. + Removes the element from the array at the given index ([param position]). If the index is out of bounds, this method fails. + If you need to return the removed element, use [method pop_at]. To remove an element by value, use [method erase] instead. + [b]Note:[/b] This method shifts every element's index after [param position] back, which may have a noticeable performance cost, especially on larger arrays. + [b]Note:[/b] The [param position] cannot be negative. To remove an element relative to the end of the array, use [code]arr.remove_at(arr.size() - (i + 1))[/code]. To remove the last element from the array, use [code]arr.resize(arr.size() - 1)[/code]. </description> </method> <method name="resize"> <return type="int" /> <param index="0" name="size" type="int" /> <description> - Resizes the array to contain a different number of elements. If the array size is smaller, elements are cleared, if bigger, new elements are [code]null[/code]. Returns [constant OK] on success, or one of the other [enum Error] values if the operation failed. - Calling [method resize] once and assigning the new values is faster than adding new elements one by one. - [b]Note:[/b] This method acts in-place and doesn't return a modified array. + Sets the array's number of elements to [param size]. If [param size] is smaller than the array's current size, the elements at the end are removed. If [param size] is greater, new default elements (usually [code]null[/code]) are added, depending on the array's type. + Returns [constant OK] on success, or one of the other [enum Error] constants if this method fails. + [b]Note:[/b] Calling this method once and assigning the new values is faster than calling [method append] for every new element. </description> </method> <method name="reverse"> <return type="void" /> <description> - Reverses the order of the elements in the array. + Reverses the order of all elements in the array. </description> </method> <method name="rfind" qualifiers="const"> @@ -596,19 +652,20 @@ <param index="0" name="what" type="Variant" /> <param index="1" name="from" type="int" default="-1" /> <description> - Searches the array in reverse order. Optionally, a start search index can be passed. If negative, the start index is considered relative to the end of the array. + Returns the index of the [b]last[/b] occurrence of [param what] in this array, or [code]-1[/code] if there are none. The search's start can be specified with [param from], continuing to the beginning of the array. This method is the reverse of [method find]. </description> </method> <method name="shuffle"> <return type="void" /> <description> - Shuffles the array such that the items will have a random order. This method uses the global random number generator common to methods such as [method @GlobalScope.randi]. Call [method @GlobalScope.randomize] to ensure that a new seed will be used each time if you want non-reproducible shuffling. + Shuffles all elements of the array in a random order. + [b]Note:[/b] Like many similar functions in the engine (such as [method @GlobalScope.randi] or [method pick_random]), this method uses a common, global random seed. To get a predictable outcome from this method, see [method @GlobalScope.seed]. </description> </method> <method name="size" qualifiers="const"> <return type="int" /> <description> - Returns the number of elements in the array. + Returns the number of elements in the array. Empty arrays ([code][][/code]) always return [code]0[/code]. See also [method is_empty]. </description> </method> <method name="slice" qualifiers="const"> @@ -618,67 +675,71 @@ <param index="2" name="step" type="int" default="1" /> <param index="3" name="deep" type="bool" default="false" /> <description> - Returns the slice of the [Array], from [param begin] (inclusive) to [param end] (exclusive), as a new [Array]. - The absolute value of [param begin] and [param end] will be clamped to the array size, so the default value for [param end] makes it slice to the size of the array by default (i.e. [code]arr.slice(1)[/code] is a shorthand for [code]arr.slice(1, arr.size())[/code]). - If either [param begin] or [param end] are negative, they will be relative to the end of the array (i.e. [code]arr.slice(0, -2)[/code] is a shorthand for [code]arr.slice(0, arr.size() - 2)[/code]). - If specified, [param step] is the relative index between source elements. It can be negative, then [param begin] must be higher than [param end]. For example, [code][0, 1, 2, 3, 4, 5].slice(5, 1, -2)[/code] returns [code][5, 3][/code]. - If [param deep] is true, each element will be copied by value rather than by reference. - [b]Note:[/b] To include the first element when [param step] is negative, use [code]arr.slice(begin, -arr.size() - 1, step)[/code] (i.e. [code][0, 1, 2].slice(1, -4, -1)[/code] returns [code][1, 0][/code]). + Returns a new [Array] containing this array's elements, from index [param begin] (inclusive) to [param end] (exclusive), every [param step] elements. + If either [param begin] or [param end] are negative, their value is relative to the end of the array. + If [param step] is negative, this method iterates through the array in reverse, returning a slice ordered backwards. For this to work, [param begin] must be greater than [param end]. + If [param deep] is [code]true[/code], all nested [Array] and [Dictionary] elements in the slice are duplicated from the original, recursively. See also [method duplicate]). + [codeblock] + var letters = ["A", "B", "C", "D", "E", "F"] + + print(letters.slice(0, 2)) # Prints ["A", "B"] + print(letters.slice(2, -2)) # Prints ["C", "D"] + print(letters.slice(-2, 6)) # Prints ["E", "F"] + + print(letters.slice(0, 6, 2)) # Prints ["A", "C", "E"] + print(letters.slice(4, 1, -1)) # Prints ["E", "D", "C"] + [/codeblock] </description> </method> <method name="sort"> <return type="void" /> <description> - Sorts the array. - [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when using [method sort]. - [b]Note:[/b] Strings are sorted in alphabetical order (as opposed to natural order). This may lead to unexpected behavior when sorting an array of strings ending with a sequence of numbers. Consider the following example: + Sorts the array in ascending order. The final order is dependent on the "less than" ([code]>[/code]) comparison between elements. [codeblocks] [gdscript] - var strings = ["string1", "string2", "string10", "string11"] - strings.sort() - print(strings) # Prints [string1, string10, string11, string2] + var numbers = [10, 5, 2.5, 8] + numbers.sort() + print(numbers) # Prints [2.5, 5, 8, 10] [/gdscript] [csharp] - var strings = new Godot.Collections.Array { "string1", "string2", "string10", "string11" }; - strings.Sort(); - GD.Print(strings); // Prints [string1, string10, string11, string2] + var numbers = new Godot.Collections.Array { 10, 5, 2.5, 8 }; + numbers.Sort(); + GD.Print(numbers); // Prints [2.5, 5, 8, 10] [/csharp] [/codeblocks] - To perform natural order sorting, you can use [method sort_custom] with [method String.naturalnocasecmp_to] as follows: - [codeblock] - var strings = ["string1", "string2", "string10", "string11"] - strings.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) - print(strings) # Prints [string1, string2, string10, string11] - [/codeblock] + [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that equivalent elements (such as [code]2[/code] and [code]2.0[/code]) may have their order changed when calling [method sort]. </description> </method> <method name="sort_custom"> <return type="void" /> <param index="0" name="func" type="Callable" /> <description> - Sorts the array using a custom method. The custom method receives two arguments (a pair of elements from the array) and must return either [code]true[/code] or [code]false[/code]. For two elements [code]a[/code] and [code]b[/code], if the given method returns [code]true[/code], element [code]b[/code] will be after element [code]a[/code] in the array. - [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when using [method sort_custom]. - [b]Note:[/b] You cannot randomize the return value as the heapsort algorithm expects a deterministic result. Randomizing the return value will result in unexpected behavior. - [codeblocks] - [gdscript] + Sorts the array using a custom [Callable]. + [param func] is called as many times as necessary, receiving two array elements as arguments. The function should return [code]true[/code] if the first element should be moved [i]behind[/i] the second one, otherwise it should return [code]false[/code]. + [codeblock] func sort_ascending(a, b): - if a[0] < b[0]: + if a[1] < b[1]: return true return false func _ready(): - var my_items = [[5, "Potato"], [9, "Rice"], [4, "Tomato"]] + var my_items = [["Tomato", 5], ["Apple", 9], ["Rice", 4]] my_items.sort_custom(sort_ascending) - print(my_items) # Prints [[4, Tomato], [5, Potato], [9, Rice]]. + print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9]] - # Descending, lambda version. + # Sort descending, using a lambda function. my_items.sort_custom(func(a, b): return a[0] > b[0]) - print(my_items) # Prints [[9, Rice], [5, Potato], [4, Tomato]]. - [/gdscript] - [csharp] - // There is no custom sort support for Godot.Collections.Array - [/csharp] - [/codeblocks] + print(my_items) # Prints [["Apple", 9], ["Tomato", 5], ["Rice", 4]] + [/codeblock] + It may also be necessary to use this method to sort strings by natural order, with [method String.naturalnocasecmp_to], as in the following example: + [codeblock] + var files = ["newfile1", "newfile2", "newfile10", "newfile11"] + files.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0) + print(files) # Prints ["newfile1", "newfile2", "newfile10", "newfile11"] + [/codeblock] + [b]Note:[/b] In C#, this method is not supported. + [b]Note:[/b] The sorting algorithm used is not [url=https://en.wikipedia.org/wiki/Sorting_algorithm#Stability]stable[/url]. This means that values considered equal may have their order changed when calling this method. + [b]Note:[/b] You should not randomize the return value of [param func], as the heapsort algorithm expects a consistent result. Randomizing the return value will result in unexpected behavior. </description> </method> </methods> @@ -687,28 +748,44 @@ <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Compares the left operand [Array] against the [param right] [Array]. Returns [code]true[/code] if the sizes or contents of the arrays are [i]not[/i] equal, [code]false[/code] otherwise. + Returns [code]true[/code] if the array's size or its elements are different than [param right]'s. </description> </operator> <operator name="operator +"> <return type="Array" /> <param index="0" name="right" type="Array" /> <description> - Concatenates two [Array]s together, with the [param right] [Array] being added to the end of the [Array] specified in the left operand. For example, [code][1, 2] + [3, 4][/code] results in [code][1, 2, 3, 4][/code]. + Appends the [param right] array to the left operand, creating a new [Array]. This is also known as an array concatenation. + [codeblocks] + [gdscript] + var array1 = ["One", 2] + var array2 = [3, "Four"] + print(array1 + array2) # Prints ["One", 2, 3, "Four"] + [/gdscript] + [csharp] + // Note that concatenation is not possible with C#'s native Array type. + var array1 = new Godot.Collections.Array{"One", 2}; + var array2 = new Godot.Collections.Array{3, "Four"}; + GD.Print(array1 + array2); // Prints ["One", 2, 3, "Four"] + [/csharp] + [/codeblocks] + [b]Note:[/b] For existing arrays, [method append_array] is much more efficient than concatenation and assignment with the [code]+=[/code] operator. </description> </operator> <operator name="operator <"> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is less, or [code]false[/code] if the element is greater. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]false[/code] if the left operand [Array] has fewer elements, otherwise it returns [code]true[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is less than [param right]'s, [code]false[/code] if this element is greater. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is less than [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator <="> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is less, or [code]false[/code] if the element is greater. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the left operand [Array] has the same number of elements or fewer, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is less than [param right]'s, [code]false[/code] if this element is greater. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is less or equal to [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator =="> @@ -722,21 +799,23 @@ <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is greater, or [code]false[/code] if the element is less. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the [param right] [Array] has more elements, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is greater than [param right]'s, [code]false[/code] if this element is less. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is greater than [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator >="> <return type="bool" /> <param index="0" name="right" type="Array" /> <description> - Performs a comparison for each index between the left operand [Array] and the [param right] [Array], considering the highest common index of both arrays for this comparison: Returns [code]true[/code] on the first occurrence of an element that is greater, or [code]false[/code] if the element is less. Note that depending on the type of data stored, this function may be recursive. If all elements are equal, it compares the length of both arrays and returns [code]true[/code] if the [param right] [Array] has more or the same number of elements, otherwise it returns [code]false[/code]. + Compares the elements of both arrays in order, starting from index [code]0[/code] and ending on the last index in common between both arrays. For each pair of elements, returns [code]true[/code] if this array's element is greater than [param right]'s, [code]false[/code] if this element is less. Otherwise, continues to the next pair. + If all searched elements are equal, returns [code]true[/code] if this array's size is greater or equal to [param right]'s, otherwise returns [code]false[/code]. </description> </operator> <operator name="operator []"> <return type="Variant" /> <param index="0" name="index" type="int" /> <description> - Returns a reference to the element of type [Variant] at the specified location. Arrays start at index 0. [param index] can be a zero or positive value to start from the beginning, or a negative value to start from the end. Out-of-bounds array access causes a run-time error, which will result in an error being printed and the project execution pausing if run from the editor. + Returns the [Variant] element at the specified [param index]. Arrays start at index 0. If [param index] is greater or equal to [code]0[/code], the element is fetched starting from the beginning of the array. If [param index] is a negative value, the element is fetched starting from the end. Accessing an array out-of-bounds will cause a run-time error, pausing the project execution if run from the editor. </description> </operator> </operators> diff --git a/doc/classes/Button.xml b/doc/classes/Button.xml index 98f25ed573..68fb918904 100644 --- a/doc/classes/Button.xml +++ b/doc/classes/Button.xml @@ -119,7 +119,7 @@ Icon modulate [Color] used when the [Button] is being pressed. </theme_item> <theme_item name="align_to_largest_stylebox" data_type="constant" type="int" default="0"> - This constant acts as a boolean. If [code]true[/code], text and icon are always aligned to the largest stylebox margins, otherwise it's aligned to the current button state stylebox margins. + This constant acts as a boolean. If [code]true[/code], the minimum size of the button and text/icon alignment is always based on the largest stylebox margins, otherwise it's based on the current button state stylebox margins. </theme_item> <theme_item name="h_separation" data_type="constant" type="int" default="4"> The horizontal space between [Button]'s icon and text. Negative values will be treated as [code]0[/code] when used. diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 4d304cf5fd..9ef911a68d 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -36,7 +36,6 @@ <description> Customize a resource. If changes are made to it, return the same or a new resource. Otherwise, return [code]null[/code]. The [i]path[/i] argument is only used when customizing an actual file, otherwise this means that this resource is part of another one and it will be empty. - Calling [method skip] inside this callback will make the file not included in the export. Implementing this method is required if [method _begin_customize_resources] returns [code]true[/code]. </description> </method> @@ -46,7 +45,6 @@ <param index="1" name="path" type="String" /> <description> Customize a scene. If changes are made to it, return the same or a new scene. Otherwise, return [code]null[/code]. If a new scene is returned, it is up to you to dispose of the old one. - Calling [method skip] inside this callback will make the file not included in the export. Implementing this method is required if [method _begin_customize_scenes] returns [code]true[/code]. </description> </method> @@ -84,9 +82,8 @@ <param index="1" name="type" type="String" /> <param index="2" name="features" type="PackedStringArray" /> <description> - Virtual method to be overridden by the user. Called for each exported file, except for imported resources (resources that have an associated [code].import[/code] file). The arguments can be used to identify the file. [param path] is the path of the file, [param type] is the [Resource] represented by the file (e.g. [PackedScene]), and [param features] is the list of features for the export. + Virtual method to be overridden by the user. Called for each exported file before [method _customize_resource] and [method _customize_scene]. The arguments can be used to identify the file. [param path] is the path of the file, [param type] is the [Resource] represented by the file (e.g. [PackedScene]), and [param features] is the list of features for the export. Calling [method skip] inside this callback will make the file not included in the export. - Use [method _customize_resource] for imported resources that are not handled by this function. </description> </method> <method name="_get_android_dependencies" qualifiers="virtual const"> @@ -235,6 +232,7 @@ <description> Adds a custom file to be exported. [param path] is the virtual path that can be used to load the file, [param file] is the binary data of the file. When called inside [method _export_file] and [param remap] is [code]true[/code], the current file will not be exported, but instead remapped to this custom file. [param remap] is ignored when called in other places. + [param file] will not be imported, so consider using [method _customize_resource] to remap imported resources. </description> </method> <method name="add_ios_bundle_file"> @@ -317,7 +315,7 @@ <method name="skip"> <return type="void" /> <description> - To be called inside [method _export_file], [method _customize_resource], or [method _customize_scene]. Skips the current file, so it's not included in the export. + To be called inside [method _export_file]. Skips the current file, so it's not included in the export. </description> </method> </methods> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index c97ae197d9..c7ff543b66 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -396,6 +396,10 @@ <member name="editors/animation/autorename_animation_tracks" type="bool" setter="" getter=""> If [code]true[/code], automatically updates animation tracks' target paths when renaming or reparenting nodes in the Scene tree dock. </member> + <member name="editors/animation/confirm_insert_track" type="bool" setter="" getter=""> + If [code]true[/code], display a confirmation dialog when adding a new track to an animation by pressing the "key" icon next to a property. Holding Shift will bypass the dialog. + If [code]false[/code], the behavior is reversed, i.e. the dialog only appears when Shift is held. + </member> <member name="editors/animation/default_create_bezier_tracks" type="bool" setter="" getter=""> If [code]true[/code], create a Bezier track instead of a standard track when pressing the "key" icon next to a property. Bezier tracks provide more control over animation curves, but are more difficult to adjust quickly. </member> diff --git a/doc/classes/Mesh.xml b/doc/classes/Mesh.xml index 6b5a50d97b..7f4dd5af37 100644 --- a/doc/classes/Mesh.xml +++ b/doc/classes/Mesh.xml @@ -223,6 +223,7 @@ </constant> <constant name="ARRAY_NORMAL" value="1" enum="ArrayType"> [PackedVector3Array] of vertex normals. + [b]Note:[/b] The array has to consist of normal vectors, otherwise they will be normalized by the engine, potentially causing visual discrepancies. </constant> <constant name="ARRAY_TANGENT" value="2" enum="ArrayType"> [PackedFloat32Array] of vertex tangents. Each element in groups of 4 floats, first 3 floats determine the tangent, and the last the binormal direction as -1 or 1. diff --git a/doc/classes/NodePath.xml b/doc/classes/NodePath.xml index b4969b8906..d0ec81ab45 100644 --- a/doc/classes/NodePath.xml +++ b/doc/classes/NodePath.xml @@ -23,11 +23,12 @@ [/codeblock] Despite their name, node paths may also point to a property: [codeblock] - ^"position" # Points to this object's position. - ^"position:x" # Points to this object's position in the x axis. + ^":position" # Points to this object's position. + ^":position:x" # Points to this object's position in the x axis. ^"Camera3D:rotation:y" # Points to the child Camera3D and its y rotation. ^"/root:size:x" # Points to the root Window and its width. [/codeblock] + In some situations, it's possible to omit the leading [code]:[/code] when pointing to an object's property. As an example, this is the case with [method Object.set_indexed] and [method Tween.tween_property], as those methods call [method NodePath.get_as_property_path] under the hood. However, it's generally recommended to keep the [code]:[/code] prefix. Node paths cannot check whether they are valid and may point to nodes or properties that do not exist. Their meaning depends entirely on the context in which they're used. You usually do not have to worry about the [NodePath] type, as strings are automatically converted to the type when necessary. There are still times when defining node paths is useful. For example, exported [NodePath] properties allow you to easily select any node within the currently edited scene. They are also automatically updated when moving, renaming or deleting nodes in the scene tree editor. See also [annotation @GDScript.@export_node_path]. See also [StringName], which is a similar type designed for optimized strings. diff --git a/doc/classes/OS.xml b/doc/classes/OS.xml index 3d048e2f63..77caea9745 100644 --- a/doc/classes/OS.xml +++ b/doc/classes/OS.xml @@ -283,7 +283,7 @@ <return type="String" /> <description> Returns the file path to the current engine executable. - [b]Note:[/b] On macOS, always use [method create_instance] instead of relying on executable path. + [b]Note:[/b] On macOS, if you want to launch another instance of Godot, always use [method create_instance] instead of relying on the executable path. </description> </method> <method name="get_granted_permissions" qualifiers="const"> diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 0f5687f091..004bbe2286 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -395,6 +395,12 @@ Returns [code]true[/code] if the specified item's shortcut is disabled. </description> </method> + <method name="is_native_menu" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if the system native menu is supported and currently used by this [PopupMenu]. + </description> + </method> <method name="is_system_menu" qualifiers="const"> <return type="bool" /> <description> @@ -636,6 +642,7 @@ </member> <member name="prefer_native_menu" type="bool" setter="set_prefer_native_menu" getter="is_prefer_native_menu" default="false"> If [code]true[/code], [MenuBar] will use native menu when supported. + [b]Note:[/b] If [PopupMenu] is linked to [StatusIndicator], [MenuBar], or another [PopupMenu] item it can use native menu regardless of this property, use [method is_native_menu] to check it. </member> <member name="submenu_popup_delay" type="float" setter="set_submenu_popup_delay" getter="get_submenu_popup_delay" default="0.3"> Sets the delay time in seconds for the submenu item to popup on mouse hovering. If the popup menu is added as a child of another (acting as a submenu), it will inherit the delay time of the parent menu item. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index b1556783d5..b0f421e932 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -633,6 +633,9 @@ <member name="debug/settings/profiler/max_functions" type="int" setter="" getter="" default="16384"> Maximum number of functions per frame allowed when profiling. </member> + <member name="debug/settings/profiler/max_timestamp_query_elements" type="int" setter="" getter="" default="256"> + Maximum number of timestamp query elements allowed per frame for visual profiling. + </member> <member name="debug/settings/stdout/print_fps" type="bool" setter="" getter="" default="false"> Print frames per second to standard output every second. </member> @@ -897,8 +900,8 @@ <member name="display/window/stretch/mode" type="String" setter="" getter="" default=""disabled""> Defines how the base size is stretched to fit the resolution of the window or screen. [b]"disabled"[/b]: No stretching happens. One unit in the scene corresponds to one pixel on the screen. In this mode, [member display/window/stretch/aspect] has no effect. Recommended for non-game applications. - [b]"canvas_items"[/b]: The base size specified in width and height in the project settings is stretched to cover the whole screen (taking [member display/window/stretch/aspect] into account). This means that everything is rendered directly at the target resolution. 3D is unaffected, while in 2D, there is no longer a 1:1 correspondence between sprite pixels and screen pixels, which may result in scaling artifacts. Recommended for most games that don't use a pixel art esthetic, although it is possible to use this stretch mode for pixel art games too (especially in 3D). - [b]"viewport"[/b]: The size of the root [Viewport] is set precisely to the base size specified in the Project Settings' Display section. The scene is rendered to this viewport first. Finally, this viewport is scaled to fit the screen (taking [member display/window/stretch/aspect] into account). Recommended for games that use a pixel art esthetic. + [b]"canvas_items"[/b]: The base size specified in width and height in the project settings is stretched to cover the whole screen (taking [member display/window/stretch/aspect] into account). This means that everything is rendered directly at the target resolution. 3D is unaffected, while in 2D, there is no longer a 1:1 correspondence between sprite pixels and screen pixels, which may result in scaling artifacts. Recommended for most games that don't use a pixel art aesthetic, although it is possible to use this stretch mode for pixel art games too (especially in 3D). + [b]"viewport"[/b]: The size of the root [Viewport] is set precisely to the base size specified in the Project Settings' Display section. The scene is rendered to this viewport first. Finally, this viewport is scaled to fit the screen (taking [member display/window/stretch/aspect] into account). Recommended for games that use a pixel art aesthetic. </member> <member name="display/window/stretch/scale" type="float" setter="" getter="" default="1.0"> The scale factor multiplier to use for 2D elements. This multiplies the final scale factor determined by [member display/window/stretch/mode]. If using the [b]Disabled[/b] stretch mode, this scale factor is applied as-is. This can be adjusted to make the UI easier to read on certain displays. @@ -931,8 +934,9 @@ Changing this value allows setting up a multi-project scenario where there are multiple [code].csproj[/code]. Keep in mind that the Godot project is considered one of the C# projects in the workspace and it's root directory should contain the [code]project.godot[/code] and [code].csproj[/code] next to each other. </member> <member name="editor/export/convert_text_resources_to_binary" type="bool" setter="" getter="" default="true"> - If [code]true[/code], text resources are converted to a binary format on export. This decreases file sizes and speeds up loading slightly. - [b]Note:[/b] If [member editor/export/convert_text_resources_to_binary] is [code]true[/code], [method @GDScript.load] will not be able to return the converted files in an exported project. Some file paths within the exported PCK will also change, such as [code]project.godot[/code] becoming [code]project.binary[/code]. If you rely on run-time loading of files present within the PCK, set [member editor/export/convert_text_resources_to_binary] to [code]false[/code]. + If [code]true[/code], text resource ([code]tres[/code]) and text scene ([code]tscn[/code]) files are converted to their corresponding binary format on export. This decreases file sizes and speeds up loading slightly. + [b]Note:[/b] Because a resource's file extension may change in an exported project, it is heavily recommended to use [method @GDScript.load] or [ResourceLoader] instead of [FileAccess] to load resources dynamically. + [b]Note:[/b] The project settings file ([code]project.godot[/code]) will always be converted to binary on export, regardless of this setting. </member> <member name="editor/import/atlas_max_width" type="int" setter="" getter="" default="2048"> The maximum width to use when importing textures as an atlas. The value will be rounded to the nearest power of two when used. Use this to prevent imported textures from growing too large in the other direction. @@ -1391,6 +1395,12 @@ Enabling this can greatly improve the responsiveness to input, specially in devices that need to run multiple physics frames per visible (process) frame, because they can't run at the target frame rate. [b]Note:[/b] Currently implemented only on Android. </member> + <member name="input_devices/buffering/android/use_accumulated_input" type="bool" setter="" getter="" default="true"> + If [code]true[/code], multiple input events will be accumulated into a single input event when possible. + </member> + <member name="input_devices/buffering/android/use_input_buffering" type="bool" setter="" getter="" default="true"> + If [code]true[/code], input events will be buffered prior to being dispatched. + </member> <member name="input_devices/compatibility/legacy_just_pressed_behavior" type="bool" setter="" getter="" default="false"> If [code]true[/code], [method Input.is_action_just_pressed] and [method Input.is_action_just_released] will only return [code]true[/code] if the action is still in the respective state, i.e. an action that is pressed [i]and[/i] released on the same frame will be missed. If [code]false[/code], no input will be lost. diff --git a/doc/classes/ScriptEditorBase.xml b/doc/classes/ScriptEditorBase.xml index 403608355a..638bc921d6 100644 --- a/doc/classes/ScriptEditorBase.xml +++ b/doc/classes/ScriptEditorBase.xml @@ -72,7 +72,7 @@ </description> </signal> <signal name="request_save_previous_state"> - <param index="0" name="line" type="int" /> + <param index="0" name="state" type="Dictionary" /> <description> Emitted when the user changes current script or moves caret by 10 or more columns within the same script. </description> diff --git a/drivers/vulkan/SCsub b/drivers/vulkan/SCsub index 80d5f35305..1efef5ad77 100644 --- a/drivers/vulkan/SCsub +++ b/drivers/vulkan/SCsub @@ -16,14 +16,14 @@ if env["use_volk"]: if env["platform"] == "android": env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_ANDROID_KHR"]) elif env["platform"] == "ios": - env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_IOS_MVK"]) + env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_IOS_MVK", "VK_USE_PLATFORM_METAL_EXT"]) elif env["platform"] == "linuxbsd": if env["x11"]: env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_XLIB_KHR"]) if env["wayland"]: env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_WAYLAND_KHR"]) elif env["platform"] == "macos": - env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_MACOS_MVK"]) + env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_MACOS_MVK", "VK_USE_PLATFORM_METAL_EXT"]) elif env["platform"] == "windows": env.AppendUnique(CPPDEFINES=["VK_USE_PLATFORM_WIN32_KHR"]) diff --git a/drivers/vulkan/rendering_context_driver_vulkan.cpp b/drivers/vulkan/rendering_context_driver_vulkan.cpp index 7cba820978..fe2ff5e0da 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_context_driver_vulkan.cpp @@ -102,6 +102,10 @@ Error RenderingContextDriverVulkan::_initialize_instance_extensions() { // This extension allows us to use the properties2 features to query additional device capabilities. _register_requested_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, false); +#if defined(USE_VOLK) && (defined(MACOS_ENABLED) || defined(IOS_ENABLED)) + _register_requested_instance_extension(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, true); +#endif + // Only enable debug utils in verbose mode or DEV_ENABLED. // End users would get spammed with messages of varying verbosity due to the // mess that thirdparty layers/extensions and drivers seem to leave in their @@ -360,6 +364,11 @@ Error RenderingContextDriverVulkan::_initialize_instance() { VkInstanceCreateInfo instance_info = {}; instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + +#if defined(USE_VOLK) && (defined(MACOS_ENABLED) || defined(IOS_ENABLED)) + instance_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif + instance_info.pApplicationInfo = &app_info; instance_info.enabledExtensionCount = enabled_extension_names.size(); instance_info.ppEnabledExtensionNames = enabled_extension_names.ptr(); diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index ad7598202f..48b9e01fd8 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -965,7 +965,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { real_t minimum_value = INFINITY; real_t maximum_value = -INFINITY; - for (const IntPair &E : selection) { + for (const IntPair &E : focused_keys) { IntPair key_pair = E; real_t time = animation->track_get_key_time(key_pair.first, key_pair.second); @@ -1096,7 +1096,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { for (int i = 0; i < animation->track_get_key_count(track); ++i) { undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, track, animation->track_get_key_time(track, i), animation->bezier_track_get_key_value(track, i), @@ -1220,7 +1221,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { //insert new point if (mb->get_position().x >= limit && mb->get_position().x < get_size().width && mb->is_command_or_control_pressed()) { float h = (get_size().height / 2.0 - mb->get_position().y) * timeline_v_zoom + timeline_v_scroll; - Array new_point = make_default_bezier_key(h); + Array new_point = animation->make_default_bezier_key(h); real_t time = ((mb->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { @@ -1370,7 +1371,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { key[0] = h; undo_redo->add_do_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, E->get().first, newpos, key[0], @@ -1391,7 +1393,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { Array key = animation->track_get_key_value(E->get().first, E->get().second); undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, E->get().first, oldpos, key[0], @@ -1409,7 +1412,8 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) { undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, 1); undo_redo->add_undo_method( this, - "_bezier_track_insert_key", + "_bezier_track_insert_key_at_anim", + animation, amr.track, amr.time, key[0], @@ -1643,19 +1647,6 @@ void AnimationBezierTrackEdit::_zoom_callback(float p_zoom_factor, Vector2 p_ori queue_redraw(); } -Array AnimationBezierTrackEdit::make_default_bezier_key(float p_value) { - Array new_point; - new_point.resize(5); - - new_point[0] = p_value; - new_point[1] = -0.25; - new_point[2] = 0; - new_point[3] = 0.25; - new_point[4] = 0; - - return new_point; -} - float AnimationBezierTrackEdit::get_bezier_key_value(Array p_bezier_key_array) { return p_bezier_key_array[0]; } @@ -1675,7 +1666,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { time += 0.001; } float h = (get_size().height / 2.0 - menu_insert_key.y) * timeline_v_zoom + timeline_v_scroll; - Array new_point = make_default_bezier_key(h); + Array new_point = animation->make_default_bezier_key(h); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Add Bezier Point")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, time, new_point); @@ -1877,7 +1868,7 @@ void AnimationBezierTrackEdit::paste_keys(real_t p_ofs, bool p_ofs_valid) { Variant value = key.value; if (key.track_type != Animation::TYPE_BEZIER) { - value = make_default_bezier_key(key.value); + value = animation->make_default_bezier_key(key.value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, dst_time, value, key.transition); @@ -1931,10 +1922,9 @@ void AnimationBezierTrackEdit::delete_selection() { } } -void AnimationBezierTrackEdit::_bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode) { - ERR_FAIL_COND(animation.is_null()); - int idx = animation->bezier_track_insert_key(p_track, p_time, p_value, p_in_handle, p_out_handle); - animation->bezier_track_set_key_handle_mode(p_track, idx, p_handle_mode); +void AnimationBezierTrackEdit::_bezier_track_insert_key_at_anim(const Ref<Animation> &p_anim, int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode) { + int idx = p_anim->bezier_track_insert_key(p_track, p_time, p_value, p_in_handle, p_out_handle); + p_anim->bezier_track_set_key_handle_mode(p_track, idx, p_handle_mode); } void AnimationBezierTrackEdit::_bind_methods() { @@ -1943,7 +1933,7 @@ void AnimationBezierTrackEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("_select_at_anim"), &AnimationBezierTrackEdit::_select_at_anim); ClassDB::bind_method(D_METHOD("_update_hidden_tracks_after"), &AnimationBezierTrackEdit::_update_hidden_tracks_after); ClassDB::bind_method(D_METHOD("_update_locked_tracks_after"), &AnimationBezierTrackEdit::_update_locked_tracks_after); - ClassDB::bind_method(D_METHOD("_bezier_track_insert_key"), &AnimationBezierTrackEdit::_bezier_track_insert_key); + ClassDB::bind_method(D_METHOD("_bezier_track_insert_key_at_anim"), &AnimationBezierTrackEdit::_bezier_track_insert_key_at_anim); ADD_SIGNAL(MethodInfo("select_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "single"), PropertyInfo(Variant::INT, "track"))); ADD_SIGNAL(MethodInfo("deselect_key", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::INT, "track"))); diff --git a/editor/animation_bezier_editor.h b/editor/animation_bezier_editor.h index 888dad5341..dd7e8758f3 100644 --- a/editor/animation_bezier_editor.h +++ b/editor/animation_bezier_editor.h @@ -198,7 +198,6 @@ protected: void _notification(int p_what); public: - static Array make_default_bezier_key(float p_value); static float get_bezier_key_value(Array p_bezier_key_array); virtual String get_tooltip(const Point2 &p_pos) const override; @@ -222,7 +221,7 @@ public: void paste_keys(real_t p_ofs, bool p_ofs_valid); void delete_selection(); - void _bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode); + void _bezier_track_insert_key_at_anim(const Ref<Animation> &p_anim, int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode); AnimationBezierTrackEdit(); }; diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index a3d0dfb89b..baf2c6502f 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -3690,7 +3690,7 @@ void AnimationTrackEditor::_name_limit_changed() { } void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_timeline_only) { - emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only); + emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only, false); } void AnimationTrackEditor::_track_remove_request(int p_track) { @@ -3787,6 +3787,7 @@ void AnimationTrackEditor::set_anim_pos(float p_pos) { } _redraw_groups(); bezier_edit->set_play_position(p_pos); + emit_signal(SNAME("timeline_changed"), p_pos, true, true); } static bool track_type_is_resettable(Animation::TrackType p_type) { @@ -3867,15 +3868,23 @@ void AnimationTrackEditor::commit_insert_queue() { } // Skip the confirmation dialog if the user holds Shift while clicking the key icon. - if (!Input::get_singleton()->is_key_pressed(Key::SHIFT) && num_tracks > 0) { - String shortcut_hint = TTR("Hold Shift when clicking the key icon to skip this dialog."); + // If `confirm_insert_track` editor setting is disabled, the behavior is reversed. + bool confirm_insert = EDITOR_GET("editors/animation/confirm_insert_track"); + if ((Input::get_singleton()->is_key_pressed(Key::SHIFT) != confirm_insert) && num_tracks > 0) { + String dialog_text; + // Potentially a new key, does not exist. if (num_tracks == 1) { // TRANSLATORS: %s will be replaced by a phrase describing the target of track. - insert_confirm_text->set_text(vformat(TTR("Create new track for %s and insert key?") + "\n\n" + shortcut_hint, last_track_query)); + dialog_text = vformat(TTR("Create new track for %s and insert key?"), last_track_query); } else { - insert_confirm_text->set_text(vformat(TTR("Create %d new tracks and insert keys?") + "\n\n" + shortcut_hint, num_tracks)); + dialog_text = vformat(TTR("Create %d new tracks and insert keys?"), num_tracks); + } + + if (confirm_insert) { + dialog_text += +"\n\n" + TTR("Hold Shift when clicking the key icon to skip this dialog."); } + insert_confirm_text->set_text(dialog_text); insert_confirm_bezier->set_visible(all_bezier); insert_confirm_reset->set_visible(reset_allowed); @@ -4456,16 +4465,8 @@ AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertD } break; case Animation::TYPE_BEZIER: { - Array array; - array.resize(5); - array[0] = p_id.value; - array[1] = -0.25; - array[2] = 0; - array[3] = 0.25; - array[4] = 0; - value = array; + value = animation->make_default_bezier_key(p_id.value); bezier_edit_icon->set_disabled(false); - } break; default: { // Other track types shouldn't use this code path. @@ -5267,15 +5268,7 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { NodePath bp; Variant value; _find_hint_for_track(p_track, bp, &value); - Array arr; - arr.resize(5); - arr[0] = value; - arr[1] = -0.25; - arr[2] = 0; - arr[3] = 0.25; - arr[4] = 0; - - id.value = arr; + id.value = animation->make_default_bezier_key(value); } break; case Animation::TYPE_AUDIO: { Dictionary ak; @@ -5819,7 +5812,7 @@ void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, i if (key_is_bezier && !track_is_bezier) { value = AnimationBezierTrackEdit::get_bezier_key_value(value); } else if (!key_is_bezier && track_is_bezier) { - value = AnimationBezierTrackEdit::make_default_bezier_key(value); + value = animation->make_default_bezier_key(value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, animation->track_get_key_transition(E->key().track, E->key().key)); @@ -5963,7 +5956,7 @@ void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p if (key_is_bezier && !track_is_bezier) { value = AnimationBezierTrackEdit::get_bezier_key_value(value); } else if (!key_is_bezier && track_is_bezier) { - value = AnimationBezierTrackEdit::make_default_bezier_key(value); + value = animation->make_default_bezier_key(value); } undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, key.transition); diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index bfc4d91af7..eb0ab1174b 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -1377,6 +1377,8 @@ void ConnectionsDock::_notification(int p_what) { slot_menu->set_item_icon(slot_menu->get_item_index(SLOT_MENU_DISCONNECT), get_editor_theme_icon(SNAME("Unlinked"))); tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor))); + + update_tree(); } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index ccb47220db..75135532aa 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -277,7 +277,7 @@ void EditorDockManager::_restore_dock_to_saved_window(Control *p_dock, const Dic p_window_dump.get("window_screen_rect", Rect2i())); } -void EditorDockManager::_dock_move_to_bottom(Control *p_dock) { +void EditorDockManager::_dock_move_to_bottom(Control *p_dock, bool p_visible) { _move_dock(p_dock, nullptr); all_docks[p_dock].at_bottom = true; @@ -288,7 +288,7 @@ void EditorDockManager::_dock_move_to_bottom(Control *p_dock) { // Force docks moved to the bottom to appear first in the list, and give them their associated shortcut to toggle their bottom panel. Button *bottom_button = EditorNode::get_bottom_panel()->add_item(all_docks[p_dock].title, p_dock, all_docks[p_dock].shortcut, true); bottom_button->connect(SceneStringName(gui_input), callable_mp(this, &EditorDockManager::_bottom_dock_button_gui_input).bind(bottom_button).bind(p_dock)); - EditorNode::get_bottom_panel()->make_item_visible(p_dock); + EditorNode::get_bottom_panel()->make_item_visible(p_dock, p_visible); } void EditorDockManager::_dock_remove_from_bottom(Control *p_dock) { @@ -548,11 +548,13 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S // Don't open disabled docks. continue; } + bool at_bottom = false; if (restore_window_on_load && floating_docks_dump.has(name)) { all_docks[dock].previous_at_bottom = dock_bottom.has(name); _restore_dock_to_saved_window(dock, floating_docks_dump[name]); } else if (dock_bottom.has(name)) { - _dock_move_to_bottom(dock); + _dock_move_to_bottom(dock, false); + at_bottom = true; } else if (i >= 0) { _move_dock(dock, dock_slot[i], 0); } @@ -564,7 +566,11 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S } else { // Make sure it is open. all_docks[dock].open = true; - dock->show(); + // It's important to not update the visibility of bottom panels. + // Visibility of bottom panels are managed in EditorBottomPanel. + if (!at_bottom) { + dock->show(); + } } all_docks[dock].dock_slot_index = i; @@ -668,7 +674,7 @@ void EditorDockManager::open_dock(Control *p_dock, bool p_set_current) { // Open dock to its previous location. if (all_docks[p_dock].previous_at_bottom) { - _dock_move_to_bottom(p_dock); + _dock_move_to_bottom(p_dock, true); } else if (all_docks[p_dock].dock_slot_index != DOCK_SLOT_NONE) { TabContainer *slot = dock_slot[all_docks[p_dock].dock_slot_index]; int tab_index = all_docks[p_dock].previous_tab_index; @@ -899,7 +905,7 @@ void DockContextPopup::_float_dock() { void DockContextPopup::_move_dock_to_bottom() { hide(); - dock_manager->_dock_move_to_bottom(context_dock); + dock_manager->_dock_move_to_bottom(context_dock, true); dock_manager->_update_layout(); } diff --git a/editor/editor_dock_manager.h b/editor/editor_dock_manager.h index 226222c55a..1e6b413d14 100644 --- a/editor/editor_dock_manager.h +++ b/editor/editor_dock_manager.h @@ -121,7 +121,7 @@ private: void _open_dock_in_window(Control *p_dock, bool p_show_window = true, bool p_reset_size = false); void _restore_dock_to_saved_window(Control *p_dock, const Dictionary &p_window_dump); - void _dock_move_to_bottom(Control *p_dock); + void _dock_move_to_bottom(Control *p_dock, bool p_visible); void _dock_remove_from_bottom(Control *p_dock); bool _is_dock_at_bottom(Control *p_dock); diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index f0dc850af0..4664defa59 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -150,6 +150,11 @@ uint64_t EditorFileSystemDirectory::get_file_modified_time(int p_idx) const { return files[p_idx]->modified_time; } +uint64_t EditorFileSystemDirectory::get_file_import_modified_time(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, files.size(), 0); + return files[p_idx]->import_modified_time; +} + String EditorFileSystemDirectory::get_file_script_class_name(int p_idx) const { return files[p_idx]->script_class_name; } @@ -326,8 +331,8 @@ void EditorFileSystem::_scan_filesystem() { FileCache fc; fc.type = split[1]; if (fc.type.contains("/")) { - fc.type = fc.type.get_slice("/", 0); - fc.resource_script_class = fc.type.get_slice("/", 1); + fc.type = split[1].get_slice("/", 0); + fc.resource_script_class = split[1].get_slice("/", 1); } fc.uid = split[2].to_int(); fc.modification_time = split[3].to_int(); @@ -720,12 +725,22 @@ bool EditorFileSystem::_update_scan_actions() { int idx = ia.dir->find_file_index(ia.file); ERR_CONTINUE(idx == -1); String full_path = ia.dir->get_file_path(idx); - if (_test_for_reimport(full_path, false)) { + + bool need_reimport = _test_for_reimport(full_path, false); + if (!need_reimport && FileAccess::exists(full_path + ".import")) { + uint64_t import_mt = ia.dir->get_file_import_modified_time(idx); + if (import_mt != FileAccess::get_modified_time(full_path + ".import")) { + need_reimport = true; + } + } + + if (need_reimport) { //must reimport reimports.push_back(full_path); Vector<String> dependencies = _get_dependencies(full_path); - for (const String &dependency_path : dependencies) { - if (import_extensions.has(dependency_path.get_extension())) { + for (const String &dep : dependencies) { + const String &dependency_path = dep.contains("::") ? dep.get_slice("::", 0) : dep; + if (import_extensions.has(dep.get_extension())) { reimports.push_back(dependency_path); } } @@ -1748,7 +1763,8 @@ String EditorFileSystem::_get_global_script_class(const String &p_type, const St void EditorFileSystem::_update_file_icon_path(EditorFileSystemDirectory::FileInfo *file_info) { String icon_path; if (file_info->script_class_icon_path.is_empty() && !file_info->deps.is_empty()) { - const String &script_path = file_info->deps[0]; // Assuming the first dependency is a script. + const String &script_dep = file_info->deps[0]; // Assuming the first dependency is a script. + const String &script_path = script_dep.contains("::") ? script_dep.get_slice("::", 2) : script_dep; if (!script_path.is_empty()) { String *cached = file_icon_cache.getptr(script_path); if (cached) { @@ -1891,7 +1907,7 @@ void EditorFileSystem::_update_scene_groups() { continue; } - const HashSet<StringName> scene_groups = _get_scene_groups(path); + const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path); if (!scene_groups.is_empty()) { ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups); } @@ -1935,12 +1951,6 @@ void EditorFileSystem::_get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet } } -HashSet<StringName> EditorFileSystem::_get_scene_groups(const String &p_path) { - Ref<PackedScene> packed_scene = ResourceLoader::load(p_path); - ERR_FAIL_COND_V(packed_scene.is_null(), HashSet<StringName>()); - return packed_scene->get_state()->get_all_groups(); -} - void EditorFileSystem::update_file(const String &p_file) { ERR_FAIL_COND(p_file.is_empty()); update_files({ p_file }); @@ -2072,7 +2082,6 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { } if (updated) { - _process_update_pending(); if (update_files_icon_cache) { _update_files_icon_path(); } else { @@ -2080,7 +2089,10 @@ void EditorFileSystem::update_files(const Vector<String> &p_script_paths) { _update_file_icon_path(fi); } } - call_deferred(SNAME("emit_signal"), "filesystem_changed"); //update later + if (!is_scanning()) { + _process_update_pending(); + call_deferred(SNAME("emit_signal"), "filesystem_changed"); //update later + } } } diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index b0c6f0de51..1bc24416eb 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -89,6 +89,7 @@ public: Vector<String> get_file_deps(int p_idx) const; bool get_file_import_is_valid(int p_idx) const; uint64_t get_file_modified_time(int p_idx) const; + uint64_t get_file_import_modified_time(int p_idx) const; String get_file_script_class_name(int p_idx) const; //used for scripts String get_file_script_class_extends(int p_idx) const; //used for scripts String get_file_script_class_icon_path(int p_idx) const; //used for scripts @@ -290,7 +291,6 @@ class EditorFileSystem : public Node { void _queue_update_scene_groups(const String &p_path); void _update_scene_groups(); void _update_pending_scene_groups(); - HashSet<StringName> _get_scene_groups(const String &p_path); void _get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet<String> &r_list); String _get_global_script_class(const String &p_type, const String &p_path, String *r_extends, String *r_icon_path) const; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 00ac1c7c6f..5725129f65 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -3761,6 +3761,12 @@ EditorHelpBit::EditorHelpBit(const String &p_symbol) { /// EditorHelpBitTooltip /// +void EditorHelpBitTooltip::_start_timer() { + if (timer->is_inside_tree() && timer->is_stopped()) { + timer->start(); + } +} + void EditorHelpBitTooltip::_safe_queue_free() { if (_pushing_input > 0) { _need_free = true; @@ -3769,13 +3775,20 @@ void EditorHelpBitTooltip::_safe_queue_free() { } } +void EditorHelpBitTooltip::_target_gui_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventMouse> mouse_event = p_event; + if (mouse_event.is_valid()) { + _start_timer(); + } +} + void EditorHelpBitTooltip::_notification(int p_what) { switch (p_what) { case NOTIFICATION_WM_MOUSE_ENTER: timer->stop(); break; case NOTIFICATION_WM_MOUSE_EXIT: - timer->start(); + _start_timer(); break; } } @@ -3783,7 +3796,7 @@ void EditorHelpBitTooltip::_notification(int p_what) { // Forwards non-mouse input to the parent viewport. void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) { if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { - hide(); // Will be deleted on its timer. + _safe_queue_free(); } else { const Ref<InputEventMouse> mouse_event = p_event; if (mouse_event.is_null()) { @@ -3801,7 +3814,7 @@ void EditorHelpBitTooltip::_input_from_window(const Ref<InputEvent> &p_event) { void EditorHelpBitTooltip::show_tooltip(EditorHelpBit *p_help_bit, Control *p_target) { ERR_FAIL_NULL(p_help_bit); EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target)); - p_help_bit->connect("request_hide", callable_mp(static_cast<Window *>(tooltip), &Window::hide)); // Will be deleted on its timer. + p_help_bit->connect("request_hide", callable_mp(tooltip, &EditorHelpBitTooltip::_safe_queue_free)); tooltip->add_child(p_help_bit); p_target->get_viewport()->add_child(tooltip); p_help_bit->update_content_height(); @@ -3858,8 +3871,8 @@ EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) { add_child(timer); ERR_FAIL_NULL(p_target); - p_target->connect(SceneStringName(mouse_entered), callable_mp(timer, &Timer::stop)); - p_target->connect(SceneStringName(mouse_exited), callable_mp(timer, &Timer::start).bind(-1)); + p_target->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorHelpBitTooltip::_start_timer)); + p_target->connect(SceneStringName(gui_input), callable_mp(this, &EditorHelpBitTooltip::_target_gui_input)); } #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) diff --git a/editor/editor_help.h b/editor/editor_help.h index 8d1fec713e..93f74cb2c1 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -329,7 +329,9 @@ class EditorHelpBitTooltip : public PopupPanel { int _pushing_input = 0; bool _need_free = false; + void _start_timer(); void _safe_queue_free(); + void _target_gui_input(const Ref<InputEvent> &p_event); protected: void _notification(int p_what); diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index 6a016c217a..0dfbcd0e0d 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -58,8 +58,8 @@ void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_f MessageType message_type = p_type == ERR_HANDLER_WARNING ? MSG_TYPE_WARNING : MSG_TYPE_ERROR; - if (self->current != Thread::get_caller_id()) { - callable_mp(self, &EditorLog::add_message).call_deferred(err_str, message_type); + if (!Thread::is_main_thread()) { + MessageQueue::get_main_singleton()->push_callable(callable_mp(self, &EditorLog::add_message), err_str, message_type); } else { self->add_message(err_str, message_type); } @@ -557,8 +557,6 @@ EditorLog::EditorLog() { eh.errfunc = _error_handler; eh.userdata = this; add_error_handler(&eh); - - current = Thread::get_caller_id(); } void EditorLog::deinit() { diff --git a/editor/editor_log.h b/editor/editor_log.h index 7012a2a43c..9c652e912a 100644 --- a/editor/editor_log.h +++ b/editor/editor_log.h @@ -156,8 +156,6 @@ private: ErrorHandlerList eh; - Thread::ID current; - //void _dragged(const Point2& p_ofs); void _meta_clicked(const String &p_meta); void _clear_request(); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 71603e6190..fd49920c6b 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -317,14 +317,10 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) { Ref<InputEventKey> k = p_event; if ((k.is_valid() && k->is_pressed() && !k->is_echo()) || Object::cast_to<InputEventShortcut>(*p_event)) { - EditorPlugin *old_editor = editor_plugin_screen; - + bool is_handled = true; if (ED_IS_SHORTCUT("editor/filter_files", p_event)) { FileSystemDock::get_singleton()->focus_on_filter(); - get_tree()->get_root()->set_input_as_handled(); - } - - if (ED_IS_SHORTCUT("editor/editor_2d", p_event)) { + } else if (ED_IS_SHORTCUT("editor/editor_2d", p_event)) { editor_select(EDITOR_2D); } else if (ED_IS_SHORTCUT("editor/editor_3d", p_event)) { editor_select(EDITOR_3D); @@ -343,9 +339,10 @@ void EditorNode::shortcut_input(const Ref<InputEvent> &p_event) { } else if (ED_IS_SHORTCUT("editor/toggle_last_opened_bottom_panel", p_event)) { bottom_panel->toggle_last_opened_bottom_panel(); } else { + is_handled = false; } - if (old_editor != editor_plugin_screen) { + if (is_handled) { get_tree()->get_root()->set_input_as_handled(); } } @@ -5261,6 +5258,14 @@ bool EditorNode::has_scenes_in_session() { return !scenes.is_empty(); } +void EditorNode::undo() { + trigger_menu_option(EDIT_UNDO, true); +} + +void EditorNode::redo() { + trigger_menu_option(EDIT_REDO, true); +} + bool EditorNode::ensure_main_scene(bool p_from_native) { pick_main_scene->set_meta("from_native", p_from_native); // Whether from play button or native run. String main_scene = GLOBAL_GET("application/run/main_scene"); @@ -5973,9 +5978,6 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins is_editable = owner->is_editable_instance(original_node); } - // For clear instance state for path recaching. - instantiated_node->set_scene_instance_state(Ref<SceneState>()); - 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(); diff --git a/editor/editor_node.h b/editor/editor_node.h index 28bd692ddf..7a26156ab8 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -916,6 +916,9 @@ public: bool has_scenes_in_session(); + void undo(); + void redo(); + int execute_and_show_output(const String &p_title, const String &p_path, const List<String> &p_arguments, bool p_close_on_ok = true, bool p_close_on_errors = false, String *r_output = nullptr); EditorNode(); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index e021be9668..5d3cc80da9 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -474,6 +474,11 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/vsync_mode", 1, "Disabled,Enabled,Adaptive,Mailbox") EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/update_continuously", false, "") +#ifdef ANDROID_ENABLED + EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/android/use_accumulated_input", true, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) + EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/android/use_input_buffering", true, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) +#endif + // Inspector EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "interface/inspector/max_array_dictionary_items_per_page", 20, "10,100,1") EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "interface/inspector/show_low_level_opentype_features", false, "") @@ -781,6 +786,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { // Animation _initial_set("editors/animation/autorename_animation_tracks", true); + _initial_set("editors/animation/confirm_insert_track", true); _initial_set("editors/animation/default_create_bezier_tracks", false); _initial_set("editors/animation/default_create_reset_tracks", true); _initial_set("editors/animation/onion_layers_past_color", Color(1, 0, 0)); diff --git a/editor/editor_settings_dialog.cpp b/editor/editor_settings_dialog.cpp index 6fd6a7103f..a71d43ad51 100644 --- a/editor/editor_settings_dialog.cpp +++ b/editor/editor_settings_dialog.cpp @@ -168,28 +168,17 @@ void EditorSettingsDialog::_notification(int p_what) { } void EditorSettingsDialog::shortcut_input(const Ref<InputEvent> &p_event) { - ERR_FAIL_COND(p_event.is_null()); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - const Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed()) { bool handled = false; if (ED_IS_SHORTCUT("ui_undo", p_event)) { - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Undo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } - undo_redo->undo(); + EditorNode::get_singleton()->undo(); handled = true; } if (ED_IS_SHORTCUT("ui_redo", p_event)) { - undo_redo->redo(); - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Redo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } + EditorNode::get_singleton()->redo(); handled = true; } diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 527544fea3..c0646dc572 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -797,10 +797,6 @@ String EditorExportPlatform::_export_customize(const String &p_path, LocalVector if (!customize_scenes_plugins.is_empty()) { for (Ref<EditorExportPlugin> &plugin : customize_scenes_plugins) { Node *customized = plugin->_customize_scene(node, p_path); - if (plugin->skipped) { - plugin->_clear(); - return String(); - } if (customized != nullptr) { node = customized; modified = true; @@ -834,10 +830,6 @@ String EditorExportPlatform::_export_customize(const String &p_path, LocalVector if (!customize_resources_plugins.is_empty()) { for (Ref<EditorExportPlugin> &plugin : customize_resources_plugins) { Ref<Resource> new_res = plugin->_customize_resource(res, p_path); - if (plugin->skipped) { - plugin->_clear(); - return String(); - } if (new_res.is_valid()) { modified = true; if (new_res != res) { @@ -1132,33 +1124,97 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } //store everything in the export medium - int idx = 0; int total = paths.size(); + // idx is incremented at the beginning of the paths loop to easily allow + // for continue statements without accidentally skipping an increment. + int idx = total > 0 ? -1 : 0; for (const String &E : paths) { + idx++; String path = E; String type = ResourceLoader::get_resource_type(path); - if (FileAccess::exists(path + ".import")) { - // Before doing this, try to see if it can be customized. + bool has_import_file = FileAccess::exists(path + ".import"); + Ref<ConfigFile> config; + if (has_import_file) { + config.instantiate(); + err = config->load(path + ".import"); + if (err != OK) { + ERR_PRINT("Could not parse: '" + path + "', not exported."); + continue; + } + + String importer_type = config->get_value("remap", "importer"); + + if (importer_type == "skip") { + // Skip file. + continue; + } + } + + bool do_export = true; + for (int i = 0; i < export_plugins.size(); i++) { + if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_file)) { + export_plugins.write[i]->_export_file_script(path, type, features_psa); + } else { + export_plugins.write[i]->_export_file(path, type, features); + } + if (p_so_func) { + for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { + err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); + if (err != OK) { + return err; + } + } + } + + for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { + err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key); + if (err != OK) { + return err; + } + if (export_plugins[i]->extra_files[j].remap) { + do_export = false; // If remap, do not. + path_remaps.push_back(path); + path_remaps.push_back(export_plugins[i]->extra_files[j].path); + } + } + + if (export_plugins[i]->skipped) { + do_export = false; + } + export_plugins.write[i]->_clear(); + + if (!do_export) { + break; + } + } + if (!do_export) { + continue; + } + + if (has_import_file) { + String importer_type = config->get_value("remap", "importer"); + + if (importer_type == "keep") { + // Just keep file as-is. + Vector<uint8_t> array = FileAccess::get_file_as_bytes(path); + err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key); + + if (err != OK) { + return err; + } - String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false); - if (export_path.is_empty()) { - // Skipped from plugin. continue; } + // Before doing this, try to see if it can be customized. + String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, false); + if (export_path != path) { // It was actually customized. // Since the original file is likely not recognized, just use the import system. - Ref<ConfigFile> config; - config.instantiate(); - err = config->load(path + ".import"); - if (err != OK) { - ERR_PRINT("Could not parse: '" + path + "', not exported."); - continue; - } config->set_value("remap", "type", ResourceLoader::get_resource_type(export_path)); // Erase all Paths. @@ -1194,33 +1250,6 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } } else { // File is imported and not customized, replace by what it imports. - Ref<ConfigFile> config; - config.instantiate(); - err = config->load(path + ".import"); - if (err != OK) { - ERR_PRINT("Could not parse: '" + path + "', not exported."); - continue; - } - - String importer_type = config->get_value("remap", "importer"); - - if (importer_type == "skip") { - // Skip file. - continue; - } - - if (importer_type == "keep") { - // Just keep file as-is. - Vector<uint8_t> array = FileAccess::get_file_as_bytes(path); - err = p_func(p_udata, path, array, idx, total, enc_in_filters, enc_ex_filters, key); - - if (err != OK) { - return err; - } - - continue; - } - List<String> remaps; config->get_section_keys("remap", &remaps); @@ -1282,66 +1311,24 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & } } else { - // Customize. + // Just store it as it comes. - bool do_export = true; - for (int i = 0; i < export_plugins.size(); i++) { - if (GDVIRTUAL_IS_OVERRIDDEN_PTR(export_plugins[i], _export_file)) { - export_plugins.write[i]->_export_file_script(path, type, features_psa); - } else { - export_plugins.write[i]->_export_file(path, type, features); - } - if (p_so_func) { - for (int j = 0; j < export_plugins[i]->shared_objects.size(); j++) { - err = p_so_func(p_udata, export_plugins[i]->shared_objects[j]); - if (err != OK) { - return err; - } - } - } - - for (int j = 0; j < export_plugins[i]->extra_files.size(); j++) { - err = p_func(p_udata, export_plugins[i]->extra_files[j].path, export_plugins[i]->extra_files[j].data, idx, total, enc_in_filters, enc_ex_filters, key); - if (err != OK) { - return err; - } - if (export_plugins[i]->extra_files[j].remap) { - do_export = false; //if remap, do not - path_remaps.push_back(path); - path_remaps.push_back(export_plugins[i]->extra_files[j].path); - } - } + // Customization only happens if plugins did not take care of it before. + bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn"); + String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary); - if (export_plugins[i]->skipped) { - do_export = false; - } - export_plugins.write[i]->_clear(); - - if (!do_export) { - break; //apologies, not exporting - } + if (export_path != path) { + // Add a remap entry. + path_remaps.push_back(path); + path_remaps.push_back(export_path); } - //just store it as it comes - if (do_export) { - // Customization only happens if plugins did not take care of it before - bool force_binary = convert_text_to_binary && (path.get_extension().to_lower() == "tres" || path.get_extension().to_lower() == "tscn"); - String export_path = _export_customize(path, customize_resources_plugins, customize_scenes_plugins, export_cache, export_base_path, force_binary); - - if (export_path != path) { - // Add a remap entry - path_remaps.push_back(path); - path_remaps.push_back(export_path); - } - Vector<uint8_t> array = FileAccess::get_file_as_bytes(export_path); - err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key); - if (err != OK) { - return err; - } + Vector<uint8_t> array = FileAccess::get_file_as_bytes(export_path); + err = p_func(p_udata, export_path, array, idx, total, enc_in_filters, enc_ex_filters, key); + if (err != OK) { + return err; } } - - idx++; } if (convert_text_to_binary || !customize_resources_plugins.is_empty() || !customize_scenes_plugins.is_empty()) { diff --git a/editor/groups_editor.cpp b/editor/groups_editor.cpp index 18ac4074da..6cfc035fc9 100644 --- a/editor/groups_editor.cpp +++ b/editor/groups_editor.cpp @@ -369,6 +369,7 @@ void GroupsEditor::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { filter->set_right_icon(get_editor_theme_icon("Search")); add->set_icon(get_editor_theme_icon("Add")); + _update_tree(); } break; case NOTIFICATION_VISIBILITY_CHANGED: { if (groups_dirty && is_visible_in_tree()) { diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index f2c4a13e05..3e74a3c94e 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -178,7 +178,11 @@ Button *EditorBottomPanel::add_item(String p_text, Control *p_item, const Ref<Sh bpi.button = tb; bpi.control = p_item; bpi.name = p_text; - items.push_back(bpi); + if (p_at_front) { + items.insert(0, bpi); + } else { + items.push_back(bpi); + } return tb; } diff --git a/editor/plugins/animation_library_editor.cpp b/editor/plugins/animation_library_editor.cpp index afe7ea83d8..b07db993ba 100644 --- a/editor/plugins/animation_library_editor.cpp +++ b/editor/plugins/animation_library_editor.cpp @@ -781,6 +781,27 @@ void AnimationLibraryEditor::_update_editor(Object *p_mixer) { emit_signal("update_editor", p_mixer); } +void AnimationLibraryEditor::shortcut_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventKey> k = p_event; + if (k.is_valid() && k->is_pressed()) { + bool handled = false; + + if (ED_IS_SHORTCUT("ui_undo", p_event)) { + EditorNode::get_singleton()->undo(); + handled = true; + } + + if (ED_IS_SHORTCUT("ui_redo", p_event)) { + EditorNode::get_singleton()->redo(); + handled = true; + } + + if (handled) { + set_input_as_handled(); + } + } +} + void AnimationLibraryEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_editor", "mixer"), &AnimationLibraryEditor::_update_editor); ADD_SIGNAL(MethodInfo("update_editor")); @@ -788,6 +809,7 @@ void AnimationLibraryEditor::_bind_methods() { AnimationLibraryEditor::AnimationLibraryEditor() { set_title(TTR("Edit Animation Libraries")); + set_process_shortcut_input(true); file_dialog = memnew(EditorFileDialog); add_child(file_dialog); diff --git a/editor/plugins/animation_library_editor.h b/editor/plugins/animation_library_editor.h index c8d9274f4f..beb34c6343 100644 --- a/editor/plugins/animation_library_editor.h +++ b/editor/plugins/animation_library_editor.h @@ -113,6 +113,7 @@ class AnimationLibraryEditor : public AcceptDialog { protected: void _notification(int p_what); void _update_editor(Object *p_mixer); + virtual void shortcut_input(const Ref<InputEvent> &p_event) override; static void _bind_methods(); public: diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 484d2b1fff..02fa582da4 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -1395,23 +1395,14 @@ void AnimationPlayerEditor::_current_animation_changed(const String &p_name) { void AnimationPlayerEditor::_animation_key_editor_anim_len_changed(float p_len) { frame->set_max(p_len); } - -void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only) { +void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timeline_only, bool p_update_position_only) { timeline_position = p_pos; - if (!is_visible_in_tree()) { - return; - } - - if (!player) { - return; - } - - if (player->is_playing()) { - return; - } - - if (!player->has_animation(player->get_assigned_animation())) { + if (!is_visible_in_tree() || + p_update_position_only || + !player || + player->is_playing() || + !player->has_animation(player->get_assigned_animation())) { return; } diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index e624522566..4a3b1f37ab 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -214,7 +214,7 @@ class AnimationPlayerEditor : public VBoxContainer { void _animation_player_changed(Object *p_pl); void _animation_libraries_updated(); - void _animation_key_editor_seek(float p_pos, bool p_timeline_only = false); + void _animation_key_editor_seek(float p_pos, bool p_timeline_only = false, bool p_update_position_only = false); void _animation_key_editor_anim_len_changed(float p_len); virtual void shortcut_input(const Ref<InputEvent> &p_ev) override; diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 4e2940a8cb..294df95874 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -5199,7 +5199,7 @@ CanvasItemEditor::CanvasItemEditor() { SceneTreeDock::get_singleton()->connect("node_created", callable_mp(this, &CanvasItemEditor::_adjust_new_node_position)); SceneTreeDock::get_singleton()->connect("add_node_used", callable_mp(this, &CanvasItemEditor::_reset_create_position)); - // Add some margin to the sides for better esthetics. + // Add some margin to the sides for better aesthetics. // This prevents the first button's hover/pressed effect from "touching" the panel's border, // which looks ugly. MarginContainer *toolbar_margin = memnew(MarginContainer); diff --git a/editor/plugins/font_config_plugin.cpp b/editor/plugins/font_config_plugin.cpp index 15b268337f..e6ce63fe36 100644 --- a/editor/plugins/font_config_plugin.cpp +++ b/editor/plugins/font_config_plugin.cpp @@ -386,15 +386,8 @@ EditorPropertyFontMetaOverride::EditorPropertyFontMetaOverride(bool p_script) { void EditorPropertyOTVariation::_property_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { if (p_property.begins_with("keys")) { Dictionary dict = object->get_dict(); - Dictionary defaults_dict = object->get_defaults(); int key = p_property.get_slice("/", 1).to_int(); dict[key] = (int)p_value; - if (defaults_dict.has(key)) { - Vector3i range = defaults_dict[key]; - if (range.z == (int)p_value) { - dict.erase(key); - } - } emit_changed(get_edited_property(), dict, "", true); @@ -422,6 +415,14 @@ void EditorPropertyOTVariation::update_property() { Dictionary supported = (fd.is_valid()) ? fd->get_supported_variation_list() : Dictionary(); + for (int i = 0; i < supported.size(); i++) { + int name_tag = supported.get_key_at_index(i); + Vector3i range = supported.get_value_at_index(i); + if ((dict.has(name_tag) && dict[name_tag].get_type() == Variant::NIL) || !dict.has(name_tag)) { + dict[name_tag] = range.z; + } + } + edit->set_text(vformat(TTR("Variation Coordinates (%d)"), supported.size())); bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property()); @@ -481,7 +482,21 @@ void EditorPropertyOTVariation::update_property() { prop->set_object_and_property(object.ptr(), "keys/" + itos(name_tag)); String name = TS->tag_to_name(name_tag); - prop->set_label(name.capitalize()); + String name_cap; + { + String aux = name.replace("_", " ").strip_edges(); + for (int j = 0; j < aux.get_slice_count(" "); j++) { + String slice = aux.get_slicec(' ', j); + if (slice.length() > 0) { + slice[0] = String::char_uppercase(slice[0]); + if (i > 0) { + name_cap += " "; + } + name_cap += slice; + } + } + } + prop->set_label(name_cap); prop->set_tooltip_text(name); prop->set_selectable(false); @@ -935,6 +950,12 @@ void FontPreview::_notification(int p_what) { font->draw_string(get_canvas_item(), Point2(0, font->get_height(font_size) + 2 * EDSCALE), TTR("Unable to preview font"), HORIZONTAL_ALIGNMENT_CENTER, get_size().x, font_size, text_color); } } break; + + case NOTIFICATION_EXIT_TREE: { + if (prev_font.is_valid()) { + prev_font->disconnect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } + } break; } } @@ -945,7 +966,17 @@ Size2 FontPreview::get_minimum_size() const { } void FontPreview::set_data(const Ref<Font> &p_f) { + if (prev_font.is_valid()) { + prev_font->disconnect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } prev_font = p_f; + if (prev_font.is_valid()) { + prev_font->connect_changed(callable_mp(this, &FontPreview::_preview_changed)); + } + queue_redraw(); +} + +void FontPreview::_preview_changed() { queue_redraw(); } diff --git a/editor/plugins/font_config_plugin.h b/editor/plugins/font_config_plugin.h index 7b2d26da4a..4e798fc3e8 100644 --- a/editor/plugins/font_config_plugin.h +++ b/editor/plugins/font_config_plugin.h @@ -225,6 +225,8 @@ protected: Ref<Font> prev_font; + void _preview_changed(); + public: virtual Size2 get_minimum_size() const override; diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index c6a0dfb888..72eea8a27e 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -8474,7 +8474,7 @@ Node3DEditor::Node3DEditor() { camera_override_viewport_id = 0; - // Add some margin to the sides for better esthetics. + // Add some margin to the sides for better aesthetics. // This prevents the first button's hover/pressed effect from "touching" the panel's border, // which looks ugly. MarginContainer *toolbar_margin = memnew(MarginContainer); diff --git a/editor/plugins/polygon_2d_editor_plugin.cpp b/editor/plugins/polygon_2d_editor_plugin.cpp index b5db7bef70..e442c37edd 100644 --- a/editor/plugins/polygon_2d_editor_plugin.cpp +++ b/editor/plugins/polygon_2d_editor_plugin.cpp @@ -52,6 +52,31 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/view_panner.h" +class UVEditDialog : public AcceptDialog { + GDCLASS(UVEditDialog, AcceptDialog); + + void shortcut_input(const Ref<InputEvent> &p_event) override { + const Ref<InputEventKey> k = p_event; + if (k.is_valid() && k->is_pressed()) { + bool handled = false; + + if (ED_IS_SHORTCUT("ui_undo", p_event)) { + EditorNode::get_singleton()->undo(); + handled = true; + } + + if (ED_IS_SHORTCUT("ui_redo", p_event)) { + EditorNode::get_singleton()->redo(); + handled = true; + } + + if (handled) { + set_input_as_handled(); + } + } + } +}; + Node2D *Polygon2DEditor::_get_node() const { return node; } @@ -1305,9 +1330,10 @@ Polygon2DEditor::Polygon2DEditor() { button_uv->connect(SceneStringName(pressed), callable_mp(this, &Polygon2DEditor::_menu_option).bind(MODE_EDIT_UV)); uv_mode = UV_MODE_EDIT_POINT; - uv_edit = memnew(AcceptDialog); - add_child(uv_edit); + uv_edit = memnew(UVEditDialog); uv_edit->set_title(TTR("Polygon 2D UV Editor")); + uv_edit->set_process_shortcut_input(true); + add_child(uv_edit); uv_edit->connect(SceneStringName(confirmed), callable_mp(this, &Polygon2DEditor::_uv_edit_popup_hide)); uv_edit->connect("canceled", callable_mp(this, &Polygon2DEditor::_uv_edit_popup_hide)); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index a670c7937b..c51eb44aee 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -272,7 +272,7 @@ void ScriptEditorBase::_bind_methods() { ADD_SIGNAL(MethodInfo("request_help", PropertyInfo(Variant::STRING, "topic"))); ADD_SIGNAL(MethodInfo("request_open_script_at_line", PropertyInfo(Variant::OBJECT, "script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("request_save_history")); - ADD_SIGNAL(MethodInfo("request_save_previous_state", PropertyInfo(Variant::INT, "line"))); + ADD_SIGNAL(MethodInfo("request_save_previous_state", PropertyInfo(Variant::DICTIONARY, "state"))); ADD_SIGNAL(MethodInfo("go_to_help", PropertyInfo(Variant::STRING, "what"))); ADD_SIGNAL(MethodInfo("search_in_files_requested", PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("replace_in_files_requested", PropertyInfo(Variant::STRING, "text"))); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 059e177874..96127ec93e 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -1819,15 +1819,25 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data CodeEdit *te = code_editor->get_text_editor(); Point2i pos = te->get_line_column_at_pos(p_point); - int row = pos.y; - int col = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = te->get_selection_at_line_column(drop_at_line, drop_at_column); + + bool line_will_be_empty = false; + if (selection_index >= 0) { + // Dropped on a selection, it will be replaced. + drop_at_line = te->get_selection_from_line(selection_index); + drop_at_column = te->get_selection_from_column(selection_index); + line_will_be_empty = drop_at_column <= te->get_first_non_whitespace_column(drop_at_line) && te->get_selection_to_column(selection_index) == te->get_line(te->get_selection_to_line(selection_index)).length(); + } + + String text_to_drop; const bool drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); - const String &line = te->get_line(row); - const bool is_empty_line = line.is_empty() || te->get_first_non_whitespace_column(row) == line.length(); + const String &line = te->get_line(drop_at_line); + const bool is_empty_line = line_will_be_empty || line.is_empty() || te->get_first_non_whitespace_column(drop_at_line) == line.length(); if (d.has("type") && String(d["type"]) == "resource") { - te->remove_secondary_carets(); Ref<Resource> resource = d["resource"]; if (resource.is_null()) { return; @@ -1840,7 +1850,6 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data return; } - String text_to_drop; if (drop_modifier_pressed) { if (resource->is_built_in()) { String warning = TTR("Preloading internal resources is not supported."); @@ -1851,19 +1860,10 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } else { text_to_drop = _quote_drop_data(path); } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && (String(d["type"]) == "files" || String(d["type"]) == "files_and_dirs")) { - te->remove_secondary_carets(); - Array files = d["files"]; - String text_to_drop; - for (int i = 0; i < files.size(); i++) { const String &path = String(files[i]); @@ -1883,15 +1883,9 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data text_to_drop += is_empty_line ? "\n" : ", "; } } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && String(d["type"]) == "nodes") { - te->remove_secondary_carets(); Node *scene_root = get_tree()->get_edited_scene_root(); if (!scene_root) { EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene.")); @@ -1909,7 +1903,6 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } Array nodes = d["nodes"]; - String text_to_drop; if (drop_modifier_pressed) { const bool use_type = EDITOR_GET("text_editor/completion/add_type_hints"); @@ -1981,27 +1974,33 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data text_to_drop += (is_unique ? "%" : "$") + path; } } - - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); } if (d.has("type") && String(d["type"]) == "obj_property") { - te->remove_secondary_carets(); - bool add_literal = EDITOR_GET("text_editor/completion/add_node_path_literals"); - String text_to_drop = add_literal ? "^" : ""; + text_to_drop = add_literal ? "^" : ""; // It is unclear whether properties may contain single or double quotes. // Assume here that double-quotes may not exist. We are escaping single-quotes if necessary. text_to_drop += _quote_drop_data(String(d["property"])); + } + + if (text_to_drop.is_empty()) { + return; + } - te->set_caret_line(row); - te->set_caret_column(col); - te->insert_text_at_caret(text_to_drop); - te->grab_focus(); + // Remove drag caret before any actions so it is not included in undo. + te->remove_drag_caret(); + te->begin_complex_operation(); + if (selection_index >= 0) { + te->delete_selection(selection_index); } + te->remove_secondary_carets(); + te->deselect(); + te->set_caret_line(drop_at_line); + te->set_caret_column(drop_at_column); + te->insert_text_at_caret(text_to_drop); + te->end_complex_operation(); + te->grab_focus(); } void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp index e8a7b3b514..f985bbc629 100644 --- a/editor/plugins/tiles/tile_data_editors.cpp +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -139,9 +139,17 @@ void GenericTilePolygonEditor::_base_control_draw() { const Ref<StyleBox> focus_stylebox = get_theme_stylebox(SNAME("Focus"), EditorStringName(EditorStyles)); // Get the background data. - TileData *tile_data = background_atlas_source->get_tile_data(background_atlas_coords, background_alternative_id); - ERR_FAIL_NULL(tile_data); - Rect2 background_region = background_atlas_source->get_tile_texture_region(background_atlas_coords); + Rect2 background_region; + TileData *tile_data = nullptr; + + if (background_atlas_source.is_valid()) { + tile_data = background_atlas_source->get_tile_data(background_atlas_coords, background_alternative_id); + ERR_FAIL_NULL(tile_data); + background_region = background_atlas_source->get_tile_texture_region(background_atlas_coords); + } else { + // If no tile was selected yet, use default size. + background_region.size = tile_set->get_tile_size(); + } // Draw the focus rectangle. if (base_control->has_focus()) { @@ -157,11 +165,14 @@ void GenericTilePolygonEditor::_base_control_draw() { base_control->draw_set_transform_matrix(xform); // Draw fill rect under texture region. - Rect2 texture_rect(-background_region.size / 2 - tile_data->get_texture_origin(), background_region.size); + Rect2 texture_rect(-background_region.size / 2, background_region.size); + if (tile_data) { + texture_rect.position -= tile_data->get_texture_origin(); + } base_control->draw_rect(texture_rect, Color(1, 1, 1, 0.3)); // Draw the background. - if (background_atlas_source->get_texture().is_valid()) { + if (tile_data && background_atlas_source->get_texture().is_valid()) { Size2 region_size = background_region.size; if (tile_data->get_flip_h()) { region_size.x = -region_size.x; @@ -174,8 +185,13 @@ void GenericTilePolygonEditor::_base_control_draw() { // Compute and draw the grid area. Rect2 grid_area = Rect2(-base_tile_size / 2, base_tile_size); - grid_area.expand_to(-background_region.get_size() / 2 - tile_data->get_texture_origin()); - grid_area.expand_to(background_region.get_size() / 2 - tile_data->get_texture_origin()); + if (tile_data) { + grid_area.expand_to(-background_region.get_size() / 2 - tile_data->get_texture_origin()); + grid_area.expand_to(background_region.get_size() / 2 - tile_data->get_texture_origin()); + } else { + grid_area.expand_to(-background_region.get_size() / 2); + grid_area.expand_to(background_region.get_size() / 2); + } base_control->draw_rect(grid_area, Color(1, 1, 1, 0.3), false); // Draw grid. diff --git a/editor/plugins/tiles/tile_map_layer_editor.cpp b/editor/plugins/tiles/tile_map_layer_editor.cpp index d3afd25502..4a59530159 100644 --- a/editor/plugins/tiles/tile_map_layer_editor.cpp +++ b/editor/plugins/tiles/tile_map_layer_editor.cpp @@ -3630,9 +3630,16 @@ TileMapLayer *TileMapLayerEditor::_get_edited_layer() const { void TileMapLayerEditor::_find_tile_map_layers_in_scene(Node *p_current, const Node *p_owner, Vector<TileMapLayer *> &r_list) const { ERR_FAIL_COND(!p_current || !p_owner); - if (p_current != p_owner && p_current->get_owner() != p_owner) { - return; + + if (p_current != p_owner) { + if (!p_current->get_owner()) { + return; + } + if (p_current->get_owner() != p_owner && !p_owner->is_editable_instance(p_current->get_owner())) { + return; + } } + TileMapLayer *layer = Object::cast_to<TileMapLayer>(p_current); if (layer) { r_list.append(layer); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 607c446e1b..2f36198b23 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -133,9 +133,9 @@ void VSRerouteNode::_notification(int p_what) { connect(SceneStringName(mouse_exited), callable_mp(this, &VSRerouteNode::_on_mouse_exited)); } break; case NOTIFICATION_DRAW: { - Vector2 offset = Vector2(0, -16); + Vector2 offset = Vector2(0, -16 * EDSCALE); Color drag_bg_color = get_theme_color(SNAME("drag_background"), SNAME("VSRerouteNode")); - draw_circle(get_size() * 0.5 + offset, 16, Color(drag_bg_color, selected ? 1 : icon_opacity)); + draw_circle(get_size() * 0.5 + offset, 16 * EDSCALE, Color(drag_bg_color, selected ? 1 : icon_opacity), true, -1, true); Ref<Texture2D> icon = get_editor_theme_icon(SNAME("ToolMove")); Point2 icon_offset = -icon->get_size() * 0.5 + get_size() * 0.5 + offset; @@ -154,6 +154,7 @@ VSRerouteNode::VSRerouteNode() { title_lbl->hide(); const Size2 size = Size2(32, 32) * EDSCALE; + print_line("VSRerouteNode size: " + size); Control *slot_area = memnew(Control); slot_area->set_custom_minimum_size(size); diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 943e345e97..bdf4e41c5f 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -235,28 +235,17 @@ void ProjectSettingsEditor::_select_type(Variant::Type p_type) { } void ProjectSettingsEditor::shortcut_input(const Ref<InputEvent> &p_event) { - ERR_FAIL_COND(p_event.is_null()); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - const Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed()) { bool handled = false; if (ED_IS_SHORTCUT("ui_undo", p_event)) { - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Undo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } - undo_redo->undo(); + EditorNode::get_singleton()->undo(); handled = true; } if (ED_IS_SHORTCUT("ui_redo", p_event)) { - undo_redo->redo(); - String action = undo_redo->get_current_action_name(); - if (!action.is_empty()) { - EditorNode::get_log()->add_message(vformat(TTR("Redo: %s"), action), EditorLog::MSG_TYPE_EDITOR); - } + EditorNode::get_singleton()->redo(); handled = true; } diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 9f0865d950..03752656c0 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1665,7 +1665,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the // GraphFrame's title Label. p_theme->set_type_variation("GraphFrameTitleLabel", "Label"); p_theme->set_stylebox(CoreStringName(normal), "GraphFrameTitleLabel", memnew(StyleBoxEmpty)); - p_theme->set_font_size(SceneStringName(font_size), "GraphFrameTitleLabel", 22); + p_theme->set_font_size(SceneStringName(font_size), "GraphFrameTitleLabel", 22 * EDSCALE); p_theme->set_color(SceneStringName(font_color), "GraphFrameTitleLabel", Color(1, 1, 1)); p_theme->set_color("font_shadow_color", "GraphFrameTitleLabel", Color(0, 0, 0, 0)); p_theme->set_color("font_outline_color", "GraphFrameTitleLabel", Color(1, 1, 1)); @@ -1680,7 +1680,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the { Ref<StyleBox> vs_reroute_panel_style = make_empty_stylebox(); Ref<StyleBox> vs_reroute_titlebar_style = vs_reroute_panel_style->duplicate(); - vs_reroute_titlebar_style->set_content_margin_all(16); + vs_reroute_titlebar_style->set_content_margin_all(16 * EDSCALE); p_theme->set_stylebox(SceneStringName(panel), "VSRerouteNode", vs_reroute_panel_style); p_theme->set_stylebox("panel_selected", "VSRerouteNode", vs_reroute_panel_style); p_theme->set_stylebox("titlebar", "VSRerouteNode", vs_reroute_titlebar_style); diff --git a/main/main.cpp b/main/main.cpp index 1cbd732747..060b3fe2f6 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -909,13 +909,11 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph // Benchmark tracking must be done after `OS::get_singleton()->initialize()` as on some // platforms, it's used to set up the time utilities. - OS::get_singleton()->benchmark_begin_measure("Startup", "Total"); - OS::get_singleton()->benchmark_begin_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Setup"); engine = memnew(Engine); MAIN_PRINT("Main: Initialize CORE"); - OS::get_singleton()->benchmark_begin_measure("Startup", "Core"); register_core_types(); register_core_driver_types(); @@ -2453,8 +2451,6 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph Thread::release_main_thread(); // If setup2() is called from another thread, that one will become main thread, so preventively release this one. set_current_thread_safe_for_nodes(false); - OS::get_singleton()->benchmark_end_measure("Startup", "Core"); - #if defined(STEAMAPI_ENABLED) if (editor || project_manager) { steam_tracker = memnew(SteamTracker); @@ -2465,7 +2461,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph return setup2(); } - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup"); return OK; error: @@ -2519,7 +2515,7 @@ error: } OS::get_singleton()->benchmark_end_measure("Startup", "Core"); - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup"); #if defined(STEAMAPI_ENABLED) if (steam_tracker) { @@ -2552,7 +2548,9 @@ Error _parse_resource_dummy(void *p_data, VariantParser::Stream *p_stream, Ref<R return OK; } -Error Main::setup2() { +Error Main::setup2(bool p_show_boot_logo) { + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Setup2"); + Thread::make_main_thread(); // Make whatever thread call this the main thread. set_current_thread_safe_for_nodes(true); @@ -2896,12 +2894,6 @@ Error Main::setup2() { MAIN_PRINT("Main: Setup Logo"); -#if !defined(TOOLS_ENABLED) && defined(WEB_ENABLED) - bool show_logo = false; -#else - bool show_logo = true; -#endif - if (init_windowed) { //do none.. } else if (init_maximized) { @@ -2913,67 +2905,11 @@ Error Main::setup2() { DisplayServer::get_singleton()->window_set_flag(DisplayServer::WINDOW_FLAG_ALWAYS_ON_TOP, true); } - MAIN_PRINT("Main: Load Boot Image"); - Color clear = GLOBAL_DEF_BASIC("rendering/environment/defaults/default_clear_color", Color(0.3, 0.3, 0.3)); RenderingServer::get_singleton()->set_default_clear_color(clear); - if (show_logo) { //boot logo! - const bool boot_logo_image = GLOBAL_DEF_BASIC("application/boot_splash/show_image", true); - const String boot_logo_path = String(GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String())).strip_edges(); - const bool boot_logo_scale = GLOBAL_DEF_BASIC("application/boot_splash/fullsize", true); - const bool boot_logo_filter = GLOBAL_DEF_BASIC("application/boot_splash/use_filter", true); - - Ref<Image> boot_logo; - - if (boot_logo_image) { - if (!boot_logo_path.is_empty()) { - boot_logo.instantiate(); - Error load_err = ImageLoader::load_image(boot_logo_path, boot_logo); - if (load_err) { - ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + "'. Loading default splash."); - } - } - } else { - // Create a 1×1 transparent image. This will effectively hide the splash image. - boot_logo.instantiate(); - boot_logo->initialize_data(1, 1, false, Image::FORMAT_RGBA8); - boot_logo->set_pixel(0, 0, Color(0, 0, 0, 0)); - } - - Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color"); - -#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) - boot_bg_color = - GLOBAL_DEF_BASIC("application/boot_splash/bg_color", - (editor || project_manager) ? boot_splash_editor_bg_color : boot_splash_bg_color); -#endif - if (boot_logo.is_valid()) { - RenderingServer::get_singleton()->set_boot_image(boot_logo, boot_bg_color, boot_logo_scale, - boot_logo_filter); - - } else { -#ifndef NO_DEFAULT_BOOT_LOGO - MAIN_PRINT("Main: Create bootsplash"); -#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) - Ref<Image> splash = (editor || project_manager) ? memnew(Image(boot_splash_editor_png)) : memnew(Image(boot_splash_png)); -#else - Ref<Image> splash = memnew(Image(boot_splash_png)); -#endif - - MAIN_PRINT("Main: ClearColor"); - RenderingServer::get_singleton()->set_default_clear_color(boot_bg_color); - MAIN_PRINT("Main: Image"); - RenderingServer::get_singleton()->set_boot_image(splash, boot_bg_color, false); -#endif - } - -#if defined(TOOLS_ENABLED) && defined(MACOS_ENABLED) - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_ICON) && OS::get_singleton()->get_bundle_icon_path().is_empty()) { - Ref<Image> icon = memnew(Image(app_icon_png)); - DisplayServer::get_singleton()->set_icon(icon); - } -#endif + if (p_show_boot_logo) { + setup_boot_logo(); } MAIN_PRINT("Main: Clear Color"); @@ -3003,6 +2939,8 @@ Error Main::setup2() { id->set_emulate_mouse_from_touch(bool(GLOBAL_DEF_BASIC("input_devices/pointing/emulate_mouse_from_touch", true))); } + GLOBAL_DEF("input_devices/buffering/android/use_accumulated_input", true); + GLOBAL_DEF("input_devices/buffering/android/use_input_buffering", true); GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_long_press_as_right_click", false); GLOBAL_DEF_BASIC("input_devices/pointing/android/enable_pan_and_scale_gestures", false); GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "input_devices/pointing/android/rotary_input_scroll_axis", PROPERTY_HINT_ENUM, "Horizontal,Vertical"), 1); @@ -3211,11 +3149,76 @@ Error Main::setup2() { print_verbose("EDITOR API HASH: " + uitos(ClassDB::get_api_hash(ClassDB::API_EDITOR))); MAIN_PRINT("Main: Done"); - OS::get_singleton()->benchmark_end_measure("Startup", "Setup"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Setup2"); return OK; } +void Main::setup_boot_logo() { + MAIN_PRINT("Main: Load Boot Image"); + +#if !defined(TOOLS_ENABLED) && defined(WEB_ENABLED) + bool show_logo = false; +#else + bool show_logo = true; +#endif + + if (show_logo) { //boot logo! + const bool boot_logo_image = GLOBAL_DEF_BASIC("application/boot_splash/show_image", true); + const String boot_logo_path = String(GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/boot_splash/image", PROPERTY_HINT_FILE, "*.png"), String())).strip_edges(); + const bool boot_logo_scale = GLOBAL_DEF_BASIC("application/boot_splash/fullsize", true); + const bool boot_logo_filter = GLOBAL_DEF_BASIC("application/boot_splash/use_filter", true); + + Ref<Image> boot_logo; + + if (boot_logo_image) { + if (!boot_logo_path.is_empty()) { + boot_logo.instantiate(); + Error load_err = ImageLoader::load_image(boot_logo_path, boot_logo); + if (load_err) { + ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + "'. Loading default splash."); + } + } + } else { + // Create a 1×1 transparent image. This will effectively hide the splash image. + boot_logo.instantiate(); + boot_logo->initialize_data(1, 1, false, Image::FORMAT_RGBA8); + boot_logo->set_pixel(0, 0, Color(0, 0, 0, 0)); + } + + Color boot_bg_color = GLOBAL_GET("application/boot_splash/bg_color"); + +#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) + boot_bg_color = GLOBAL_DEF_BASIC("application/boot_splash/bg_color", (editor || project_manager) ? boot_splash_editor_bg_color : boot_splash_bg_color); +#endif + if (boot_logo.is_valid()) { + RenderingServer::get_singleton()->set_boot_image(boot_logo, boot_bg_color, boot_logo_scale, boot_logo_filter); + + } else { +#ifndef NO_DEFAULT_BOOT_LOGO + MAIN_PRINT("Main: Create bootsplash"); +#if defined(TOOLS_ENABLED) && !defined(NO_EDITOR_SPLASH) + Ref<Image> splash = (editor || project_manager) ? memnew(Image(boot_splash_editor_png)) : memnew(Image(boot_splash_png)); +#else + Ref<Image> splash = memnew(Image(boot_splash_png)); +#endif + + MAIN_PRINT("Main: ClearColor"); + RenderingServer::get_singleton()->set_default_clear_color(boot_bg_color); + MAIN_PRINT("Main: Image"); + RenderingServer::get_singleton()->set_boot_image(splash, boot_bg_color, false); +#endif + } + +#if defined(TOOLS_ENABLED) && defined(MACOS_ENABLED) + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_ICON) && OS::get_singleton()->get_bundle_icon_path().is_empty()) { + Ref<Image> icon = memnew(Image(app_icon_png)); + DisplayServer::get_singleton()->set_icon(icon); + } +#endif + } +} + String Main::get_rendering_driver_name() { return rendering_driver; } @@ -3227,6 +3230,8 @@ static MainTimerSync main_timer_sync; // and should move on to `OS::run`, and EXIT_FAILURE otherwise for // an early exit with that error code. int Main::start() { + OS::get_singleton()->benchmark_begin_measure("Startup", "Main::Start"); + ERR_FAIL_COND_V(!_start_success, false); bool has_icon = false; @@ -3950,7 +3955,7 @@ int Main::start() { } } - OS::get_singleton()->benchmark_end_measure("Startup", "Total"); + OS::get_singleton()->benchmark_end_measure("Startup", "Main::Start"); OS::get_singleton()->benchmark_dump(); return EXIT_SUCCESS; @@ -4038,6 +4043,7 @@ bool Main::iteration() { } Engine::get_singleton()->_in_physics = true; + Engine::get_singleton()->_physics_frames++; uint64_t physics_begin = OS::get_singleton()->get_ticks_usec(); @@ -4085,7 +4091,6 @@ bool Main::iteration() { physics_process_ticks = MAX(physics_process_ticks, OS::get_singleton()->get_ticks_usec() - physics_begin); // keep the largest one for reference physics_process_max = MAX(OS::get_singleton()->get_ticks_usec() - physics_begin, physics_process_max); - Engine::get_singleton()->_physics_frames++; Engine::get_singleton()->_in_physics = false; } @@ -4218,7 +4223,7 @@ void Main::force_redraw() { * The order matters as some of those steps are linked with each other. */ void Main::cleanup(bool p_force) { - OS::get_singleton()->benchmark_begin_measure("Shutdown", "Total"); + OS::get_singleton()->benchmark_begin_measure("Shutdown", "Main::Cleanup"); if (!p_force) { ERR_FAIL_COND(!_start_success); } @@ -4376,7 +4381,7 @@ void Main::cleanup(bool p_force) { unregister_core_types(); - OS::get_singleton()->benchmark_end_measure("Shutdown", "Total"); + OS::get_singleton()->benchmark_end_measure("Shutdown", "Main::Cleanup"); OS::get_singleton()->benchmark_dump(); OS::get_singleton()->finalize_core(); diff --git a/main/main.h b/main/main.h index ff0fba6b51..b1cfcd3c2d 100644 --- a/main/main.h +++ b/main/main.h @@ -72,8 +72,9 @@ public: static int test_entrypoint(int argc, char *argv[], bool &tests_need_run); static Error setup(const char *execpath, int argc, char *argv[], bool p_second_phase = true); - static Error setup2(); // The thread calling setup2() will effectively become the main thread. + static Error setup2(bool p_show_boot_logo = true); // The thread calling setup2() will effectively become the main thread. static String get_rendering_driver_name(); + static void setup_boot_logo(); #ifdef TESTS_ENABLED static Error test_setup(); static void test_cleanup(); diff --git a/misc/dist/html/editor.html b/misc/dist/html/editor.html index 5959b7b664..3a22055546 100644 --- a/misc/dist/html/editor.html +++ b/misc/dist/html/editor.html @@ -227,7 +227,6 @@ a:active { line-height: 1.3; visibility: visible; padding: 4px 6px; - visibility: visible; } </style> </head> diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 7b93df70fa..4b0d22a1aa 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -372,3 +372,10 @@ GH-91382 Validate extension JSON: Error: Field 'classes/AudioStreamPlaybackPolyphonic/methods/play_stream/arguments': size changed value in new API, from 4 to 6. Optional arguments added. Compatibility methods registered. + + +GH-93982 +-------- +Validate extension JSON: Error: Field 'classes/Sprite3D/properties/frame_coords': type changed value in new API, from "Vector2" to "Vector2i". + +The type was wrong to begin with and has been corrected. Vector2 and Vector2i are convertible, so it should be compatible. diff --git a/modules/enet/doc_classes/ENetConnection.xml b/modules/enet/doc_classes/ENetConnection.xml index 5795dd8976..ebd1577172 100644 --- a/modules/enet/doc_classes/ENetConnection.xml +++ b/modules/enet/doc_classes/ENetConnection.xml @@ -141,7 +141,7 @@ <return type="Array" /> <param index="0" name="timeout" type="int" default="0" /> <description> - Waits for events on the host specified and shuttles packets between the host and its peers. The returned [Array] will have 4 elements. An [enum EventType], the [ENetPacketPeer] which generated the event, the event associated data (if any), the event associated channel (if any). If the generated event is [constant EVENT_RECEIVE], the received packet will be queued to the associated [ENetPacketPeer]. + Waits for events on the specified host and shuttles packets between the host and its peers, with the given [param timeout] (in milliseconds). The returned [Array] will have 4 elements. An [enum EventType], the [ENetPacketPeer] which generated the event, the event associated data (if any), the event associated channel (if any). If the generated event is [constant EVENT_RECEIVE], the received packet will be queued to the associated [ENetPacketPeer]. Call this function regularly to handle connections, disconnections, and to receive new packets. </description> </method> diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index f557727718..b58b44973e 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1959,11 +1959,14 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, break; } - if (base.value.in(index.value)) { - Variant value = base.value.get(index.value); - r_type = _type_from_variant(value, p_context); - found = true; - break; + { + bool valid; + Variant value = base.value.get(index.value, &valid); + if (valid) { + r_type = _type_from_variant(value, p_context); + found = true; + break; + } } // Look if it is a dictionary node. diff --git a/modules/gdscript/gdscript_lambda_callable.cpp b/modules/gdscript/gdscript_lambda_callable.cpp index 626ef6ccb0..2162a727b3 100644 --- a/modules/gdscript/gdscript_lambda_callable.cpp +++ b/modules/gdscript/gdscript_lambda_callable.cpp @@ -84,7 +84,7 @@ int GDScriptLambdaCallable::get_argument_count(bool &r_is_valid) const { return 0; } r_is_valid = true; - return function->get_argument_count(); + return function->get_argument_count() - captures.size(); } void GDScriptLambdaCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { @@ -198,13 +198,17 @@ ObjectID GDScriptLambdaSelfCallable::get_object() const { return object->get_instance_id(); } +StringName GDScriptLambdaSelfCallable::get_method() const { + return function->get_name(); +} + int GDScriptLambdaSelfCallable::get_argument_count(bool &r_is_valid) const { if (function == nullptr) { r_is_valid = false; return 0; } r_is_valid = true; - return function->get_argument_count(); + return function->get_argument_count() - captures.size(); } void GDScriptLambdaSelfCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const { diff --git a/modules/gdscript/gdscript_lambda_callable.h b/modules/gdscript/gdscript_lambda_callable.h index 45c0235913..2d27b8d679 100644 --- a/modules/gdscript/gdscript_lambda_callable.h +++ b/modules/gdscript/gdscript_lambda_callable.h @@ -87,6 +87,7 @@ public: CompareEqualFunc get_compare_equal_func() const override; CompareLessFunc get_compare_less_func() const override; ObjectID get_object() const override; + StringName get_method() const override; int get_argument_count(bool &r_is_valid) const override; void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override; diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd new file mode 100644 index 0000000000..67225cad6a --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.gd @@ -0,0 +1,18 @@ +# https://github.com/godotengine/godot/issues/93952 + +func foo(): + pass + +func test(): + var a: int + + var lambda_self := func (x: int) -> void: + foo() + print(a, x) + + print(lambda_self.get_argument_count()) # Should print 1. + + var lambda_non_self := func (x: int) -> void: + print(a, x) + + print(lambda_non_self.get_argument_count()) # Should print 1. diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out new file mode 100644 index 0000000000..04b4638adf --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_bind_argument_count.out @@ -0,0 +1,3 @@ +GDTEST_OK +1 +1 diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd new file mode 100644 index 0000000000..160e43a797 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.gd @@ -0,0 +1,21 @@ +# https://github.com/godotengine/godot/issues/94074 + +func foo(): + pass + +func test(): + var lambda_self := func test() -> void: + foo() + var anon_lambda_self := func() -> void: + foo() + + print(lambda_self.get_method()) # Should print "test". + print(anon_lambda_self.get_method()) # Should print "<anonymous lambda>". + + var lambda_non_self := func test() -> void: + pass + var anon_lambda_non_self := func() -> void: + pass + + print(lambda_non_self.get_method()) # Should print "test". + print(anon_lambda_non_self.get_method()) # Should print "<anonymous lambda>". diff --git a/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out new file mode 100644 index 0000000000..17ee47fca2 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/lambda_get_method.out @@ -0,0 +1,5 @@ +GDTEST_OK +test +<anonymous lambda> +test +<anonymous lambda> diff --git a/modules/gltf/doc_classes/GLTFAccessor.xml b/modules/gltf/doc_classes/GLTFAccessor.xml index 54762faed7..dd059e6b79 100644 --- a/modules/gltf/doc_classes/GLTFAccessor.xml +++ b/modules/gltf/doc_classes/GLTFAccessor.xml @@ -12,7 +12,7 @@ <link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link> </tutorials> <members> - <member name="accessor_type" type="int" setter="set_accessor_type" getter="get_accessor_type" default="0"> + <member name="accessor_type" type="int" setter="set_accessor_type" getter="get_accessor_type" enum="GLTFAccessor.GLTFAccessorType" default="0"> The GLTF accessor type as an enum. Possible values are 0 for "SCALAR", 1 for "VEC2", 2 for "VEC3", 3 for "VEC4", 4 for "MAT2", 5 for "MAT3", and 6 for "MAT4". </member> <member name="buffer_view" type="int" setter="set_buffer_view" getter="get_buffer_view" default="-1"> @@ -54,8 +54,31 @@ <member name="sparse_values_byte_offset" type="int" setter="set_sparse_values_byte_offset" getter="get_sparse_values_byte_offset" default="0"> The offset relative to the start of the bufferView in bytes. </member> - <member name="type" type="int" setter="set_type" getter="get_type" default="0" deprecated="Use [member accessor_type] instead."> + <member name="type" type="int" setter="set_type" getter="get_type" deprecated="Use [member accessor_type] instead."> The GLTF accessor type as an enum. Use [member accessor_type] instead. </member> </members> + <constants> + <constant name="TYPE_SCALAR" value="0" enum="GLTFAccessorType"> + Accessor type "SCALAR". For the glTF object model, this can be used to map to a single float, int, or bool value, or a float array. + </constant> + <constant name="TYPE_VEC2" value="1" enum="GLTFAccessorType"> + Accessor type "VEC2". For the glTF object model, this maps to "float2", represented in the glTF JSON as an array of two floats. + </constant> + <constant name="TYPE_VEC3" value="2" enum="GLTFAccessorType"> + Accessor type "VEC3". For the glTF object model, this maps to "float3", represented in the glTF JSON as an array of three floats. + </constant> + <constant name="TYPE_VEC4" value="3" enum="GLTFAccessorType"> + Accessor type "VEC4". For the glTF object model, this maps to "float4", represented in the glTF JSON as an array of four floats. + </constant> + <constant name="TYPE_MAT2" value="4" enum="GLTFAccessorType"> + Accessor type "MAT2". For the glTF object model, this maps to "float2x2", represented in the glTF JSON as an array of four floats. + </constant> + <constant name="TYPE_MAT3" value="5" enum="GLTFAccessorType"> + Accessor type "MAT3". For the glTF object model, this maps to "float3x3", represented in the glTF JSON as an array of nine floats. + </constant> + <constant name="TYPE_MAT4" value="6" enum="GLTFAccessorType"> + Accessor type "MAT4". For the glTF object model, this maps to "float4x4", represented in the glTF JSON as an array of sixteen floats. + </constant> + </constants> </class> diff --git a/modules/gltf/editor/editor_import_blend_runner.cpp b/modules/gltf/editor/editor_import_blend_runner.cpp index 330310d92a..22c8adfe88 100644 --- a/modules/gltf/editor/editor_import_blend_runner.cpp +++ b/modules/gltf/editor/editor_import_blend_runner.cpp @@ -43,6 +43,7 @@ from xmlrpc.server import SimpleXMLRPCServer req = threading.Condition() res = threading.Condition() info = None +export_err = None def xmlrpc_server(): server = SimpleXMLRPCServer(('127.0.0.1', %d)) server.register_function(export_gltf) @@ -54,6 +55,10 @@ def export_gltf(opts): req.notify() with res: res.wait() + if export_err: + raise export_err + # Important to return a value to prevent the error 'cannot marshal None unless allow_none is enabled'. + return 'BLENDER_GODOT_EXPORT_SUCCESSFUL' if bpy.app.version < (3, 0, 0): print('Blender 3.0 or higher is required.', file=sys.stderr) threading.Thread(target=xmlrpc_server).start() @@ -64,12 +69,13 @@ while True: method, opts = info if method == 'export_gltf': try: + export_err = None bpy.ops.wm.open_mainfile(filepath=opts['path']) if opts['unpack_all']: bpy.ops.file.unpack_all(method='USE_LOCAL') bpy.ops.export_scene.gltf(**opts['gltf_options']) - except: - pass + except Exception as e: + export_err = e info = None with res: res.notify() @@ -184,7 +190,9 @@ Error EditorImportBlendRunner::do_import(const Dictionary &p_options) { EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0); rpc_port = 0; } - err = do_import_direct(p_options); + if (err != ERR_QUERY_FAILED) { + err = do_import_direct(p_options); + } } return err; } else { @@ -259,6 +267,7 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { // Wait for response. bool done = false; + PackedByteArray response; while (!done) { status = client->get_status(); switch (status) { @@ -268,7 +277,10 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { } case HTTPClient::STATUS_BODY: { client->poll(); - // Parse response here if needed. For now we can just ignore it. + response.append_array(client->read_response_body_chunk()); + break; + } + case HTTPClient::STATUS_CONNECTED: { done = true; break; } @@ -278,9 +290,56 @@ Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) { } } + String response_text = "No response from Blender."; + if (response.size() > 0) { + response_text = String::utf8((const char *)response.ptr(), response.size()); + } + + if (client->get_response_code() != HTTPClient::RESPONSE_OK) { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Error received from Blender - status code: %s, error: %s", client->get_response_code(), response_text)); + } else if (response_text.find("BLENDER_GODOT_EXPORT_SUCCESSFUL") < 0) { + // Previous versions of Godot used a Python script where the RPC function did not return + // a value, causing the error 'cannot marshal None unless allow_none is enabled'. + // If an older version of Godot is running and has started Blender with this script, + // we will receive the error, but there's a good chance that the import was successful. + // We are discarding this error to maintain backward compatibility and prevent situations + // where the user needs to close the older version of Godot or kill Blender. + if (response_text.find("cannot marshal None unless allow_none is enabled") < 0) { + String error_message; + if (_extract_error_message_xml(response, error_message)) { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", error_message)); + } else { + ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", response_text)); + } + } + } + return OK; } +bool EditorImportBlendRunner::_extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message) { + // Based on RPC Xml spec from: https://xmlrpc.com/spec.md + Ref<XMLParser> parser = memnew(XMLParser); + Error err = parser->open_buffer(p_response_data); + if (err) { + return false; + } + + r_error_message = String(); + while (parser->read() == OK) { + if (parser->get_node_type() == XMLParser::NODE_TEXT) { + if (parser->get_node_data().size()) { + if (r_error_message.size()) { + r_error_message += " "; + } + r_error_message += parser->get_node_data().trim_suffix("\n"); + } + } + } + + return r_error_message.size(); +} + Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) { // Export glTF directly. String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options)); diff --git a/modules/gltf/editor/editor_import_blend_runner.h b/modules/gltf/editor/editor_import_blend_runner.h index 626f3c9eba..b3b49ebfb2 100644 --- a/modules/gltf/editor/editor_import_blend_runner.h +++ b/modules/gltf/editor/editor_import_blend_runner.h @@ -47,6 +47,7 @@ class EditorImportBlendRunner : public Node { void _resources_reimported(const PackedStringArray &p_files); void _kill_blender(); void _notification(int p_what); + bool _extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message); protected: int rpc_port = 0; diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index 79a2184745..b474128fd6 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -132,12 +132,10 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ } #endif - source_global = source_global.c_escape(); - const String blend_basename = p_path.get_file().get_basename(); const String sink = ProjectSettings::get_singleton()->get_imported_files_path().path_join( vformat("%s-%s.gltf", blend_basename, p_path.md5_text())); - const String sink_global = ProjectSettings::get_singleton()->globalize_path(sink).c_escape(); + const String sink_global = ProjectSettings::get_singleton()->globalize_path(sink); // Handle configuration options. @@ -188,10 +186,18 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ } else { parameters_map["export_lights"] = false; } - if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { - parameters_map["export_colors"] = true; + if (blender_major_version > 4 || (blender_major_version == 4 && blender_minor_version >= 2)) { + if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { + parameters_map["export_vertex_color"] = "MATERIAL"; + } else { + parameters_map["export_vertex_color"] = "NONE"; + } } else { - parameters_map["export_colors"] = false; + if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) { + parameters_map["export_colors"] = true; + } else { + parameters_map["export_colors"] = false; + } } if (p_options.has(SNAME("blender/nodes/visible"))) { int32_t visible = p_options["blender/nodes/visible"]; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index e0bdd4cf33..c0232e6d0c 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -934,58 +934,58 @@ Error GLTFDocument::_encode_accessors(Ref<GLTFState> p_state) { return OK; } -String GLTFDocument::_get_accessor_type_name(const GLTFAccessorType p_accessor_type) { - if (p_accessor_type == GLTFAccessorType::TYPE_SCALAR) { +String GLTFDocument::_get_accessor_type_name(const GLTFAccessor::GLTFAccessorType p_accessor_type) { + if (p_accessor_type == GLTFAccessor::TYPE_SCALAR) { return "SCALAR"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC2) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC2) { return "VEC2"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC3) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC3) { return "VEC3"; } - if (p_accessor_type == GLTFAccessorType::TYPE_VEC4) { + if (p_accessor_type == GLTFAccessor::TYPE_VEC4) { return "VEC4"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT2) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT2) { return "MAT2"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { return "MAT3"; } - if (p_accessor_type == GLTFAccessorType::TYPE_MAT4) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT4) { return "MAT4"; } ERR_FAIL_V("SCALAR"); } -GLTFAccessorType GLTFDocument::_get_accessor_type_from_str(const String &p_string) { +GLTFAccessor::GLTFAccessorType GLTFDocument::_get_accessor_type_from_str(const String &p_string) { if (p_string == "SCALAR") { - return GLTFAccessorType::TYPE_SCALAR; + return GLTFAccessor::TYPE_SCALAR; } if (p_string == "VEC2") { - return GLTFAccessorType::TYPE_VEC2; + return GLTFAccessor::TYPE_VEC2; } if (p_string == "VEC3") { - return GLTFAccessorType::TYPE_VEC3; + return GLTFAccessor::TYPE_VEC3; } if (p_string == "VEC4") { - return GLTFAccessorType::TYPE_VEC4; + return GLTFAccessor::TYPE_VEC4; } if (p_string == "MAT2") { - return GLTFAccessorType::TYPE_MAT2; + return GLTFAccessor::TYPE_MAT2; } if (p_string == "MAT3") { - return GLTFAccessorType::TYPE_MAT3; + return GLTFAccessor::TYPE_MAT3; } if (p_string == "MAT4") { - return GLTFAccessorType::TYPE_MAT4; + return GLTFAccessor::TYPE_MAT4; } - ERR_FAIL_V(GLTFAccessorType::TYPE_SCALAR); + ERR_FAIL_V(GLTFAccessor::TYPE_SCALAR); } Error GLTFDocument::_parse_accessors(Ref<GLTFState> p_state) { @@ -1088,7 +1088,7 @@ String GLTFDocument::_get_component_type_name(const uint32_t p_component) { return "<Error>"; } -Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, const int p_count, const GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { +Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_vertex_indices) { const int component_count_for_type[7] = { 1, 2, 3, 4, 4, 9, 16 }; @@ -1103,18 +1103,18 @@ Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_ switch (p_component_type) { case COMPONENT_TYPE_BYTE: case COMPONENT_TYPE_UNSIGNED_BYTE: { - if (p_accessor_type == TYPE_MAT2) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; } - if (p_accessor_type == TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 3; skip_bytes = 1; } } break; case COMPONENT_TYPE_SHORT: case COMPONENT_TYPE_UNSIGNED_SHORT: { - if (p_accessor_type == TYPE_MAT3) { + if (p_accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; } @@ -1296,7 +1296,7 @@ Error GLTFDocument::_encode_buffer_view(Ref<GLTFState> p_state, const double *p_ return OK; } -Error GLTFDocument::_decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { +Error GLTFDocument::_decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex) { const Ref<GLTFBufferView> bv = p_state->buffer_views[p_buffer_view]; int stride = p_element_size; @@ -1427,12 +1427,12 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF switch (a->component_type) { case COMPONENT_TYPE_BYTE: case COMPONENT_TYPE_UNSIGNED_BYTE: { - if (a->accessor_type == TYPE_MAT2) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT2) { skip_every = 2; skip_bytes = 2; element_size = 8; //override for this case } - if (a->accessor_type == TYPE_MAT3) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 3; skip_bytes = 1; element_size = 12; //override for this case @@ -1440,7 +1440,7 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF } break; case COMPONENT_TYPE_SHORT: case COMPONENT_TYPE_UNSIGNED_SHORT: { - if (a->accessor_type == TYPE_MAT3) { + if (a->accessor_type == GLTFAccessor::TYPE_MAT3) { skip_every = 6; skip_bytes = 4; element_size = 16; //override for this case @@ -1474,7 +1474,7 @@ Vector<double> GLTFDocument::_decode_accessor(Ref<GLTFState> p_state, const GLTF indices.resize(a->sparse_count); const int indices_component_size = _get_component_type_size(a->sparse_indices_component_type); - Error err = _decode_buffer_view(p_state, indices.ptrw(), a->sparse_indices_buffer_view, 0, 0, indices_component_size, a->sparse_count, TYPE_SCALAR, 1, a->sparse_indices_component_type, indices_component_size, false, a->sparse_indices_byte_offset, false); + Error err = _decode_buffer_view(p_state, indices.ptrw(), a->sparse_indices_buffer_view, 0, 0, indices_component_size, a->sparse_count, GLTFAccessor::TYPE_SCALAR, 1, a->sparse_indices_component_type, indices_component_size, false, a->sparse_indices_byte_offset, false); if (err != OK) { return Vector<double>(); } @@ -1536,7 +1536,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_ints(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_SCALAR; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; int component_type; if (max_index > 65535 || p_for_vertex) { component_type = GLTFDocument::COMPONENT_TYPE_INT; @@ -1650,7 +1650,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec2(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC2; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC2; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1703,7 +1703,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_color(Ref<GLTFState> p_state p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1770,7 +1770,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_weights(Ref<GLTFState> p_sta p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1821,7 +1821,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_joints(Ref<GLTFState> p_stat p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; accessor->max = type_max; @@ -1874,7 +1874,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_quaternions(Ref<GLTFState> p p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1949,7 +1949,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_floats(Ref<GLTFState> p_stat p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_SCALAR; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_SCALAR; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -1999,7 +1999,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_vec3(Ref<GLTFState> p_state, p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC3; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -2075,7 +2075,7 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref<GLTFState> p p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_VEC3; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_VEC3; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; sparse_accessor->normalized = false; @@ -2103,7 +2103,7 @@ GLTFAccessorIndex GLTFDocument::_encode_sparse_accessor_as_vec3(Ref<GLTFState> p } else { sparse_accessor->sparse_indices_component_type = GLTFDocument::COMPONENT_TYPE_UNSIGNED_SHORT; } - if (_encode_buffer_view(p_state, changed_indices.ptr(), changed_indices.size(), GLTFAccessorType::TYPE_SCALAR, sparse_accessor->sparse_indices_component_type, sparse_accessor->normalized, sparse_accessor->sparse_indices_byte_offset, false, buffer_view_i_indices) != OK) { + if (_encode_buffer_view(p_state, changed_indices.ptr(), changed_indices.size(), GLTFAccessor::TYPE_SCALAR, sparse_accessor->sparse_indices_component_type, sparse_accessor->normalized, sparse_accessor->sparse_indices_byte_offset, false, buffer_view_i_indices) != OK) { return -1; } // We use changed_indices.size() here, because we must pass the number of vec3 values rather than the number of components. @@ -2180,7 +2180,7 @@ GLTFAccessorIndex GLTFDocument::_encode_accessor_as_xform(Ref<GLTFState> p_state p_state->buffers.push_back(Vector<uint8_t>()); } int64_t size = p_state->buffers[0].size(); - const GLTFAccessorType accessor_type = GLTFAccessorType::TYPE_MAT4; + const GLTFAccessor::GLTFAccessorType accessor_type = GLTFAccessor::TYPE_MAT4; const int component_type = GLTFDocument::COMPONENT_TYPE_FLOAT; accessor->max = type_max; @@ -2234,9 +2234,9 @@ Vector<Color> GLTFDocument::_decode_accessor_as_color(Ref<GLTFState> p_state, co } const int accessor_type = p_state->accessors[p_accessor]->accessor_type; - ERR_FAIL_COND_V(!(accessor_type == TYPE_VEC3 || accessor_type == TYPE_VEC4), ret); + ERR_FAIL_COND_V(!(accessor_type == GLTFAccessor::TYPE_VEC3 || accessor_type == GLTFAccessor::TYPE_VEC4), ret); int vec_len = 3; - if (accessor_type == TYPE_VEC4) { + if (accessor_type == GLTFAccessor::TYPE_VEC4) { vec_len = 4; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 4f92ceccca..d37544750d 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -111,7 +111,7 @@ private: int _get_component_type_size(const int p_component_type); Error _parse_scenes(Ref<GLTFState> p_state); Error _parse_nodes(Ref<GLTFState> p_state); - String _get_accessor_type_name(const GLTFAccessorType p_accessor_type); + String _get_accessor_type_name(const GLTFAccessor::GLTFAccessorType p_accessor_type); String _sanitize_animation_name(const String &p_name); String _gen_unique_animation_name(Ref<GLTFState> p_state, const String &p_name); String _sanitize_bone_name(const String &p_name); @@ -131,13 +131,13 @@ private: void _compute_node_heights(Ref<GLTFState> p_state); Error _parse_buffers(Ref<GLTFState> p_state, const String &p_base_path); Error _parse_buffer_views(Ref<GLTFState> p_state); - GLTFAccessorType _get_accessor_type_from_str(const String &p_string); + GLTFAccessor::GLTFAccessorType _get_accessor_type_from_str(const String &p_string); Error _parse_accessors(Ref<GLTFState> p_state); Error _decode_buffer_view(Ref<GLTFState> p_state, double *p_dst, const GLTFBufferViewIndex p_buffer_view, const int p_skip_every, const int p_skip_bytes, const int p_element_size, const int p_count, - const GLTFAccessorType p_accessor_type, const int p_component_count, + const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_count, const int p_component_type, const int p_component_size, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex); @@ -266,7 +266,7 @@ private: const Vector<Transform3D> p_attribs, const bool p_for_vertex); Error _encode_buffer_view(Ref<GLTFState> p_state, const double *p_src, - const int p_count, const GLTFAccessorType p_accessor_type, + const int p_count, const GLTFAccessor::GLTFAccessorType p_accessor_type, const int p_component_type, const bool p_normalized, const int p_byte_offset, const bool p_for_vertex, GLTFBufferViewIndex &r_accessor, const bool p_for_indices = false); diff --git a/modules/gltf/structures/gltf_accessor.cpp b/modules/gltf/structures/gltf_accessor.cpp index 602f0d9dc4..1ebc00a514 100644 --- a/modules/gltf/structures/gltf_accessor.cpp +++ b/modules/gltf/structures/gltf_accessor.cpp @@ -31,6 +31,14 @@ #include "gltf_accessor.h" void GLTFAccessor::_bind_methods() { + BIND_ENUM_CONSTANT(TYPE_SCALAR); + BIND_ENUM_CONSTANT(TYPE_VEC2); + BIND_ENUM_CONSTANT(TYPE_VEC3); + BIND_ENUM_CONSTANT(TYPE_VEC4); + BIND_ENUM_CONSTANT(TYPE_MAT2); + BIND_ENUM_CONSTANT(TYPE_MAT3); + BIND_ENUM_CONSTANT(TYPE_MAT4); + ClassDB::bind_method(D_METHOD("get_buffer_view"), &GLTFAccessor::get_buffer_view); ClassDB::bind_method(D_METHOD("set_buffer_view", "buffer_view"), &GLTFAccessor::set_buffer_view); ClassDB::bind_method(D_METHOD("get_byte_offset"), &GLTFAccessor::get_byte_offset); @@ -43,8 +51,8 @@ void GLTFAccessor::_bind_methods() { ClassDB::bind_method(D_METHOD("set_count", "count"), &GLTFAccessor::set_count); ClassDB::bind_method(D_METHOD("get_accessor_type"), &GLTFAccessor::get_accessor_type); ClassDB::bind_method(D_METHOD("set_accessor_type", "accessor_type"), &GLTFAccessor::set_accessor_type); - ClassDB::bind_method(D_METHOD("get_type"), &GLTFAccessor::get_accessor_type); - ClassDB::bind_method(D_METHOD("set_type", "type"), &GLTFAccessor::set_accessor_type); + ClassDB::bind_method(D_METHOD("get_type"), &GLTFAccessor::get_type); + ClassDB::bind_method(D_METHOD("set_type", "type"), &GLTFAccessor::set_type); ClassDB::bind_method(D_METHOD("get_min"), &GLTFAccessor::get_min); ClassDB::bind_method(D_METHOD("set_min", "min"), &GLTFAccessor::set_min); ClassDB::bind_method(D_METHOD("get_max"), &GLTFAccessor::get_max); @@ -67,8 +75,8 @@ void GLTFAccessor::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "component_type"), "set_component_type", "get_component_type"); // int ADD_PROPERTY(PropertyInfo(Variant::BOOL, "normalized"), "set_normalized", "get_normalized"); // bool ADD_PROPERTY(PropertyInfo(Variant::INT, "count"), "set_count", "get_count"); // int - ADD_PROPERTY(PropertyInfo(Variant::INT, "accessor_type"), "set_accessor_type", "get_accessor_type"); // GLTFAccessorType - ADD_PROPERTY(PropertyInfo(Variant::INT, "type"), "set_type", "get_type"); // Deprecated, GLTFAccessorType + ADD_PROPERTY(PropertyInfo(Variant::INT, "accessor_type"), "set_accessor_type", "get_accessor_type"); // GLTFAccessor::GLTFAccessorType + ADD_PROPERTY(PropertyInfo(Variant::INT, "type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_type", "get_type"); // Deprecated, int for GLTFAccessor::GLTFAccessorType ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT64_ARRAY, "min"), "set_min", "get_min"); // Vector<real_t> ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT64_ARRAY, "max"), "set_max", "get_max"); // Vector<real_t> ADD_PROPERTY(PropertyInfo(Variant::INT, "sparse_count"), "set_sparse_count", "get_sparse_count"); // int @@ -119,11 +127,19 @@ void GLTFAccessor::set_count(int p_count) { count = p_count; } -int GLTFAccessor::get_accessor_type() { +GLTFAccessor::GLTFAccessorType GLTFAccessor::get_accessor_type() { + return accessor_type; +} + +void GLTFAccessor::set_accessor_type(GLTFAccessorType p_accessor_type) { + accessor_type = p_accessor_type; +} + +int GLTFAccessor::get_type() { return (int)accessor_type; } -void GLTFAccessor::set_accessor_type(int p_accessor_type) { +void GLTFAccessor::set_type(int p_accessor_type) { accessor_type = (GLTFAccessorType)p_accessor_type; // TODO: Register enum } diff --git a/modules/gltf/structures/gltf_accessor.h b/modules/gltf/structures/gltf_accessor.h index 51ca282630..1a3a2cb494 100644 --- a/modules/gltf/structures/gltf_accessor.h +++ b/modules/gltf/structures/gltf_accessor.h @@ -35,20 +35,21 @@ #include "core/io/resource.h" -enum GLTFAccessorType { - TYPE_SCALAR, - TYPE_VEC2, - TYPE_VEC3, - TYPE_VEC4, - TYPE_MAT2, - TYPE_MAT3, - TYPE_MAT4, -}; - struct GLTFAccessor : public Resource { GDCLASS(GLTFAccessor, Resource); friend class GLTFDocument; +public: + enum GLTFAccessorType { + TYPE_SCALAR, + TYPE_VEC2, + TYPE_VEC3, + TYPE_VEC4, + TYPE_MAT2, + TYPE_MAT3, + TYPE_MAT4, + }; + private: GLTFBufferViewIndex buffer_view = -1; int byte_offset = 0; @@ -84,8 +85,11 @@ public: int get_count(); void set_count(int p_count); - int get_accessor_type(); - void set_accessor_type(int p_accessor_type); + GLTFAccessorType get_accessor_type(); + void set_accessor_type(GLTFAccessorType p_accessor_type); + + int get_type(); + void set_type(int p_accessor_type); Vector<double> get_min(); void set_min(Vector<double> p_min); @@ -112,4 +116,6 @@ public: void set_sparse_values_byte_offset(int p_sparse_values_byte_offset); }; +VARIANT_ENUM_CAST(GLTFAccessor::GLTFAccessorType); + #endif // GLTF_ACCESSOR_H diff --git a/modules/jpg/image_loader_jpegd.cpp b/modules/jpg/image_loader_jpegd.cpp index ada0cd01fa..53046de740 100644 --- a/modules/jpg/image_loader_jpegd.cpp +++ b/modules/jpg/image_loader_jpegd.cpp @@ -162,7 +162,7 @@ static Error _jpgd_save_to_output_stream(jpge::output_stream *p_output_stream, c ERR_FAIL_COND_V_MSG(error != OK, error, "Couldn't decompress image."); } if (image->get_format() != Image::FORMAT_RGB8) { - image = p_img->duplicate(); + image = image->duplicate(); image->convert(Image::FORMAT_RGB8); } diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 0c87199635..499ddb703b 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -5248,7 +5248,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) { if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) { - // Insert an additional space when cutting word bound for esthetics. + // Insert an additional space when cutting word bound for aesthetics. if (cut_per_word && (ellipsis_pos > 0)) { Glyph gl; gl.count = 1; diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 6cf6b236ed..b45c004011 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -4061,7 +4061,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) { if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) { - // Insert an additional space when cutting word bound for esthetics. + // Insert an additional space when cutting word bound for aesthetics. if (cut_per_word && (ellipsis_pos > 0)) { Glyph gl; gl.count = 1; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4abc6548bf..0cc929d226 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -24,6 +24,10 @@ android:hasFragileUserData="false" android:requestLegacyExternalStorage="false" tools:ignore="GoogleAppIndexingWarning" > + <profileable + android:shell="true" + android:enabled="true" + tools:targetApi="29" /> <!-- Records the version of the Godot editor used for building --> <meta-data diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 01759a1b2f..eb9ad9de05 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -7,7 +7,7 @@ ext.versions = [ targetSdk : 34, buildTools : '34.0.0', kotlinVersion : '1.9.20', - fragmentVersion : '1.6.2', + fragmentVersion : '1.7.1', nexusPublishVersion: '1.3.0', javaVersion : JavaVersion.VERSION_17, // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated. diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 55fe2a22fe..37f68d295a 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -9,7 +9,7 @@ dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" implementation project(":lib") - implementation "androidx.window:window:1.2.0" + implementation "androidx.window:window:1.3.0" implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" implementation "androidx.constraintlayout:constraintlayout:2.1.4" } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 5515347bd6..dad397de61 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -117,6 +117,10 @@ open class GodotEditor : GodotActivity() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() + val useInputBuffering = useInputBuffering() + val useAccumulatedInput = useAccumulatedInput() + GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) + checkForProjectPermissionsToEnable() runOnUiThread { @@ -124,6 +128,7 @@ open class GodotEditor : GodotActivity() { godotFragment?.godot?.renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) + enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) } } } @@ -275,6 +280,13 @@ open class GodotEditor : GodotActivity() { java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) /** + * Use input buffering for the Godot Android editor. + */ + protected open fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_input_buffering")) + + protected open fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_accumulated_input")) + + /** * Whether we should launch the new godot instance in an adjacent window * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT */ 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 8e4e089211..f50b5577c3 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,8 @@ package org.godotengine.editor +import org.godotengine.godot.GodotLib + /** * Drives the 'run project' window of the Godot Editor. */ @@ -39,9 +41,13 @@ class GodotGame : GodotEditor() { override fun overrideOrientationRequest() = false - override fun enableLongPressGestures() = false + override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + + override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + + override fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) - override fun enablePanAndScaleGestures() = false + override fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) override fun checkForProjectPermissionsToEnable() { // Nothing to do.. by the time we get here, the project permissions will have already diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 290be727ab..c188a97ca5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -84,17 +84,20 @@ class Godot(private val context: Context) : SensorEventListener { private companion object { private val TAG = Godot::class.java.simpleName - } - private val windowManager: WindowManager by lazy { - requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager + // Supported build flavors + const val EDITOR_FLAVOR = "editor" + const val TEMPLATE_FLAVOR = "template" } + + private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } - private val mSensorManager: SensorManager by lazy { - requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager - } private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } @@ -107,9 +110,6 @@ class Godot(private val context: Context) : SensorEventListener { private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } - private val mClipboard: ClipboardManager by lazy { - requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - } private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { @@ -192,6 +192,8 @@ class Godot(private val context: Context) : SensorEventListener { return } + Log.v(TAG, "OnCreate: $primaryHost") + darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES beginBenchmarkMeasure("Startup", "Godot::onCreate") @@ -200,6 +202,8 @@ class Godot(private val context: Context) : SensorEventListener { val activity = requireActivity() val window = activity.window window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + + Log.v(TAG, "Initializing Godot plugin registry") GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) if (io == null) { io = GodotIO(activity) @@ -323,13 +327,17 @@ class Godot(private val context: Context) : SensorEventListener { return false } - if (expansionPackPath.isNotEmpty()) { - commandLine.add("--main-pack") - commandLine.add(expansionPackPath) - } - val activity = requireActivity() - if (!nativeLayerInitializeCompleted) { - nativeLayerInitializeCompleted = GodotLib.initialize( + Log.v(TAG, "OnInitNativeLayer: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") + try { + if (expansionPackPath.isNotEmpty()) { + commandLine.add("--main-pack") + commandLine.add(expansionPackPath) + } + val activity = requireActivity() + if (!nativeLayerInitializeCompleted) { + nativeLayerInitializeCompleted = GodotLib.initialize( activity, this, activity.assets, @@ -338,15 +346,20 @@ class Godot(private val context: Context) : SensorEventListener { directoryAccessHandler, fileAccessHandler, useApkExpansion, - ) - } + ) + Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted") + } - if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { - nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) - if (!nativeLayerSetupCompleted) { - Log.e(TAG, "Unable to setup the Godot engine! Aborting...") - alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit) + if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { + nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) + if (!nativeLayerSetupCompleted) { + throw IllegalStateException("Unable to setup the Godot engine! Aborting...") + } else { + Log.v(TAG, "Godot native layer setup completed") + } } + } finally { + endBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") } return isNativeInitialized() } @@ -370,6 +383,9 @@ class Godot(private val context: Context) : SensorEventListener { throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view") } + Log.v(TAG, "OnInitRenderView: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitRenderView") try { val activity: Activity = host.activity containerLayout = providedContainerLayout @@ -392,8 +408,7 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.addView(editText) renderView = if (usesVulkan()) { if (!meetsVulkanRequirements(activity.packageManager)) { - alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit) - return null + throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } GodotVulkanRenderView(host, this) } else { @@ -482,11 +497,14 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.removeAllViews() containerLayout = null } + + endBenchmarkMeasure("Startup", "Godot::onInitRenderView") } return containerLayout } fun onStart(host: GodotHost) { + Log.v(TAG, "OnStart: $host") if (host != primaryHost) { return } @@ -495,6 +513,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onResume(host: GodotHost) { + Log.v(TAG, "OnResume: $host") if (host != primaryHost) { return } @@ -527,6 +546,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onPause(host: GodotHost) { + Log.v(TAG, "OnPause: $host") if (host != primaryHost) { return } @@ -539,6 +559,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onStop(host: GodotHost) { + Log.v(TAG, "OnStop: $host") if (host != primaryHost) { return } @@ -547,6 +568,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onDestroy(primaryHost: GodotHost) { + Log.v(TAG, "OnDestroy: $primaryHost") if (this.primaryHost != primaryHost) { return } @@ -604,18 +626,29 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot setup is complete. */ private fun onGodotSetupCompleted() { - Log.d(TAG, "OnGodotSetupCompleted") - - // These properties are defined after Godot setup completion, so we retrieve them here. - val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) - val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")) - - runOnUiThread { - renderView?.inputHandler?.apply { - enableLongPress(longPressEnabled) - enablePanningAndScalingGestures(panScaleEnabled) - setRotaryInputAxis(rotaryInputAxis) + Log.v(TAG, "OnGodotSetupCompleted") + + if (!isEditorBuild()) { + // These properties are defined after Godot setup completion, so we retrieve them here. + val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + val rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis") + + val useInputBuffering = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) + val useAccumulatedInput = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) + GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) + + runOnUiThread { + renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) + try { + setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue)) + } catch (e: NumberFormatException) { + Log.w(TAG, e) + } + } } } @@ -629,7 +662,7 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot main loop has started. */ private fun onGodotMainLoopStarted() { - Log.d(TAG, "OnGodotMainLoopStarted") + Log.v(TAG, "OnGodotMainLoopStarted") for (plugin in pluginRegistry.allPlugins) { plugin.onGodotMainLoopStarted() @@ -646,12 +679,7 @@ class Godot(private val context: Context) : SensorEventListener { decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) } - @Keep - private fun alert(message: String, title: String) { - alert(message, title, null) - } - - private fun alert( + fun alert( @StringRes messageResId: Int, @StringRes titleResId: Int, okCallback: Runnable? @@ -660,7 +688,9 @@ class Godot(private val context: Context) : SensorEventListener { alert(res.getString(messageResId), res.getString(titleResId), okCallback) } - private fun alert(message: String, title: String, okCallback: Runnable?) { + @JvmOverloads + @Keep + fun alert(message: String, title: String, okCallback: Runnable? = null) { val activity: Activity = getActivity() ?: return runOnUiThread { val builder = AlertDialog.Builder(activity) @@ -759,6 +789,11 @@ class Godot(private val context: Context) : SensorEventListener { return mClipboard.hasPrimaryClip() } + /** + * @return true if this is an editor build, false if this is a template build + */ + fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR + fun getClipboard(): String { val clipData = mClipboard.primaryClip ?: return "" val text = clipData.getItemAt(0).text ?: return "" @@ -770,7 +805,7 @@ class Godot(private val context: Context) : SensorEventListener { mClipboard.setPrimaryClip(clip) } - private fun forceQuit() { + fun forceQuit() { forceQuit(0) } @@ -881,7 +916,6 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun vibrate(durationMs: Int, amplitude: Int) { if (durationMs > 0 && requestPermission("VIBRATE")) { - val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (amplitude <= -1) { vibratorService.vibrate( diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index a323045e1b..1612ddd0b3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -42,6 +42,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Messenger; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -203,6 +204,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH if (godotContainerLayout == null) { throw new IllegalStateException("Unable to initialize engine render view"); } + } catch (IllegalStateException e) { + Log.e(TAG, "Engine initialization failed", e); + final String errorMessage = TextUtils.isEmpty(e.getMessage()) + ? getString(R.string.error_engine_setup_message) + : e.getMessage(); + godot.alert(errorMessage, getString(R.string.text_error_title), godot::forceQuit); } catch (IllegalArgumentException ignored) { final Activity activity = getActivity(); Intent notifierIntent = new Intent(activity, activity.getClass()); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 4b51bd778d..219631284a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -121,7 +121,7 @@ public class GodotIO { activity.startActivity(intent); return 0; - } catch (ActivityNotFoundException e) { + } catch (Exception e) { Log.e(TAG, "Unable to open uri " + uriString, e); return 1; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index d0c3d4a687..37e889daf7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -240,4 +240,11 @@ public class GodotLib { * @see GodotRenderer#onActivityPaused() */ public static native void onRendererPaused(); + + /** + * Invoked on the GL thread to update the input dispatch settings + * @param useAccumulatedInput True to use accumulated input, false otherwise + * @param useInputBuffering True to use input buffering, false otherwise + */ + public static native void updateInputDispatchSettings(boolean useAccumulatedInput, boolean useInputBuffering); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index c316812404..c9421a3257 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -1704,15 +1704,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mHasSurface = true; mFinishedCreatingEglSurface = false; sGLThreadManager.notifyAll(); - while (mWaitingForSurface - && !mFinishedCreatingEglSurface - && !mExited) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1723,13 +1714,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mHasSurface = false; sGLThreadManager.notifyAll(); - while((!mWaitingForSurface) && (!mExited)) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1740,16 +1724,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mRequestPaused = true; sGLThreadManager.notifyAll(); - while ((! mExited) && (! mPaused)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onPause waiting for mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1762,16 +1736,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); - while ((! mExited) && mPaused && (!mRenderComplete)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onResume waiting for !mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1793,19 +1757,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } sGLThreadManager.notifyAll(); - - // Wait for thread to react to resize and render a frame - while (! mExited && !mPaused && !mRenderComplete - && ableToDraw()) { - if (LOG_SURFACE) { - Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 49b34a5229..4cd3bd8db9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -44,7 +44,7 @@ import org.godotengine.godot.GodotLib * @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener * @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener */ -internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener { +internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener { companion object { private val TAG = GodotGestureHandler::class.java.simpleName @@ -65,13 +65,13 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private var lastDragY: Float = 0.0f override fun onDown(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) nextDownIsDoubleTap = false return true } override fun onSingleTapUp(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } @@ -85,10 +85,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } // Cancel the previous down event - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) // Turn a context click into a single tap right mouse button click. - GodotInputHandler.handleMouseEvent( + inputHandler.handleMouseEvent( event, MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_SECONDARY, @@ -104,7 +104,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (!hasCapture) { // Dispatch a mouse relative ACTION_UP event to signal the end of the capture - GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) + inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) } pointerCaptureInProgress = hasCapture } @@ -131,9 +131,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) { // This may be an ACTION_BUTTON_RELEASE event which we don't handle, // so we convert it to an ACTION_UP event. - GodotInputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) + inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) } else { - GodotInputHandler.handleTouchEvent(event) + inputHandler.handleTouchEvent(event) } pointerCaptureInProgress = false dragInProgress = false @@ -148,7 +148,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private fun onActionMove(event: MotionEvent): Boolean { if (contextClickInProgress) { - GodotInputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) + inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) return true } else if (!scaleInProgress) { // The 'onScroll' event is triggered with a long delay. @@ -158,7 +158,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) { lastDragX = event.getX(0) lastDragY = event.getY(0) - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } } @@ -168,9 +168,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi override fun onDoubleTapEvent(event: MotionEvent): Boolean { if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } return true @@ -191,7 +191,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) { if (originEvent != null) { // Cancel the drag - GodotInputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) } dragInProgress = false lastDragX = 0.0f @@ -202,12 +202,12 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi val x = terminusEvent.x val y = terminusEvent.y if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { - GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) + inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f) } else if (!scaleInProgress) { dragInProgress = true lastDragX = terminusEvent.getX(0) lastDragY = terminusEvent.getY(0) - GodotInputHandler.handleMotionEvent(terminusEvent) + inputHandler.handleMotionEvent(terminusEvent) } return true } @@ -218,11 +218,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor) } return true } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 83e76e49c9..889618914d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -75,7 +75,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { */ private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; - private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + + private boolean dispatchInputToRenderThread = false; public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); @@ -83,7 +85,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); - this.godotGestureHandler = new GodotGestureHandler(); + this.godotGestureHandler = new GodotGestureHandler(this); this.gestureDetector = new GestureDetector(context, godotGestureHandler); this.gestureDetector.setIsLongpressEnabled(false); this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler); @@ -109,6 +111,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } /** + * Specifies whether input should be dispatch on the UI thread or on the Render thread. + * @param enable true to dispatch input on the Render thread, false to dispatch input on the UI thread + */ + public void enableInputDispatchToRenderThread(boolean enable) { + this.dispatchInputToRenderThread = enable; + } + + /** + * @return true if input must be dispatched from the render thread. If false, input is + * dispatched from the UI thread. + */ + private boolean shouldDispatchInputToRenderThread() { + return dispatchInputToRenderThread; + } + + /** * On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default. */ public void setRotaryInputAxis(int axis) { @@ -151,14 +169,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, false); + handleJoystickButtonEvent(godotJoyId, button, false); } } else { // getKeyCode(): The physical key that was pressed. final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); }; return true; @@ -187,13 +205,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, true); + handleJoystickButtonEvent(godotJoyId, button, true); } } else { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); } return true; @@ -248,7 +266,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) { // save value to prevent repeats joystick.axesValues.put(axis, value); - GodotLib.joyaxis(godotJoyId, i, value); + handleJoystickAxisEvent(godotJoyId, i, value); } } @@ -258,7 +276,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.hatX != hatX || joystick.hatY != hatY) { joystick.hatX = hatX; joystick.hatY = hatY; - GodotLib.joyhat(godotJoyId, hatX, hatY); + handleJoystickHatEvent(godotJoyId, hatX, hatY); } } return true; @@ -284,10 +302,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { int[] deviceIds = mInputManager.getInputDeviceIds(); for (int deviceId : deviceIds) { InputDevice device = mInputManager.getInputDevice(deviceId); - if (DEBUG) { - Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + if (device != null) { + if (DEBUG) { + Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + } + onInputDeviceAdded(deviceId); } - onInputDeviceAdded(deviceId); } } @@ -364,7 +384,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } mJoysticksDevices.put(deviceId, joystick); - GodotLib.joyconnectionchanged(id, true, joystick.name); + handleJoystickConnectionChangedEvent(id, true, joystick.name); } @Override @@ -378,7 +398,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int godotJoyId = mJoystickIds.get(deviceId); mJoystickIds.delete(deviceId); mJoysticksDevices.delete(deviceId); - GodotLib.joyconnectionchanged(godotJoyId, false, ""); + handleJoystickConnectionChangedEvent(godotJoyId, false, ""); } @Override @@ -482,15 +502,15 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } } - static boolean handleMotionEvent(final MotionEvent event) { + boolean handleMotionEvent(final MotionEvent event) { return handleMotionEvent(event, event.getActionMasked()); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { return handleMotionEvent(event, eventActionOverride, false); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { if (isMouseEvent(event)) { return handleMouseEvent(event, eventActionOverride, doubleTap); } @@ -523,19 +543,19 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)Math.cos(orientation) * tiltMult; } - static boolean handleMouseEvent(final MotionEvent event) { + boolean handleMouseEvent(final MotionEvent event) { return handleMouseEvent(event, event.getActionMasked()); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { return handleMouseEvent(event, eventActionOverride, false); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { return handleMouseEvent(event, eventActionOverride, event.getButtonState(), doubleTap); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { final float x = event.getX(); final float y = event.getY(); @@ -564,11 +584,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event)); } - static boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { + boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { return handleMouseEvent(eventAction, 0, 0f, 0f, 0f, 0f, false, sourceMouseRelative, 1f, 0f, 0f); } - static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -584,6 +604,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { break; } + final int updatedButtonsMask = buttonsMask; // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. @@ -596,22 +617,26 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY)); + } else { + GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + } return true; } } return false; } - static boolean handleTouchEvent(final MotionEvent event) { + boolean handleTouchEvent(final MotionEvent event) { return handleTouchEvent(event, event.getActionMasked()); } - static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { + boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { return handleTouchEvent(event, eventActionOverride, false); } - static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { final int pointerCount = event.getPointerCount(); if (pointerCount == 0) { return true; @@ -636,10 +661,70 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap)); + } else { + GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); + } return true; } } return false; } + + void handleMagnifyEvent(float x, float y, float factor) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.magnify(x, y, factor)); + } else { + GodotLib.magnify(x, y, factor); + } + } + + void handlePanEvent(float x, float y, float deltaX, float deltaY) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.pan(x, y, deltaX, deltaY)); + } else { + GodotLib.pan(x, y, deltaX, deltaY); + } + } + + private void handleJoystickButtonEvent(int device, int button, boolean pressed) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joybutton(device, button, pressed)); + } else { + GodotLib.joybutton(device, button, pressed); + } + } + + private void handleJoystickAxisEvent(int device, int axis, float value) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyaxis(device, axis, value)); + } else { + GodotLib.joyaxis(device, axis, value); + } + } + + private void handleJoystickHatEvent(int device, int hatX, int hatY) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyhat(device, hatX, hatY)); + } else { + GodotLib.joyhat(device, hatX, hatY); + } + } + + private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.joyconnectionchanged(device, connected, name)); + } else { + GodotLib.joyconnectionchanged(device, connected, name); + } + } + + void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + if (shouldDispatchInputToRenderThread()) { + mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo)); + } else { + GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo); + } + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 06b565c30f..e545669970 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, false, false); if (mHasSelection) { mHasSelection = false; @@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene // Return keys are handled through action events continue; } - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } @@ -127,18 +127,16 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene if (characters != null) { for (int i = 0; i < characters.length(); i++) { final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } } if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed - mRenderView.queueOnRenderThread(() -> { - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); - }); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); mRenderView.getView().requestFocus(); return true; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt index 69748c0a8d..d39f2309b8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -81,7 +81,8 @@ fun beginBenchmarkMeasure(scope: String, label: String) { * * * Note: Only enabled on 'editorDev' build variant. */ -fun endBenchmarkMeasure(scope: String, label: String) { +@JvmOverloads +fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } @@ -93,6 +94,10 @@ fun endBenchmarkMeasure(scope: String, label: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Trace.endAsyncSection("[$scope] $label", 0) } + + if (dumpBenchmark) { + dumpBenchmark() + } } /** @@ -102,11 +107,11 @@ fun endBenchmarkMeasure(scope: String, label: String) { * * Note: Only enabled on 'editorDev' build variant. */ @JvmOverloads -fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { +fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } - if (!useBenchmark) { + if (!useBenchmark || benchmarkTracker.isEmpty()) { return } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 40068745d6..87d4281c5a 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -67,6 +67,13 @@ static AndroidInputHandler *input_handler = nullptr; static GodotJavaWrapper *godot_java = nullptr; static GodotIOJavaWrapper *godot_io_java = nullptr; +enum StartupStep { + STEP_TERMINATED = -1, + STEP_SETUP, + STEP_SHOW_LOGO, + STEP_STARTED +}; + static SafeNumeric<int> step; // Shared between UI and render threads static Size2 new_size; @@ -76,7 +83,7 @@ static Vector3 magnetometer; static Vector3 gyroscope; static void _terminate(JNIEnv *env, bool p_restart = false) { - step.set(-1); // Ensure no further steps are attempted and no further events are sent + step.set(STEP_TERMINATED); // Ensure no further steps are attempted and no further events are sent // lets cleanup // Unregister android plugins @@ -203,7 +210,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j os_android->set_display_size(Size2i(p_width, p_height)); // No need to reset the surface during startup - if (step.get() > 0) { + if (step.get() > STEP_SETUP) { if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); os_android->set_native_window(native_window); @@ -216,7 +223,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface) { if (os_android) { - if (step.get() == 0) { + if (step.get() == STEP_SETUP) { // During startup if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); @@ -230,7 +237,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *en } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz) { - if (step.get() == 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -244,20 +251,26 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *e } JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { - if (step.get() == -1) { + if (step.get() == STEP_TERMINATED) { return true; } - if (step.get() == 0) { + if (step.get() == STEP_SETUP) { // Since Godot is initialized on the UI thread, main_thread_id was set to that thread's id, // but for Godot purposes, the main thread is the one running the game loop - Main::setup2(); + Main::setup2(false); // The logo is shown in the next frame otherwise we run into rendering issues input_handler = new AndroidInputHandler(); step.increment(); return true; } - if (step.get() == 1) { + if (step.get() == STEP_SHOW_LOGO) { + Main::setup_boot_logo(); + step.increment(); + return true; + } + + if (step.get() == STEP_STARTED) { if (Main::start() != EXIT_SUCCESS) { return true; // should exit instead and print the error } @@ -283,7 +296,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JNIEnv *env, jclass clazz, jint p_event_type, jint p_button_mask, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y, jboolean p_double_click, jboolean p_source_mouse_relative, jfloat p_pressure, jfloat p_tilt_x, jfloat p_tilt_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -292,7 +305,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchMouseEvent(JN // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JNIEnv *env, jclass clazz, jint ev, jint pointer, jint pointer_count, jfloatArray position, jboolean p_double_tap) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -313,7 +326,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_dispatchTouchEvent(JN // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_factor) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_magnify(Point2(p_x, p_y), p_factor); @@ -321,7 +334,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_magnify(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jclass clazz, jfloat p_x, jfloat p_y, jfloat p_delta_x, jfloat p_delta_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_pan(Point2(p_x, p_y), Vector2(p_delta_x, p_delta_y)); @@ -329,7 +342,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_pan(JNIEnv *env, jcla // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env, jclass clazz, jint p_device, jint p_button, jboolean p_pressed) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -344,7 +357,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joybutton(JNIEnv *env // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, jclass clazz, jint p_device, jint p_axis, jfloat p_value) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -359,7 +372,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyaxis(JNIEnv *env, // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyhat(JNIEnv *env, jclass clazz, jint p_device, jint p_hat_x, jint p_hat_y) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -396,7 +409,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_joyconnectionchanged( // Called on the UI thread JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_key(JNIEnv *env, jclass clazz, jint p_physical_keycode, jint p_unicode, jint p_key_label, jboolean p_pressed, jboolean p_echo) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } input_handler->process_key_event(p_physical_keycode, p_unicode, p_key_label, p_pressed, p_echo); @@ -419,7 +432,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_gyroscope(JNIEnv *env } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -427,7 +440,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env, } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -516,7 +529,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -528,7 +541,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz) { - if (step.get() <= 0) { + if (step.get() <= STEP_SETUP) { return; } @@ -536,4 +549,11 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PAUSED); } } + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering) { + if (Input::get_singleton()) { + Input::get_singleton()->set_use_accumulated_input(p_use_accumulated_input); + Input::get_singleton()->set_use_input_buffering(p_use_input_buffering); + } +} } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index f32ffc291a..852c475e7e 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -69,6 +69,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering); } #endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 341cc517e3..7bdc75db29 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -2049,9 +2049,14 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z PointerData &pd = ss->pointer_data_buffer; + WindowState *ws = wl_surface_get_window_state(ss->pointed_surface); + ERR_FAIL_NULL(ws); + pd.relative_motion.x = wl_fixed_to_double(dx); pd.relative_motion.y = wl_fixed_to_double(dy); + pd.relative_motion *= window_state_get_scale_factor(ws); + pd.relative_motion_time = uptime_lo; } diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index a1a91345ac..da45391995 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -568,23 +568,7 @@ void DisplayServerMacOS::menu_callback(id p_sender) { } GodotMenuItem *value = [p_sender representedObject]; - if (value) { - if (value->max_states > 0) { - value->state++; - if (value->state >= value->max_states) { - value->state = 0; - } - } - - if (value->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ([p_sender state] == NSControlStateValueOff) { - [p_sender setState:NSControlStateValueOn]; - } else { - [p_sender setState:NSControlStateValueOff]; - } - } - if (value->callback.is_valid()) { MenuCall mc; mc.tag = value->meta; diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 942c351ac0..eebaed0eaf 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -41,8 +41,8 @@ int main(int argc, char **argv) { #if defined(VULKAN_ENABLED) - // MoltenVK - enable full component swizzling support. - setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); + setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); // MoltenVK - enable full component swizzling support. + setenv("MVK_CONFIG_SWAPCHAIN_MIN_MAG_FILTER_USE_NEAREST", "0", 1); // MoltenVK - use linear surface scaling. TODO: remove when full DPI scaling is implemented. #endif #if defined(SANITIZERS_ENABLED) diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index b6e2d41c08..e1af317259 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -52,6 +52,7 @@ enum GlobalMenuCheckType { Callable hover_callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked; int max_states; int state; Ref<Image> img; diff --git a/platform/macos/godot_menu_item.mm b/platform/macos/godot_menu_item.mm index 30dac9be9b..479542113a 100644 --- a/platform/macos/godot_menu_item.mm +++ b/platform/macos/godot_menu_item.mm @@ -31,4 +31,18 @@ #include "godot_menu_item.h" @implementation GodotMenuItem + +- (id)init { + self = [super init]; + + self->callback = Callable(); + self->key_callback = Callable(); + self->checkable_type = GlobalMenuCheckType::CHECKABLE_TYPE_NONE; + self->checked = false; + self->max_states = 0; + self->state = 0; + + return self; +} + @end diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 1ae1137ca0..802d58dc26 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -373,12 +373,7 @@ int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, c menu_item = [md->menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = Callable(); - obj->key_callback = Callable(); obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setRepresentedObject:obj]; [md_sub->menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; @@ -417,9 +412,6 @@ int NativeMenuMacOS::add_item(const RID &p_rid, const String &p_label, const Cal obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -438,8 +430,6 @@ int NativeMenuMacOS::add_check_item(const RID &p_rid, const String &p_label, con obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -457,9 +447,6 @@ int NativeMenuMacOS::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_ico obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -489,8 +476,6 @@ int NativeMenuMacOS::add_icon_check_item(const RID &p_rid, const Ref<Texture2D> obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -520,8 +505,6 @@ int NativeMenuMacOS::add_radio_check_item(const RID &p_rid, const String &p_labe obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -540,8 +523,6 @@ int NativeMenuMacOS::add_icon_radio_check_item(const RID &p_rid, const Ref<Textu obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -570,7 +551,6 @@ int NativeMenuMacOS::add_multistate_item(const RID &p_rid, const String &p_label obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; obj->max_states = p_max_states; obj->state = p_default_state; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; @@ -640,7 +620,10 @@ bool NativeMenuMacOS::is_item_checked(const RID &p_rid, int p_idx) const { ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - return ([menu_item state] == NSControlStateValueOn); + const GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checked; + } } return false; } @@ -958,10 +941,14 @@ void NativeMenuMacOS::set_item_checked(const RID &p_rid, int p_idx, bool p_check ERR_FAIL_COND(p_idx >= item_start + item_count); NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - if (p_checked) { - [menu_item setState:NSControlStateValueOn]; - } else { - [menu_item setState:NSControlStateValueOff]; + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + obj->checked = p_checked; + if (p_checked) { + [menu_item setState:NSControlStateValueOn]; + } else { + [menu_item setState:NSControlStateValueOff]; + } } } } diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index dd986e650c..b24c6cb1fd 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -33,6 +33,8 @@ #include "godot_audio.h" #include "core/config/project_settings.h" +#include "core/object/object.h" +#include "scene/main/node.h" #include "servers/audio/audio_stream.h" #include <emscripten.h> @@ -51,6 +53,33 @@ void AudioDriverWeb::_latency_update_callback(float p_latency) { AudioDriverWeb::audio_context.output_latency = p_latency; } +void AudioDriverWeb::_sample_playback_finished_callback(const char *p_playback_object_id) { + const ObjectID playback_id = ObjectID(String::to_int(p_playback_object_id)); + + Object *playback_object = ObjectDB::get_instance(playback_id); + if (playback_object == nullptr) { + return; + } + Ref<AudioSamplePlayback> playback = Object::cast_to<AudioSamplePlayback>(playback_object); + if (playback.is_null()) { + return; + } + + Object *player_object = ObjectDB::get_instance(playback->player_id); + if (player_object == nullptr) { + return; + } + Node *player = Object::cast_to<Node>(player_object); + if (player == nullptr) { + return; + } + + const StringName finished = SNAME("finished"); + if (player->has_signal(finished)) { + player->emit_signal(finished); + } +} + void AudioDriverWeb::_audio_driver_process(int p_from, int p_samples) { int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); const int max_samples = memarr_len(output_rb); @@ -132,6 +161,9 @@ Error AudioDriverWeb::init() { if (!input_rb) { return ERR_OUT_OF_MEMORY; } + + godot_audio_sample_set_finished_callback(&_sample_playback_finished_callback); + return OK; } diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h index 298ad90fae..46c5ce4de1 100644 --- a/platform/web/audio_driver_web.h +++ b/platform/web/audio_driver_web.h @@ -58,6 +58,7 @@ private: WASM_EXPORT static void _state_change_callback(int p_state); WASM_EXPORT static void _latency_update_callback(float p_latency); + WASM_EXPORT static void _sample_playback_finished_callback(const char *p_playback_object_id); static AudioDriverWeb *singleton; diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h index 8bebbcf7de..dd5bec00cf 100644 --- a/platform/web/godot_audio.h +++ b/platform/web/godot_audio.h @@ -57,6 +57,7 @@ extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool extern int godot_audio_sample_is_active(const char *p_playback_object_id); extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale); extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size); +extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *)); extern void godot_audio_sample_bus_set_count(int p_count); extern void godot_audio_sample_bus_remove(int p_index); diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js index 531dbdaeab..0b16b07261 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -687,9 +687,15 @@ class SampleNode { } switch (self.getSample().loopMode) { - case 'disabled': + case 'disabled': { + const id = this.id; self.stop(); - break; + if (GodotAudio.sampleFinishedCallback != null) { + const idCharPtr = GodotRuntime.allocString(id); + GodotAudio.sampleFinishedCallback(idCharPtr); + GodotRuntime.free(idCharPtr); + } + } break; case 'forward': case 'backward': self.restart(); @@ -1090,6 +1096,12 @@ const _GodotAudio = { busSolo: null, Bus, + /** + * Callback to signal that a sample has finished. + * @type {(playbackObjectIdPtr: number) => void | null} + */ + sampleFinishedCallback: null, + /** @type {AudioContext} */ ctx: null, input: null, @@ -1764,6 +1776,17 @@ const _GodotAudio = { godot_audio_sample_bus_set_mute: function (bus, enable) { GodotAudio.set_sample_bus_mute(bus, Boolean(enable)); }, + + godot_audio_sample_set_finished_callback__proxy: 'sync', + godot_audio_sample_set_finished_callback__sig: 'vi', + /** + * Sets the finished callback + * @param {Number} callbackPtr Finished callback pointer + * @returns {void} + */ + godot_audio_sample_set_finished_callback: function (callbackPtr) { + GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr); + }, }; autoAddDeps(_GodotAudio, '$GodotAudio'); diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js index 7ea89d553f..6e3b97023d 100644 --- a/platform/web/js/libs/library_godot_input.js +++ b/platform/web/js/libs/library_godot_input.js @@ -112,6 +112,7 @@ const GodotIME = { ime.style.top = '0px'; ime.style.width = '100%'; ime.style.height = '40px'; + ime.style.pointerEvents = 'none'; ime.style.display = 'none'; ime.contentEditable = 'true'; diff --git a/platform/web/js/libs/library_godot_javascript_singleton.js b/platform/web/js/libs/library_godot_javascript_singleton.js index b17fde1544..6bb69bca95 100644 --- a/platform/web/js/libs/library_godot_javascript_singleton.js +++ b/platform/web/js/libs/library_godot_javascript_singleton.js @@ -81,11 +81,16 @@ const GodotJSWrapper = { case 0: return null; case 1: - return !!GodotRuntime.getHeapValue(val, 'i64'); - case 2: - return GodotRuntime.getHeapValue(val, 'i64'); + return Boolean(GodotRuntime.getHeapValue(val, 'i64')); + case 2: { + // `heap_value` may be a bigint. + const heap_value = GodotRuntime.getHeapValue(val, 'i64'); + return heap_value >= Number.MIN_SAFE_INTEGER && heap_value <= Number.MAX_SAFE_INTEGER + ? Number(heap_value) + : heap_value; + } case 3: - return GodotRuntime.getHeapValue(val, 'double'); + return Number(GodotRuntime.getHeapValue(val, 'double')); case 4: return GodotRuntime.parseString(GodotRuntime.getHeapValue(val, '*')); case 24: // OBJECT @@ -110,6 +115,9 @@ const GodotJSWrapper = { } GodotRuntime.setHeapValue(p_exchange, p_val, 'double'); return 3; // FLOAT + } else if (type === 'bigint') { + GodotRuntime.setHeapValue(p_exchange, p_val, 'i64'); + return 2; // INT } else if (type === 'string') { const c_str = GodotRuntime.allocString(p_val); GodotRuntime.setHeapValue(p_exchange, c_str, '*'); diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp index 04513f6d57..d0c3bd7c0e 100644 --- a/platform/web/web_main.cpp +++ b/platform/web/web_main.cpp @@ -35,6 +35,8 @@ #include "core/config/engine.h" #include "core/io/resource_loader.h" #include "main/main.h" +#include "scene/main/scene_tree.h" +#include "scene/main/window.h" // SceneTree only forward declares it. #include <emscripten/emscripten.h> #include <stdlib.h> @@ -130,7 +132,7 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { if (Engine::get_singleton()->is_project_manager_hint() && FileAccess::exists("/tmp/preload.zip")) { PackedStringArray ps; ps.push_back("/tmp/preload.zip"); - os->get_main_loop()->emit_signal(SNAME("files_dropped"), ps, -1); + SceneTree::get_singleton()->get_root()->emit_signal(SNAME("files_dropped"), ps); } #endif emscripten_set_main_loop(main_loop_callback, -1, false); diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 8d26a705a9..c1b3540f68 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -38,6 +38,7 @@ #include "core/version.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" +#include "scene/resources/texture.h" #if defined(VULKAN_ENABLED) #include "rendering_context_driver_vulkan_windows.h" @@ -3807,9 +3808,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ACTIVATE: { // Activation can happen just after the window has been created, even before the callbacks are set. // Therefore, it's safer to defer the delivery of the event. - if (!windows[window_id].activate_timer_id) { - windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); - } + // It's important to set an nIDEvent different from the SetTimer for move_timer_id because + // if the same nIDEvent is passed, the timer is replaced and the same timer_id is returned. + windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_WINDOW_ACTIVATION, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); windows[window_id].activate_state = GET_WM_ACTIVATE_STATE(wParam, lParam); return 0; } break; @@ -4727,7 +4728,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ENTERSIZEMOVE: { Input::get_singleton()->release_pressed_events(); - windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); + windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_MOVE_REDRAW, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); } break; case WM_EXITSIZEMOVE: { KillTimer(windows[window_id].hWnd, windows[window_id].move_timer_id); diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 382f18c239..c2f4de7d81 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -332,6 +332,11 @@ class DisplayServerWindows : public DisplayServer { String tablet_driver; Vector<String> tablet_drivers; + enum TimerID { + TIMER_ID_MOVE_REDRAW = 1, + TIMER_ID_WINDOW_ACTIVATION = 2, + }; + enum { KEY_EVENT_BUFFER_SIZE = 512 }; diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp index d9dc28e9d9..fde55918e4 100644 --- a/platform/windows/native_menu_windows.cpp +++ b/platform/windows/native_menu_windows.cpp @@ -81,22 +81,6 @@ void NativeMenuWindows::_menu_activate(HMENU p_menu, int p_index) const { if (GetMenuItemInfoW(md->menu, p_index, true, &item)) { MenuItemData *item_data = (MenuItemData *)item.dwItemData; if (item_data) { - if (item_data->max_states > 0) { - item_data->state++; - if (item_data->state >= item_data->max_states) { - item_data->state = 0; - } - } - - if (item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ((item.fState & MFS_CHECKED) == MFS_CHECKED) { - item.fState &= ~MFS_CHECKED; - } else { - item.fState |= MFS_CHECKED; - } - SetMenuItemInfoW(md->menu, p_index, true, &item); - } - if (item_data->callback.is_valid()) { Variant ret; Callable::CallError ce; @@ -619,9 +603,12 @@ bool NativeMenuWindows::is_item_checked(const RID &p_rid, int p_idx) const { MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - return (item.fState & MFS_CHECKED) == MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->checked; + } } return false; } @@ -861,12 +848,16 @@ void NativeMenuWindows::set_item_checked(const RID &p_rid, int p_idx, bool p_che MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - if (p_checked) { - item.fState |= MFS_CHECKED; - } else { - item.fState &= ~MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->checked = p_checked; + if (p_checked) { + item.fState |= MFS_CHECKED; + } else { + item.fState &= ~MFS_CHECKED; + } } SetMenuItemInfoW(md->menu, p_idx, true, &item); } diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h index 5c4aaa52c8..235a4b332a 100644 --- a/platform/windows/native_menu_windows.h +++ b/platform/windows/native_menu_windows.h @@ -51,6 +51,7 @@ class NativeMenuWindows : public NativeMenu { Callable callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked = false; int max_states = 0; int state = 0; Ref<Image> img; diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index 514c5e7a8f..7020d162fe 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -302,6 +302,12 @@ void Camera2D::_notification(int p_what) { _interpolation_data.xform_prev = _interpolation_data.xform_curr; } break; + case NOTIFICATION_PAUSED: { + if (is_physics_interpolated_and_enabled()) { + _update_scroll(); + } + } break; + case NOTIFICATION_TRANSFORM_CHANGED: { if ((!position_smoothing_enabled && !is_physics_interpolated_and_enabled()) || _is_editing_in_editor()) { _update_scroll(); diff --git a/scene/2d/camera_2d.h b/scene/2d/camera_2d.h index 8754e35e88..be2da8b97a 100644 --- a/scene/2d/camera_2d.h +++ b/scene/2d/camera_2d.h @@ -108,7 +108,7 @@ protected: struct InterpolationData { Transform2D xform_curr; Transform2D xform_prev; - uint32_t last_update_physics_tick = 0; + uint32_t last_update_physics_tick = UINT32_MAX; // Ensure tick 0 is detected as a change. } _interpolation_data; void _ensure_update_interpolation_data(); diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp index 9e3e6ea583..d0fae611d8 100644 --- a/scene/2d/navigation_agent_2d.cpp +++ b/scene/2d/navigation_agent_2d.cpp @@ -671,8 +671,6 @@ void NavigationAgent2D::_update_navigation() { return; } - update_frame_id = Engine::get_singleton()->get_physics_frames(); - Vector2 origin = agent_parent->get_global_position(); bool reload_path = false; @@ -767,7 +765,6 @@ void NavigationAgent2D::_request_repath() { target_reached = false; navigation_finished = false; last_waypoint_reached = false; - update_frame_id = 0; } bool NavigationAgent2D::_is_last_waypoint() const { diff --git a/scene/2d/navigation_agent_2d.h b/scene/2d/navigation_agent_2d.h index 0acfc82162..8741f578d0 100644 --- a/scene/2d/navigation_agent_2d.h +++ b/scene/2d/navigation_agent_2d.h @@ -91,8 +91,6 @@ class NavigationAgent2D : public Node { bool target_reached = false; bool navigation_finished = true; bool last_waypoint_reached = false; - // No initialized on purpose - uint32_t update_frame_id = 0; // Debug properties for exposed bindings bool debug_enabled = false; diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index aacab3213d..b3586a1da0 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -86,11 +86,10 @@ void Parallax2D::_update_scroll() { } Point2 scroll_ofs = screen_offset; - Size2 vps = get_viewport_rect().size; - if (Engine::get_singleton()->is_editor_hint()) { - vps = Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height")); - } else { + if (!Engine::get_singleton()->is_editor_hint()) { + Size2 vps = get_viewport_rect().size; + if (limit_begin.x <= limit_end.x - vps.x) { scroll_ofs.x = CLAMP(scroll_ofs.x, limit_begin.x, limit_end.x - vps.x); } diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp index dff413f5d2..5bbb724e2f 100644 --- a/scene/3d/navigation_agent_3d.cpp +++ b/scene/3d/navigation_agent_3d.cpp @@ -737,8 +737,6 @@ void NavigationAgent3D::_update_navigation() { return; } - update_frame_id = Engine::get_singleton()->get_physics_frames(); - Vector3 origin = agent_parent->get_global_position(); bool reload_path = false; @@ -835,7 +833,6 @@ void NavigationAgent3D::_request_repath() { target_reached = false; navigation_finished = false; last_waypoint_reached = false; - update_frame_id = 0; } bool NavigationAgent3D::_is_last_waypoint() const { diff --git a/scene/3d/navigation_agent_3d.h b/scene/3d/navigation_agent_3d.h index ade6afd445..d5721a56c8 100644 --- a/scene/3d/navigation_agent_3d.h +++ b/scene/3d/navigation_agent_3d.h @@ -98,8 +98,6 @@ class NavigationAgent3D : public Node { bool target_reached = false; bool navigation_finished = true; bool last_waypoint_reached = false; - // No initialized on purpose - uint32_t update_frame_id = 0; // Debug properties for exposed bindings bool debug_enabled = false; diff --git a/scene/3d/sprite_3d.cpp b/scene/3d/sprite_3d.cpp index d08aeb1de2..8ac585719c 100644 --- a/scene/3d/sprite_3d.cpp +++ b/scene/3d/sprite_3d.cpp @@ -984,7 +984,7 @@ void Sprite3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "hframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_hframes", "get_hframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vframes", PROPERTY_HINT_RANGE, "1,16384,1"), "set_vframes", "get_vframes"); ADD_PROPERTY(PropertyInfo(Variant::INT, "frame"), "set_frame", "get_frame"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "frame_coords", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_frame_coords", "get_frame_coords"); ADD_GROUP("Region", "region_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "region_enabled"), "set_region_enabled", "is_region_enabled"); ADD_PROPERTY(PropertyInfo(Variant::RECT2, "region_rect", PROPERTY_HINT_NONE, "suffix:px"), "set_region_rect", "get_region_rect"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index f270f32193..508b2c49fa 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -95,9 +95,9 @@ private: } bool operator<(const BlendKey &bk) const { if (from == bk.from) { - return to < bk.to; + return StringName::AlphCompare()(to, bk.to); } else { - return from < bk.from; + return StringName::AlphCompare()(from, bk.from); } } }; diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp index b3a55ddee0..6653e01f25 100644 --- a/scene/audio/audio_stream_player_internal.cpp +++ b/scene/audio/audio_stream_player_internal.cpp @@ -152,6 +152,7 @@ Ref<AudioStreamPlayback> AudioStreamPlayerInternal::play_basic() { Ref<AudioSamplePlayback> sample_playback; sample_playback.instantiate(); sample_playback->stream = stream; + sample_playback->player_id = node->get_instance_id(); stream_playback->set_sample_playback(sample_playback); } } else if (!stream->is_meta_stream()) { diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index 4e738216de..dd344121e1 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -530,7 +530,7 @@ Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Textu } } - return _get_largest_stylebox_size() + minsize; + return (theme_cache.align_to_largest_stylebox ? _get_largest_stylebox_size() : _get_current_stylebox()->get_minimum_size()) + minsize; } void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) { diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 0682c11a9b..1d53edbfa6 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -1732,11 +1732,15 @@ void Control::_size_changed() { new_size_cache.height = minimum_size.height; } - bool pos_changed = new_pos_cache != data.pos_cache; - bool size_changed = new_size_cache != data.size_cache; + bool pos_changed = !new_pos_cache.is_equal_approx(data.pos_cache); + bool size_changed = !new_size_cache.is_equal_approx(data.size_cache); - data.pos_cache = new_pos_cache; - data.size_cache = new_size_cache; + if (pos_changed) { + data.pos_cache = new_pos_cache; + } + if (size_changed) { + data.size_cache = new_size_cache; + } if (is_inside_tree()) { if (pos_changed || size_changed) { @@ -1751,12 +1755,10 @@ void Control::_size_changed() { } if (pos_changed && !size_changed) { - _update_canvas_item_transform(); //move because it won't be updated - } - } else { - if (pos_changed) { - _notify_transform(); + _update_canvas_item_transform(); } + } else if (pos_changed) { + _notify_transform(); } } diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 33756dc1fd..55a2c607e3 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -546,6 +546,11 @@ void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { GraphNode *graph_node = Object::cast_to<GraphNode>(p_node); ERR_FAIL_NULL(graph_node); + // Update all adjacent connections during the next redraw. + for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) { + conn->_cache.dirty = true; + } + minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); @@ -782,7 +787,9 @@ Rect2 GraphEdit::_compute_shrinked_frame_rect(const GraphFrame *p_frame) { return Rect2(p_frame->get_position_offset(), Size2()); } - min_point -= Size2(autoshrink_margin, autoshrink_margin); + const Size2 titlebar_size = p_frame->get_titlebar_size(); + + min_point -= Size2(autoshrink_margin, MAX(autoshrink_margin, titlebar_size.y)); max_point += Size2(autoshrink_margin, autoshrink_margin); return Rect2(min_point, max_point - min_point); diff --git a/scene/gui/graph_frame.cpp b/scene/gui/graph_frame.cpp index 8cd7dbbeb5..e85d007262 100644 --- a/scene/gui/graph_frame.cpp +++ b/scene/gui/graph_frame.cpp @@ -262,6 +262,10 @@ HBoxContainer *GraphFrame::get_titlebar_hbox() { return titlebar_hbox; } +Size2 GraphFrame::get_titlebar_size() const { + return titlebar_hbox->get_size() + theme_cache.titlebar->get_minimum_size(); +} + void GraphFrame::set_drag_margin(int p_margin) { drag_margin = p_margin; } diff --git a/scene/gui/graph_frame.h b/scene/gui/graph_frame.h index 21346586c8..2af09cf872 100644 --- a/scene/gui/graph_frame.h +++ b/scene/gui/graph_frame.h @@ -89,6 +89,7 @@ public: int get_autoshrink_margin() const; HBoxContainer *get_titlebar_hbox(); + Size2 get_titlebar_size() const; void set_drag_margin(int p_margin); int get_drag_margin() const; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 4f07fdb87b..7f795ea710 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -2314,6 +2314,16 @@ bool PopupMenu::is_prefer_native_menu() const { return prefer_native; } +bool PopupMenu::is_native_menu() const { +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene()) { + return false; + } +#endif + + return global_menu.is_valid(); +} + bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only) { ERR_FAIL_COND_V(p_event.is_null(), false); Key code = Key::NONE; @@ -2643,6 +2653,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("set_prefer_native_menu", "enabled"), &PopupMenu::set_prefer_native_menu); ClassDB::bind_method(D_METHOD("is_prefer_native_menu"), &PopupMenu::is_prefer_native_menu); + ClassDB::bind_method(D_METHOD("is_native_menu"), &PopupMenu::is_native_menu); ClassDB::bind_method(D_METHOD("add_item", "label", "id", "accel"), &PopupMenu::add_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_icon_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_item, DEFVAL(-1), DEFVAL(0)); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index c6eef03aca..5313dae404 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -330,6 +330,8 @@ public: void set_prefer_native_menu(bool p_enabled); bool is_prefer_native_menu() const; + bool is_native_menu() const; + void scroll_to_item(int p_idx); bool activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only = false); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 7b682daa83..9cc59f1def 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -1643,21 +1643,14 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_END: { - if (is_drag_successful()) { - if (selection_drag_attempt) { - // Dropped elsewhere. - if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { - delete_selection(); - } else if (deselect_on_focus_loss_enabled) { - deselect(); - } - } - } - if (drag_caret_index >= 0) { - if (drag_caret_index < carets.size()) { - remove_caret(drag_caret_index); + remove_drag_caret(); + if (selection_drag_attempt && is_drag_successful()) { + // Dropped elsewhere. + if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + delete_selection(); + } else if (deselect_on_focus_loss_enabled) { + deselect(); } - drag_caret_index = -1; } selection_drag_attempt = false; drag_action = false; @@ -4606,6 +4599,15 @@ void TextEdit::remove_caret(int p_caret) { } } +void TextEdit::remove_drag_caret() { + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } +} + void TextEdit::remove_secondary_carets() { if (carets.size() == 1) { return; diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 6ed5cf4bdc..4d9d169c1c 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -881,6 +881,7 @@ public: int add_caret(int p_line, int p_column); void remove_caret(int p_caret); + void remove_drag_caret(); void remove_secondary_carets(); int get_caret_count() const; void add_caret_at_carets(bool p_below); diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 5bbf8ebff4..0396f3ab4a 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -177,6 +177,12 @@ void Node::_notification(int p_notification) { } } break; + case NOTIFICATION_PAUSED: { + if (is_physics_interpolated_and_enabled() && is_inside_tree()) { + reset_physics_interpolation(); + } + } break; + case NOTIFICATION_PATH_RENAMED: { if (data.path_cache) { memdelete(data.path_cache); diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index 254bd38be7..c0ab636adc 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -3189,6 +3189,20 @@ StringName Animation::method_track_get_name(int p_track, int p_key_idx) const { return pm->methods[p_key_idx].method; } +Array Animation::make_default_bezier_key(float p_value) { + const double max_width = length / 2.0; + Array new_point; + new_point.resize(5); + + new_point[0] = p_value; + new_point[1] = MAX(-0.25, -max_width); + new_point[2] = 0; + new_point[3] = MIN(0.25, max_width); + new_point[4] = 0; + + return new_point; +} + int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle) { ERR_FAIL_INDEX_V(p_track, tracks.size(), -1); Track *t = tracks[p_track]; diff --git a/scene/resources/animation.h b/scene/resources/animation.h index e9bfc298a5..cb12b12c0e 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -455,6 +455,7 @@ public: void track_set_interpolation_type(int p_track, InterpolationType p_interp); InterpolationType track_get_interpolation_type(int p_track) const; + Array make_default_bezier_key(float p_value); int bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle); void bezier_track_set_key_value(int p_track, int p_index, real_t p_value); void bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle, real_t p_balanced_value_time_ratio = 1.0); diff --git a/scene/resources/font.cpp b/scene/resources/font.cpp index bc8e0b9015..37d9d57722 100644 --- a/scene/resources/font.cpp +++ b/scene/resources/font.cpp @@ -3213,7 +3213,6 @@ void SystemFont::_update_base_font() { } _invalidate_rids(); - notify_property_list_changed(); } void SystemFont::reset_state() { diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index b1742bd5a3..900629f5f8 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -2124,6 +2124,56 @@ void PackedScene::recreate_state() { #endif } +#ifdef TOOLS_ENABLED +HashSet<StringName> PackedScene::get_scene_groups(const String &p_path) { + { + Ref<PackedScene> packed_scene = ResourceCache::get_ref(p_path); + if (packed_scene.is_valid()) { + return packed_scene->get_state()->get_all_groups(); + } + } + + if (p_path.get_extension() == "tscn") { + Ref<FileAccess> scene_file = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V(scene_file.is_null(), HashSet<StringName>()); + + HashSet<StringName> ret; + while (!scene_file->eof_reached()) { + const String line = scene_file->get_line(); + if (!line.begins_with("[node")) { + continue; + } + + int i = line.find("groups=["); + if (i == -1) { + continue; + } + + int j = line.find_char(']', i); + while (i < j) { + i = line.find_char('"', i); + if (i == -1) { + break; + } + + int k = line.find_char('"', i + 1); + if (k == -1) { + break; + } + + ret.insert(line.substr(i + 1, k - i - 1)); + i = k + 1; + } + } + return ret; + } else { + Ref<PackedScene> packed_scene = ResourceLoader::load(p_path); + ERR_FAIL_COND_V(packed_scene.is_null(), HashSet<StringName>()); + return packed_scene->get_state()->get_all_groups(); + } +} +#endif + Ref<SceneState> PackedScene::get_state() const { return state; } diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h index c46a4dd5fe..e26b9f7b90 100644 --- a/scene/resources/packed_scene.h +++ b/scene/resources/packed_scene.h @@ -270,6 +270,7 @@ public: state->set_last_modified_time(p_time); } + static HashSet<StringName> get_scene_groups(const String &p_path); #endif Ref<SceneState> get_state() const; diff --git a/servers/audio/audio_stream.h b/servers/audio/audio_stream.h index 0ca4777d5c..9149109381 100644 --- a/servers/audio/audio_stream.h +++ b/servers/audio/audio_stream.h @@ -49,6 +49,7 @@ class AudioSamplePlayback : public RefCounted { public: Ref<AudioStream> stream; + ObjectID player_id; float offset = 0.0f; Vector<AudioFrame> volume_vector; StringName bus; diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 59f7b3d9e1..801ad1b825 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -5380,7 +5380,7 @@ Error RenderingDevice::initialize(RenderingContextDriver *p_context, DisplayServ frame = 0; frames.resize(frame_count); - max_timestamp_query_elements = 256; + max_timestamp_query_elements = GLOBAL_GET("debug/settings/profiler/max_timestamp_query_elements"); device = context->device_get(device_index); err = driver->initialize(device_index, frame_count); @@ -5631,7 +5631,7 @@ void RenderingDevice::_free_rids(T &p_owner, const char *p_type) { void RenderingDevice::capture_timestamp(const String &p_name) { ERR_FAIL_COND_MSG(draw_list != nullptr && draw_list->state.draw_count > 0, "Capturing timestamps during draw list creation is not allowed. Offending timestamp was: " + p_name); ERR_FAIL_COND_MSG(compute_list != nullptr && compute_list->state.dispatch_count > 0, "Capturing timestamps during compute list creation is not allowed. Offending timestamp was: " + p_name); - ERR_FAIL_COND(frames[frame].timestamp_count >= max_timestamp_query_elements); + ERR_FAIL_COND_MSG(frames[frame].timestamp_count >= max_timestamp_query_elements, vformat("Tried capturing more timestamps than the configured maximum (%d). You can increase this limit in the project settings under 'Debug/Settings' called 'Max Timestamp Query Elements'.", max_timestamp_query_elements)); draw_graph.add_capture_timestamp(frames[frame].timestamp_pool, frames[frame].timestamp_count); diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index dd3491f62c..70b585d683 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -347,6 +347,22 @@ void _get_tbn_from_axis_angle(const Vector3 &p_axis, float p_angle, Vector3 &r_n r_normal = tbn.rows[2]; } +AABB _compute_aabb_from_points(const Vector3 *p_data, int p_length) { + if (p_length == 0) { + return AABB(); + } + + Vector3 min = p_data[0]; + Vector3 max = p_data[0]; + + for (int i = 1; i < p_length; ++i) { + min = min.min(p_data[i]); + max = max.max(p_data[i]); + } + + return AABB(min, max - min); +} + Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint32_t *p_offsets, uint32_t p_vertex_stride, uint32_t p_normal_stride, uint32_t p_attrib_stride, uint32_t p_skin_stride, Vector<uint8_t> &r_vertex_array, Vector<uint8_t> &r_attrib_array, Vector<uint8_t> &r_skin_array, int p_vertex_array_len, Vector<uint8_t> &r_index_array, int p_index_array_len, AABB &r_aabb, Vector<AABB> &r_bone_aabb, Vector4 &r_uv_scale) { uint8_t *vw = r_vertex_array.ptrw(); uint8_t *aw = r_attrib_array.ptrw(); @@ -440,18 +456,10 @@ Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint const Vector3 *src = array.ptr(); - r_aabb = AABB(); + r_aabb = _compute_aabb_from_points(src, p_vertex_array_len); + r_aabb.size = r_aabb.size.max(SMALL_VEC3); if (p_format & ARRAY_FLAG_COMPRESS_ATTRIBUTES) { - // First we need to generate the AABB for the entire surface. - for (int i = 0; i < p_vertex_array_len; i++) { - if (i == 0) { - r_aabb = AABB(src[i], SMALL_VEC3); - } else { - r_aabb.expand_to(src[i]); - } - } - if (!(p_format & RS::ARRAY_FORMAT_NORMAL)) { // Early out if we are only setting vertex positions. for (int i = 0; i < p_vertex_array_len; i++) { @@ -592,12 +600,6 @@ Error RenderingServer::_surface_set_data(Array p_arrays, uint64_t p_format, uint float vector[3] = { (float)src[i].x, (float)src[i].y, (float)src[i].z }; memcpy(&vw[p_offsets[ai] + i * p_vertex_stride], vector, sizeof(float) * 3); - - if (i == 0) { - r_aabb = AABB(src[i], SMALL_VEC3); - } else { - r_aabb.expand_to(src[i]); - } } } } |