summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/extension/gdextension_library_loader.cpp4
-rw-r--r--core/extension/gdextension_library_loader.h1
-rw-r--r--core/extension/gdextension_loader.h1
-rw-r--r--core/extension/gdextension_manager.cpp3
-rw-r--r--core/string/ustring.cpp25
-rw-r--r--core/string/ustring.h2
-rw-r--r--doc/classes/@GlobalScope.xml2
-rw-r--r--doc/classes/Animation.xml71
-rw-r--r--doc/classes/AnimationPlayer.xml89
-rw-r--r--doc/classes/CameraAttributesPhysical.xml2
-rw-r--r--doc/classes/EditorExportPlugin.xml9
-rw-r--r--doc/classes/EditorInspector.xml1
-rw-r--r--doc/classes/EditorSettings.xml5
-rw-r--r--doc/classes/PopupMenu.xml2
-rw-r--r--doc/classes/PopupPanel.xml2
-rw-r--r--doc/classes/ProjectSettings.xml2
-rw-r--r--doc/classes/RenderingServer.xml2
-rw-r--r--doc/classes/Sprite2D.xml2
-rw-r--r--doc/classes/TreeItem.xml16
-rw-r--r--doc/classes/Vector4i.xml2
-rw-r--r--drivers/gles3/shaders/canvas.glsl3
-rw-r--r--drivers/vulkan/godot_vulkan.h42
-rw-r--r--drivers/vulkan/rendering_context_driver_vulkan.h6
-rw-r--r--drivers/vulkan/rendering_device_driver_vulkan.h6
-rw-r--r--drivers/vulkan/vulkan_hooks.h6
-rw-r--r--editor/animation_track_editor.cpp1398
-rw-r--r--editor/animation_track_editor.h216
-rw-r--r--editor/debugger/editor_debugger_server.cpp4
-rw-r--r--editor/editor_about.cpp30
-rw-r--r--editor/editor_about.h5
-rw-r--r--editor/editor_inspector.cpp24
-rw-r--r--editor/editor_inspector.h7
-rw-r--r--editor/editor_node.cpp1
-rw-r--r--editor/editor_properties.cpp5
-rw-r--r--editor/editor_settings.cpp12
-rw-r--r--editor/editor_settings.h7
-rw-r--r--editor/export/editor_export_plugin.cpp7
-rw-r--r--editor/export/editor_export_plugin.h2
-rw-r--r--editor/export/editor_export_preset.cpp25
-rw-r--r--editor/gui/editor_bottom_panel.cpp29
-rw-r--r--editor/gui/editor_bottom_panel.h3
-rw-r--r--editor/gui/editor_version_button.cpp85
-rw-r--r--editor/gui/editor_version_button.h61
-rw-r--r--editor/icons/Marker.svg1
-rw-r--r--editor/icons/MarkerSelected.svg1
-rw-r--r--editor/inspector_dock.cpp2
-rw-r--r--editor/plugins/animation_blend_tree_editor_plugin.cpp166
-rw-r--r--editor/plugins/animation_blend_tree_editor_plugin.h47
-rw-r--r--editor/plugins/animation_player_editor_plugin.cpp58
-rw-r--r--editor/plugins/animation_player_editor_plugin.h26
-rw-r--r--editor/plugins/asset_library_editor_plugin.cpp3
-rw-r--r--editor/project_manager.cpp24
-rw-r--r--editor/project_manager.h7
-rw-r--r--editor/scene_tree_dock.cpp89
-rw-r--r--main/main.cpp15
-rw-r--r--modules/multiplayer/SCsub7
-rw-r--r--modules/multiplayer/doc_classes/SceneMultiplayer.xml2
-rw-r--r--modules/multiplayer/tests/test_scene_multiplayer.h284
-rw-r--r--modules/websocket/editor/editor_debugger_server_websocket.cpp4
-rw-r--r--modules/websocket/emws_peer.cpp3
-rw-r--r--modules/websocket/wsl_peer.cpp3
-rw-r--r--modules/webxr/doc_classes/WebXRInterface.xml2
-rw-r--r--platform/android/api/api.cpp10
-rw-r--r--platform/android/api/java_class_wrapper.h13
-rw-r--r--platform/android/api/jni_singleton.h197
-rw-r--r--platform/android/export/export_plugin.cpp3
-rw-r--r--platform/android/java/app/build.gradle6
-rw-r--r--platform/android/java/app/config.gradle5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt63
-rw-r--r--platform/android/java_class_wrapper.cpp10
-rw-r--r--platform/android/java_godot_lib_jni.cpp4
-rw-r--r--platform/android/plugin/godot_plugin_jni.cpp25
-rw-r--r--platform/android/rendering_context_driver_vulkan_android.cpp6
-rw-r--r--platform/ios/display_server_ios.h6
-rw-r--r--platform/ios/os_ios.mm6
-rw-r--r--platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp6
-rw-r--r--platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp6
-rw-r--r--platform/windows/rendering_context_driver_vulkan_windows.cpp6
-rw-r--r--scene/2d/cpu_particles_2d.cpp2
-rw-r--r--scene/2d/gpu_particles_2d.cpp12
-rw-r--r--scene/animation/animation_blend_tree.cpp2
-rw-r--r--scene/animation/animation_mixer.cpp93
-rw-r--r--scene/animation/animation_mixer.h2
-rw-r--r--scene/animation/animation_player.cpp187
-rw-r--r--scene/animation/animation_player.h16
-rw-r--r--scene/gui/tree.cpp49
-rw-r--r--scene/gui/tree.h7
-rw-r--r--scene/main/http_request.cpp3
-rw-r--r--scene/resources/animation.cpp146
-rw-r--r--scene/resources/animation.h27
-rw-r--r--scene/resources/material.cpp19
-rw-r--r--scene/resources/style_box_flat.cpp147
-rw-r--r--servers/rendering/renderer_rd/shaders/canvas.glsl3
-rw-r--r--tests/core/io/test_packet_peer.h204
-rw-r--r--tests/core/io/test_stream_peer.h289
-rw-r--r--tests/core/io/test_stream_peer_buffer.h185
-rw-r--r--tests/core/string/test_string.h40
-rw-r--r--tests/scene/test_sky.h141
-rw-r--r--tests/test_main.cpp4
-rw-r--r--thirdparty/README.md2
-rw-r--r--thirdparty/misc/bcdec.h58
-rw-r--r--thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch (renamed from thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch)10
-rw-r--r--thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch (renamed from thirdparty/vulkan/patches/VMA-use-volk.patch)15
-rw-r--r--thirdparty/vulkan/vk_enum_string_helper.h6
-rw-r--r--thirdparty/vulkan/vk_mem_alloc.h6
106 files changed, 4360 insertions, 664 deletions
diff --git a/core/extension/gdextension_library_loader.cpp b/core/extension/gdextension_library_loader.cpp
index 5ba4933c35..d5f2eb668f 100644
--- a/core/extension/gdextension_library_loader.cpp
+++ b/core/extension/gdextension_library_loader.cpp
@@ -259,6 +259,10 @@ bool GDExtensionLibraryLoader::has_library_changed() const {
return false;
}
+bool GDExtensionLibraryLoader::library_exists() const {
+ return FileAccess::exists(resource_path);
+}
+
Error GDExtensionLibraryLoader::parse_gdextension_file(const String &p_path) {
resource_path = p_path;
diff --git a/core/extension/gdextension_library_loader.h b/core/extension/gdextension_library_loader.h
index f4372a75d4..f781611b30 100644
--- a/core/extension/gdextension_library_loader.h
+++ b/core/extension/gdextension_library_loader.h
@@ -77,6 +77,7 @@ public:
virtual void close_library() override;
virtual bool is_library_open() const override;
virtual bool has_library_changed() const override;
+ virtual bool library_exists() const override;
Error parse_gdextension_file(const String &p_path);
};
diff --git a/core/extension/gdextension_loader.h b/core/extension/gdextension_loader.h
index 7d779858b7..2289550329 100644
--- a/core/extension/gdextension_loader.h
+++ b/core/extension/gdextension_loader.h
@@ -42,6 +42,7 @@ public:
virtual void close_library() = 0;
virtual bool is_library_open() const = 0;
virtual bool has_library_changed() const = 0;
+ virtual bool library_exists() const = 0;
};
#endif // GDEXTENSION_LOADER_H
diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp
index 01efe0d96e..fff938858f 100644
--- a/core/extension/gdextension_manager.cpp
+++ b/core/extension/gdextension_manager.cpp
@@ -302,7 +302,8 @@ bool GDExtensionManager::ensure_extensions_loaded(const HashSet<String> &p_exten
for (const String &loaded_extension : loaded_extensions) {
if (!p_extensions.has(loaded_extension)) {
// The extension may not have a .gdextension file.
- if (!FileAccess::exists(loaded_extension)) {
+ const Ref<GDExtension> extension = GDExtensionManager::get_singleton()->get_extension(loaded_extension);
+ if (!extension->get_loader()->library_exists()) {
extensions_removed.push_back(loaded_extension);
}
}
diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp
index 391a203d5b..e6f7492a18 100644
--- a/core/string/ustring.cpp
+++ b/core/string/ustring.cpp
@@ -221,18 +221,35 @@ void CharString::copy_from(const char *p_cstr) {
/* String */
/*************************************************************************/
-Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const {
- // Splits the URL into scheme, host, port, path. Strip credentials when present.
+Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const {
+ // Splits the URL into scheme, host, port, path, fragment. Strip credentials when present.
String base = *this;
r_scheme = "";
r_host = "";
r_port = 0;
r_path = "";
+ r_fragment = "";
+
int pos = base.find("://");
// Scheme
if (pos != -1) {
- r_scheme = base.substr(0, pos + 3).to_lower();
- base = base.substr(pos + 3, base.length() - pos - 3);
+ bool is_scheme_valid = true;
+ for (int i = 0; i < pos; i++) {
+ if (!is_ascii_alphanumeric_char(base[i]) && base[i] != '+' && base[i] != '-' && base[i] != '.') {
+ is_scheme_valid = false;
+ break;
+ }
+ }
+ if (is_scheme_valid) {
+ r_scheme = base.substr(0, pos + 3).to_lower();
+ base = base.substr(pos + 3, base.length() - pos - 3);
+ }
+ }
+ pos = base.find("#");
+ // Fragment
+ if (pos != -1) {
+ r_fragment = base.substr(pos + 1);
+ base = base.substr(0, pos);
}
pos = base.find("/");
// Path
diff --git a/core/string/ustring.h b/core/string/ustring.h
index 5d4b209c25..aa62c9cb18 100644
--- a/core/string/ustring.h
+++ b/core/string/ustring.h
@@ -452,7 +452,7 @@ public:
String c_escape_multiline() const;
String c_unescape() const;
String json_escape() const;
- Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const;
+ Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const;
String property_name_encode() const;
diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 63d20242d6..55d00b6cf9 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -1209,7 +1209,7 @@
<return type="int" />
<param index="0" name="x" type="int" />
<description>
- Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if if [param x] is zero.
+ Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if [param x] is zero.
[codeblock]
signi(-6) # Returns -1
signi(0) # Returns 0
diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml
index 887e9cda81..609d7eff39 100644
--- a/doc/classes/Animation.xml
+++ b/doc/classes/Animation.xml
@@ -34,6 +34,14 @@
<link title="Animation documentation index">$DOCS_URL/tutorials/animation/index.html</link>
</tutorials>
<methods>
+ <method name="add_marker">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" />
+ <param index="1" name="time" type="float" />
+ <description>
+ Adds a marker to this Animation.
+ </description>
+ </method>
<method name="add_track">
<return type="int" />
<param index="0" name="type" type="int" enum="Animation.TrackType" />
@@ -271,12 +279,60 @@
Returns the index of the specified track. If the track is not found, return -1.
</description>
</method>
+ <method name="get_marker_at_time" qualifiers="const">
+ <return type="StringName" />
+ <param index="0" name="time" type="float" />
+ <description>
+ Returns the name of the marker located at the given time.
+ </description>
+ </method>
+ <method name="get_marker_color" qualifiers="const">
+ <return type="Color" />
+ <param index="0" name="name" type="StringName" />
+ <description>
+ Returns the given marker's color.
+ </description>
+ </method>
+ <method name="get_marker_names" qualifiers="const">
+ <return type="PackedStringArray" />
+ <description>
+ Returns every marker in this Animation, sorted ascending by time.
+ </description>
+ </method>
+ <method name="get_marker_time" qualifiers="const">
+ <return type="float" />
+ <param index="0" name="name" type="StringName" />
+ <description>
+ Returns the given marker's time.
+ </description>
+ </method>
+ <method name="get_next_marker" qualifiers="const">
+ <return type="StringName" />
+ <param index="0" name="time" type="float" />
+ <description>
+ Returns the closest marker that comes after the given time. If no such marker exists, an empty string is returned.
+ </description>
+ </method>
+ <method name="get_prev_marker" qualifiers="const">
+ <return type="StringName" />
+ <param index="0" name="time" type="float" />
+ <description>
+ Returns the closest marker that comes before the given time. If no such marker exists, an empty string is returned.
+ </description>
+ </method>
<method name="get_track_count" qualifiers="const">
<return type="int" />
<description>
Returns the amount of tracks in the animation.
</description>
</method>
+ <method name="has_marker" qualifiers="const">
+ <return type="bool" />
+ <param index="0" name="name" type="StringName" />
+ <description>
+ Returns [code]true[/code] if this Animation contains a marker with the given name.
+ </description>
+ </method>
<method name="method_track_get_name" qualifiers="const">
<return type="StringName" />
<param index="0" name="track_idx" type="int" />
@@ -320,6 +376,13 @@
Returns the interpolated position value at the given time (in seconds). The [param track_idx] must be the index of a 3D position track.
</description>
</method>
+ <method name="remove_marker">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" />
+ <description>
+ Removes the marker with the given name from this Animation.
+ </description>
+ </method>
<method name="remove_track">
<return type="void" />
<param index="0" name="track_idx" type="int" />
@@ -363,6 +426,14 @@
Returns the interpolated scale value at the given time (in seconds). The [param track_idx] must be the index of a 3D scale track.
</description>
</method>
+ <method name="set_marker_color">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" />
+ <param index="1" name="color" type="Color" />
+ <description>
+ Sets the given marker's color.
+ </description>
+ </method>
<method name="track_find_key" qualifiers="const">
<return type="int" />
<param index="0" name="track_idx" type="int" />
diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml
index 1ca8ac2fa5..9aeb4b7162 100644
--- a/doc/classes/AnimationPlayer.xml
+++ b/doc/classes/AnimationPlayer.xml
@@ -75,6 +75,24 @@
Returns the node which node path references will travel from.
</description>
</method>
+ <method name="get_section_end_time" qualifiers="const">
+ <return type="float" />
+ <description>
+ Returns the end time of the section currently being played.
+ </description>
+ </method>
+ <method name="get_section_start_time" qualifiers="const">
+ <return type="float" />
+ <description>
+ Returns the start time of the section currently being played.
+ </description>
+ </method>
+ <method name="has_section" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns [code]true[/code] if an animation is currently playing with section.
+ </description>
+ </method>
<method name="is_playing" qualifiers="const">
<return type="bool" />
<description>
@@ -110,6 +128,54 @@
This method is a shorthand for [method play] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], so see its description for more information.
</description>
</method>
+ <method name="play_section">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="1" name="start_time" type="float" default="-1" />
+ <param index="2" name="end_time" type="float" default="-1" />
+ <param index="3" name="custom_blend" type="float" default="-1" />
+ <param index="4" name="custom_speed" type="float" default="1.0" />
+ <param index="5" name="from_end" type="bool" default="false" />
+ <description>
+ Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time]. See also [method play].
+ Setting [param start_time] to a value outside the range of the animation means the start of the animation will be used instead, and setting [param end_time] to a value outside the range of the animation means the end of the animation will be used instead. [param start_time] cannot be equal to [param end_time].
+ </description>
+ </method>
+ <method name="play_section_backwards">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="1" name="start_time" type="float" default="-1" />
+ <param index="2" name="end_time" type="float" default="-1" />
+ <param index="3" name="custom_blend" type="float" default="-1" />
+ <description>
+ Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time] in reverse.
+ This method is a shorthand for [method play_section] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information.
+ </description>
+ </method>
+ <method name="play_section_with_markers">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="1" name="start_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="2" name="end_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="3" name="custom_blend" type="float" default="-1" />
+ <param index="4" name="custom_speed" type="float" default="1.0" />
+ <param index="5" name="from_end" type="bool" default="false" />
+ <description>
+ Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker].
+ If the start marker is empty, the section starts from the beginning of the animation. If the end marker is empty, the section ends on the end of the animation. See also [method play].
+ </description>
+ </method>
+ <method name="play_section_with_markers_backwards">
+ <return type="void" />
+ <param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="1" name="start_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="2" name="end_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="3" name="custom_blend" type="float" default="-1" />
+ <description>
+ Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker] in reverse.
+ This method is a shorthand for [method play_section_with_markers] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information.
+ </description>
+ </method>
<method name="play_with_capture">
<return type="void" />
<param index="0" name="name" type="StringName" default="&amp;&quot;&quot;" />
@@ -139,6 +205,12 @@
[b]Note:[/b] If a looped animation is currently playing, the queued animation will never play unless the looped animation is stopped somehow.
</description>
</method>
+ <method name="reset_section">
+ <return type="void" />
+ <description>
+ Resets the current section if section is set.
+ </description>
+ </method>
<method name="seek">
<return type="void" />
<param index="0" name="seconds" type="float" />
@@ -180,6 +252,23 @@
Sets the node which node path references will travel from.
</description>
</method>
+ <method name="set_section">
+ <return type="void" />
+ <param index="0" name="start_time" type="float" default="-1" />
+ <param index="1" name="end_time" type="float" default="-1" />
+ <description>
+ Changes the start and end times of the section being played. The current playback position will be clamped within the new section. See also [method play_section].
+ </description>
+ </method>
+ <method name="set_section_with_markers">
+ <return type="void" />
+ <param index="0" name="start_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <param index="1" name="end_marker" type="StringName" default="&amp;&quot;&quot;" />
+ <description>
+ Changes the start and end markers of the section being played. The current playback position will be clamped within the new section. See also [method play_section_with_markers].
+ If the argument is empty, the section uses the beginning or end of the animation. If both are empty, it means that the section is not set.
+ </description>
+ </method>
<method name="stop">
<return type="void" />
<param index="0" name="keep_state" type="bool" default="false" />
diff --git a/doc/classes/CameraAttributesPhysical.xml b/doc/classes/CameraAttributesPhysical.xml
index faedfee712..e2036162c7 100644
--- a/doc/classes/CameraAttributesPhysical.xml
+++ b/doc/classes/CameraAttributesPhysical.xml
@@ -25,7 +25,7 @@
The maximum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing below a certain brightness, resulting in a cut off point where the scene will remain bright.
</member>
<member name="auto_exposure_min_exposure_value" type="float" setter="set_auto_exposure_min_exposure_value" getter="get_auto_exposure_min_exposure_value" default="-8.0">
- The minimum luminance luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark.
+ The minimum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark.
</member>
<member name="exposure_aperture" type="float" setter="set_aperture" getter="get_aperture" default="16.0">
Size of the aperture of the camera, measured in f-stops. An f-stop is a unitless ratio between the focal length of the camera and the diameter of the aperture. A high aperture setting will result in a smaller aperture which leads to a dimmer image and sharper focus. A low aperture results in a wide aperture which lets in more light resulting in a brighter, less-focused image. Default is appropriate for outdoors at daytime (i.e. for use with a default [DirectionalLight3D]), for indoor lighting, a value between 2 and 4 is more appropriate.
diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml
index 42e1968eb0..aa8e4b3d56 100644
--- a/doc/classes/EditorExportPlugin.xml
+++ b/doc/classes/EditorExportPlugin.xml
@@ -159,6 +159,15 @@
Return a [PackedStringArray] of additional features this preset, for the given [param platform], should have.
</description>
</method>
+ <method name="_get_export_option_visibility" qualifiers="virtual const">
+ <return type="bool" />
+ <param index="0" name="platform" type="EditorExportPlatform" />
+ <param index="1" name="option" type="String" />
+ <description>
+ [b]Optional.[/b]
+ Validates [param option] and returns the visibility for the specified [param platform]. The default implementation returns [code]true[/code] for all options.
+ </description>
+ </method>
<method name="_get_export_option_warning" qualifiers="virtual const">
<return type="String" />
<param index="0" name="platform" type="EditorExportPlatform" />
diff --git a/doc/classes/EditorInspector.xml b/doc/classes/EditorInspector.xml
index cfdc172fd1..6b25be490e 100644
--- a/doc/classes/EditorInspector.xml
+++ b/doc/classes/EditorInspector.xml
@@ -28,6 +28,7 @@
</method>
</methods>
<members>
+ <member name="follow_focus" type="bool" setter="set_follow_focus" getter="is_following_focus" overrides="ScrollContainer" default="true" />
<member name="horizontal_scroll_mode" type="int" setter="set_horizontal_scroll_mode" getter="get_horizontal_scroll_mode" overrides="ScrollContainer" enum="ScrollContainer.ScrollMode" default="0" />
</members>
<signals>
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 5eb8ac6199..748a621114 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -772,7 +772,7 @@
Translations are provided by the community. If you spot a mistake, [url=$DOCS_URL/contributing/documentation/editor_and_docs_localization.html]contribute to editor translations on Weblate![/url]
</member>
<member name="interface/editor/editor_screen" type="int" setter="" getter="">
- The preferred monitor to display the editor.
+ The preferred monitor to display the editor. If [b]Auto[/b], the editor will remember the last screen it was displayed on across restarts.
</member>
<member name="interface/editor/expand_to_title" type="bool" setter="" getter="">
Expanding main editor window content to the title, if supported by [DisplayServer]. See [constant DisplayServer.WINDOW_FLAG_EXTEND_TO_TITLE].
@@ -826,9 +826,6 @@
<member name="interface/editor/project_manager_screen" type="int" setter="" getter="">
The preferred monitor to display the project manager.
</member>
- <member name="interface/editor/remember_window_size_and_position" type="bool" setter="" getter="">
- If [code]true[/code], the editor window will remember its size, position, and which screen it was displayed on across restarts.
- </member>
<member name="interface/editor/save_each_scene_on_quit" type="bool" setter="" getter="">
If [code]false[/code], the editor will save all scenes when confirming the [b]Save[/b] action when quitting the editor or quitting to the project list. If [code]true[/code], the editor will ask to save each scene individually.
</member>
diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml
index 004bbe2286..d73cda7460 100644
--- a/doc/classes/PopupMenu.xml
+++ b/doc/classes/PopupMenu.xml
@@ -776,7 +776,7 @@
[StyleBox] for the right side of labeled separator. See [method add_separator].
</theme_item>
<theme_item name="panel" data_type="style" type="StyleBox">
- [StyleBox] for the the background panel.
+ [StyleBox] for the background panel.
</theme_item>
<theme_item name="separator" data_type="style" type="StyleBox">
[StyleBox] used for the separators. See [method add_separator].
diff --git a/doc/classes/PopupPanel.xml b/doc/classes/PopupPanel.xml
index 399e285402..b581f32686 100644
--- a/doc/classes/PopupPanel.xml
+++ b/doc/classes/PopupPanel.xml
@@ -10,7 +10,7 @@
</tutorials>
<theme_items>
<theme_item name="panel" data_type="style" type="StyleBox">
- [StyleBox] for the the background panel.
+ [StyleBox] for the background panel.
</theme_item>
</theme_items>
</class>
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index af5ec41b60..4266bab2a1 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -1369,7 +1369,7 @@
macOS specific override for the shortcut to select the word currently under the caret.
</member>
<member name="input/ui_text_skip_selection_for_next_occurrence" type="Dictionary" setter="" getter="">
- If no selection is currently active with the last caret in text fields, searches for the next occurrence of the the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret.
+ If no selection is currently active with the last caret in text fields, searches for the next occurrence of the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret.
If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret, selects the next occurrence then deselects the previous selection and its associated caret. The action can be performed sequentially for other occurrences of the selection of the last caret.
The viewport is adjusted to the latest newly added caret.
[b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified.
diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml
index a57f6adec8..91af70b565 100644
--- a/doc/classes/RenderingServer.xml
+++ b/doc/classes/RenderingServer.xml
@@ -913,7 +913,7 @@
<param index="1" name="transform" type="Transform2D" />
<description>
Transforms both the current and previous stored transform for a canvas light.
- This allows transforming a light without creating a "glitch" in the interpolation, which is is particularly useful for large worlds utilizing a shifting origin.
+ This allows transforming a light without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilizing a shifting origin.
</description>
</method>
<method name="canvas_occluder_polygon_create">
diff --git a/doc/classes/Sprite2D.xml b/doc/classes/Sprite2D.xml
index d73cb02d94..239c4dcf09 100644
--- a/doc/classes/Sprite2D.xml
+++ b/doc/classes/Sprite2D.xml
@@ -76,7 +76,7 @@
If [code]true[/code], texture is cut from a larger atlas texture. See [member region_rect].
</member>
<member name="region_filter_clip_enabled" type="bool" setter="set_region_filter_clip_enabled" getter="is_region_filter_clip_enabled" default="false">
- If [code]true[/code], the outermost pixels get blurred out. [member region_enabled] must be [code]true[/code].
+ If [code]true[/code], the area outside of the [member region_rect] is clipped to avoid bleeding of the surrounding texture pixels. [member region_enabled] must be [code]true[/code].
</member>
<member name="region_rect" type="Rect2" setter="set_region_rect" getter="get_region_rect" default="Rect2(0, 0, 0, 0)">
The region of the atlas texture to display. [member region_enabled] must be [code]true[/code].
diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml
index 861a53aaad..132ecc3f92 100644
--- a/doc/classes/TreeItem.xml
+++ b/doc/classes/TreeItem.xml
@@ -73,6 +73,13 @@
Removes the button at index [param button_index] in column [param column].
</description>
</method>
+ <method name="get_auto_translate_mode" qualifiers="const">
+ <return type="int" enum="Node.AutoTranslateMode" />
+ <param index="0" name="column" type="int" />
+ <description>
+ Returns the column's auto translate mode.
+ </description>
+ </method>
<method name="get_autowrap_mode" qualifiers="const">
<return type="int" enum="TextServer.AutowrapMode" />
<param index="0" name="column" type="int" />
@@ -493,6 +500,15 @@
Selects the given [param column].
</description>
</method>
+ <method name="set_auto_translate_mode">
+ <return type="void" />
+ <param index="0" name="column" type="int" />
+ <param index="1" name="mode" type="int" enum="Node.AutoTranslateMode" />
+ <description>
+ Sets the given column's auto translate mode to [param mode].
+ All columns use [constant Node.AUTO_TRANSLATE_MODE_INHERIT] by default, which uses the same auto translate mode as the [Tree] itself.
+ </description>
+ </method>
<method name="set_autowrap_mode">
<return type="void" />
<param index="0" name="column" type="int" />
diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml
index 8f54b767e0..b351f2ccb6 100644
--- a/doc/classes/Vector4i.xml
+++ b/doc/classes/Vector4i.xml
@@ -216,7 +216,7 @@
<return type="Vector4i" />
<param index="0" name="right" type="int" />
<description>
- Gets the remainder of each component of the [Vector4i] with the the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers.
+ Gets the remainder of each component of the [Vector4i] with the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers.
[codeblock]
print(Vector4i(10, -20, 30, -40) % 7) # Prints "(3, -6, 2, -5)"
[/codeblock]
diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl
index 76881c8032..5e7fb3b338 100644
--- a/drivers/gles3/shaders/canvas.glsl
+++ b/drivers/gles3/shaders/canvas.glsl
@@ -579,7 +579,8 @@ void main() {
#endif
if (bool(read_draw_data_flags & FLAGS_CLIP_RECT_UV)) {
- uv = clamp(uv, read_draw_data_src_rect.xy, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw));
+ vec2 half_texpixel = read_draw_data_color_texture_pixel_size * 0.5;
+ uv = clamp(uv, read_draw_data_src_rect.xy + half_texpixel, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw) - half_texpixel);
}
#endif
diff --git a/drivers/vulkan/godot_vulkan.h b/drivers/vulkan/godot_vulkan.h
new file mode 100644
index 0000000000..f911c5520a
--- /dev/null
+++ b/drivers/vulkan/godot_vulkan.h
@@ -0,0 +1,42 @@
+/**************************************************************************/
+/* godot_vulkan.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef GODOT_VULKAN_H
+#define GODOT_VULKAN_H
+
+#ifdef USE_VOLK
+#include <volk.h>
+#else
+#include <stdint.h>
+#define VK_NO_STDINT_H
+#include <vulkan/vulkan.h>
+#endif
+
+#endif // GODOT_VULKAN_H
diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h
index f9352d617b..26de386206 100644
--- a/drivers/vulkan/rendering_context_driver_vulkan.h
+++ b/drivers/vulkan/rendering_context_driver_vulkan.h
@@ -40,11 +40,7 @@
#define VK_TRACK_DEVICE_MEMORY
#endif
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
class RenderingContextDriverVulkan : public RenderingContextDriver {
public:
diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h
index 81f4256941..cc15c0a0fe 100644
--- a/drivers/vulkan/rendering_device_driver_vulkan.h
+++ b/drivers/vulkan/rendering_device_driver_vulkan.h
@@ -43,11 +43,7 @@
#endif
#include "thirdparty/vulkan/vk_mem_alloc.h"
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
// Design principles:
// - Vulkan structs are zero-initialized and fields not requiring a non-zero value are omitted (except in cases where expresivity reasons apply).
diff --git a/drivers/vulkan/vulkan_hooks.h b/drivers/vulkan/vulkan_hooks.h
index bb30b29cec..82bcc9a064 100644
--- a/drivers/vulkan/vulkan_hooks.h
+++ b/drivers/vulkan/vulkan_hooks.h
@@ -31,11 +31,7 @@
#ifndef VULKAN_HOOKS_H
#define VULKAN_HOOKS_H
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
class VulkanHooks {
private:
diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp
index d277ba2f6d..63f86607e5 100644
--- a/editor/animation_track_editor.cpp
+++ b/editor/animation_track_editor.cpp
@@ -39,6 +39,7 @@
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_spin_slider.h"
+#include "editor/gui/editor_validation_panel.h"
#include "editor/gui/scene_tree_editor.h"
#include "editor/inspector_dock.h"
#include "editor/multi_node_edit.h"
@@ -48,6 +49,7 @@
#include "scene/animation/animation_player.h"
#include "scene/animation/tween.h"
#include "scene/gui/check_box.h"
+#include "scene/gui/color_picker.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/option_button.h"
#include "scene/gui/panel_container.h"
@@ -1467,7 +1469,7 @@ void AnimationTimelineEdit::_notification(int p_what) {
case NOTIFICATION_DRAW: {
int key_range = get_size().width - get_buttons_width() - get_name_limit();
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
@@ -1522,6 +1524,18 @@ void AnimationTimelineEdit::_notification(int p_what) {
}
}
+ PackedStringArray markers = animation->get_marker_names();
+ if (markers.size() > 0) {
+ float min_marker = animation->get_marker_time(markers[0]);
+ float max_marker = animation->get_marker_time(markers[markers.size() - 1]);
+ if (min_marker < time_min) {
+ time_min = min_marker;
+ }
+ if (max_marker > time_max) {
+ time_max = max_marker;
+ }
+ }
+
float extra = (zoomw / scale) * 0.5;
time_max += extra;
@@ -1701,7 +1715,7 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) {
}
void AnimationTimelineEdit::auto_fit() {
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
@@ -1780,7 +1794,7 @@ void AnimationTimelineEdit::update_play_position() {
}
void AnimationTimelineEdit::update_values() {
- if (!animation.is_valid() || editing) {
+ if (animation.is_null() || editing) {
return;
}
@@ -1792,6 +1806,7 @@ void AnimationTimelineEdit::update_values() {
time_icon->set_tooltip_text(TTR("Animation length (frames)"));
if (track_edit) {
track_edit->editor->_update_key_edit();
+ track_edit->editor->marker_edit->_update_key_edit();
}
} else {
length->set_value(animation->get_length());
@@ -1821,7 +1836,7 @@ void AnimationTimelineEdit::update_values() {
}
void AnimationTimelineEdit::_play_position_draw() {
- if (!animation.is_valid() || play_position_pos < 0) {
+ if (animation.is_null() || play_position_pos < 0) {
return;
}
@@ -1972,6 +1987,7 @@ AnimationTimelineEdit::AnimationTimelineEdit() {
Control *expander = memnew(Control);
expander->set_h_size_flags(SIZE_EXPAND_FILL);
+ expander->set_mouse_filter(MOUSE_FILTER_IGNORE);
len_hb->add_child(expander);
time_icon = memnew(TextureRect);
time_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
@@ -2124,6 +2140,62 @@ void AnimationTrackEdit::_notification(int p_what) {
draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE));
}
+ // Marker sections.
+
+ {
+ float scale = timeline->get_zoom_scale();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+
+ PackedStringArray section = editor->get_selected_section();
+ if (section.size() == 2) {
+ StringName start_marker = section[0];
+ StringName end_marker = section[1];
+ double start_time = animation->get_marker_time(start_marker);
+ double end_time = animation->get_marker_time(end_marker);
+
+ // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {
+ start_time += editor->get_marker_moving_selection_offset();
+ end_time += editor->get_marker_moving_selection_offset();
+ }
+
+ if (start_time < animation->get_length() && end_time >= 0) {
+ float start_ofs = MAX(0, start_time) - timeline->get_value();
+ float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();
+ start_ofs = start_ofs * scale + limit;
+ end_ofs = end_ofs * scale + limit;
+ start_ofs = MAX(start_ofs, limit);
+ end_ofs = MIN(end_ofs, limit_end);
+ Rect2 rect;
+ rect.set_position(Vector2(start_ofs, 0));
+ rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));
+
+ draw_rect(rect, Color(1, 0.1, 0.1, 0.2));
+ }
+ }
+ }
+
+ // Marker overlays.
+
+ {
+ float scale = timeline->get_zoom_scale();
+ PackedStringArray markers = animation->get_marker_names();
+ for (const StringName marker : markers) {
+ double time = animation->get_marker_time(marker);
+ if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {
+ time += editor->get_marker_moving_selection_offset();
+ }
+ if (time >= 0) {
+ float offset = time - timeline->get_value();
+ offset = offset * scale + limit;
+ Color marker_color = animation->get_marker_color(marker);
+ marker_color.a = 0.2;
+ draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color);
+ }
+ }
+ }
+
// Keyframes.
draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin);
@@ -2352,7 +2424,7 @@ void AnimationTrackEdit::_notification(int p_what) {
}
int AnimationTrackEdit::get_key_height() const {
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return 0;
}
@@ -2360,7 +2432,7 @@ int AnimationTrackEdit::get_key_height() const {
}
Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) {
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return Rect2();
}
Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height);
@@ -2399,7 +2471,7 @@ void AnimationTrackEdit::draw_key_link(int p_index, float p_pixels_sec, int p_x,
}
void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) {
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
@@ -2573,7 +2645,7 @@ void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) {
}
void AnimationTrackEdit::_play_position_draw() {
- if (!animation.is_valid() || play_position_pos < 0) {
+ if (animation.is_null() || play_position_pos < 0) {
return;
}
@@ -3522,6 +3594,64 @@ void AnimationTrackEditGroup::_notification(int p_what) {
draw_style_box(stylebox_header, Rect2(Point2(), get_size()));
+ int limit = timeline->get_name_limit();
+
+ // Section preview.
+
+ {
+ float scale = timeline->get_zoom_scale();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+
+ PackedStringArray section = editor->get_selected_section();
+ if (section.size() == 2) {
+ StringName start_marker = section[0];
+ StringName end_marker = section[1];
+ double start_time = editor->get_current_animation()->get_marker_time(start_marker);
+ double end_time = editor->get_current_animation()->get_marker_time(end_marker);
+
+ // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (editor->is_marker_moving_selection() && !(player && player->is_playing())) {
+ start_time += editor->get_marker_moving_selection_offset();
+ end_time += editor->get_marker_moving_selection_offset();
+ }
+
+ if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) {
+ float start_ofs = MAX(0, start_time) - timeline->get_value();
+ float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value();
+ start_ofs = start_ofs * scale + limit;
+ end_ofs = end_ofs * scale + limit;
+ start_ofs = MAX(start_ofs, limit);
+ end_ofs = MIN(end_ofs, limit_end);
+ Rect2 rect;
+ rect.set_position(Vector2(start_ofs, 0));
+ rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));
+
+ draw_rect(rect, Color(1, 0.1, 0.1, 0.2));
+ }
+ }
+ }
+
+ // Marker overlays.
+
+ {
+ float scale = timeline->get_zoom_scale();
+ PackedStringArray markers = editor->get_current_animation()->get_marker_names();
+ for (const StringName marker : markers) {
+ double time = editor->get_current_animation()->get_marker_time(marker);
+ if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) {
+ time += editor->get_marker_moving_selection_offset();
+ }
+ if (time >= 0) {
+ float offset = time - timeline->get_value();
+ offset = offset * scale + limit;
+ Color marker_color = editor->get_current_animation()->get_marker_color(marker);
+ marker_color.a = 0.2;
+ draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color);
+ }
+ }
+ }
+
draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE));
draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE));
draw_line(Point2(get_size().width - timeline->get_buttons_width() - outer_margin, 0), Point2(get_size().width - timeline->get_buttons_width() - outer_margin, get_size().height), v_line_color, Math::round(EDSCALE));
@@ -3590,6 +3720,10 @@ void AnimationTrackEditGroup::set_root(Node *p_root) {
queue_redraw();
}
+void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) {
+ editor = p_editor;
+}
+
void AnimationTrackEditGroup::_zoom_changed() {
queue_redraw();
}
@@ -3623,6 +3757,9 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re
read_only = p_read_only;
timeline->set_animation(p_anim, read_only);
+ marker_edit->set_animation(p_anim, read_only);
+ marker_edit->set_play_position(timeline->get_play_position());
+
_cancel_bezier_edit();
_update_tracks();
@@ -3873,6 +4010,7 @@ void AnimationTrackEditor::_track_grab_focus(int p_track) {
void AnimationTrackEditor::set_anim_pos(float p_pos) {
timeline->set_play_position(p_pos);
+ marker_edit->set_play_position(p_pos);
for (int i = 0; i < track_edits.size(); i++) {
track_edits[i]->set_play_position(p_pos);
}
@@ -4043,7 +4181,7 @@ void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_
if (!keying) {
return;
}
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
@@ -4083,7 +4221,7 @@ bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const
if (!keying) {
return false;
}
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return false;
}
@@ -4230,6 +4368,22 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p
_query_insert(id);
}
+PackedStringArray AnimationTrackEditor::get_selected_section() const {
+ return marker_edit->get_selected_section();
+}
+
+bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const {
+ return marker_edit->is_marker_selected(p_marker);
+}
+
+bool AnimationTrackEditor::is_marker_moving_selection() const {
+ return marker_edit->is_moving_selection();
+}
+
+float AnimationTrackEditor::get_marker_moving_selection_offset() const {
+ return marker_edit->get_moving_selection_offset();
+}
+
void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) {
EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history();
@@ -4316,7 +4470,7 @@ void AnimationTrackEditor::_confirm_insert_list() {
PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) {
r_base_path = NodePath();
- ERR_FAIL_COND_V(!animation.is_valid(), PropertyInfo());
+ ERR_FAIL_COND_V(animation.is_null(), PropertyInfo());
ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo());
if (!root) {
@@ -4769,6 +4923,7 @@ void AnimationTrackEditor::_update_tracks() {
g->set_root(root);
g->set_tooltip_text(tooltip);
g->set_timeline(timeline);
+ g->set_editor(this);
groups.push_back(g);
VBoxContainer *vb = memnew(VBoxContainer);
vb->add_theme_constant_override("separation", 0);
@@ -4860,12 +5015,13 @@ void AnimationTrackEditor::_snap_mode_changed(int p_mode) {
if (key_edit) {
key_edit->set_use_fps(use_fps);
}
+ marker_edit->set_use_fps(use_fps);
step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL);
_update_step_spinbox();
}
void AnimationTrackEditor::_update_step_spinbox() {
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
step->set_block_signals(true);
@@ -4978,6 +5134,7 @@ void AnimationTrackEditor::_notification(int p_what) {
void AnimationTrackEditor::_update_scroll(double) {
_redraw_tracks();
_redraw_groups();
+ marker_edit->queue_redraw();
}
void AnimationTrackEditor::_update_step(double p_new_step) {
@@ -5253,6 +5410,8 @@ void AnimationTrackEditor::_timeline_value_changed(double) {
bezier_edit->queue_redraw();
bezier_edit->update_play_position();
+
+ marker_edit->update_play_position();
}
int AnimationTrackEditor::_get_track_selected() {
@@ -5445,6 +5604,8 @@ void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track)
_redraw_tracks();
_update_key_edit();
+
+ marker_edit->_clear_selection(marker_edit->is_selection_active());
}
void AnimationTrackEditor::_key_deselected(int p_key, int p_track) {
@@ -5513,7 +5674,7 @@ void AnimationTrackEditor::_clear_selection(bool p_update) {
void AnimationTrackEditor::_update_key_edit() {
_clear_key_edit();
- if (!animation.is_valid()) {
+ if (animation.is_null()) {
return;
}
@@ -5600,6 +5761,8 @@ void AnimationTrackEditor::_select_at_anim(const Ref<Animation> &p_anim, int p_t
selection.insert(sk, ki);
_update_key_edit();
+
+ marker_edit->_clear_selection(marker_edit->is_selection_active());
}
void AnimationTrackEditor::_move_selection_commit() {
@@ -7311,6 +7474,15 @@ AnimationTrackEditor::AnimationTrackEditor() {
box_selection_container->set_clip_contents(true);
timeline_vbox->add_child(box_selection_container);
+ marker_edit = memnew(AnimationMarkerEdit);
+ timeline->get_child(0)->add_child(marker_edit);
+ marker_edit->set_editor(this);
+ marker_edit->set_timeline(timeline);
+ marker_edit->set_h_size_flags(SIZE_EXPAND_FILL);
+ marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);
+ marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups));
+ marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks));
+
scroll = memnew(ScrollContainer);
box_selection_container->add_child(scroll);
scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
@@ -7826,3 +7998,1203 @@ AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref<Animation> p_animat
AnimationTrackKeyEditEditor::~AnimationTrackKeyEditEditor() {
}
+
+void AnimationMarkerEdit::_zoom_changed() {
+ queue_redraw();
+ play_position->queue_redraw();
+}
+
+void AnimationMarkerEdit::_menu_selected(int p_index) {
+ switch (p_index) {
+ case MENU_KEY_INSERT: {
+ _insert_marker(insert_at_pos);
+ } break;
+ case MENU_KEY_RENAME: {
+ if (selection.size() > 0) {
+ _rename_marker(*selection.last());
+ }
+ } break;
+ case MENU_KEY_DELETE: {
+ _delete_selected_markers();
+ } break;
+ case MENU_KEY_TOGGLE_MARKER_NAMES: {
+ should_show_all_marker_names = !should_show_all_marker_names;
+ queue_redraw();
+ } break;
+ }
+}
+
+void AnimationMarkerEdit::_play_position_draw() {
+ if (animation.is_null() || play_position_pos < 0) {
+ return;
+ }
+
+ float scale = timeline->get_zoom_scale();
+ int h = get_size().height;
+
+ int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit();
+
+ if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) {
+ Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
+ play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE));
+ }
+}
+
+bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) {
+ int limit = timeline->get_name_limit();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+ // Left Border including space occupied by keyframes on t=0.
+ int limit_start_hitbox = limit - type_icon->get_width();
+
+ if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {
+ int key_idx = -1;
+ float key_distance = 1e20;
+ PackedStringArray names = animation->get_marker_names();
+ for (int i = 0; i < names.size(); i++) {
+ Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());
+ float offset = animation->get_marker_time(names[i]) - timeline->get_value();
+ offset = offset * timeline->get_zoom_scale() + limit;
+ rect.position.x += offset;
+ if (rect.has_point(p_pos)) {
+ if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {
+ float distance = Math::abs(offset - p_pos.x);
+ if (key_idx == -1 || distance < key_distance) {
+ key_idx = i;
+ key_distance = distance;
+ }
+ } else {
+ // First one does it.
+ break;
+ }
+ }
+ }
+
+ if (key_idx != -1) {
+ if (p_aggregate) {
+ StringName name = names[key_idx];
+ if (selection.has(name)) {
+ if (p_deselectable) {
+ call_deferred("_deselect_key", name);
+ moving_selection_pivot = 0.0f;
+ moving_selection_mouse_begin_x = 0.0f;
+ }
+ } else {
+ call_deferred("_select_key", name, false);
+ moving_selection_attempt = true;
+ moving_selection_effective = false;
+ select_single_attempt = StringName();
+ moving_selection_pivot = animation->get_marker_time(name);
+ moving_selection_mouse_begin_x = p_pos.x;
+ }
+
+ } else {
+ StringName name = names[key_idx];
+ if (!selection.has(name)) {
+ call_deferred("_select_key", name, true);
+ select_single_attempt = StringName();
+ } else {
+ select_single_attempt = name;
+ }
+
+ moving_selection_attempt = true;
+ moving_selection_effective = false;
+ moving_selection_pivot = animation->get_marker_time(name);
+ moving_selection_mouse_begin_x = p_pos.x;
+ }
+
+ if (read_only) {
+ moving_selection_attempt = false;
+ moving_selection_pivot = 0.0f;
+ moving_selection_mouse_begin_x = 0.0f;
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) {
+ int limit = timeline->get_name_limit();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+
+ if (p_pos.x >= limit && p_pos.x <= limit_end) {
+ PackedStringArray section = get_selected_section();
+ if (!section.is_empty()) {
+ StringName start_marker = section[0];
+ StringName end_marker = section[1];
+ float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;
+ float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit;
+ return p_pos.x >= start_offset && p_pos.x <= end_offset;
+ }
+ }
+
+ return false;
+}
+
+HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const {
+ HBoxContainer *hbox = memnew(HBoxContainer);
+ Label *label = memnew(Label);
+ label->set_text(p_text);
+ hbox->add_child(label);
+ hbox->add_child(p_control);
+ hbox->set_h_size_flags(SIZE_EXPAND_FILL);
+ label->set_h_size_flags(SIZE_EXPAND_FILL);
+ label->set_stretch_ratio(1.0);
+ p_control->set_h_size_flags(SIZE_EXPAND_FILL);
+ p_control->set_stretch_ratio(1.0);
+ return hbox;
+}
+
+void AnimationMarkerEdit::_update_key_edit() {
+ _clear_key_edit();
+ if (animation.is_null()) {
+ return;
+ }
+
+ if (selection.size() == 1) {
+ key_edit = memnew(AnimationMarkerKeyEdit);
+ key_edit->animation = animation;
+ key_edit->animation_read_only = read_only;
+ key_edit->marker_name = *selection.begin();
+ key_edit->use_fps = timeline->is_using_fps();
+ key_edit->marker_edit = this;
+
+ EditorNode::get_singleton()->push_item(key_edit);
+
+ InspectorDock::get_singleton()->set_info(TTR("Marker name is read-only in the inspector."), TTR("A marker's name can only be changed by right-clicking it in the animation editor and selecting \"Rename Marker\", in order to make sure that marker names are all unique."), true);
+ } else if (selection.size() > 1) {
+ multi_key_edit = memnew(AnimationMultiMarkerKeyEdit);
+ multi_key_edit->animation = animation;
+ multi_key_edit->animation_read_only = read_only;
+ multi_key_edit->marker_edit = this;
+ for (const StringName &name : selection) {
+ multi_key_edit->marker_names.push_back(name);
+ }
+
+ EditorNode::get_singleton()->push_item(multi_key_edit);
+ }
+}
+
+void AnimationMarkerEdit::_clear_key_edit() {
+ if (key_edit) {
+ // If key edit is the object being inspected, remove it first.
+ if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) {
+ EditorNode::get_singleton()->push_item(nullptr);
+ }
+
+ // Then actually delete it.
+ memdelete(key_edit);
+ key_edit = nullptr;
+ }
+
+ if (multi_key_edit) {
+ if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) {
+ EditorNode::get_singleton()->push_item(nullptr);
+ }
+
+ memdelete(multi_key_edit);
+ multi_key_edit = nullptr;
+ }
+}
+
+void AnimationMarkerEdit::_bind_methods() {
+ ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim);
+ ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key);
+ ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key);
+}
+
+void AnimationMarkerEdit::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ if (animation.is_null()) {
+ return;
+ }
+
+ type_icon = get_editor_theme_icon(SNAME("Marker"));
+ selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ if (animation.is_null()) {
+ return;
+ }
+
+ int limit = timeline->get_name_limit();
+
+ Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));
+ Color color = get_theme_color(SceneStringName(font_color), SNAME("Label"));
+ int hsep = get_theme_constant(SNAME("h_separation"), SNAME("ItemList"));
+ Color linecolor = color;
+ linecolor.a = 0.2;
+
+ // SECTION PREVIEW //
+
+ {
+ float scale = timeline->get_zoom_scale();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+
+ PackedStringArray section = get_selected_section();
+ if (section.size() == 2) {
+ StringName start_marker = section[0];
+ StringName end_marker = section[1];
+ double start_time = animation->get_marker_time(start_marker);
+ double end_time = animation->get_marker_time(end_marker);
+
+ // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section.
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (moving_selection && !(player && player->is_playing())) {
+ start_time += moving_selection_offset;
+ end_time += moving_selection_offset;
+ }
+
+ if (start_time < animation->get_length() && end_time >= 0) {
+ float start_ofs = MAX(0, start_time) - timeline->get_value();
+ float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value();
+ start_ofs = start_ofs * scale + limit;
+ end_ofs = end_ofs * scale + limit;
+ start_ofs = MAX(start_ofs, limit);
+ end_ofs = MIN(end_ofs, limit_end);
+ Rect2 rect;
+ rect.set_position(Vector2(start_ofs, 0));
+ rect.set_size(Vector2(end_ofs - start_ofs, get_size().height));
+
+ draw_rect(rect, Color(1, 0.1, 0.1, 0.2));
+ }
+ }
+ }
+
+ // KEYFRAMES //
+
+ draw_bg(limit, get_size().width - timeline->get_buttons_width());
+
+ {
+ float scale = timeline->get_zoom_scale();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+
+ PackedStringArray names = animation->get_marker_names();
+ for (int i = 0; i < names.size(); i++) {
+ StringName name = names[i];
+ bool is_selected = selection.has(name);
+ float offset = animation->get_marker_time(name) - timeline->get_value();
+ if (is_selected && moving_selection) {
+ offset += moving_selection_offset;
+ }
+
+ offset = offset * scale + limit;
+
+ draw_key(name, scale, int(offset), is_selected, limit, limit_end);
+
+ const int font_size = 16;
+ Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size);
+ if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) {
+ float bottom = get_size().height + string_size.y - font->get_descent(font_size);
+ float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted.
+ Color marker_color = animation->get_marker_color(name);
+ draw_string(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color);
+ draw_string_outline(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color);
+ }
+ }
+ }
+
+ draw_fg(limit, get_size().width - timeline->get_buttons_width());
+
+ // BUTTONS //
+
+ {
+ int ofs = get_size().width - timeline->get_buttons_width();
+
+ draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), linecolor, Math::round(EDSCALE));
+
+ ofs += hsep;
+ }
+
+ draw_line(Vector2(0, get_size().height), get_size(), linecolor, Math::round(EDSCALE));
+ } break;
+
+ case NOTIFICATION_MOUSE_ENTER:
+ hovered = true;
+ queue_redraw();
+ break;
+ case NOTIFICATION_MOUSE_EXIT:
+ hovered = false;
+ // When the mouse cursor exits the track, we're no longer hovering any keyframe.
+ hovering_marker = StringName();
+ queue_redraw();
+ break;
+ }
+}
+
+void AnimationMarkerEdit::gui_input(const Ref<InputEvent> &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ if (animation.is_null()) {
+ return;
+ }
+
+ if (p_event->is_pressed()) {
+ if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) {
+ if (!read_only) {
+ _menu_selected(MENU_KEY_RENAME);
+ }
+ }
+
+ if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) {
+ if (!read_only) {
+ _menu_selected(MENU_KEY_DELETE);
+ }
+ }
+
+ if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) {
+ if (!read_only) {
+ _menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES);
+ }
+ }
+ }
+
+ Ref<InputEventMouseButton> mb = p_event;
+
+ if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
+ Point2 pos = mb->get_position();
+ if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) {
+ accept_event();
+ } else if (!_is_ui_pos_in_current_section(pos)) {
+ _clear_selection_for_anim(animation);
+ }
+ }
+
+ if (mb.is_valid() && moving_selection_attempt) {
+ if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
+ moving_selection_attempt = false;
+ if (moving_selection && moving_selection_effective) {
+ if (Math::abs(moving_selection_offset) > CMP_EPSILON) {
+ _move_selection_commit();
+ accept_event(); // So play position doesn't snap to the end of move selection.
+ }
+ } else if (select_single_attempt) {
+ call_deferred("_select_key", select_single_attempt, true);
+
+ // First select click should not affect play position.
+ if (!selection.has(select_single_attempt)) {
+ accept_event();
+ } else {
+ // Second click and onwards should snap to marker time.
+ double ofs = animation->get_marker_time(select_single_attempt);
+ timeline->set_play_position(ofs);
+ timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed());
+ accept_event();
+ }
+ } else {
+ // First select click should not affect play position.
+ if (!selection.has(select_single_attempt)) {
+ accept_event();
+ }
+ }
+
+ moving_selection = false;
+ select_single_attempt = StringName();
+ }
+
+ if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
+ moving_selection_attempt = false;
+ moving_selection = false;
+ _move_selection_cancel();
+ }
+ }
+
+ if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
+ Point2 pos = mb->get_position();
+ if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) {
+ // Can do something with menu too! show insert key.
+ float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale();
+ if (!read_only) {
+ bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false);
+
+ menu->clear();
+ menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT);
+
+ if (selected || selection.size() > 0) {
+ menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME);
+ menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE);
+ }
+
+ menu->add_icon_item(get_editor_theme_icon(should_show_all_marker_names ? SNAME("GuiChecked") : SNAME("GuiUnchecked")), TTR("Show All Marker Names"), MENU_KEY_TOGGLE_MARKER_NAMES);
+ menu->reset_size();
+
+ moving_selection_attempt = false;
+ moving_selection = false;
+
+ menu->set_position(get_screen_position() + get_local_mouse_position());
+ menu->popup();
+
+ insert_at_pos = offset + timeline->get_value();
+ accept_event();
+ }
+ }
+ }
+
+ Ref<InputEventMouseMotion> mm = p_event;
+
+ if (mm.is_valid()) {
+ const StringName previous_hovering_marker = hovering_marker;
+
+ // Hovering compressed keyframes for editing is not possible.
+ const float scale = timeline->get_zoom_scale();
+ const int limit = timeline->get_name_limit();
+ const int limit_end = get_size().width - timeline->get_buttons_width();
+ // Left Border including space occupied by keyframes on t=0.
+ const int limit_start_hitbox = limit - type_icon->get_width();
+ const Point2 pos = mm->get_position();
+
+ if (pos.x >= limit_start_hitbox && pos.x <= limit_end) {
+ // Use the same logic as key selection to ensure that hovering accurately represents
+ // which key will be selected when clicking.
+ int key_idx = -1;
+ float key_distance = 1e20;
+
+ hovering_marker = StringName();
+
+ PackedStringArray names = animation->get_marker_names();
+
+ // Hovering should happen in the opposite order of drawing for more accurate overlap hovering.
+ for (int i = names.size() - 1; i >= 0; i--) {
+ StringName name = names[i];
+ Rect2 rect = get_key_rect(scale);
+ float offset = animation->get_marker_time(name) - timeline->get_value();
+ offset = offset * scale + limit;
+ rect.position.x += offset;
+
+ if (rect.has_point(pos)) {
+ if (is_key_selectable_by_distance()) {
+ const float distance = Math::abs(offset - pos.x);
+ if (key_idx == -1 || distance < key_distance) {
+ key_idx = i;
+ key_distance = distance;
+ hovering_marker = name;
+ }
+ } else {
+ // First one does it.
+ hovering_marker = name;
+ break;
+ }
+ }
+ }
+
+ if (hovering_marker != previous_hovering_marker) {
+ // Required to draw keyframe hover feedback on the correct keyframe.
+ queue_redraw();
+ }
+ }
+ }
+
+ if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) {
+ if (!moving_selection) {
+ moving_selection = true;
+ _move_selection_begin();
+ }
+
+ float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();
+ float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();
+ float delta = new_time - moving_begin_time;
+ float snapped_time = editor->snap_time(moving_selection_pivot + delta);
+
+ float offset = 0.0;
+ if (Math::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) {
+ offset = snapped_time - moving_selection_pivot;
+ moving_selection_effective = true;
+ }
+
+ _move_selection(offset);
+ }
+}
+
+String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const {
+ if (animation.is_null()) {
+ return Control::get_tooltip(p_pos);
+ }
+
+ int limit = timeline->get_name_limit();
+ int limit_end = get_size().width - timeline->get_buttons_width();
+ // Left Border including space occupied by keyframes on t=0.
+ int limit_start_hitbox = limit - type_icon->get_width();
+
+ if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) {
+ int key_idx = -1;
+ float key_distance = 1e20;
+
+ PackedStringArray names = animation->get_marker_names();
+
+ // Select should happen in the opposite order of drawing for more accurate overlap select.
+ for (int i = names.size() - 1; i >= 0; i--) {
+ StringName name = names[i];
+ Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale());
+ float offset = animation->get_marker_time(name) - timeline->get_value();
+ offset = offset * timeline->get_zoom_scale() + limit;
+ rect.position.x += offset;
+
+ if (rect.has_point(p_pos)) {
+ if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) {
+ float distance = ABS(offset - p_pos.x);
+ if (key_idx == -1 || distance < key_distance) {
+ key_idx = i;
+ key_distance = distance;
+ }
+ } else {
+ // First one does it.
+ break;
+ }
+ }
+ }
+
+ if (key_idx != -1) {
+ String name = names[key_idx];
+ String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001))) + "\n";
+ text += TTR("Marker:") + " " + name + "\n";
+ return text;
+ }
+ }
+
+ return Control::get_tooltip(p_pos);
+}
+
+int AnimationMarkerEdit::get_key_height() const {
+ if (animation.is_null()) {
+ return 0;
+ }
+
+ return type_icon->get_height();
+}
+
+Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const {
+ if (animation.is_null()) {
+ return Rect2();
+ }
+
+ Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height);
+
+ // Make it a big easier to click.
+ rect.position.x -= rect.size.x * 0.5;
+ rect.size.x *= 2;
+ return rect;
+}
+
+PackedStringArray AnimationMarkerEdit::get_selected_section() const {
+ if (selection.size() >= 2) {
+ PackedStringArray arr;
+ arr.push_back(""); // Marker with smallest time.
+ arr.push_back(""); // Marker with largest time.
+ double min_time = INFINITY;
+ double max_time = -INFINITY;
+ for (const StringName &marker_name : selection) {
+ double time = animation->get_marker_time(marker_name);
+ if (time < min_time) {
+ arr.set(0, marker_name);
+ min_time = time;
+ }
+ if (time > max_time) {
+ arr.set(1, marker_name);
+ max_time = time;
+ }
+ }
+ return arr;
+ }
+
+ return PackedStringArray();
+}
+
+bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const {
+ return selection.has(p_marker);
+}
+
+bool AnimationMarkerEdit::is_key_selectable_by_distance() const {
+ return true;
+}
+
+void AnimationMarkerEdit::draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) {
+ if (animation.is_null()) {
+ return;
+ }
+
+ if (p_x < p_clip_left || p_x > p_clip_right) {
+ return;
+ }
+
+ Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon;
+
+ Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height()));
+
+ // Don't apply custom marker color when the key is selected.
+ Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name);
+
+ // Use a different color for the currently hovered key.
+ // The color multiplier is chosen to work with both dark and light editor themes,
+ // and on both unselected and selected key icons.
+ draw_texture(
+ icon_to_draw,
+ ofs,
+ p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color);
+}
+
+void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) {
+}
+
+void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) {
+}
+
+Ref<Animation> AnimationMarkerEdit::get_animation() const {
+ return animation;
+}
+
+void AnimationMarkerEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) {
+ if (animation.is_valid()) {
+ _clear_selection_for_anim(animation);
+ }
+ animation = p_animation;
+ read_only = p_read_only;
+ type_icon = get_editor_theme_icon(SNAME("Marker"));
+ selected_icon = get_editor_theme_icon(SNAME("MarkerSelected"));
+
+ queue_redraw();
+}
+
+Size2 AnimationMarkerEdit::get_minimum_size() const {
+ Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object"));
+ Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));
+ int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));
+ int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList"));
+
+ int max_h = MAX(texture->get_height(), font->get_height(font_size));
+ max_h = MAX(max_h, get_key_height());
+
+ return Vector2(1, max_h + separation);
+}
+
+void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) {
+ timeline = p_timeline;
+ timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));
+ timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed));
+}
+
+void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) {
+ editor = p_editor;
+}
+
+void AnimationMarkerEdit::set_play_position(float p_pos) {
+ play_position_pos = p_pos;
+ play_position->queue_redraw();
+}
+
+void AnimationMarkerEdit::update_play_position() {
+ play_position->queue_redraw();
+}
+
+void AnimationMarkerEdit::set_use_fps(bool p_use_fps) {
+ if (key_edit) {
+ key_edit->use_fps = p_use_fps;
+ key_edit->notify_property_list_changed();
+ }
+}
+
+void AnimationMarkerEdit::_move_selection_begin() {
+ moving_selection = true;
+ moving_selection_offset = 0;
+}
+
+void AnimationMarkerEdit::_move_selection(float p_offset) {
+ moving_selection_offset = p_offset;
+ queue_redraw();
+}
+
+void AnimationMarkerEdit::_move_selection_commit() {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Animation Move Markers"));
+
+ for (HashSet<StringName>::Iterator E = selection.last(); E; --E) {
+ StringName name = *E;
+ double time = animation->get_marker_time(name);
+ float newpos = time + moving_selection_offset;
+ undo_redo->add_do_method(animation.ptr(), "remove_marker", name);
+ undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos);
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));
+ undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));
+
+ // add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass.
+ if (StringName overlap = animation->get_marker_at_time(newpos)) {
+ if (select_single_attempt == overlap) {
+ select_single_attempt = "";
+ }
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap));
+ }
+ }
+
+ moving_selection = false;
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (player) {
+ PackedStringArray selected_section = get_selected_section();
+ if (selected_section.size() >= 2) {
+ undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);
+ undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]);
+ }
+ }
+ undo_redo->add_do_method(timeline, "queue_redraw");
+ undo_redo->add_undo_method(timeline, "queue_redraw");
+ undo_redo->add_do_method(this, "queue_redraw");
+ undo_redo->add_undo_method(this, "queue_redraw");
+ undo_redo->commit_action();
+ _update_key_edit();
+}
+
+void AnimationMarkerEdit::_delete_selected_markers() {
+ if (selection.size()) {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Animation Delete Keys"));
+ for (const StringName &name : selection) {
+ double time = animation->get_marker_time(name);
+ undo_redo->add_do_method(animation.ptr(), "remove_marker", name);
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name));
+ }
+ _clear_selection_for_anim(animation);
+
+ undo_redo->add_do_method(this, "queue_redraw");
+ undo_redo->add_undo_method(this, "queue_redraw");
+ undo_redo->commit_action();
+ _update_key_edit();
+ }
+}
+
+void AnimationMarkerEdit::_move_selection_cancel() {
+ moving_selection = false;
+ queue_redraw();
+}
+
+void AnimationMarkerEdit::_clear_selection(bool p_update) {
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (player) {
+ player->reset_section();
+ }
+
+ selection.clear();
+
+ if (p_update) {
+ queue_redraw();
+ }
+
+ _clear_key_edit();
+}
+
+void AnimationMarkerEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) {
+ if (animation != p_anim) {
+ return;
+ }
+
+ _clear_selection(true);
+}
+
+void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) {
+ if (is_single) {
+ _clear_selection(false);
+ }
+
+ selection.insert(p_name);
+
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (player) {
+ if (selection.size() >= 2) {
+ PackedStringArray selected_section = get_selected_section();
+ double start_time = animation->get_marker_time(selected_section[0]);
+ double end_time = animation->get_marker_time(selected_section[1]);
+ player->set_section(start_time, end_time);
+ } else {
+ player->reset_section();
+ }
+ }
+
+ queue_redraw();
+ _update_key_edit();
+
+ editor->_clear_selection(editor->is_selection_active());
+}
+
+void AnimationMarkerEdit::_deselect_key(const StringName &p_name) {
+ selection.erase(p_name);
+
+ AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player();
+ if (player) {
+ if (selection.size() >= 2) {
+ PackedStringArray selected_section = get_selected_section();
+ double start_time = animation->get_marker_time(selected_section[0]);
+ double end_time = animation->get_marker_time(selected_section[1]);
+ player->set_section(start_time, end_time);
+ } else {
+ player->reset_section();
+ }
+ }
+
+ queue_redraw();
+ _update_key_edit();
+}
+
+void AnimationMarkerEdit::_insert_marker(float p_ofs) {
+ if (editor->is_snap_timeline_enabled()) {
+ p_ofs = editor->snap_time(p_ofs);
+ }
+
+ marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE);
+ marker_insert_color->set_pick_color(Color(1, 1, 1));
+
+ String base = "new_marker";
+ int count = 1;
+ while (true) {
+ String attempt = base;
+ if (count > 1) {
+ attempt += vformat("_%d", count);
+ }
+ if (animation->has_marker(attempt)) {
+ count++;
+ continue;
+ }
+ base = attempt;
+ break;
+ }
+
+ marker_insert_new_name->set_text(base);
+ _marker_insert_new_name_changed(base);
+ marker_insert_ofs = p_ofs;
+}
+
+void AnimationMarkerEdit::_rename_marker(const StringName &p_name) {
+ marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE);
+ marker_rename_prev_name = p_name;
+ marker_rename_new_name->set_text(p_name);
+}
+
+void AnimationMarkerEdit::_marker_insert_confirmed() {
+ StringName name = marker_insert_new_name->get_text();
+
+ if (animation->has_marker(name)) {
+ marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name));
+ marker_insert_error_dialog->popup_centered();
+ return;
+ }
+
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+
+ undo_redo->create_action(TTR("Add Marker Key"));
+ undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs);
+ undo_redo->add_undo_method(animation.ptr(), "remove_marker", name);
+ StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs);
+ if (existing_marker) {
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));
+ }
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color());
+
+ undo_redo->add_do_method(this, "queue_redraw");
+ undo_redo->add_undo_method(this, "queue_redraw");
+
+ undo_redo->commit_action();
+
+ marker_insert_confirm->hide();
+}
+
+void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) {
+ marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty());
+}
+
+void AnimationMarkerEdit::_marker_rename_confirmed() {
+ StringName new_name = marker_rename_new_name->get_text();
+ StringName prev_name = marker_rename_prev_name;
+
+ if (new_name == StringName()) {
+ marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed."));
+ marker_rename_error_dialog->popup_centered();
+ return;
+ }
+
+ if (new_name != prev_name && animation->has_marker(new_name)) {
+ marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name));
+ marker_rename_error_dialog->popup_centered();
+ return;
+ }
+
+ if (prev_name != new_name) {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Rename Marker"));
+ undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name);
+ undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name));
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name));
+ undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name);
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name));
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name));
+ undo_redo->add_do_method(this, "_select_key", new_name, true);
+ undo_redo->add_undo_method(this, "_select_key", prev_name, true);
+ undo_redo->commit_action();
+ select_single_attempt = StringName();
+ }
+ marker_rename_confirm->hide();
+}
+
+void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) {
+ marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty());
+}
+
+AnimationMarkerEdit::AnimationMarkerEdit() {
+ play_position = memnew(Control);
+ play_position->set_mouse_filter(MOUSE_FILTER_PASS);
+ add_child(play_position);
+ play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw));
+ set_focus_mode(FOCUS_CLICK);
+ set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection.
+
+ menu = memnew(PopupMenu);
+ add_child(menu);
+ menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected));
+ menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTR("Rename Marker"), Key::R), MENU_KEY_RENAME);
+ menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTR("Delete Markers (s)"), Key::KEY_DELETE), MENU_KEY_DELETE);
+ menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTR("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES);
+
+ marker_insert_confirm = memnew(ConfirmationDialog);
+ marker_insert_confirm->set_title(TTR("Insert Marker"));
+ marker_insert_confirm->set_hide_on_ok(false);
+ marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed));
+ add_child(marker_insert_confirm);
+ VBoxContainer *marker_insert_vbox = memnew(VBoxContainer);
+ marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);
+ marker_insert_confirm->add_child(marker_insert_vbox);
+ marker_insert_new_name = memnew(LineEdit);
+ marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed));
+ marker_insert_confirm->register_text_enter(marker_insert_new_name);
+ marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name));
+ marker_insert_color = memnew(ColorPickerButton);
+ marker_insert_color->set_edit_alpha(false);
+ marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker()));
+ marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color));
+ marker_insert_error_dialog = memnew(AcceptDialog);
+ marker_insert_error_dialog->set_ok_button_text(TTR("Close"));
+ marker_insert_error_dialog->set_title(TTR("Error!"));
+ marker_insert_confirm->add_child(marker_insert_error_dialog);
+
+ marker_rename_confirm = memnew(ConfirmationDialog);
+ marker_rename_confirm->set_title(TTR("Rename Marker"));
+ marker_rename_confirm->set_hide_on_ok(false);
+ marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed));
+ add_child(marker_rename_confirm);
+ VBoxContainer *marker_rename_vbox = memnew(VBoxContainer);
+ marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT);
+ marker_rename_confirm->add_child(marker_rename_vbox);
+ Label *marker_rename_new_name_label = memnew(Label);
+ marker_rename_new_name_label->set_text(TTR("Change Marker Name:"));
+ marker_rename_vbox->add_child(marker_rename_new_name_label);
+ marker_rename_new_name = memnew(LineEdit);
+ marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed));
+ marker_rename_confirm->register_text_enter(marker_rename_new_name);
+ marker_rename_vbox->add_child(marker_rename_new_name);
+
+ marker_rename_error_dialog = memnew(AcceptDialog);
+ marker_rename_error_dialog->set_ok_button_text(TTR("Close"));
+ marker_rename_error_dialog->set_title(TTR("Error!"));
+ marker_rename_confirm->add_child(marker_rename_error_dialog);
+}
+
+AnimationMarkerEdit::~AnimationMarkerEdit() {
+}
+
+float AnimationMarkerKeyEdit::get_time() const {
+ return animation->get_marker_time(marker_name);
+}
+
+void AnimationMarkerKeyEdit::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector);
+ ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector);
+ ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo);
+ ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only);
+ ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name);
+}
+
+void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) {
+ marker_name = p_name;
+}
+
+bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+
+ if (p_name == "color") {
+ Color color = p_value;
+ Color prev_color = animation->get_marker_color(marker_name);
+ if (color != prev_color) {
+ undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS);
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color);
+ undo_redo->add_do_method(marker_edit, "queue_redraw");
+ undo_redo->add_undo_method(marker_edit, "queue_redraw");
+ undo_redo->commit_action();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {
+ if (p_name == "name") {
+ r_ret = marker_name;
+ return true;
+ }
+
+ if (p_name == "color") {
+ r_ret = animation->get_marker_color(marker_name);
+ return true;
+ }
+
+ return false;
+}
+
+void AnimationMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {
+ if (animation.is_null()) {
+ return;
+ }
+
+ p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR));
+ p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));
+}
+
+void AnimationMultiMarkerKeyEdit::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector);
+ ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector);
+ ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo);
+ ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only);
+}
+
+bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ if (p_name == "color") {
+ Color color = p_value;
+
+ undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS);
+
+ for (const StringName &marker_name : marker_names) {
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name));
+ }
+
+ undo_redo->add_do_method(marker_edit, "queue_redraw");
+ undo_redo->add_undo_method(marker_edit, "queue_redraw");
+ undo_redo->commit_action();
+
+ return true;
+ }
+
+ return false;
+}
+
+bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const {
+ if (p_name == "color") {
+ r_ret = animation->get_marker_color(marker_names[0]);
+ return true;
+ }
+
+ return false;
+}
+
+void AnimationMultiMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const {
+ if (animation.is_null()) {
+ return;
+ }
+
+ p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA));
+}
+
+// AnimationMarkerKeyEditEditorPlugin
+
+void AnimationMarkerKeyEditEditor::_time_edit_entered() {
+}
+
+void AnimationMarkerKeyEditEditor::_time_edit_exited() {
+ real_t new_time = spinner->get_value();
+
+ if (use_fps) {
+ real_t fps = animation->get_step();
+ if (fps > 0) {
+ fps = 1.0 / fps;
+ }
+ new_time /= fps;
+ }
+
+ real_t prev_time = animation->get_marker_time(marker_name);
+
+ if (Math::is_equal_approx(new_time, prev_time)) {
+ return; // No change.
+ }
+
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Animation Change Marker Time"), UndoRedo::MERGE_ENDS);
+
+ Color color = animation->get_marker_color(marker_name);
+ undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time);
+ undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color);
+ undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name);
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time);
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color);
+ StringName existing_marker = animation->get_marker_at_time(new_time);
+ if (existing_marker) {
+ undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker));
+ undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker));
+ }
+ AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();
+ if (ape) {
+ AnimationTrackEditor *ate = ape->get_track_editor();
+ if (ate) {
+ AnimationMarkerEdit *ame = ate->marker_edit;
+ undo_redo->add_do_method(ame, "queue_redraw");
+ undo_redo->add_undo_method(ame, "queue_redraw");
+ }
+ }
+ undo_redo->commit_action();
+}
+
+AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps) {
+ if (p_animation.is_null()) {
+ return;
+ }
+
+ animation = p_animation;
+ use_fps = p_use_fps;
+ marker_name = p_name;
+
+ set_label("Time");
+
+ spinner = memnew(EditorSpinSlider);
+ spinner->set_focus_mode(Control::FOCUS_CLICK);
+ spinner->set_min(0);
+ spinner->set_allow_greater(true);
+ spinner->set_allow_lesser(true);
+
+ float time = animation->get_marker_time(marker_name);
+
+ if (use_fps) {
+ spinner->set_step(FPS_DECIMAL);
+ real_t fps = animation->get_step();
+ if (fps > 0) {
+ fps = 1.0 / fps;
+ }
+ spinner->set_value(time * fps);
+ } else {
+ spinner->set_step(SECOND_DECIMAL);
+ spinner->set_value(time);
+ spinner->set_max(animation->get_length());
+ }
+
+ add_child(spinner);
+
+ spinner->connect("grabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);
+ spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);
+ spinner->connect("value_focus_entered", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED);
+ spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED);
+}
+
+AnimationMarkerKeyEditEditor::~AnimationMarkerKeyEditEditor() {
+}
diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h
index 6b9140ddaa..0da474afd4 100644
--- a/editor/animation_track_editor.h
+++ b/editor/animation_track_editor.h
@@ -41,9 +41,11 @@
#include "scene/gui/tree.h"
#include "scene/resources/animation.h"
+class AnimationMarkerEdit;
class AnimationTrackEditor;
class AnimationTrackEdit;
class CheckBox;
+class ColorPickerButton;
class EditorSpinSlider;
class HSlider;
class OptionButton;
@@ -52,6 +54,7 @@ class SceneTreeDialog;
class SpinBox;
class TextureRect;
class ViewPanner;
+class EditorValidationPanel;
class AnimationTrackKeyEdit : public Object {
GDCLASS(AnimationTrackKeyEdit, Object);
@@ -128,6 +131,58 @@ protected:
void _get_property_list(List<PropertyInfo> *p_list) const;
};
+class AnimationMarkerKeyEdit : public Object {
+ GDCLASS(AnimationMarkerKeyEdit, Object);
+
+public:
+ bool animation_read_only = false;
+
+ Ref<Animation> animation;
+ StringName marker_name;
+ bool use_fps = false;
+
+ AnimationMarkerEdit *marker_edit = nullptr;
+
+ bool _hide_script_from_inspector() { return true; }
+ bool _hide_metadata_from_inspector() { return true; }
+ bool _dont_undo_redo() { return true; }
+
+ bool _is_read_only() { return animation_read_only; }
+
+ float get_time() const;
+
+protected:
+ static void _bind_methods();
+ void _set_marker_name(const StringName &p_name);
+ bool _set(const StringName &p_name, const Variant &p_value);
+ bool _get(const StringName &p_name, Variant &r_ret) const;
+ void _get_property_list(List<PropertyInfo> *p_list) const;
+};
+
+class AnimationMultiMarkerKeyEdit : public Object {
+ GDCLASS(AnimationMultiMarkerKeyEdit, Object);
+
+public:
+ bool animation_read_only = false;
+
+ Ref<Animation> animation;
+ Vector<StringName> marker_names;
+
+ AnimationMarkerEdit *marker_edit = nullptr;
+
+ bool _hide_script_from_inspector() { return true; }
+ bool _hide_metadata_from_inspector() { return true; }
+ bool _dont_undo_redo() { return true; }
+
+ bool _is_read_only() { return animation_read_only; }
+
+protected:
+ static void _bind_methods();
+ bool _set(const StringName &p_name, const Variant &p_value);
+ bool _get(const StringName &p_name, Variant &r_ret) const;
+ void _get_property_list(List<PropertyInfo> *p_list) const;
+};
+
class AnimationTimelineEdit : public Range {
GDCLASS(AnimationTimelineEdit, Range);
@@ -218,6 +273,140 @@ public:
AnimationTimelineEdit();
};
+class AnimationMarkerEdit : public Control {
+ GDCLASS(AnimationMarkerEdit, Control);
+ friend class AnimationTimelineEdit;
+
+ enum {
+ MENU_KEY_INSERT,
+ MENU_KEY_RENAME,
+ MENU_KEY_DELETE,
+ MENU_KEY_TOGGLE_MARKER_NAMES,
+ };
+
+ AnimationTimelineEdit *timeline = nullptr;
+ Control *play_position = nullptr; // Separate control used to draw so updates for only position changed are much faster.
+ float play_position_pos = 0.0f;
+
+ HashSet<StringName> selection;
+
+ Ref<Animation> animation;
+ bool read_only = false;
+
+ Ref<Texture2D> type_icon;
+ Ref<Texture2D> selected_icon;
+
+ PopupMenu *menu = nullptr;
+
+ bool hovered = false;
+ StringName hovering_marker;
+
+ void _zoom_changed();
+
+ Ref<Texture2D> icon_cache;
+
+ void _menu_selected(int p_index);
+
+ void _play_position_draw();
+ bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable);
+ bool _is_ui_pos_in_current_section(const Point2 &p_pos);
+
+ float insert_at_pos = 0.0f;
+ bool moving_selection_attempt = false;
+ bool moving_selection_effective = false;
+ float moving_selection_offset = 0.0f;
+ float moving_selection_pivot = 0.0f;
+ float moving_selection_mouse_begin_x = 0.0f;
+ float moving_selection_mouse_begin_y = 0.0f;
+ StringName select_single_attempt;
+ bool moving_selection = false;
+ void _move_selection_begin();
+ void _move_selection(float p_offset);
+ void _move_selection_commit();
+ void _move_selection_cancel();
+
+ void _clear_selection_for_anim(const Ref<Animation> &p_anim);
+ void _select_key(const StringName &p_name, bool is_single = false);
+ void _deselect_key(const StringName &p_name);
+
+ void _insert_marker(float p_ofs);
+ void _rename_marker(const StringName &p_name);
+ void _delete_selected_markers();
+
+ ConfirmationDialog *marker_insert_confirm = nullptr;
+ LineEdit *marker_insert_new_name = nullptr;
+ ColorPickerButton *marker_insert_color = nullptr;
+ AcceptDialog *marker_insert_error_dialog = nullptr;
+ float marker_insert_ofs = 0;
+
+ ConfirmationDialog *marker_rename_confirm = nullptr;
+ LineEdit *marker_rename_new_name = nullptr;
+ StringName marker_rename_prev_name;
+
+ AcceptDialog *marker_rename_error_dialog = nullptr;
+
+ bool should_show_all_marker_names = false;
+
+ ////////////// edit menu stuff
+
+ void _marker_insert_confirmed();
+ void _marker_insert_new_name_changed(const String &p_text);
+ void _marker_rename_confirmed();
+ void _marker_rename_new_name_changed(const String &p_text);
+
+ AnimationTrackEditor *editor = nullptr;
+
+ HBoxContainer *_create_hbox_labeled_control(const String &p_text, Control *p_control) const;
+
+ void _update_key_edit();
+ void _clear_key_edit();
+
+ AnimationMarkerKeyEdit *key_edit = nullptr;
+ AnimationMultiMarkerKeyEdit *multi_key_edit = nullptr;
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_what);
+
+ virtual void gui_input(const Ref<InputEvent> &p_event) override;
+
+public:
+ virtual String get_tooltip(const Point2 &p_pos) const override;
+
+ virtual int get_key_height() const;
+ virtual Rect2 get_key_rect(float p_pixels_sec) const;
+ virtual bool is_key_selectable_by_distance() const;
+ virtual void draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right);
+ virtual void draw_bg(int p_clip_left, int p_clip_right);
+ virtual void draw_fg(int p_clip_left, int p_clip_right);
+
+ Ref<Animation> get_animation() const;
+ AnimationTimelineEdit *get_timeline() const { return timeline; }
+ AnimationTrackEditor *get_editor() const { return editor; }
+ bool is_selection_active() const { return !selection.is_empty(); }
+ bool is_moving_selection() const { return moving_selection; }
+ float get_moving_selection_offset() const { return moving_selection_offset; }
+ void set_animation(const Ref<Animation> &p_animation, bool p_read_only);
+ virtual Size2 get_minimum_size() const override;
+
+ void set_timeline(AnimationTimelineEdit *p_timeline);
+ void set_editor(AnimationTrackEditor *p_editor);
+
+ void set_play_position(float p_pos);
+ void update_play_position();
+
+ void set_use_fps(bool p_use_fps);
+
+ PackedStringArray get_selected_section() const;
+ bool is_marker_selected(const StringName &p_marker) const;
+
+ // For use by AnimationTrackEditor.
+ void _clear_selection(bool p_update);
+
+ AnimationMarkerEdit();
+ ~AnimationMarkerEdit();
+};
+
class AnimationTrackEdit : public Control {
GDCLASS(AnimationTrackEdit, Control);
friend class AnimationTimelineEdit;
@@ -367,6 +556,7 @@ class AnimationTrackEditGroup : public Control {
NodePath node;
Node *root = nullptr;
AnimationTimelineEdit *timeline = nullptr;
+ AnimationTrackEditor *editor = nullptr;
void _zoom_changed();
@@ -380,6 +570,7 @@ public:
virtual Size2 get_minimum_size() const override;
void set_timeline(AnimationTimelineEdit *p_timeline);
void set_root(Node *p_root);
+ void set_editor(AnimationTrackEditor *p_editor);
AnimationTrackEditGroup();
};
@@ -388,6 +579,7 @@ class AnimationTrackEditor : public VBoxContainer {
GDCLASS(AnimationTrackEditor, VBoxContainer);
friend class AnimationTimelineEdit;
friend class AnimationBezierTrackEdit;
+ friend class AnimationMarkerKeyEditEditor;
Ref<Animation> animation;
bool read_only = false;
@@ -405,6 +597,7 @@ class AnimationTrackEditor : public VBoxContainer {
Label *info_message = nullptr;
AnimationTimelineEdit *timeline = nullptr;
+ AnimationMarkerEdit *marker_edit = nullptr;
HSlider *zoom = nullptr;
EditorSpinSlider *step = nullptr;
TextureRect *zoom_icon = nullptr;
@@ -743,6 +936,10 @@ public:
float get_moving_selection_offset() const;
float snap_time(float p_value, bool p_relative = false);
bool is_grouping_tracks();
+ PackedStringArray get_selected_section() const;
+ bool is_marker_selected(const StringName &p_marker) const;
+ bool is_marker_moving_selection() const;
+ float get_marker_moving_selection_offset() const;
/** If `p_from_mouse_event` is `true`, handle Shift key presses for precise snapping. */
void goto_prev_step(bool p_from_mouse_event);
@@ -781,4 +978,23 @@ public:
~AnimationTrackKeyEditEditor();
};
+// AnimationMarkerKeyEditEditorPlugin
+
+class AnimationMarkerKeyEditEditor : public EditorProperty {
+ GDCLASS(AnimationMarkerKeyEditEditor, EditorProperty);
+
+ Ref<Animation> animation;
+ StringName marker_name;
+ bool use_fps = false;
+
+ EditorSpinSlider *spinner = nullptr;
+
+ void _time_edit_entered();
+ void _time_edit_exited();
+
+public:
+ AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps);
+ ~AnimationMarkerKeyEditEditor();
+};
+
#endif // ANIMATION_TRACK_EDITOR_H
diff --git a/editor/debugger/editor_debugger_server.cpp b/editor/debugger/editor_debugger_server.cpp
index c0efc6a1fc..9ec6058132 100644
--- a/editor/debugger/editor_debugger_server.cpp
+++ b/editor/debugger/editor_debugger_server.cpp
@@ -77,8 +77,8 @@ Error EditorDebuggerServerTCP::start(const String &p_uri) {
// Optionally override
if (!p_uri.is_empty() && p_uri != "tcp://") {
- String scheme, path;
- Error err = p_uri.parse_url(scheme, bind_host, bind_port, path);
+ String scheme, path, fragment;
+ Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment);
ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER);
}
diff --git a/editor/editor_about.cpp b/editor/editor_about.cpp
index dc943fc783..34f432aa7e 100644
--- a/editor/editor_about.cpp
+++ b/editor/editor_about.cpp
@@ -33,16 +33,12 @@
#include "core/authors.gen.h"
#include "core/donors.gen.h"
#include "core/license.gen.h"
-#include "core/os/time.h"
-#include "core/version.h"
#include "editor/editor_string_names.h"
+#include "editor/gui/editor_version_button.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/item_list.h"
#include "scene/resources/style_box.h"
-// The metadata key used to store and retrieve the version text to copy to the clipboard.
-const String EditorAbout::META_TEXT_TO_COPY = "text_to_copy";
-
void EditorAbout::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
@@ -81,10 +77,6 @@ void EditorAbout::_license_tree_selected() {
_tpl_text->set_text(selected->get_metadata(0));
}
-void EditorAbout::_version_button_pressed() {
- DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY));
-}
-
void EditorAbout::_item_with_website_selected(int p_id, ItemList *p_il) {
const String website = p_il->get_item_metadata(p_id);
if (!website.is_empty()) {
@@ -198,25 +190,7 @@ EditorAbout::EditorAbout() {
Control *v_spacer = memnew(Control);
version_info_vbc->add_child(v_spacer);
- version_btn = memnew(LinkButton);
- String hash = String(VERSION_HASH);
- if (hash.length() != 0) {
- hash = " " + vformat("[%s]", hash.left(9));
- }
- version_btn->set_text(VERSION_FULL_NAME + hash);
- // Set the text to copy in metadata as it slightly differs from the button's text.
- version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash);
- version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
- String build_date;
- if (VERSION_TIMESTAMP > 0) {
- build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC";
- } else {
- build_date = TTR("(unknown)");
- }
- version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version number."), build_date));
-
- version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorAbout::_version_button_pressed));
- version_info_vbc->add_child(version_btn);
+ version_info_vbc->add_child(memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD)));
Label *about_text = memnew(Label);
about_text->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
diff --git a/editor/editor_about.h b/editor/editor_about.h
index fc3d6cedce..6f33d502d7 100644
--- a/editor/editor_about.h
+++ b/editor/editor_about.h
@@ -33,7 +33,6 @@
#include "scene/gui/dialogs.h"
#include "scene/gui/item_list.h"
-#include "scene/gui/link_button.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/separator.h"
@@ -49,16 +48,12 @@
class EditorAbout : public AcceptDialog {
GDCLASS(EditorAbout, AcceptDialog);
- static const String META_TEXT_TO_COPY;
-
private:
void _license_tree_selected();
- void _version_button_pressed();
void _item_with_website_selected(int p_id, ItemList *p_il);
void _item_list_resized(ItemList *p_il);
ScrollContainer *_populate_list(const String &p_name, const List<String> &p_sections, const char *const *const p_src[], int p_single_column_flags = 0, bool p_allow_website = false);
- LinkButton *version_btn = nullptr;
Tree *_tpl_tree = nullptr;
RichTextLabel *license_text_label = nullptr;
RichTextLabel *_tpl_text = nullptr;
diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp
index a215662f16..21f67772ea 100644
--- a/editor/editor_inspector.cpp
+++ b/editor/editor_inspector.cpp
@@ -2763,8 +2763,9 @@ void EditorInspector::update_tree() {
// TODO: Can be useful to store more context for the focusable, such as the caret position in LineEdit.
StringName current_selected = property_selected;
int current_focusable = -1;
- // Temporarily disable focus following to avoid jumping while the inspector is updating.
- set_follow_focus(false);
+
+ // Temporarily disable focus following on the root inspector to avoid jumping while the inspector is updating.
+ get_root_inspector()->set_follow_focus(false);
if (property_focusable != -1) {
// Check that focusable is actually focusable.
@@ -2792,6 +2793,7 @@ void EditorInspector::update_tree() {
_clear(!object);
if (!object) {
+ get_root_inspector()->set_follow_focus(true);
return;
}
@@ -3529,7 +3531,8 @@ void EditorInspector::update_tree() {
// Updating inspector might invalidate some editing owners.
EditorNode::get_singleton()->hide_unused_editors();
}
- set_follow_focus(true);
+
+ get_root_inspector()->set_follow_focus(true);
}
void EditorInspector::update_property(const String &p_prop) {
@@ -3774,11 +3777,10 @@ void EditorInspector::set_use_wide_editors(bool p_enable) {
wide_editors = p_enable;
}
-void EditorInspector::set_sub_inspector(bool p_enable) {
- sub_inspector = p_enable;
- if (!is_inside_tree()) {
- return;
- }
+void EditorInspector::set_root_inspector(EditorInspector *p_root_inspector) {
+ root_inspector = p_root_inspector;
+ // Only the root inspector should follow focus.
+ set_follow_focus(false);
}
void EditorInspector::set_use_deletable_properties(bool p_enabled) {
@@ -4096,13 +4098,13 @@ void EditorInspector::_notification(int p_what) {
EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &EditorInspector::_feature_profile_changed));
set_process(is_visible_in_tree());
add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree")));
- if (!sub_inspector) {
+ if (!is_sub_inspector()) {
get_tree()->connect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
}
} break;
case NOTIFICATION_PREDELETE: {
- if (!sub_inspector && is_inside_tree()) {
+ if (!is_sub_inspector() && is_inside_tree()) {
get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
}
edit(nullptr);
@@ -4161,7 +4163,7 @@ void EditorInspector::_notification(int p_what) {
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
bool needs_update = false;
- if (EditorThemeManager::is_generated_theme_outdated() && !sub_inspector) {
+ if (!is_sub_inspector() && EditorThemeManager::is_generated_theme_outdated()) {
add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree")));
}
diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h
index 14b6ff0907..0309213b76 100644
--- a/editor/editor_inspector.h
+++ b/editor/editor_inspector.h
@@ -486,6 +486,7 @@ class EditorInspector : public ScrollContainer {
static Ref<EditorInspectorPlugin> inspector_plugins[MAX_PLUGINS];
static int inspector_plugin_count;
+ EditorInspector *root_inspector = nullptr;
VBoxContainer *main_vbox = nullptr;
// Map used to cache the instantiated editors.
@@ -514,7 +515,6 @@ class EditorInspector : public ScrollContainer {
bool update_all_pending = false;
bool read_only = false;
bool keying = false;
- bool sub_inspector = false;
bool wide_editors = false;
bool deletable_properties = false;
@@ -644,8 +644,9 @@ public:
String get_object_class() const;
void set_use_wide_editors(bool p_enable);
- void set_sub_inspector(bool p_enable);
- bool is_sub_inspector() const { return sub_inspector; }
+ void set_root_inspector(EditorInspector *p_root_inspector);
+ EditorInspector *get_root_inspector() { return is_sub_inspector() ? root_inspector : this; }
+ bool is_sub_inspector() const { return root_inspector != nullptr; }
void set_use_deletable_properties(bool p_enabled);
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 665255b9b2..3bae9ae984 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -7722,6 +7722,7 @@ EditorNode::EditorNode() {
add_editor_plugin(memnew(AnimationPlayerEditorPlugin));
add_editor_plugin(memnew(AnimationTrackKeyEditEditorPlugin));
+ add_editor_plugin(memnew(AnimationMarkerKeyEditEditorPlugin));
add_editor_plugin(memnew(CanvasItemEditorPlugin));
add_editor_plugin(memnew(Node3DEditorPlugin));
add_editor_plugin(memnew(ScriptEditorPlugin));
diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp
index 0fb57ce40e..897e7835fd 100644
--- a/editor/editor_properties.cpp
+++ b/editor/editor_properties.cpp
@@ -3246,7 +3246,10 @@ void EditorPropertyResource::update_property() {
sub_inspector->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
sub_inspector->set_use_doc_hints(true);
- sub_inspector->set_sub_inspector(true);
+ EditorInspector *parent_inspector = get_parent_inspector();
+ ERR_FAIL_NULL(parent_inspector);
+ sub_inspector->set_root_inspector(parent_inspector->get_root_inspector());
+
sub_inspector->set_property_name_style(InspectorDock::get_singleton()->get_property_name_style());
sub_inspector->connect("property_keyed", callable_mp(this, &EditorPropertyResource::_sub_inspector_property_keyed));
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index c972a6ab27..6e8312e8c5 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -426,12 +426,17 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/display_scale", 0, display_scale_hint_string, PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING)
EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_RANGE, "interface/editor/custom_display_scale", 1.0, "0.5,3,0.01", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING)
- String ed_screen_hints = "Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2"; // Note: Main Window Screen:-1 is not used for the main window.
+ String ed_screen_hints = "Auto (Remembers last position):-5,Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2";
for (int i = 0; i < DisplayServer::get_singleton()->get_screen_count(); i++) {
ed_screen_hints += ",Screen " + itos(i + 1) + ":" + itos(i);
}
- EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/editor_screen", -2, ed_screen_hints)
- EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/project_manager_screen", -2, ed_screen_hints)
+ EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/editor_screen", EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO, ed_screen_hints)
+
+ String project_manager_screen_hints = "Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2";
+ for (int i = 0; i < DisplayServer::get_singleton()->get_screen_count(); i++) {
+ project_manager_screen_hints += ",Screen " + itos(i + 1) + ":" + itos(i);
+ }
+ EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/project_manager_screen", EditorSettings::InitialScreen::INITIAL_SCREEN_PRIMARY, project_manager_screen_hints)
{
EngineUpdateLabel::UpdateMode default_update_mode = EngineUpdateLabel::UpdateMode::NEWEST_UNSTABLE;
@@ -466,7 +471,6 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
_initial_set("interface/editor/separate_distraction_mode", false, true);
_initial_set("interface/editor/automatically_open_screenshots", true, true);
EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/single_window_mode", false, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING)
- _initial_set("interface/editor/remember_window_size_and_position", true, true);
_initial_set("interface/editor/mouse_extra_buttons_navigate_history", true);
_initial_set("interface/editor/save_each_scene_on_quit", true, true); // Regression
EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/save_on_focus_loss", false, "")
diff --git a/editor/editor_settings.h b/editor/editor_settings.h
index e850406839..d1ccedfe6c 100644
--- a/editor/editor_settings.h
+++ b/editor/editor_settings.h
@@ -62,6 +62,13 @@ public:
NETWORK_ONLINE,
};
+ enum InitialScreen {
+ INITIAL_SCREEN_AUTO = -5, // Remembers last screen position.
+ INITIAL_SCREEN_WITH_MOUSE_FOCUS = -4,
+ INITIAL_SCREEN_WITH_KEYBOARD_FOCUS = -3,
+ INITIAL_SCREEN_PRIMARY = -2,
+ };
+
private:
struct VariantContainer {
int order = 0;
diff --git a/editor/export/editor_export_plugin.cpp b/editor/export/editor_export_plugin.cpp
index 2f57010c2c..bd9c3503f7 100644
--- a/editor/export/editor_export_plugin.cpp
+++ b/editor/export/editor_export_plugin.cpp
@@ -295,6 +295,12 @@ bool EditorExportPlugin::_should_update_export_options(const Ref<EditorExportPla
return ret;
}
+bool EditorExportPlugin::_get_export_option_visibility(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const {
+ bool ret = true;
+ GDVIRTUAL_CALL(_get_export_option_visibility, p_export_platform, p_option_name, ret);
+ return ret;
+}
+
String EditorExportPlugin::_get_export_option_warning(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const {
String ret;
GDVIRTUAL_CALL(_get_export_option_warning, p_export_platform, p_option_name, ret);
@@ -354,6 +360,7 @@ void EditorExportPlugin::_bind_methods() {
GDVIRTUAL_BIND(_get_export_options, "platform");
GDVIRTUAL_BIND(_get_export_options_overrides, "platform");
GDVIRTUAL_BIND(_should_update_export_options, "platform");
+ GDVIRTUAL_BIND(_get_export_option_visibility, "platform", "option");
GDVIRTUAL_BIND(_get_export_option_warning, "platform", "option");
GDVIRTUAL_BIND(_get_export_features, "platform", "debug");
diff --git a/editor/export/editor_export_plugin.h b/editor/export/editor_export_plugin.h
index 8c744b3108..79d3d0954d 100644
--- a/editor/export/editor_export_plugin.h
+++ b/editor/export/editor_export_plugin.h
@@ -132,6 +132,7 @@ protected:
GDVIRTUAL1RC(TypedArray<Dictionary>, _get_export_options, const Ref<EditorExportPlatform> &);
GDVIRTUAL1RC(Dictionary, _get_export_options_overrides, const Ref<EditorExportPlatform> &);
GDVIRTUAL1RC(bool, _should_update_export_options, const Ref<EditorExportPlatform> &);
+ GDVIRTUAL2RC(bool, _get_export_option_visibility, const Ref<EditorExportPlatform> &, String);
GDVIRTUAL2RC(String, _get_export_option_warning, const Ref<EditorExportPlatform> &, String);
GDVIRTUAL0RC_REQUIRED(String, _get_name)
@@ -160,6 +161,7 @@ protected:
virtual void _get_export_options(const Ref<EditorExportPlatform> &p_export_platform, List<EditorExportPlatform::ExportOption> *r_options) const;
virtual Dictionary _get_export_options_overrides(const Ref<EditorExportPlatform> &p_export_platform) const;
virtual bool _should_update_export_options(const Ref<EditorExportPlatform> &p_export_platform) const;
+ virtual bool _get_export_option_visibility(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const;
virtual String _get_export_option_warning(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const;
public:
diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp
index 1ca72348e2..da7059b777 100644
--- a/editor/export/editor_export_preset.cpp
+++ b/editor/export/editor_export_preset.cpp
@@ -136,8 +136,29 @@ String EditorExportPreset::_get_property_warning(const StringName &p_name) const
void EditorExportPreset::_get_property_list(List<PropertyInfo> *p_list) const {
for (const KeyValue<StringName, PropertyInfo> &E : properties) {
- if (!value_overrides.has(E.key) && platform->get_export_option_visibility(this, E.key)) {
- p_list->push_back(E.value);
+ if (!value_overrides.has(E.key)) {
+ bool property_visible = platform->get_export_option_visibility(this, E.key);
+ if (!property_visible) {
+ continue;
+ }
+
+ // Get option visibility from editor export plugins.
+ Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins();
+ for (int i = 0; i < export_plugins.size(); i++) {
+ if (!export_plugins[i]->supports_platform(platform)) {
+ continue;
+ }
+
+ export_plugins.write[i]->set_export_preset(Ref<EditorExportPreset>(this));
+ property_visible = export_plugins[i]->_get_export_option_visibility(platform, E.key);
+ if (!property_visible) {
+ break;
+ }
+ }
+
+ if (property_visible) {
+ p_list->push_back(E.value);
+ }
}
}
}
diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp
index 4b2fd9cb2f..f6ba74fe95 100644
--- a/editor/gui/editor_bottom_panel.cpp
+++ b/editor/gui/editor_bottom_panel.cpp
@@ -30,8 +30,6 @@
#include "editor_bottom_panel.h"
-#include "core/os/time.h"
-#include "core/version.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/editor_about.h"
#include "editor/editor_command_palette.h"
@@ -39,13 +37,10 @@
#include "editor/editor_string_names.h"
#include "editor/engine_update_label.h"
#include "editor/gui/editor_toaster.h"
+#include "editor/gui/editor_version_button.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
-#include "scene/gui/link_button.h"
-
-// The metadata key used to store and retrieve the version text to copy to the clipboard.
-static const String META_TEXT_TO_COPY = "text_to_copy";
void EditorBottomPanel::_notification(int p_what) {
switch (p_what) {
@@ -110,10 +105,6 @@ void EditorBottomPanel::_expand_button_toggled(bool p_pressed) {
EditorNode::get_top_split()->set_visible(!p_pressed);
}
-void EditorBottomPanel::_version_button_pressed() {
- DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY));
-}
-
bool EditorBottomPanel::_button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control) {
if (!p_button->is_pressed()) {
_switch_by_control(true, p_control);
@@ -262,25 +253,9 @@ EditorBottomPanel::EditorBottomPanel() {
editor_toaster = memnew(EditorToaster);
bottom_hbox->add_child(editor_toaster);
- version_btn = memnew(LinkButton);
- version_btn->set_text(VERSION_FULL_CONFIG);
- String hash = String(VERSION_HASH);
- if (hash.length() != 0) {
- hash = " " + vformat("[%s]", hash.left(9));
- }
- // Set the text to copy in metadata as it slightly differs from the button's text.
- version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash);
+ EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_BASIC));
// Fade out the version label to be less prominent, but still readable.
version_btn->set_self_modulate(Color(1, 1, 1, 0.65));
- version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
- String build_date;
- if (VERSION_TIMESTAMP > 0) {
- build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC";
- } else {
- build_date = TTR("(unknown)");
- }
- version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date));
- version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorBottomPanel::_version_button_pressed));
version_btn->set_v_size_flags(Control::SIZE_SHRINK_CENTER);
bottom_hbox->add_child(version_btn);
diff --git a/editor/gui/editor_bottom_panel.h b/editor/gui/editor_bottom_panel.h
index 95c767dae5..3d44b3750a 100644
--- a/editor/gui/editor_bottom_panel.h
+++ b/editor/gui/editor_bottom_panel.h
@@ -37,7 +37,6 @@ class Button;
class ConfigFile;
class EditorToaster;
class HBoxContainer;
-class LinkButton;
class VBoxContainer;
class EditorBottomPanel : public PanelContainer {
@@ -55,14 +54,12 @@ class EditorBottomPanel : public PanelContainer {
HBoxContainer *bottom_hbox = nullptr;
HBoxContainer *button_hbox = nullptr;
EditorToaster *editor_toaster = nullptr;
- LinkButton *version_btn = nullptr;
Button *expand_button = nullptr;
Control *last_opened_control = nullptr;
void _switch_by_control(bool p_visible, Control *p_control);
void _switch_to_item(bool p_visible, int p_idx);
void _expand_button_toggled(bool p_pressed);
- void _version_button_pressed();
bool _button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control);
diff --git a/editor/gui/editor_version_button.cpp b/editor/gui/editor_version_button.cpp
new file mode 100644
index 0000000000..635d66f42a
--- /dev/null
+++ b/editor/gui/editor_version_button.cpp
@@ -0,0 +1,85 @@
+/**************************************************************************/
+/* editor_version_button.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_version_button.h"
+
+#include "core/os/time.h"
+#include "core/version.h"
+
+String _get_version_string(EditorVersionButton::VersionFormat p_format) {
+ String main;
+ switch (p_format) {
+ case EditorVersionButton::FORMAT_BASIC: {
+ return VERSION_FULL_CONFIG;
+ } break;
+ case EditorVersionButton::FORMAT_WITH_BUILD: {
+ main = "v" VERSION_FULL_BUILD;
+ } break;
+ case EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD: {
+ main = VERSION_FULL_NAME;
+ } break;
+ default: {
+ ERR_FAIL_V_MSG(VERSION_FULL_NAME, "Unexpected format: " + itos(p_format));
+ } break;
+ }
+
+ String hash = VERSION_HASH;
+ if (!hash.is_empty()) {
+ hash = vformat(" [%s]", hash.left(9));
+ }
+ return main + hash;
+}
+
+void EditorVersionButton::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_POSTINITIALIZE: {
+ // This can't be done in the constructor because theme cache is not ready yet.
+ set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+ set_text(_get_version_string(format));
+ } break;
+ }
+}
+
+void EditorVersionButton::pressed() {
+ DisplayServer::get_singleton()->clipboard_set(_get_version_string(FORMAT_WITH_BUILD));
+}
+
+EditorVersionButton::EditorVersionButton(VersionFormat p_format) {
+ format = p_format;
+ set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
+
+ String build_date;
+ if (VERSION_TIMESTAMP > 0) {
+ build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC";
+ } else {
+ build_date = TTR("(unknown)");
+ }
+ set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date));
+}
diff --git a/editor/gui/editor_version_button.h b/editor/gui/editor_version_button.h
new file mode 100644
index 0000000000..591c3d483e
--- /dev/null
+++ b/editor/gui/editor_version_button.h
@@ -0,0 +1,61 @@
+/**************************************************************************/
+/* editor_version_button.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef EDITOR_VERSION_BUTTON_H
+#define EDITOR_VERSION_BUTTON_H
+
+#include "scene/gui/link_button.h"
+
+class EditorVersionButton : public LinkButton {
+ GDCLASS(EditorVersionButton, LinkButton);
+
+public:
+ enum VersionFormat {
+ // 4.3.2.stable
+ FORMAT_BASIC,
+ // v4.3.2.stable.mono [HASH]
+ FORMAT_WITH_BUILD,
+ // Godot Engine v4.3.2.stable.mono.official [HASH]
+ FORMAT_WITH_NAME_AND_BUILD,
+ };
+
+private:
+ VersionFormat format = FORMAT_WITH_NAME_AND_BUILD;
+
+protected:
+ void _notification(int p_what);
+
+ virtual void pressed() override;
+
+public:
+ EditorVersionButton(VersionFormat p_format);
+};
+
+#endif // EDITOR_VERSION_BUTTON_H
diff --git a/editor/icons/Marker.svg b/editor/icons/Marker.svg
new file mode 100644
index 0000000000..ff91a4a947
--- /dev/null
+++ b/editor/icons/Marker.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="-959.5 540.5 10 10"><path fill="#e0e0e0" d="m-954.5 550-3-3v-6h6v6z"/></svg> \ No newline at end of file
diff --git a/editor/icons/MarkerSelected.svg b/editor/icons/MarkerSelected.svg
new file mode 100644
index 0000000000..c581a3a651
--- /dev/null
+++ b/editor/icons/MarkerSelected.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="-959.5 540.5 10 10"><path fill="#5fb2ff" d="m-952 541.5v5.086l-2.5 2.5-2.5-2.5v-5.086zm1-1h-7v6.5l3.5 3.5 3.5-3.5z"/><path fill="#003e7a" d="m-957 546.586 2.5 2.5 2.5-2.5v-5.086h-5z"/></svg> \ No newline at end of file
diff --git a/editor/inspector_dock.cpp b/editor/inspector_dock.cpp
index 3a3598c0b9..d13a022d52 100644
--- a/editor/inspector_dock.cpp
+++ b/editor/inspector_dock.cpp
@@ -800,7 +800,7 @@ InspectorDock::InspectorDock(EditorData &p_editor_data) {
inspector->set_use_folding(!bool(EDITOR_GET("interface/inspector/disable_folding")));
inspector->register_text_enter(search);
- inspector->set_use_filter(true); // TODO: check me
+ inspector->set_use_filter(true);
inspector->connect("resource_selected", callable_mp(this, &InspectorDock::_resource_selected));
diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp
index a28fe01666..9e282cb3fa 100644
--- a/editor/plugins/animation_blend_tree_editor_plugin.cpp
+++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp
@@ -36,6 +36,7 @@
#include "core/os/keyboard.h"
#include "editor/editor_inspector.h"
#include "editor/editor_node.h"
+#include "editor/editor_properties.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
@@ -45,6 +46,7 @@
#include "scene/animation/animation_player.h"
#include "scene/gui/check_box.h"
#include "scene/gui/menu_button.h"
+#include "scene/gui/option_button.h"
#include "scene/gui/panel.h"
#include "scene/gui/progress_bar.h"
#include "scene/gui/separator.h"
@@ -1262,4 +1264,168 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() {
open_file->set_title(TTR("Open Animation Node"));
open_file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
open_file->connect("file_selected", callable_mp(this, &AnimationNodeBlendTreeEditor::_file_opened));
+
+ animation_node_inspector_plugin = Ref<EditorInspectorPluginAnimationNodeAnimation>(memnew(EditorInspectorPluginAnimationNodeAnimation));
+ EditorInspector::add_inspector_plugin(animation_node_inspector_plugin);
+}
+
+AnimationNodeBlendTreeEditor::~AnimationNodeBlendTreeEditor() {
+}
+
+// EditorPluginAnimationNodeAnimation
+
+void AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog() {
+ AnimationTree *tree = AnimationTreeEditor::get_singleton()->get_animation_tree();
+ StringName anim_name = animation_node_animation->get_animation();
+ PackedStringArray markers = tree->has_animation(anim_name) ? tree->get_animation(anim_name)->get_marker_names() : PackedStringArray();
+
+ dialog->select_start->clear();
+ dialog->select_start->add_icon_item(get_editor_theme_icon(SNAME("PlayStart")), TTR("Start of Animation"));
+ dialog->select_start->add_separator();
+ dialog->select_end->clear();
+ dialog->select_end->add_icon_item(get_editor_theme_icon(SNAME("PlayStartBackwards")), TTR("End of Animation"));
+ dialog->select_end->add_separator();
+
+ for (const String &marker : markers) {
+ dialog->select_start->add_item(marker);
+ dialog->select_end->add_item(marker);
+ }
+
+ // Because the default selections are always valid, and marker times won't change during the dialog, we can ensure that the user can only select valid markers.
+ // This invariant is maintained by _validate_markers.
+ dialog->select_start->select(0);
+ dialog->select_end->select(0);
+
+ dialog->popup_centered(Size2(200, 0) * EDSCALE);
+}
+
+void AnimationNodeAnimationEditor::_validate_markers(int p_id) {
+ // Note: p_id is ignored. It is included because OptionButton's item_changed signal always passes it.
+ int start_id = dialog->select_start->get_selected_id();
+ int end_id = dialog->select_end->get_selected_id();
+
+ StringName anim_name = animation_node_animation->get_animation();
+ Ref<Animation> animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(anim_name);
+ ERR_FAIL_COND(animation.is_null());
+
+ double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id));
+ double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id));
+
+ // p_start and p_end have the same item count.
+ for (int i = 2; i < dialog->select_start->get_item_count(); i++) {
+ String start_marker = dialog->select_start->get_item_text(i);
+ String end_marker = dialog->select_end->get_item_text(i);
+ dialog->select_start->set_item_disabled(i, end_id >= 2 && (i == end_id || animation->get_marker_time(start_marker) > end_time));
+ dialog->select_end->set_item_disabled(i, start_id >= 2 && (i == start_id || start_time > animation->get_marker_time(end_marker)));
+ }
+}
+
+void AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog() {
+ int start_id = dialog->select_start->get_selected_id();
+ int end_id = dialog->select_end->get_selected_id();
+
+ Ref<Animation> animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(animation_node_animation->get_animation());
+ ERR_FAIL_COND(animation.is_null());
+ double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id));
+ double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id));
+ double length = end_time - start_time;
+
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Set Custom Timeline from Marker"));
+ undo_redo->add_do_method(*animation_node_animation, "set_start_offset", start_time);
+ undo_redo->add_undo_method(*animation_node_animation, "set_start_offset", animation_node_animation->get_start_offset());
+ undo_redo->add_do_method(*animation_node_animation, "set_stretch_time_scale", false);
+ undo_redo->add_undo_method(*animation_node_animation, "set_stretch_time_scale", animation_node_animation->is_stretching_time_scale());
+ undo_redo->add_do_method(*animation_node_animation, "set_timeline_length", length);
+ undo_redo->add_undo_method(*animation_node_animation, "set_timeline_length", animation_node_animation->get_timeline_length());
+ undo_redo->add_do_method(*animation_node_animation, "notify_property_list_changed");
+ undo_redo->add_undo_method(*animation_node_animation, "notify_property_list_changed");
+ undo_redo->commit_action();
+}
+
+AnimationNodeAnimationEditor::AnimationNodeAnimationEditor(Ref<AnimationNodeAnimation> p_animation_node_animation) {
+ animation_node_animation = p_animation_node_animation;
+
+ dialog = memnew(AnimationNodeAnimationEditorDialog);
+ add_child(dialog);
+ dialog->set_hide_on_ok(false);
+ dialog->select_start->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers));
+ dialog->select_end->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers));
+ dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog));
+
+ Control *top_spacer = memnew(Control);
+ add_child(top_spacer);
+ top_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE);
+
+ button = memnew(Button);
+ add_child(button);
+ button->set_text(TTR("Set Custom Timeline from Marker"));
+ button->set_h_size_flags(Control::SIZE_SHRINK_CENTER);
+ button->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog));
+
+ Control *bottom_spacer = memnew(Control);
+ add_child(bottom_spacer);
+ bottom_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE);
+}
+
+AnimationNodeAnimationEditor::~AnimationNodeAnimationEditor() {
+}
+
+void AnimationNodeAnimationEditor::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ button->set_theme_type_variation(SNAME("InspectorActionButton"));
+ button->set_icon(get_editor_theme_icon(SNAME("Edit")));
+ } break;
+ }
+}
+
+bool EditorInspectorPluginAnimationNodeAnimation::can_handle(Object *p_object) {
+ Ref<AnimationNodeAnimation> ana(Object::cast_to<AnimationNodeAnimation>(p_object));
+ return ana.is_valid() && ana->is_using_custom_timeline();
+}
+
+bool EditorInspectorPluginAnimationNodeAnimation::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
+ Ref<AnimationNodeAnimation> ana(Object::cast_to<AnimationNodeAnimation>(p_object));
+ ERR_FAIL_COND_V(ana.is_null(), false);
+
+ if (p_path == "timeline_length") {
+ add_custom_control(memnew(AnimationNodeAnimationEditor(ana)));
+ }
+
+ return false;
+}
+
+AnimationNodeAnimationEditorDialog::AnimationNodeAnimationEditorDialog() {
+ set_title(TTR("Select Markers..."));
+ VBoxContainer *vbox = memnew(VBoxContainer);
+ add_child(vbox);
+ vbox->set_offsets_preset(Control::PRESET_FULL_RECT);
+
+ HBoxContainer *container_start = memnew(HBoxContainer);
+ vbox->add_child(container_start);
+ Label *label_start = memnew(Label);
+ container_start->add_child(label_start);
+ label_start->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ label_start->set_stretch_ratio(1);
+ label_start->set_text(TTR("Start Marker"));
+ select_start = memnew(OptionButton);
+ container_start->add_child(select_start);
+ select_start->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ select_start->set_stretch_ratio(2);
+
+ HBoxContainer *container_end = memnew(HBoxContainer);
+ vbox->add_child(container_end);
+ Label *label_end = memnew(Label);
+ container_end->add_child(label_end);
+ label_end->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ label_end->set_stretch_ratio(1);
+ label_end->set_text(TTR("End Marker"));
+ select_end = memnew(OptionButton);
+ container_end->add_child(select_end);
+ select_end->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ select_end->set_stretch_ratio(2);
+}
+
+AnimationNodeAnimationEditorDialog::~AnimationNodeAnimationEditorDialog() {
}
diff --git a/editor/plugins/animation_blend_tree_editor_plugin.h b/editor/plugins/animation_blend_tree_editor_plugin.h
index ee6f087e07..9e7793977b 100644
--- a/editor/plugins/animation_blend_tree_editor_plugin.h
+++ b/editor/plugins/animation_blend_tree_editor_plugin.h
@@ -32,9 +32,11 @@
#define ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H
#include "core/object/script_language.h"
+#include "editor/editor_inspector.h"
#include "editor/plugins/animation_tree_editor_plugin.h"
#include "scene/animation/animation_blend_tree.h"
#include "scene/gui/button.h"
+#include "scene/gui/dialogs.h"
#include "scene/gui/graph_edit.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/popup.h"
@@ -47,6 +49,7 @@ class EditorFileDialog;
class EditorProperty;
class MenuButton;
class PanelContainer;
+class EditorInspectorPluginAnimationNodeAnimation;
class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin {
GDCLASS(AnimationNodeBlendTreeEditor, AnimationTreeNodeEditorPlugin);
@@ -147,6 +150,8 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin {
MENU_LOAD_FILE_CONFIRM = 1002
};
+ Ref<EditorInspectorPluginAnimationNodeAnimation> animation_node_inspector_plugin;
+
protected:
void _notification(int p_what);
static void _bind_methods();
@@ -165,6 +170,48 @@ public:
void update_graph();
AnimationNodeBlendTreeEditor();
+ ~AnimationNodeBlendTreeEditor();
+};
+
+// EditorPluginAnimationNodeAnimation
+
+class EditorInspectorPluginAnimationNodeAnimation : public EditorInspectorPlugin {
+ GDCLASS(EditorInspectorPluginAnimationNodeAnimation, EditorInspectorPlugin);
+
+public:
+ virtual bool can_handle(Object *p_object) override;
+ virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) override;
+};
+
+class AnimationNodeAnimationEditorDialog : public ConfirmationDialog {
+ GDCLASS(AnimationNodeAnimationEditorDialog, ConfirmationDialog);
+
+ friend class AnimationNodeAnimationEditor;
+
+ OptionButton *select_start = nullptr;
+ OptionButton *select_end = nullptr;
+
+public:
+ AnimationNodeAnimationEditorDialog();
+ ~AnimationNodeAnimationEditorDialog();
+};
+
+class AnimationNodeAnimationEditor : public VBoxContainer {
+ GDCLASS(AnimationNodeAnimationEditor, VBoxContainer);
+
+ Ref<AnimationNodeAnimation> animation_node_animation;
+ Button *button = nullptr;
+ AnimationNodeAnimationEditorDialog *dialog = nullptr;
+ void _open_set_custom_timeline_from_marker_dialog();
+ void _validate_markers(int p_id);
+ void _confirm_set_custom_timeline_from_marker_dialog();
+
+public:
+ AnimationNodeAnimationEditor(Ref<AnimationNodeAnimation> p_animation_node_animation);
+ ~AnimationNodeAnimationEditor();
+
+protected:
+ void _notification(int p_what);
};
#endif // ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H
diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp
index 5cb558abbe..e6afc85e9e 100644
--- a/editor/plugins/animation_player_editor_plugin.cpp
+++ b/editor/plugins/animation_player_editor_plugin.cpp
@@ -41,6 +41,7 @@
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_bottom_panel.h"
#include "editor/gui/editor_file_dialog.h"
+#include "editor/gui/editor_validation_panel.h"
#include "editor/inspector_dock.h"
#include "editor/plugins/canvas_item_editor_plugin.h" // For onion skinning.
#include "editor/plugins/node_3d_editor_plugin.h" // For onion skinning.
@@ -295,7 +296,14 @@ void AnimationPlayerEditor::_play_pressed() {
player->stop(); //so it won't blend with itself
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
- player->play(current);
+ PackedStringArray markers = track_editor->get_selected_section();
+ if (markers.size() == 2) {
+ StringName start_marker = markers[0];
+ StringName end_marker = markers[1];
+ player->play_section_with_markers(current, start_marker, end_marker);
+ } else {
+ player->play(current);
+ }
}
//unstop
@@ -312,7 +320,14 @@ void AnimationPlayerEditor::_play_from_pressed() {
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
player->seek_internal(time, true, true, true);
- player->play(current);
+ PackedStringArray markers = track_editor->get_selected_section();
+ if (markers.size() == 2) {
+ StringName start_marker = markers[0];
+ StringName end_marker = markers[1];
+ player->play_section_with_markers(current, start_marker, end_marker);
+ } else {
+ player->play(current);
+ }
}
//unstop
@@ -333,7 +348,14 @@ void AnimationPlayerEditor::_play_bw_pressed() {
player->stop(); //so it won't blend with itself
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
- player->play_backwards(current);
+ PackedStringArray markers = track_editor->get_selected_section();
+ if (markers.size() == 2) {
+ StringName start_marker = markers[0];
+ StringName end_marker = markers[1];
+ player->play_section_with_markers_backwards(current, start_marker, end_marker);
+ } else {
+ player->play_backwards(current);
+ }
}
//unstop
@@ -350,7 +372,14 @@ void AnimationPlayerEditor::_play_bw_from_pressed() {
}
ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing.");
player->seek_internal(time, true, true, true);
- player->play_backwards(current);
+ PackedStringArray markers = track_editor->get_selected_section();
+ if (markers.size() == 2) {
+ StringName start_marker = markers[0];
+ StringName end_marker = markers[1];
+ player->play_section_with_markers_backwards(current, start_marker, end_marker);
+ } else {
+ player->play_backwards(current);
+ }
}
//unstop
@@ -2397,3 +2426,24 @@ AnimationTrackKeyEditEditorPlugin::AnimationTrackKeyEditEditorPlugin() {
bool AnimationTrackKeyEditEditorPlugin::handles(Object *p_object) const {
return p_object->is_class("AnimationTrackKeyEdit");
}
+
+bool EditorInspectorPluginAnimationMarkerKeyEdit::can_handle(Object *p_object) {
+ return Object::cast_to<AnimationMarkerKeyEdit>(p_object) != nullptr;
+}
+
+void EditorInspectorPluginAnimationMarkerKeyEdit::parse_begin(Object *p_object) {
+ AnimationMarkerKeyEdit *amk = Object::cast_to<AnimationMarkerKeyEdit>(p_object);
+ ERR_FAIL_NULL(amk);
+
+ amk_editor = memnew(AnimationMarkerKeyEditEditor(amk->animation, amk->marker_name, amk->use_fps));
+ add_custom_control(amk_editor);
+}
+
+AnimationMarkerKeyEditEditorPlugin::AnimationMarkerKeyEditEditorPlugin() {
+ amk_plugin = memnew(EditorInspectorPluginAnimationMarkerKeyEdit);
+ EditorInspector::add_inspector_plugin(amk_plugin);
+}
+
+bool AnimationMarkerKeyEditEditorPlugin::handles(Object *p_object) const {
+ return p_object->is_class("AnimationMarkerKeyEdit");
+}
diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h
index e4ca6c17c3..349ed7b5cd 100644
--- a/editor/plugins/animation_player_editor_plugin.h
+++ b/editor/plugins/animation_player_editor_plugin.h
@@ -338,4 +338,30 @@ public:
AnimationTrackKeyEditEditorPlugin();
};
+// AnimationMarkerKeyEditEditorPlugin
+
+class EditorInspectorPluginAnimationMarkerKeyEdit : public EditorInspectorPlugin {
+ GDCLASS(EditorInspectorPluginAnimationMarkerKeyEdit, EditorInspectorPlugin);
+
+ AnimationMarkerKeyEditEditor *amk_editor = nullptr;
+
+public:
+ virtual bool can_handle(Object *p_object) override;
+ virtual void parse_begin(Object *p_object) override;
+};
+
+class AnimationMarkerKeyEditEditorPlugin : public EditorPlugin {
+ GDCLASS(AnimationMarkerKeyEditEditorPlugin, EditorPlugin);
+
+ EditorInspectorPluginAnimationMarkerKeyEdit *amk_plugin = nullptr;
+
+public:
+ bool has_main_screen() const override { return false; }
+ virtual bool handles(Object *p_object) const override;
+
+ virtual String get_name() const override { return "AnimationMarkerKeyEdit"; }
+
+ AnimationMarkerKeyEditEditorPlugin();
+};
+
#endif // ANIMATION_PLAYER_EDITOR_PLUGIN_H
diff --git a/editor/plugins/asset_library_editor_plugin.cpp b/editor/plugins/asset_library_editor_plugin.cpp
index af4b3a1643..fec8d4b897 100644
--- a/editor/plugins/asset_library_editor_plugin.cpp
+++ b/editor/plugins/asset_library_editor_plugin.cpp
@@ -993,7 +993,8 @@ void EditorAssetLibrary::_request_image(ObjectID p_for, int p_asset_id, String p
String url_host;
int url_port;
String url_path;
- Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path);
+ String url_fragment;
+ Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path, url_fragment);
if (err != OK) {
if (is_print_verbose_enabled()) {
ERR_PRINT(vformat("Asset Library: Invalid image URL '%s' for asset # %d.", trimmed_url, p_asset_id));
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index 8411c0edea..30878a2488 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -38,7 +38,6 @@
#include "core/io/stream_peer_tls.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
-#include "core/os/time.h"
#include "core/version.h"
#include "editor/editor_about.h"
#include "editor/editor_settings.h"
@@ -46,6 +45,7 @@
#include "editor/engine_update_label.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/gui/editor_title_bar.h"
+#include "editor/gui/editor_version_button.h"
#include "editor/plugins/asset_library_editor_plugin.h"
#include "editor/project_manager/project_dialog.h"
#include "editor/project_manager/project_list.h"
@@ -398,12 +398,6 @@ void ProjectManager::_restart_confirmed() {
get_tree()->quit();
}
-// Footer.
-
-void ProjectManager::_version_button_pressed() {
- DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
-}
-
// Project list.
void ProjectManager::_update_list_placeholder() {
@@ -1459,23 +1453,9 @@ ProjectManager::ProjectManager() {
update_label->connect("offline_clicked", callable_mp(this, &ProjectManager::_show_quick_settings));
#endif
- version_btn = memnew(LinkButton);
- String hash = String(VERSION_HASH);
- if (hash.length() != 0) {
- hash = " " + vformat("[%s]", hash.left(9));
- }
- version_btn->set_text("v" VERSION_FULL_BUILD + hash);
+ EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_BUILD));
// Fade the version label to be less prominent, but still readable.
version_btn->set_self_modulate(Color(1, 1, 1, 0.6));
- version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
- String build_date;
- if (VERSION_TIMESTAMP > 0) {
- build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC";
- } else {
- build_date = TTR("(unknown)");
- }
- version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date));
- version_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_version_button_pressed));
footer_bar->add_child(version_btn);
}
diff --git a/editor/project_manager.h b/editor/project_manager.h
index aad51d0e98..07da0059c0 100644
--- a/editor/project_manager.h
+++ b/editor/project_manager.h
@@ -41,7 +41,6 @@ class EditorFileDialog;
class EditorTitleBar;
class HFlowContainer;
class LineEdit;
-class LinkButton;
class MarginContainer;
class OptionButton;
class PanelContainer;
@@ -124,12 +123,6 @@ class ProjectManager : public Control {
void _show_quick_settings();
void _restart_confirmed();
- // Footer.
-
- LinkButton *version_btn = nullptr;
-
- void _version_button_pressed();
-
// Project list.
VBoxContainer *empty_list_placeholder = nullptr;
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index 7187da851e..82fb0865d2 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -88,6 +88,8 @@ void SceneTreeDock::_inspect_hovered_node() {
tree_item_inspected = item;
tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
}
+ EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
+ editor_history->add_object(node_hovered_now->get_instance_id());
InspectorDock::get_inspector_singleton()->edit(node_hovered_now);
InspectorDock::get_inspector_singleton()->propagate_notification(NOTIFICATION_DRAG_BEGIN); // Enable inspector drag preview after it updated.
InspectorDock::get_singleton()->update(node_hovered_now);
@@ -133,14 +135,6 @@ void SceneTreeDock::input(const Ref<InputEvent> &p_event) {
_push_item(pending_click_select);
pending_click_select = nullptr;
}
-
- if (mb->is_released()) {
- if (tree_item_inspected) {
- tree_item_inspected->clear_custom_color(0);
- tree_item_inspected = nullptr;
- }
- _reset_hovering_timer();
- }
}
if (tree_clicked && get_viewport()->gui_is_dragging()) {
@@ -436,6 +430,22 @@ void SceneTreeDock::_replace_with_branch_scene(const String &p_file, Node *base)
instantiated_scene->set_unique_name_in_owner(base->is_unique_name_in_owner());
+ Node2D *copy_2d = Object::cast_to<Node2D>(instantiated_scene);
+ Node2D *base_2d = Object::cast_to<Node2D>(base);
+ if (copy_2d && base_2d) {
+ copy_2d->set_position(base_2d->get_position());
+ copy_2d->set_rotation(base_2d->get_rotation());
+ copy_2d->set_scale(base_2d->get_scale());
+ }
+
+ Node3D *copy_3d = Object::cast_to<Node3D>(instantiated_scene);
+ Node3D *base_3d = Object::cast_to<Node3D>(base);
+ if (copy_3d && base_3d) {
+ copy_3d->set_position(base_3d->get_position());
+ copy_3d->set_rotation(base_3d->get_rotation());
+ copy_3d->set_scale(base_3d->get_scale());
+ }
+
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Replace with Branch Scene"));
@@ -1702,13 +1712,30 @@ void SceneTreeDock::_notification(int p_what) {
case NOTIFICATION_DRAG_END: {
_reset_hovering_timer();
- if (select_node_hovered_at_end_of_drag && !hovered_but_reparenting) {
- Node *node_inspected = Object::cast_to<Node>(InspectorDock::get_inspector_singleton()->get_edited_object());
- if (node_inspected) {
+ if (tree_item_inspected) {
+ tree_item_inspected->clear_custom_color(0);
+ tree_item_inspected = nullptr;
+ } else {
+ return;
+ }
+ if (!hovered_but_reparenting) {
+ InspectorDock *inspector_dock = InspectorDock::get_singleton();
+ if (!inspector_dock->get_rect().has_point(inspector_dock->get_local_mouse_position())) {
+ List<Node *> full_selection = editor_selection->get_full_selected_node_list();
editor_selection->clear();
- editor_selection->add_node(node_inspected);
- scene_tree->set_selected(node_inspected);
- select_node_hovered_at_end_of_drag = false;
+ for (Node *E : full_selection) {
+ editor_selection->add_node(E);
+ }
+ return;
+ }
+ if (select_node_hovered_at_end_of_drag) {
+ Node *node_inspected = Object::cast_to<Node>(InspectorDock::get_inspector_singleton()->get_edited_object());
+ if (node_inspected) {
+ editor_selection->clear();
+ editor_selection->add_node(node_inspected);
+ scene_tree->set_selected(node_inspected);
+ select_node_hovered_at_end_of_drag = false;
+ }
}
}
hovered_but_reparenting = false;
@@ -3259,6 +3286,36 @@ void SceneTreeDock::_new_scene_from(const String &p_file) {
// Root node cannot ever be unique name in its own Scene!
copy->set_unique_name_in_owner(false);
+ const Dictionary dict = new_scene_from_dialog->get_selected_options();
+ bool reset_position = dict.get(TTR("Reset Position"), true);
+ bool reset_scale = dict.get(TTR("Reset Scale"), false);
+ bool reset_rotation = dict.get(TTR("Reset Rotation"), false);
+
+ Node2D *copy_2d = Object::cast_to<Node2D>(copy);
+ if (copy_2d != nullptr) {
+ if (reset_position) {
+ copy_2d->set_position(Vector2(0, 0));
+ }
+ if (reset_rotation) {
+ copy_2d->set_rotation(0);
+ }
+ if (reset_scale) {
+ copy_2d->set_scale(Size2(1, 1));
+ }
+ }
+ Node3D *copy_3d = Object::cast_to<Node3D>(copy);
+ if (copy_3d != nullptr) {
+ if (reset_position) {
+ copy_3d->set_position(Vector3(0, 0, 0));
+ }
+ if (reset_rotation) {
+ copy_3d->set_rotation(Vector3(0, 0, 0));
+ }
+ if (reset_scale) {
+ copy_3d->set_scale(Vector3(0, 0, 0));
+ }
+ }
+
Ref<PackedScene> sdata = memnew(PackedScene);
Error err = sdata->pack(copy);
memdelete(copy);
@@ -4598,7 +4655,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec
scene_tree->connect("files_dropped", callable_mp(this, &SceneTreeDock::_files_dropped));
scene_tree->connect("script_dropped", callable_mp(this, &SceneTreeDock::_script_dropped));
scene_tree->connect("nodes_dragged", callable_mp(this, &SceneTreeDock::_nodes_drag_begin));
- scene_tree->connect(SceneStringName(mouse_exited), callable_mp(this, &SceneTreeDock::_reset_hovering_timer));
scene_tree->get_scene_tree()->connect(SceneStringName(gui_input), callable_mp(this, &SceneTreeDock::_scene_tree_gui_input));
scene_tree->get_scene_tree()->connect("item_icon_double_clicked", callable_mp(this, &SceneTreeDock::_focus_node));
@@ -4668,6 +4724,9 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec
new_scene_from_dialog = memnew(EditorFileDialog);
new_scene_from_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE);
+ new_scene_from_dialog->add_option(TTR("Reset Position"), Vector<String>(), true);
+ new_scene_from_dialog->add_option(TTR("Reset Rotation"), Vector<String>(), false);
+ new_scene_from_dialog->add_option(TTR("Reset Scale"), Vector<String>(), false);
add_child(new_scene_from_dialog);
new_scene_from_dialog->connect("file_selected", callable_mp(this, &SceneTreeDock::_new_scene_from));
diff --git a/main/main.cpp b/main/main.cpp
index 9014bfad29..36912c4fa3 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -2755,7 +2755,6 @@ Error Main::setup2(bool p_show_boot_logo) {
bool prefer_wayland_found = false;
bool prefer_wayland = false;
- bool remember_window_size_and_position_found = false;
if (editor) {
screen_property = "interface/editor/editor_screen";
@@ -2771,7 +2770,7 @@ Error Main::setup2(bool p_show_boot_logo) {
prefer_wayland_found = true;
}
- while (!screen_found || !prefer_wayland_found || !remember_window_size_and_position_found) {
+ while (!screen_found || !prefer_wayland_found) {
assign = Variant();
next_tag.fields.clear();
next_tag.name = String();
@@ -2785,17 +2784,16 @@ Error Main::setup2(bool p_show_boot_logo) {
if (!screen_found && assign == screen_property) {
init_screen = value;
screen_found = true;
+
+ if (editor) {
+ restore_editor_window_layout = value.operator int() == EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO;
+ }
}
if (!prefer_wayland_found && assign == "run/platforms/linuxbsd/prefer_wayland") {
prefer_wayland = value;
prefer_wayland_found = true;
}
-
- if (!remember_window_size_and_position_found && assign == "interface/editor/remember_window_size_and_position") {
- restore_editor_window_layout = value;
- remember_window_size_and_position_found = true;
- }
}
}
@@ -4095,8 +4093,7 @@ int Main::start() {
if (editor_embed_subwindows) {
sml->get_root()->set_embedding_subwindows(true);
}
- restore_editor_window_layout = EditorSettings::get_singleton()->get_setting(
- "interface/editor/remember_window_size_and_position");
+ restore_editor_window_layout = EditorSettings::get_singleton()->get_setting("interface/editor/editor_screen").operator int() == EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO;
}
#endif
diff --git a/modules/multiplayer/SCsub b/modules/multiplayer/SCsub
index 97f91c5674..f9f4e579e8 100644
--- a/modules/multiplayer/SCsub
+++ b/modules/multiplayer/SCsub
@@ -13,3 +13,10 @@ if env.editor_build:
env_mp.add_source_files(module_obj, "editor/*.cpp")
env.modules_sources += module_obj
+
+if env["tests"]:
+ env_mp.Append(CPPDEFINES=["TESTS_ENABLED"])
+ env_mp.add_source_files(env.modules_sources, "./tests/*.cpp")
+
+ if env["disable_exceptions"]:
+ env_mp.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"])
diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
index 42f32d4848..3277f1ff3e 100644
--- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml
+++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
@@ -65,7 +65,7 @@
[b]Warning:[/b] Deserialized objects can contain code which gets executed. Do not use this option if the serialized object comes from untrusted sources to avoid potential security threat such as remote code execution.
</member>
<member name="auth_callback" type="Callable" setter="set_auth_callback" getter="get_auth_callback" default="Callable()">
- The callback to execute when when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect.
+ The callback to execute when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect.
</member>
<member name="auth_timeout" type="float" setter="set_auth_timeout" getter="get_auth_timeout" default="3.0">
If set to a value greater than [code]0.0[/code], the maximum duration in seconds peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals.
diff --git a/modules/multiplayer/tests/test_scene_multiplayer.h b/modules/multiplayer/tests/test_scene_multiplayer.h
new file mode 100644
index 0000000000..5e526c9be6
--- /dev/null
+++ b/modules/multiplayer/tests/test_scene_multiplayer.h
@@ -0,0 +1,284 @@
+/**************************************************************************/
+/* test_scene_multiplayer.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_SCENE_MULTIPLAYER_H
+#define TEST_SCENE_MULTIPLAYER_H
+
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+#include "../scene_multiplayer.h"
+
+namespace TestSceneMultiplayer {
+
+static inline Array build_array() {
+ return Array();
+}
+template <typename... Targs>
+static inline Array build_array(Variant item, Targs... Fargs) {
+ Array a = build_array(Fargs...);
+ a.push_front(item);
+ return a;
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer] Defaults") {
+ Ref<SceneMultiplayer> scene_multiplayer;
+ scene_multiplayer.instantiate();
+
+ REQUIRE(scene_multiplayer->has_multiplayer_peer());
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ REQUIRE_MESSAGE(Object::cast_to<OfflineMultiplayerPeer>(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance.");
+ CHECK_EQ(scene_multiplayer->poll(), Error::OK);
+ CHECK_EQ(scene_multiplayer->get_unique_id(), MultiplayerPeer::TARGET_PEER_SERVER);
+ CHECK_EQ(scene_multiplayer->get_peer_ids(), Vector<int>());
+ CHECK_EQ(scene_multiplayer->get_remote_sender_id(), 0);
+ CHECK_EQ(scene_multiplayer->get_root_path(), NodePath());
+ CHECK(scene_multiplayer->get_connected_peers().is_empty());
+ CHECK_FALSE(scene_multiplayer->is_refusing_new_connections());
+ CHECK_FALSE(scene_multiplayer->is_object_decoding_allowed());
+ CHECK(scene_multiplayer->is_server_relay_enabled());
+ CHECK_EQ(scene_multiplayer->get_max_sync_packet_size(), 1350);
+ CHECK_EQ(scene_multiplayer->get_max_delta_packet_size(), 65535);
+ CHECK(scene_multiplayer->is_server());
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] SceneTree has a OfflineMultiplayerPeer by default") {
+ Ref<SceneMultiplayer> scene_multiplayer = SceneTree::get_singleton()->get_multiplayer();
+ REQUIRE(scene_multiplayer->has_multiplayer_peer());
+
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ REQUIRE_MESSAGE(Object::cast_to<OfflineMultiplayerPeer>(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance.");
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Object configuration add/remove") {
+ Ref<SceneMultiplayer> scene_multiplayer;
+ scene_multiplayer.instantiate();
+
+ SUBCASE("Returns invalid parameter") {
+ CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER);
+ CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER);
+
+ NodePath foo_path("/Foo");
+ NodePath bar_path("/Bar");
+ CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK);
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, bar_path), Error::ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("Sets root path") {
+ NodePath foo_path("/Foo");
+ CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK);
+
+ CHECK_EQ(scene_multiplayer->get_root_path(), foo_path);
+ }
+
+ SUBCASE("Unsets root path") {
+ NodePath foo_path("/Foo");
+ CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK);
+
+ CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, foo_path), Error::OK);
+ CHECK_EQ(scene_multiplayer->get_root_path(), NodePath());
+ }
+
+ SUBCASE("Add/Remove a MultiplayerSpawner") {
+ Node2D *node = memnew(Node2D);
+ MultiplayerSpawner *spawner = memnew(MultiplayerSpawner);
+
+ CHECK_EQ(scene_multiplayer->object_configuration_add(node, spawner), Error::OK);
+ CHECK_EQ(scene_multiplayer->object_configuration_remove(node, spawner), Error::OK);
+
+ memdelete(spawner);
+ memdelete(node);
+ }
+
+ SUBCASE("Add/Remove a MultiplayerSynchronizer") {
+ Node2D *node = memnew(Node2D);
+ MultiplayerSynchronizer *synchronizer = memnew(MultiplayerSynchronizer);
+
+ CHECK_EQ(scene_multiplayer->object_configuration_add(node, synchronizer), Error::OK);
+ CHECK_EQ(scene_multiplayer->object_configuration_remove(node, synchronizer), Error::OK);
+
+ memdelete(synchronizer);
+ memdelete(node);
+ }
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer] Root Path") {
+ Ref<SceneMultiplayer> scene_multiplayer;
+ scene_multiplayer.instantiate();
+
+ SUBCASE("Is set") {
+ NodePath foo_path("/Foo");
+ scene_multiplayer->set_root_path(foo_path);
+
+ CHECK_EQ(scene_multiplayer->get_root_path(), foo_path);
+ }
+
+ SUBCASE("Fails when path is empty") {
+ ERR_PRINT_OFF;
+ scene_multiplayer->set_root_path(NodePath());
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("Fails when path is relative") {
+ NodePath foo_path("Foo");
+ ERR_PRINT_OFF;
+ scene_multiplayer->set_root_path(foo_path);
+ ERR_PRINT_ON;
+
+ CHECK_EQ(scene_multiplayer->get_root_path(), NodePath());
+ }
+}
+
+// This one could be a dummy callback because the current set of test is not actually testing the full auth flow.
+static Variant auth_callback(Variant sv, Variant pvav) {
+ return Variant();
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Send Authentication") {
+ Ref<SceneMultiplayer> scene_multiplayer;
+ scene_multiplayer.instantiate();
+ SceneTree::get_singleton()->set_multiplayer(scene_multiplayer);
+ scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback));
+
+ SUBCASE("Is properly sent") {
+ SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authenticating");
+
+ // Adding a peer to MultiplayerPeer.
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ int peer_id = 42;
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id);
+ SIGNAL_CHECK("peer_authenticating", build_array(build_array(peer_id)));
+
+ CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK);
+
+ Vector<int> expected_peer_ids = { peer_id };
+ CHECK_EQ(scene_multiplayer->get_authenticating_peer_ids(), expected_peer_ids);
+
+ SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authenticating");
+ }
+
+ SUBCASE("peer_authentication_failed is emitted when a peer is deleted before authentication is completed") {
+ SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed");
+
+ // Adding a peer to MultiplayerPeer.
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ int peer_id = 42;
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id);
+ multiplayer_peer->emit_signal(SNAME("peer_disconnected"), peer_id);
+ SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(peer_id)));
+
+ SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed");
+ }
+
+ SUBCASE("peer_authentication_failed is emitted when authentication timeout") {
+ SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed");
+ scene_multiplayer->set_auth_timeout(0.01);
+ CHECK_EQ(scene_multiplayer->get_auth_timeout(), 0.01);
+
+ // Adding two peesr to MultiplayerPeer.
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ int first_peer_id = 42;
+ int second_peer_id = 84;
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), first_peer_id);
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), second_peer_id);
+
+ // Let timeout happens.
+ OS::get_singleton()->delay_usec(500000);
+
+ CHECK_EQ(scene_multiplayer->poll(), Error::OK);
+
+ SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(first_peer_id), build_array(second_peer_id)));
+
+ SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed");
+ }
+
+ SUBCASE("Fails when there is no MultiplayerPeer configured") {
+ scene_multiplayer->set_multiplayer_peer(nullptr);
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->send_auth(42, Vector<uint8_t>()), Error::ERR_UNCONFIGURED);
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("Fails when the peer to send the auth is not pending") {
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->send_auth(42, String("It's me").to_ascii_buffer()), Error::ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+ }
+}
+
+TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Complete Authentication") {
+ Ref<SceneMultiplayer> scene_multiplayer;
+ scene_multiplayer.instantiate();
+ SceneTree::get_singleton()->set_multiplayer(scene_multiplayer);
+ scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback));
+
+ SUBCASE("Is properly completed") {
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ int peer_id = 42;
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id);
+ CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK);
+
+ CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK);
+ }
+
+ SUBCASE("Fails when there is no MultiplayerPeer configured") {
+ scene_multiplayer->set_multiplayer_peer(nullptr);
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_UNCONFIGURED);
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("Fails when the peer to complete the auth is not pending") {
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_INVALID_PARAMETER);
+ ERR_PRINT_ON;
+ }
+
+ SUBCASE("Fails to send auth or completed for a second time") {
+ Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer();
+ int peer_id = 42;
+ multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id);
+ CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK);
+ CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK);
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::ERR_FILE_CANT_WRITE);
+ CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::ERR_FILE_CANT_WRITE);
+ ERR_PRINT_ON;
+ }
+}
+
+} // namespace TestSceneMultiplayer
+
+#endif // TEST_SCENE_MULTIPLAYER_H
diff --git a/modules/websocket/editor/editor_debugger_server_websocket.cpp b/modules/websocket/editor/editor_debugger_server_websocket.cpp
index a28fc53440..344a0356c5 100644
--- a/modules/websocket/editor/editor_debugger_server_websocket.cpp
+++ b/modules/websocket/editor/editor_debugger_server_websocket.cpp
@@ -77,8 +77,8 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) {
// Optionally override
if (!p_uri.is_empty() && p_uri != "ws://") {
- String scheme, path;
- Error err = p_uri.parse_url(scheme, bind_host, bind_port, path);
+ String scheme, path, fragment;
+ Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment);
ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER);
}
diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp
index 03a530909b..c5768c9f0b 100644
--- a/modules/websocket/emws_peer.cpp
+++ b/modules/websocket/emws_peer.cpp
@@ -68,8 +68,9 @@ Error EMWSPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_tls_option
String host;
String path;
String scheme;
+ String fragment;
int port = 0;
- Error err = p_url.parse_url(scheme, host, port, path);
+ Error err = p_url.parse_url(scheme, host, port, path, fragment);
ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url);
if (scheme.is_empty()) {
diff --git a/modules/websocket/wsl_peer.cpp b/modules/websocket/wsl_peer.cpp
index 0a9a4053e3..0c0a046805 100644
--- a/modules/websocket/wsl_peer.cpp
+++ b/modules/websocket/wsl_peer.cpp
@@ -482,8 +482,9 @@ Error WSLPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_options) {
String host;
String path;
String scheme;
+ String fragment;
int port = 0;
- Error err = p_url.parse_url(scheme, host, port, path);
+ Error err = p_url.parse_url(scheme, host, port, path, fragment);
ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url);
if (scheme.is_empty()) {
scheme = "ws://";
diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml
index bd7192520a..829279e7bb 100644
--- a/modules/webxr/doc_classes/WebXRInterface.xml
+++ b/modules/webxr/doc_classes/WebXRInterface.xml
@@ -283,7 +283,7 @@
</signals>
<constants>
<constant name="TARGET_RAY_MODE_UNKNOWN" value="0" enum="TargetRayMode">
- We don't know the the target ray mode.
+ We don't know the target ray mode.
</constant>
<constant name="TARGET_RAY_MODE_GAZE" value="1" enum="TargetRayMode">
Target ray originates at the viewer's eyes and points in the direction they are looking.
diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp
index 6920f801e5..078b9ab748 100644
--- a/platform/android/api/api.cpp
+++ b/platform/android/api/api.cpp
@@ -41,13 +41,11 @@ static JavaClassWrapper *java_class_wrapper = nullptr;
void register_android_api() {
#if !defined(ANDROID_ENABLED)
- // On Android platforms, the `java_class_wrapper` instantiation and the
- // `JNISingleton` registration occurs in
+ // On Android platforms, the `java_class_wrapper` instantiation occurs in
// `platform/android/java_godot_lib_jni.cpp#Java_org_godotengine_godot_GodotLib_setup`
- java_class_wrapper = memnew(JavaClassWrapper); // Dummy
- GDREGISTER_CLASS(JNISingleton);
+ java_class_wrapper = memnew(JavaClassWrapper);
#endif
-
+ GDREGISTER_CLASS(JNISingleton);
GDREGISTER_CLASS(JavaClass);
GDREGISTER_CLASS(JavaObject);
GDREGISTER_CLASS(JavaClassWrapper);
@@ -108,7 +106,7 @@ Ref<JavaClass> JavaObject::get_java_class() const {
JavaClassWrapper *JavaClassWrapper::singleton = nullptr;
-Ref<JavaClass> JavaClassWrapper::wrap(const String &) {
+Ref<JavaClass> JavaClassWrapper::_wrap(const String &, bool) {
return Ref<JavaClass>();
}
diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h
index 71f9c32318..c74cef8dd0 100644
--- a/platform/android/api/java_class_wrapper.h
+++ b/platform/android/api/java_class_wrapper.h
@@ -262,6 +262,8 @@ class JavaClassWrapper : public Object {
bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig);
#endif
+ Ref<JavaClass> _wrap(const String &p_class, bool p_allow_private_methods_access);
+
static JavaClassWrapper *singleton;
protected:
@@ -270,15 +272,14 @@ protected:
public:
static JavaClassWrapper *get_singleton() { return singleton; }
- Ref<JavaClass> wrap(const String &p_class);
+ Ref<JavaClass> wrap(const String &p_class) {
+ return _wrap(p_class, false);
+ }
#ifdef ANDROID_ENABLED
- Ref<JavaClass> wrap_jclass(jclass p_class);
-
- JavaClassWrapper(jobject p_activity = nullptr);
-#else
- JavaClassWrapper();
+ Ref<JavaClass> wrap_jclass(jclass p_class, bool p_allow_private_methods_access = false);
#endif
+ JavaClassWrapper();
};
#endif // JAVA_CLASS_WRAPPER_H
diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h
index 06afc4eb78..5e940819bc 100644
--- a/platform/android/api/jni_singleton.h
+++ b/platform/android/api/jni_singleton.h
@@ -31,193 +31,53 @@
#ifndef JNI_SINGLETON_H
#define JNI_SINGLETON_H
+#include "java_class_wrapper.h"
+
#include "core/config/engine.h"
#include "core/variant/variant.h"
-#ifdef ANDROID_ENABLED
-#include "jni_utils.h"
-#endif
-
class JNISingleton : public Object {
GDCLASS(JNISingleton, Object);
-#ifdef ANDROID_ENABLED
struct MethodData {
- jmethodID method;
Variant::Type ret_type;
Vector<Variant::Type> argtypes;
};
- jobject instance;
RBMap<StringName, MethodData> method_map;
-#endif
+ Ref<JavaObject> wrapped_object;
public:
virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override {
-#ifdef ANDROID_ENABLED
- RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
-
- // Check the method we're looking for is in the JNISingleton map and that
- // the arguments match.
- bool call_error = !E || E->get().argtypes.size() != p_argcount;
- if (!call_error) {
- for (int i = 0; i < p_argcount; i++) {
- if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
- call_error = true;
- break;
+ if (wrapped_object.is_valid()) {
+ RBMap<StringName, MethodData>::Element *E = method_map.find(p_method);
+
+ // Check the method we're looking for is in the JNISingleton map and that
+ // the arguments match.
+ bool call_error = !E || E->get().argtypes.size() != p_argcount;
+ if (!call_error) {
+ for (int i = 0; i < p_argcount; i++) {
+ if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
+ call_error = true;
+ break;
+ }
}
}
- }
-
- if (call_error) {
- // The method is not in this map, defaulting to the regular instance calls.
- return Object::callp(p_method, p_args, p_argcount, r_error);
- }
-
- ERR_FAIL_NULL_V(instance, Variant());
-
- r_error.error = Callable::CallError::CALL_OK;
-
- jvalue *v = nullptr;
- if (p_argcount) {
- v = (jvalue *)alloca(sizeof(jvalue) * p_argcount);
- }
-
- JNIEnv *env = get_jni_env();
-
- int res = env->PushLocalFrame(16);
-
- ERR_FAIL_COND_V(res != 0, Variant());
-
- List<jobject> to_erase;
- for (int i = 0; i < p_argcount; i++) {
- jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]);
- v[i] = vr.val;
- if (vr.obj) {
- to_erase.push_back(vr.obj);
+ if (!call_error) {
+ return wrapped_object->callp(p_method, p_args, p_argcount, r_error);
}
}
- Variant ret;
-
- switch (E->get().ret_type) {
- case Variant::NIL: {
- env->CallVoidMethodA(instance, E->get().method, v);
- } break;
- case Variant::BOOL: {
- ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE;
- } break;
- case Variant::INT: {
- ret = env->CallIntMethodA(instance, E->get().method, v);
- } break;
- case Variant::FLOAT: {
- ret = env->CallFloatMethodA(instance, E->get().method, v);
- } break;
- case Variant::STRING: {
- jobject o = env->CallObjectMethodA(instance, E->get().method, v);
- ret = jstring_to_string((jstring)o, env);
- env->DeleteLocalRef(o);
- } break;
- case Variant::PACKED_STRING_ARRAY: {
- jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v);
-
- ret = _jobject_to_variant(env, arr);
-
- env->DeleteLocalRef(arr);
- } break;
- case Variant::PACKED_INT32_ARRAY: {
- jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v);
-
- int fCount = env->GetArrayLength(arr);
- Vector<int> sarr;
- sarr.resize(fCount);
-
- int *w = sarr.ptrw();
- env->GetIntArrayRegion(arr, 0, fCount, w);
- ret = sarr;
- env->DeleteLocalRef(arr);
- } break;
- case Variant::PACKED_INT64_ARRAY: {
- jlongArray arr = (jlongArray)env->CallObjectMethodA(instance, E->get().method, v);
-
- int fCount = env->GetArrayLength(arr);
- Vector<int64_t> sarr;
- sarr.resize(fCount);
-
- int64_t *w = sarr.ptrw();
- env->GetLongArrayRegion(arr, 0, fCount, w);
- ret = sarr;
- env->DeleteLocalRef(arr);
- } break;
- case Variant::PACKED_FLOAT32_ARRAY: {
- jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v);
-
- int fCount = env->GetArrayLength(arr);
- Vector<float> sarr;
- sarr.resize(fCount);
-
- float *w = sarr.ptrw();
- env->GetFloatArrayRegion(arr, 0, fCount, w);
- ret = sarr;
- env->DeleteLocalRef(arr);
- } break;
- case Variant::PACKED_FLOAT64_ARRAY: {
- jdoubleArray arr = (jdoubleArray)env->CallObjectMethodA(instance, E->get().method, v);
-
- int fCount = env->GetArrayLength(arr);
- Vector<double> sarr;
- sarr.resize(fCount);
-
- double *w = sarr.ptrw();
- env->GetDoubleArrayRegion(arr, 0, fCount, w);
- ret = sarr;
- env->DeleteLocalRef(arr);
- } break;
- case Variant::DICTIONARY: {
- jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
- ret = _jobject_to_variant(env, obj);
- env->DeleteLocalRef(obj);
-
- } break;
- case Variant::OBJECT: {
- jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
- ret = _jobject_to_variant(env, obj);
- env->DeleteLocalRef(obj);
- } break;
- default: {
- env->PopLocalFrame(nullptr);
- ERR_FAIL_V(Variant());
- } break;
- }
-
- while (to_erase.size()) {
- env->DeleteLocalRef(to_erase.front()->get());
- to_erase.pop_front();
- }
-
- env->PopLocalFrame(nullptr);
-
- return ret;
-#else // ANDROID_ENABLED
-
- // Defaulting to the regular instance calls.
return Object::callp(p_method, p_args, p_argcount, r_error);
-#endif
}
-#ifdef ANDROID_ENABLED
- jobject get_instance() const {
- return instance;
+ Ref<JavaObject> get_wrapped_object() const {
+ return wrapped_object;
}
- void set_instance(jobject p_instance) {
- instance = p_instance;
- }
-
- void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
+ void add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
MethodData md;
- md.method = p_method;
md.argtypes = p_args;
md.ret_type = p_ret_type;
method_map[p_name] = md;
@@ -232,24 +92,15 @@ public:
ADD_SIGNAL(mi);
}
-#endif
+ JNISingleton() {}
- JNISingleton() {
-#ifdef ANDROID_ENABLED
- instance = nullptr;
-#endif
+ JNISingleton(const Ref<JavaObject> &p_wrapped_object) {
+ wrapped_object = p_wrapped_object;
}
~JNISingleton() {
-#ifdef ANDROID_ENABLED
method_map.clear();
- if (instance) {
- JNIEnv *env = get_jni_env();
- ERR_FAIL_NULL(env);
-
- env->DeleteGlobalRef(instance);
- }
-#endif
+ wrapped_object.unref();
}
};
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index cfd258cddc..41f460ca8f 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -3263,8 +3263,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
cmdline.push_back(apk_build_command);
}
+ String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons");
+
cmdline.push_back("-p"); // argument to specify the start directory.
cmdline.push_back(build_path); // start directory.
+ cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies
cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name.
cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code.
cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name.
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index fdc5753798..308f126d5d 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -63,6 +63,12 @@ dependencies {
implementation files(pluginsBinaries)
}
+ // Automatically pick up local dependencies in res://addons
+ String addonsDirectory = getAddonsDirectory()
+ if (addonsDirectory != null && !addonsDirectory.isBlank()) {
+ implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar'])
+ }
+
// .NET dependencies
String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar'
if (file(jar).exists()) {
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index 597a4d5c14..e8921e1bb1 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -408,3 +408,8 @@ ext.shouldUseLegacyPackaging = { ->
// Default behavior for minSdk >= 23
return false
}
+
+ext.getAddonsDirectory = { ->
+ String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : ""
+ return addonsDirectory
+}
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 5b1d09e749..567b134234 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -58,6 +58,8 @@ import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.input.GodotInputHandler
import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler
+import org.godotengine.godot.plugin.AndroidRuntimePlugin
+import org.godotengine.godot.plugin.GodotPlugin
import org.godotengine.godot.plugin.GodotPluginRegistry
import org.godotengine.godot.tts.GodotTTS
import org.godotengine.godot.utils.CommandLineFileParser
@@ -228,7 +230,9 @@ class Godot(private val context: Context) {
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
Log.v(TAG, "Initializing Godot plugin registry")
- GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this))
+ val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this))
+ runtimePlugins.addAll(primaryHost.getHostPlugins(this))
+ GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins)
if (io == null) {
io = GodotIO(activity)
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt
new file mode 100644
index 0000000000..edb4e7c357
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt
@@ -0,0 +1,63 @@
+/**************************************************************************/
+/* AndroidRuntimePlugin.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.godot.plugin
+
+import org.godotengine.godot.Godot
+
+/**
+ * Provides access to the Android runtime capabilities.
+ *
+ * For example, from gdscript, developers can use [getApplicationContext] to access system services
+ * and check if the device supports vibration.
+ *
+ * var android_runtime = Engine.get_singleton("AndroidRuntime")
+ * if android_runtime:
+ * print("Checking if the device supports vibration")
+ * var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator")
+ * if vibrator_service:
+ * if vibrator_service.hasVibrator():
+ * print("Vibration is supported on device!")
+ * else:
+ * printerr("Vibration is not supported on device")
+ * else:
+ * printerr("Unable to retrieve the vibrator service")
+ * else:
+ * printerr("Couldn't find AndroidRuntime singleton")
+ */
+class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
+ override fun getPluginName() = "AndroidRuntime"
+
+ @UsedByGodot
+ fun getApplicationContext() = activity?.applicationContext
+
+ @UsedByGodot
+ override fun getActivity() = super.getActivity()
+}
diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp
index c92717e922..6bedbfd157 100644
--- a/platform/android/java_class_wrapper.cpp
+++ b/platform/android/java_class_wrapper.cpp
@@ -1120,7 +1120,7 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va
return false;
}
-Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
+Ref<JavaClass> JavaClassWrapper::_wrap(const String &p_class, bool p_allow_private_methods_access) {
String class_name_dots = p_class.replace("/", ".");
if (class_cache.has(class_name_dots)) {
return class_cache[class_name_dots];
@@ -1175,7 +1175,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
jint mods = env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers);
- if (!(mods & 0x0001)) {
+ if (!(mods & 0x0001) && (is_constructor || !p_allow_private_methods_access)) {
env->DeleteLocalRef(obj);
continue; //not public bye
}
@@ -1336,7 +1336,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) {
return java_class;
}
-Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class) {
+Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class, bool p_allow_private_methods_access) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, Ref<JavaClass>());
@@ -1344,12 +1344,12 @@ Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class) {
String class_name_string = jstring_to_string(class_name, env);
env->DeleteLocalRef(class_name);
- return wrap(class_name_string);
+ return _wrap(class_name_string, p_allow_private_methods_access);
}
JavaClassWrapper *JavaClassWrapper::singleton = nullptr;
-JavaClassWrapper::JavaClassWrapper(jobject p_activity) {
+JavaClassWrapper::JavaClassWrapper() {
singleton = this;
JNIEnv *env = get_jni_env();
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index 6086f67a1e..1a256959cd 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -32,7 +32,6 @@
#include "android_input_handler.h"
#include "api/java_class_wrapper.h"
-#include "api/jni_singleton.h"
#include "dir_access_jandroid.h"
#include "display_server_android.h"
#include "file_access_android.h"
@@ -209,8 +208,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env
TTS_Android::setup(p_godot_tts);
- java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity()));
- GDREGISTER_CLASS(JNISingleton);
+ java_class_wrapper = memnew(JavaClassWrapper);
return true;
}
diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp
index 75c8dd9528..acb18cc5c5 100644
--- a/platform/android/plugin/godot_plugin_jni.cpp
+++ b/platform/android/plugin/godot_plugin_jni.cpp
@@ -30,6 +30,7 @@
#include "godot_plugin_jni.h"
+#include "api/java_class_wrapper.h"
#include "api/jni_singleton.h"
#include "jni_utils.h"
#include "string_android.h"
@@ -57,11 +58,15 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeR
ERR_FAIL_COND_V(jni_singletons.has(singname), false);
- JNISingleton *s = (JNISingleton *)ClassDB::instantiate("JNISingleton");
- s->set_instance(env->NewGlobalRef(obj));
- jni_singletons[singname] = s;
+ jclass java_class = env->GetObjectClass(obj);
+ Ref<JavaClass> java_class_wrapped = JavaClassWrapper::get_singleton()->wrap_jclass(java_class, true);
+ env->DeleteLocalRef(java_class);
- Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s));
+ Ref<JavaObject> plugin_object = memnew(JavaObject(java_class_wrapped, obj));
+ JNISingleton *plugin_singleton = memnew(JNISingleton(plugin_object));
+ jni_singletons[singname] = plugin_singleton;
+
+ Engine::get_singleton()->add_singleton(Engine::Singleton(singname, plugin_singleton));
return true;
}
@@ -75,7 +80,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis
String mname = jstring_to_string(name, env);
String retval = jstring_to_string(ret, env);
Vector<Variant::Type> types;
- String cs = "(";
int stringCount = env->GetArrayLength(args);
@@ -83,18 +87,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis
jstring string = (jstring)env->GetObjectArrayElement(args, i);
const String rawString = jstring_to_string(string, env);
types.push_back(get_jni_type(rawString));
- cs += get_jni_sig(rawString);
- }
-
- cs += ")";
- cs += get_jni_sig(retval);
- jclass cls = env->GetObjectClass(s->get_instance());
- jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data());
- if (!mid) {
- print_line("Failed getting method ID " + mname);
}
- s->add_method(mname, mid, types, get_jni_type(retval));
+ s->add_method(mname, types, get_jni_type(retval));
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types) {
diff --git a/platform/android/rendering_context_driver_vulkan_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp
index a306a121f8..51fb1ca18f 100644
--- a/platform/android/rendering_context_driver_vulkan_android.cpp
+++ b/platform/android/rendering_context_driver_vulkan_android.cpp
@@ -32,11 +32,7 @@
#ifdef VULKAN_ENABLED
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
const char *RenderingContextDriverVulkanAndroid::_get_platform_surface_extension() const {
return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME;
diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h
index bbb758074d..0631b50f0a 100644
--- a/platform/ios/display_server_ios.h
+++ b/platform/ios/display_server_ios.h
@@ -41,11 +41,7 @@
#if defined(VULKAN_ENABLED)
#import "rendering_context_driver_vulkan_ios.h"
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
#endif // VULKAN_ENABLED
#if defined(METAL_ENABLED)
diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm
index 35b87ea647..590238be77 100644
--- a/platform/ios/os_ios.mm
+++ b/platform/ios/os_ios.mm
@@ -56,11 +56,7 @@
#import <QuartzCore/CAMetalLayer.h>
#if defined(VULKAN_ENABLED)
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
#endif // VULKAN_ENABLED
#endif
diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
index 0417ba95eb..8abcc464ba 100644
--- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
+++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp
@@ -32,11 +32,7 @@
#include "rendering_context_driver_vulkan_wayland.h"
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
const char *RenderingContextDriverVulkanWayland::_get_platform_surface_extension() const {
return VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME;
diff --git a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
index 3f505d000c..cbcf07852b 100644
--- a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
+++ b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp
@@ -32,11 +32,7 @@
#include "rendering_context_driver_vulkan_x11.h"
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
const char *RenderingContextDriverVulkanX11::_get_platform_surface_extension() const {
return VK_KHR_XLIB_SURFACE_EXTENSION_NAME;
diff --git a/platform/windows/rendering_context_driver_vulkan_windows.cpp b/platform/windows/rendering_context_driver_vulkan_windows.cpp
index 445388af89..8ca677fe64 100644
--- a/platform/windows/rendering_context_driver_vulkan_windows.cpp
+++ b/platform/windows/rendering_context_driver_vulkan_windows.cpp
@@ -34,11 +34,7 @@
#include "rendering_context_driver_vulkan_windows.h"
-#ifdef USE_VOLK
-#include <volk.h>
-#else
-#include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
const char *RenderingContextDriverVulkanWindows::_get_platform_surface_extension() const {
return VK_KHR_WIN32_SURFACE_EXTENSION_NAME;
diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp
index 9c9ba93b41..754afb0527 100644
--- a/scene/2d/cpu_particles_2d.cpp
+++ b/scene/2d/cpu_particles_2d.cpp
@@ -1288,7 +1288,7 @@ void CPUParticles2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emitting"), "set_emitting", "is_emitting");
ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount");
ADD_GROUP("Time", "");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale");
diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp
index bfbdb49f22..cfdcbee86a 100644
--- a/scene/2d/gpu_particles_2d.cpp
+++ b/scene/2d/gpu_particles_2d.cpp
@@ -821,19 +821,17 @@ void GPUParticles2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amount_ratio", PROPERTY_HINT_RANGE, "0,1,0.0001"), "set_amount_ratio", "get_amount_ratio");
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "sub_emitter", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "GPUParticles2D"), "set_sub_emitter", "get_sub_emitter");
- ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material");
- ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture");
ADD_GROUP("Time", "");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,exp,suffix:s"), "set_pre_process_time", "get_pre_process_time");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "explosiveness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_explosiveness_ratio", "get_explosiveness_ratio");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "randomness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_randomness_ratio", "get_randomness_ratio");
ADD_PROPERTY(PropertyInfo(Variant::INT, "fixed_fps", PROPERTY_HINT_RANGE, "0,1000,1,suffix:FPS"), "set_fixed_fps", "get_fixed_fps");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "interpolate"), "set_interpolate", "get_interpolate");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fract_delta"), "set_fractional_delta", "get_fractional_delta");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end");
ADD_GROUP("Collision", "collision_");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_base_size", PROPERTY_HINT_RANGE, "0,128,0.01,or_greater"), "set_collision_base_size", "get_collision_base_size");
ADD_GROUP("Drawing", "");
@@ -845,7 +843,9 @@ void GPUParticles2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "trail_lifetime", PROPERTY_HINT_RANGE, "0.01,10,0.01,or_greater,suffix:s"), "set_trail_lifetime", "get_trail_lifetime");
ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_sections", PROPERTY_HINT_RANGE, "2,128,1"), "set_trail_sections", "get_trail_sections");
ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_section_subdivisions", PROPERTY_HINT_RANGE, "1,1024,1"), "set_trail_section_subdivisions", "get_trail_section_subdivisions");
-
+ ADD_GROUP("Process Material", "");
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture");
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material");
BIND_ENUM_CONSTANT(DRAW_ORDER_INDEX);
BIND_ENUM_CONSTANT(DRAW_ORDER_LIFETIME);
BIND_ENUM_CONSTANT(DRAW_ORDER_REVERSE_LIFETIME);
diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp
index a96417738f..a2aef60417 100644
--- a/scene/animation/animation_blend_tree.cpp
+++ b/scene/animation/animation_blend_tree.cpp
@@ -245,6 +245,8 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe
if (!p_test_only) {
AnimationMixer::PlaybackInfo pi = p_playback_info;
+ pi.start = 0.0;
+ pi.end = cur_len;
if (play_mode == PLAY_MODE_FORWARD) {
pi.time = cur_playback_time;
pi.delta = cur_delta;
diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp
index 664302d45e..eb8bc8c382 100644
--- a/scene/animation/animation_mixer.cpp
+++ b/scene/animation/animation_mixer.cpp
@@ -1117,6 +1117,8 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
Ref<Animation> a = ai.animation_data.animation;
double time = ai.playback_info.time;
double delta = ai.playback_info.delta;
+ double start = ai.playback_info.start;
+ double end = ai.playback_info.end;
bool seeked = ai.playback_info.seeked;
Animation::LoopedFlag looped_flag = ai.playback_info.looped_flag;
bool is_external_seeking = ai.playback_info.is_external_seeking;
@@ -1168,32 +1170,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
if (track->root_motion && calc_root) {
double prev_time = time - delta;
if (!backward) {
- if (Animation::is_less_approx(prev_time, 0)) {
+ if (Animation::is_less_approx(prev_time, start)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = 0;
+ prev_time = start;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
}
}
} else {
- if (Animation::is_greater_approx(prev_time, (double)a_length)) {
+ if (Animation::is_greater_approx(prev_time, end)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = (double)a_length;
+ prev_time = end;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
@@ -1208,10 +1210,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx);
- a->try_position_track_interpolate(i, (double)a_length, &loc[1]);
+ a->try_position_track_interpolate(i, end, &loc[1]);
loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx);
root_motion_cache.loc += (loc[1] - loc[0]) * blend;
- prev_time = 0;
+ prev_time = start;
}
} else {
if (Animation::is_less_approx(prev_time, time)) {
@@ -1220,10 +1222,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx);
- a->try_position_track_interpolate(i, 0, &loc[1]);
+ a->try_position_track_interpolate(i, start, &loc[1]);
loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx);
root_motion_cache.loc += (loc[1] - loc[0]) * blend;
- prev_time = (double)a_length;
+ prev_time = end;
}
}
Error err = a->try_position_track_interpolate(i, prev_time, &loc[0]);
@@ -1234,7 +1236,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
a->try_position_track_interpolate(i, time, &loc[1]);
loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx);
root_motion_cache.loc += (loc[1] - loc[0]) * blend;
- prev_time = !backward ? 0 : (double)a_length;
+ prev_time = !backward ? start : end;
}
{
Vector3 loc;
@@ -1256,32 +1258,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
if (track->root_motion && calc_root) {
double prev_time = time - delta;
if (!backward) {
- if (Animation::is_less_approx(prev_time, 0)) {
+ if (Animation::is_less_approx(prev_time, start)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = 0;
+ prev_time = start;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
}
}
} else {
- if (Animation::is_greater_approx(prev_time, (double)a_length)) {
+ if (Animation::is_greater_approx(prev_time, end)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = (double)a_length;
+ prev_time = end;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
@@ -1296,10 +1298,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx);
- a->try_rotation_track_interpolate(i, (double)a_length, &rot[1]);
+ a->try_rotation_track_interpolate(i, end, &rot[1]);
rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx);
root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized();
- prev_time = 0;
+ prev_time = start;
}
} else {
if (Animation::is_less_approx(prev_time, time)) {
@@ -1308,9 +1310,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx);
- a->try_rotation_track_interpolate(i, 0, &rot[1]);
+ a->try_rotation_track_interpolate(i, start, &rot[1]);
root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized();
- prev_time = (double)a_length;
+ prev_time = end;
}
}
Error err = a->try_rotation_track_interpolate(i, prev_time, &rot[0]);
@@ -1321,7 +1323,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
a->try_rotation_track_interpolate(i, time, &rot[1]);
rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx);
root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized();
- prev_time = !backward ? 0 : (double)a_length;
+ prev_time = !backward ? start : end;
}
{
Quaternion rot;
@@ -1343,32 +1345,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
if (track->root_motion && calc_root) {
double prev_time = time - delta;
if (!backward) {
- if (Animation::is_less_approx(prev_time, 0)) {
+ if (Animation::is_less_approx(prev_time, start)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = 0;
+ prev_time = start;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
}
}
} else {
- if (Animation::is_greater_approx(prev_time, (double)a_length)) {
+ if (Animation::is_greater_approx(prev_time, end)) {
switch (a->get_loop_mode()) {
case Animation::LOOP_NONE: {
- prev_time = (double)a_length;
+ prev_time = end;
} break;
case Animation::LOOP_LINEAR: {
- prev_time = Math::fposmod(prev_time, (double)a_length);
+ prev_time = Math::fposmod(prev_time - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- prev_time = Math::pingpong(prev_time, (double)a_length);
+ prev_time = Math::pingpong(prev_time - start, end - start) + start;
} break;
default:
break;
@@ -1383,10 +1385,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx);
- a->try_scale_track_interpolate(i, (double)a_length, &scale[1]);
+ a->try_scale_track_interpolate(i, end, &scale[1]);
root_motion_cache.scale += (scale[1] - scale[0]) * blend;
scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx);
- prev_time = 0;
+ prev_time = start;
}
} else {
if (Animation::is_less_approx(prev_time, time)) {
@@ -1395,10 +1397,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx);
- a->try_scale_track_interpolate(i, 0, &scale[1]);
+ a->try_scale_track_interpolate(i, start, &scale[1]);
scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx);
root_motion_cache.scale += (scale[1] - scale[0]) * blend;
- prev_time = (double)a_length;
+ prev_time = end;
}
}
Error err = a->try_scale_track_interpolate(i, prev_time, &scale[0]);
@@ -1409,7 +1411,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
a->try_scale_track_interpolate(i, time, &scale[1]);
scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx);
root_motion_cache.scale += (scale[1] - scale[0]) * blend;
- prev_time = !backward ? 0 : (double)a_length;
+ prev_time = !backward ? start : end;
}
{
Vector3 scale;
@@ -1671,6 +1673,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
if (!player2) {
continue;
}
+ // TODO: Make it possible to embed section info in animation track keys.
if (seeked) {
// Seek.
int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true);
@@ -1683,19 +1686,19 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) {
continue;
}
Ref<Animation> anim = player2->get_animation(anim_name);
- double at_anim_pos = 0.0;
+ double at_anim_pos = start;
switch (anim->get_loop_mode()) {
case Animation::LOOP_NONE: {
- if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + (double)anim->get_length())) || (backward && Animation::is_less_or_equal_approx(time, pos)))) {
+ if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + end)) || (backward && Animation::is_less_or_equal_approx(time, pos + start)))) {
continue; // Do nothing if current time is outside of length when started.
}
- at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end.
+ at_anim_pos = MIN(end, time - pos); // Seek to end.
} break;
case Animation::LOOP_LINEAR: {
- at_anim_pos = Math::fposmod(time - pos, (double)anim->get_length()); // Seek to loop.
+ at_anim_pos = Math::fposmod(time - pos - start, end - start) + start; // Seek to loop.
} break;
case Animation::LOOP_PINGPONG: {
- at_anim_pos = Math::pingpong(time - pos, (double)a_length);
+ at_anim_pos = Math::pingpong(time - pos - start, end - start) + start;
} break;
default:
break;
@@ -2092,6 +2095,8 @@ Ref<AnimatedValuesBackup> AnimationMixer::make_backup() {
PlaybackInfo pi;
pi.time = 0;
pi.delta = 0;
+ pi.start = 0;
+ pi.end = reset_anim->get_length();
pi.seeked = true;
pi.weight = 1.0;
make_animation_instance(SceneStringName(RESET), pi);
diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h
index 5482197fbd..27c9a00a9c 100644
--- a/scene/animation/animation_mixer.h
+++ b/scene/animation/animation_mixer.h
@@ -85,6 +85,8 @@ public:
struct PlaybackInfo {
double time = 0.0;
double delta = 0.0;
+ double start = 0.0;
+ double end = 0.0;
bool seeked = false;
bool is_external_seeking = false;
Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE;
diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp
index a4aa383a9d..bc951e4e14 100644
--- a/scene/animation/animation_player.cpp
+++ b/scene/animation/animation_player.cpp
@@ -164,39 +164,41 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f
double delta = p_started ? 0 : p_delta * speed;
double next_pos = cd.pos + delta;
- double len = cd.from->animation->get_length();
+ double start = get_section_start_time();
+ double end = get_section_end_time();
+
Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE;
switch (cd.from->animation->get_loop_mode()) {
case Animation::LOOP_NONE: {
- if (Animation::is_less_approx(next_pos, 0)) {
- next_pos = 0;
- } else if (Animation::is_greater_approx(next_pos, len)) {
- next_pos = len;
+ if (Animation::is_less_approx(next_pos, start)) {
+ next_pos = start;
+ } else if (Animation::is_greater_approx(next_pos, end)) {
+ next_pos = end;
}
delta = next_pos - cd.pos; // Fix delta (after determination of backwards because negative zero is lost here).
} break;
case Animation::LOOP_LINEAR: {
- if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) {
+ if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) {
looped_flag = Animation::LOOPED_FLAG_START;
}
- if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) {
+ if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) {
looped_flag = Animation::LOOPED_FLAG_END;
}
- next_pos = Math::fposmod(next_pos, (double)len);
+ next_pos = Math::fposmod(next_pos - start, end - start) + start;
} break;
case Animation::LOOP_PINGPONG: {
- if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) {
+ if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) {
cd.speed_scale *= -1.0;
looped_flag = Animation::LOOPED_FLAG_START;
}
- if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) {
+ if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) {
cd.speed_scale *= -1.0;
looped_flag = Animation::LOOPED_FLAG_END;
}
- next_pos = Math::pingpong(next_pos, (double)len);
+ next_pos = Math::pingpong(next_pos - start, end - start) + start;
} break;
default:
@@ -208,18 +210,18 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f
// End detection.
if (p_is_current) {
if (cd.from->animation->get_loop_mode() == Animation::LOOP_NONE) {
- if (!backwards && Animation::is_less_or_equal_approx(prev_pos, len) && Math::is_equal_approx(next_pos, len)) {
+ if (!backwards && Animation::is_less_or_equal_approx(prev_pos, end) && Math::is_equal_approx(next_pos, end)) {
// Playback finished.
- next_pos = len; // Snap to the edge.
+ next_pos = end; // Snap to the edge.
end_reached = true;
- end_notify = Animation::is_less_approx(prev_pos, len); // Notify only if not already at the end.
+ end_notify = Animation::is_less_approx(prev_pos, end); // Notify only if not already at the end.
p_blend = 1.0;
}
- if (backwards && Animation::is_greater_or_equal_approx(prev_pos, 0) && Math::is_equal_approx(next_pos, 0)) {
+ if (backwards && Animation::is_greater_or_equal_approx(prev_pos, start) && Math::is_equal_approx(next_pos, start)) {
// Playback finished.
- next_pos = 0; // Snap to the edge.
+ next_pos = start; // Snap to the edge.
end_reached = true;
- end_notify = Animation::is_greater_approx(prev_pos, 0); // Notify only if not already at the beginning.
+ end_notify = Animation::is_greater_approx(prev_pos, start); // Notify only if not already at the beginning.
p_blend = 1.0;
}
}
@@ -231,10 +233,14 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f
if (p_started) {
pi.time = prev_pos;
pi.delta = 0;
+ pi.start = start;
+ pi.end = end;
pi.seeked = true;
} else {
pi.time = next_pos;
pi.delta = delta;
+ pi.start = start;
+ pi.end = end;
pi.seeked = p_seeked;
}
if (Math::is_zero_approx(pi.delta) && backwards) {
@@ -378,6 +384,14 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b
play(p_name, p_custom_blend, -1, true);
}
+void AnimationPlayer::play_section_with_markers_backwards(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend) {
+ play_section_with_markers(p_name, p_start_marker, p_end_marker, p_custom_blend, -1, true);
+}
+
+void AnimationPlayer::play_section_backwards(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend) {
+ play_section(p_name, p_start_time, p_end_time, -1, true);
+}
+
void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) {
if (auto_capture) {
play_with_capture(p_name, auto_capture_duration, p_custom_blend, p_custom_scale, p_from_end, auto_capture_transition_type, auto_capture_ease_type);
@@ -387,6 +401,10 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa
}
void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) {
+ play_section_with_markers(p_name, StringName(), StringName(), p_custom_blend, p_custom_scale, p_from_end);
+}
+
+void AnimationPlayer::play_section_with_markers(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend, float p_custom_scale, bool p_from_end) {
StringName name = p_name;
if (name == StringName()) {
@@ -395,6 +413,38 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo
ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name));
+ Ref<Animation> animation = animation_set[name].animation;
+
+ ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker));
+ ERR_FAIL_COND_MSG(p_start_marker && !animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, name));
+ ERR_FAIL_COND_MSG(p_end_marker && !animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, name));
+
+ double start_time = p_start_marker ? animation->get_marker_time(p_start_marker) : -1;
+ double end_time = p_end_marker ? animation->get_marker_time(p_end_marker) : -1;
+
+ ERR_FAIL_COND_MSG(p_start_marker && p_end_marker && Animation::is_greater_approx(start_time, end_time), vformat("End marker %s is placed earlier than start marker %s in animation: %s.", p_end_marker, p_start_marker, name));
+
+ if (p_start_marker && Animation::is_less_approx(start_time, 0)) {
+ WARN_PRINT_ED(vformat("Negative time start marker: %s is invalid in the section, so the start of the animation: %s is used instead.", p_start_marker, playback.current.from->animation->get_name()));
+ }
+ if (p_end_marker && Animation::is_less_approx(end_time, 0)) {
+ WARN_PRINT_ED(vformat("Negative time end marker: %s is invalid in the section, so the end of the animation: %s is used instead.", p_end_marker, playback.current.from->animation->get_name()));
+ }
+
+ play_section(name, start_time, end_time, p_custom_blend, p_custom_scale, p_from_end);
+}
+
+void AnimationPlayer::play_section(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend, float p_custom_scale, bool p_from_end) {
+ StringName name = p_name;
+
+ if (name == StringName()) {
+ name = playback.assigned;
+ }
+
+ ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name));
+ ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Math::is_equal_approx(p_start_time, p_end_time), "Start time and end time must not equal to each other.");
+ ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Animation::is_greater_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time));
+
Playback &c = playback;
if (c.current.from) {
@@ -442,22 +492,27 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo
c.current.from = &animation_set[name];
c.current.speed_scale = p_custom_scale;
+ c.current.start_time = p_start_time;
+ c.current.end_time = p_end_time;
+
+ double start = get_section_start_time();
+ double end = get_section_end_time();
if (!end_reached) {
playback_queue.clear();
}
if (c.assigned != name) { // Reset.
- c.current.pos = p_from_end ? c.current.from->animation->get_length() : 0;
+ c.current.pos = p_from_end ? end : start;
c.assigned = name;
emit_signal(SNAME("current_animation_changed"), c.assigned);
} else {
- if (p_from_end && Math::is_zero_approx(c.current.pos)) {
+ if (p_from_end && Math::is_equal_approx(c.current.pos, start)) {
// Animation reset but played backwards, set position to the end.
- seek_internal(c.current.from->animation->get_length(), true, true, true);
- } else if (!p_from_end && Math::is_equal_approx(c.current.pos, (double)c.current.from->animation->get_length())) {
+ seek_internal(end, true, true, true);
+ } else if (!p_from_end && Math::is_equal_approx(c.current.pos, end)) {
// Animation resumed but already ended, set position to the beginning.
- seek_internal(0, true, true, true);
+ seek_internal(start, true, true, true);
} else if (playing) {
return;
}
@@ -551,6 +606,8 @@ void AnimationPlayer::set_assigned_animation(const String &p_animation) {
ERR_FAIL_COND_MSG(!animation_set.has(p_animation), vformat("Animation not found: %s.", p_animation));
playback.current.pos = 0;
playback.current.from = &animation_set[p_animation];
+ playback.current.start_time = -1;
+ playback.current.end_time = -1;
playback.assigned = p_animation;
emit_signal(SNAME("current_animation_changed"), playback.assigned);
}
@@ -603,6 +660,12 @@ void AnimationPlayer::seek_internal(double p_time, bool p_update, bool p_update_
}
}
+ double start = get_section_start_time();
+ double end = get_section_end_time();
+
+ // Clamp the seek position.
+ p_time = CLAMP(p_time, start, end);
+
playback.seeked = true;
playback.internal_seeked = p_is_internal_seek;
@@ -641,6 +704,55 @@ double AnimationPlayer::get_current_animation_length() const {
return playback.current.from->animation->get_length();
}
+void AnimationPlayer::set_section_with_markers(const StringName &p_start_marker, const StringName &p_end_marker) {
+ ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation.");
+ ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker));
+ ERR_FAIL_COND_MSG(p_start_marker && !playback.current.from->animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, playback.current.from->animation->get_name()));
+ ERR_FAIL_COND_MSG(p_end_marker && !playback.current.from->animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, playback.current.from->animation->get_name()));
+ double start_time = p_start_marker ? playback.current.from->animation->get_marker_time(p_start_marker) : -1;
+ double end_time = p_end_marker ? playback.current.from->animation->get_marker_time(p_end_marker) : -1;
+ if (p_start_marker && Animation::is_less_approx(start_time, 0)) {
+ WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_start_marker, playback.current.from->animation->get_name()));
+ }
+ if (p_end_marker && Animation::is_less_approx(end_time, 0)) {
+ WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_end_marker, playback.current.from->animation->get_name()));
+ }
+ set_section(start_time, end_time);
+}
+
+void AnimationPlayer::set_section(double p_start_time, double p_end_time) {
+ ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation.");
+ ERR_FAIL_COND_MSG(Animation::is_greater_or_equal_approx(p_start_time, 0) && Animation::is_greater_or_equal_approx(p_end_time, 0) && Animation::is_greater_or_equal_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time));
+ playback.current.start_time = p_start_time;
+ playback.current.end_time = p_end_time;
+ playback.current.pos = CLAMP(playback.current.pos, get_section_start_time(), get_section_end_time());
+}
+
+void AnimationPlayer::reset_section() {
+ playback.current.start_time = -1;
+ playback.current.end_time = -1;
+}
+
+double AnimationPlayer::get_section_start_time() const {
+ ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.start_time, "AnimationPlayer has no current animation.");
+ if (Animation::is_less_approx(playback.current.start_time, 0) || playback.current.start_time > playback.current.from->animation->get_length()) {
+ return 0;
+ }
+ return playback.current.start_time;
+}
+
+double AnimationPlayer::get_section_end_time() const {
+ ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.end_time, "AnimationPlayer has no current animation.");
+ if (Animation::is_less_approx(playback.current.end_time, 0) || playback.current.end_time > playback.current.from->animation->get_length()) {
+ return playback.current.from->animation->get_length();
+ }
+ return playback.current.end_time;
+}
+
+bool AnimationPlayer::has_section() const {
+ return Animation::is_greater_or_equal_approx(playback.current.start_time, 0) || Animation::is_greater_or_equal_approx(playback.current.end_time, 0);
+}
+
void AnimationPlayer::set_autoplay(const String &p_name) {
if (is_inside_tree() && !Engine::get_singleton()->is_editor_hint()) {
WARN_PRINT("Setting autoplay after the node has been added to the scene has no effect.");
@@ -665,13 +777,14 @@ void AnimationPlayer::_stop_internal(bool p_reset, bool p_keep_state) {
_clear_caches();
Playback &c = playback;
// c.blend.clear();
+ double start = get_section_start_time();
if (p_reset) {
c.blend.clear();
if (p_keep_state) {
- c.current.pos = 0;
+ c.current.pos = start;
} else {
is_stopping = true;
- seek_internal(0, true, true, true);
+ seek_internal(start, true, true, true);
is_stopping = false;
}
c.current.from = nullptr;
@@ -763,20 +876,6 @@ Tween::EaseType AnimationPlayer::get_auto_capture_ease_type() const {
return auto_capture_ease_type;
}
-#ifdef TOOLS_ENABLED
-void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const {
- const String pf = p_function;
- if (p_idx == 0 && (pf == "play" || pf == "play_backwards" || pf == "has_animation" || pf == "queue")) {
- List<StringName> al;
- get_animation_list(&al);
- for (const StringName &name : al) {
- r_options->push_back(String(name).quote());
- }
- }
- AnimationMixer::get_argument_options(p_function, p_idx, r_options);
-}
-#endif
-
void AnimationPlayer::_animation_removed(const StringName &p_name, const StringName &p_library) {
AnimationMixer::_animation_removed(p_name, p_library);
@@ -863,7 +962,11 @@ void AnimationPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_auto_capture_ease_type"), &AnimationPlayer::get_auto_capture_ease_type);
ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("play_section_with_markers", "name", "start_marker", "end_marker", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("play_section", "name", "start_time", "end_time", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false));
ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(StringName()), DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("play_section_with_markers_backwards", "name", "start_marker", "end_marker", "custom_blend"), &AnimationPlayer::play_section_with_markers_backwards, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("play_section_backwards", "name", "start_time", "end_time", "custom_blend"), &AnimationPlayer::play_section_backwards, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1));
ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(StringName()), DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN));
ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause);
ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false));
@@ -893,6 +996,14 @@ void AnimationPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_current_animation_position"), &AnimationPlayer::get_current_animation_position);
ClassDB::bind_method(D_METHOD("get_current_animation_length"), &AnimationPlayer::get_current_animation_length);
+ ClassDB::bind_method(D_METHOD("set_section_with_markers", "start_marker", "end_marker"), &AnimationPlayer::set_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName()));
+ ClassDB::bind_method(D_METHOD("set_section", "start_time", "end_time"), &AnimationPlayer::set_section, DEFVAL(-1), DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("reset_section"), &AnimationPlayer::reset_section);
+
+ ClassDB::bind_method(D_METHOD("get_section_start_time"), &AnimationPlayer::get_section_start_time);
+ ClassDB::bind_method(D_METHOD("get_section_end_time"), &AnimationPlayer::get_section_end_time);
+ ClassDB::bind_method(D_METHOD("has_section"), &AnimationPlayer::has_section);
+
ClassDB::bind_method(D_METHOD("seek", "seconds", "update", "update_only"), &AnimationPlayer::seek, DEFVAL(false), DEFVAL(false));
ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "current_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_EDITOR), "set_current_animation", "get_current_animation");
diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h
index e05a2c9935..3223e2522d 100644
--- a/scene/animation/animation_player.h
+++ b/scene/animation/animation_player.h
@@ -68,6 +68,8 @@ private:
AnimationData *from = nullptr;
double pos = 0.0;
float speed_scale = 1.0;
+ double start_time = 0.0;
+ double end_time = 0.0;
};
struct Blend {
@@ -177,7 +179,11 @@ public:
Tween::EaseType get_auto_capture_ease_type() const;
void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false);
+ void play_section_with_markers(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false);
+ void play_section(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false);
void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1);
+ void play_section_with_markers_backwards(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1);
+ void play_section_backwards(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1);
void play_with_capture(const StringName &p_name = StringName(), double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN);
void queue(const StringName &p_name);
Vector<String> get_queue();
@@ -207,9 +213,13 @@ public:
double get_current_animation_position() const;
double get_current_animation_length() const;
-#ifdef TOOLS_ENABLED
- void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
-#endif
+ void set_section_with_markers(const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName());
+ void set_section(double p_start_time = -1, double p_end_time = -1);
+ void reset_section();
+
+ double get_section_start_time() const;
+ double get_section_end_time() const;
+ bool has_section() const;
virtual void advance(double p_time) override;
diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp
index e4f52ee8ee..8238d54381 100644
--- a/scene/gui/tree.cpp
+++ b/scene/gui/tree.cpp
@@ -185,6 +185,26 @@ TreeItem::TreeCellMode TreeItem::get_cell_mode(int p_column) const {
return cells[p_column].mode;
}
+/* auto translate mode */
+void TreeItem::set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode) {
+ ERR_FAIL_INDEX(p_column, cells.size());
+
+ if (cells[p_column].auto_translate_mode == p_mode) {
+ return;
+ }
+
+ cells.write[p_column].auto_translate_mode = p_mode;
+ cells.write[p_column].dirty = true;
+ cells.write[p_column].cached_minimum_size_dirty = true;
+
+ _changed_notify(p_column);
+}
+
+Node::AutoTranslateMode TreeItem::get_auto_translate_mode(int p_column) const {
+ ERR_FAIL_INDEX_V(p_column, cells.size(), Node::AUTO_TRANSLATE_MODE_INHERIT);
+ return cells[p_column].auto_translate_mode;
+}
+
/* multiline editable */
void TreeItem::set_edit_multiline(int p_column, bool p_multiline) {
ERR_FAIL_INDEX(p_column, cells.size());
@@ -247,6 +267,24 @@ void TreeItem::propagate_check(int p_column, bool p_emit_signal) {
_propagate_check_through_parents(p_column, p_emit_signal);
}
+String TreeItem::atr(int p_column, const String &p_text) const {
+ ERR_FAIL_INDEX_V(p_column, cells.size(), tree->atr(p_text));
+
+ switch (cells[p_column].auto_translate_mode) {
+ case Node::AUTO_TRANSLATE_MODE_INHERIT: {
+ return tree->atr(p_text);
+ } break;
+ case Node::AUTO_TRANSLATE_MODE_ALWAYS: {
+ return tree->tr(p_text);
+ } break;
+ case Node::AUTO_TRANSLATE_MODE_DISABLED: {
+ return p_text;
+ } break;
+ }
+
+ ERR_FAIL_V_MSG(tree->atr(p_text), "Unexpected auto translate mode: " + itos(cells[p_column].auto_translate_mode));
+}
+
void TreeItem::_propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal) {
TreeItem *current = get_first_child();
while (current) {
@@ -323,7 +361,7 @@ void TreeItem::set_text(int p_column, String p_text) {
} else {
// Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user.
if (tree && (!cells[p_column].editable || cells[p_column].mode != TreeItem::CELL_MODE_STRING)) {
- cells.write[p_column].xl_text = tree->atr(p_text);
+ cells.write[p_column].xl_text = atr(p_column, p_text);
} else {
cells.write[p_column].xl_text = p_text;
}
@@ -1621,6 +1659,9 @@ void TreeItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_cell_mode", "column", "mode"), &TreeItem::set_cell_mode);
ClassDB::bind_method(D_METHOD("get_cell_mode", "column"), &TreeItem::get_cell_mode);
+ ClassDB::bind_method(D_METHOD("set_auto_translate_mode", "column", "mode"), &TreeItem::set_auto_translate_mode);
+ ClassDB::bind_method(D_METHOD("get_auto_translate_mode", "column"), &TreeItem::get_auto_translate_mode);
+
ClassDB::bind_method(D_METHOD("set_edit_multiline", "column", "multiline"), &TreeItem::set_edit_multiline);
ClassDB::bind_method(D_METHOD("is_edit_multiline", "column"), &TreeItem::is_edit_multiline);
@@ -2009,7 +2050,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) {
int option = (int)p_item->cells[p_col].val;
- valtext = atr(ETR("(Other)"));
+ valtext = p_item->atr(p_col, ETR("(Other)"));
Vector<String> strings = p_item->cells[p_col].text.split(",");
for (int j = 0; j < strings.size(); j++) {
int value = j;
@@ -2017,7 +2058,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) {
value = strings[j].get_slicec(':', 1).to_int();
}
if (option == value) {
- valtext = atr(strings[j].get_slicec(':', 0));
+ valtext = p_item->atr(p_col, strings[j].get_slicec(':', 0));
break;
}
}
@@ -2028,7 +2069,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) {
} else {
// Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user.
if (!p_item->cells[p_col].editable || p_item->cells[p_col].mode != TreeItem::CELL_MODE_STRING) {
- p_item->cells.write[p_col].xl_text = atr(p_item->cells[p_col].text);
+ p_item->cells.write[p_col].xl_text = p_item->atr(p_col, p_item->cells[p_col].text);
} else {
p_item->cells.write[p_col].xl_text = p_item->cells[p_col].text;
}
diff --git a/scene/gui/tree.h b/scene/gui/tree.h
index 17ea31a733..86efdfec52 100644
--- a/scene/gui/tree.h
+++ b/scene/gui/tree.h
@@ -64,6 +64,7 @@ private:
Rect2i icon_region;
String text;
String xl_text;
+ Node::AutoTranslateMode auto_translate_mode = Node::AUTO_TRANSLATE_MODE_INHERIT;
bool edit_multiline = false;
String suffix;
Ref<TextParagraph> text_buf;
@@ -210,6 +211,10 @@ public:
void set_cell_mode(int p_column, TreeCellMode p_mode);
TreeCellMode get_cell_mode(int p_column) const;
+ /* auto translate mode */
+ void set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode);
+ Node::AutoTranslateMode get_auto_translate_mode(int p_column) const;
+
/* multiline editable */
void set_edit_multiline(int p_column, bool p_multiline);
bool is_edit_multiline(int p_column) const;
@@ -222,6 +227,8 @@ public:
void propagate_check(int p_column, bool p_emit_signal = true);
+ String atr(int p_column, const String &p_text) const;
+
private:
// Check helpers.
void _propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal);
diff --git a/scene/main/http_request.cpp b/scene/main/http_request.cpp
index 3469b806a6..8526611093 100644
--- a/scene/main/http_request.cpp
+++ b/scene/main/http_request.cpp
@@ -49,7 +49,8 @@ Error HTTPRequest::_parse_url(const String &p_url) {
redirections = 0;
String scheme;
- Error err = p_url.parse_url(scheme, url, port, request_string);
+ String fragment;
+ Error err = p_url.parse_url(scheme, url, port, request_string, fragment);
ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error parsing URL: '%s'.", p_url));
if (scheme == "https://") {
diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp
index 1dac4b97ad..57a4e35f7a 100644
--- a/scene/resources/animation.cpp
+++ b/scene/resources/animation.cpp
@@ -63,6 +63,23 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) {
}
compression.enabled = true;
return true;
+ } else if (prop_name == SNAME("markers")) {
+ Array markers = p_value;
+ for (const Dictionary marker : markers) {
+ ERR_FAIL_COND_V(!marker.has("name"), false);
+ ERR_FAIL_COND_V(!marker.has("time"), false);
+ StringName marker_name = marker["name"];
+ double time = marker["time"];
+ _marker_insert(time, marker_names, MarkerKey(time, marker_name));
+ marker_times.insert(marker_name, time);
+ Color color = Color(1, 1, 1);
+ if (marker.has("color")) {
+ color = marker["color"];
+ }
+ marker_colors.insert(marker_name, color);
+ }
+
+ return true;
} else if (prop_name.begins_with("tracks/")) {
int track = prop_name.get_slicec('/', 1).to_int();
String what = prop_name.get_slicec('/', 2);
@@ -470,6 +487,18 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
r_ret = comp;
return true;
+ } else if (prop_name == SNAME("markers")) {
+ Array markers;
+
+ for (HashMap<StringName, double>::ConstIterator E = marker_times.begin(); E; ++E) {
+ Dictionary d;
+ d["name"] = E->key;
+ d["time"] = E->value;
+ d["color"] = marker_colors[E->key];
+ markers.push_back(d);
+ }
+
+ r_ret = markers;
} else if (prop_name == "length") {
r_ret = length;
} else if (prop_name == "loop_mode") {
@@ -839,6 +868,7 @@ void Animation::_get_property_list(List<PropertyInfo> *p_list) const {
if (compression.enabled) {
p_list->push_back(PropertyInfo(Variant::DICTIONARY, "_compression", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
}
+ p_list->push_back(PropertyInfo(Variant::ARRAY, "markers", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
for (int i = 0; i < tracks.size(); i++) {
p_list->push_back(PropertyInfo(Variant::STRING, "tracks/" + itos(i) + "/type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/imported", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
@@ -1087,6 +1117,27 @@ int Animation::_insert(double p_time, T &p_keys, const V &p_value) {
return -1;
}
+int Animation::_marker_insert(double p_time, Vector<MarkerKey> &p_keys, const MarkerKey &p_value) {
+ int idx = p_keys.size();
+
+ while (true) {
+ // Condition for replacement.
+ if (idx > 0 && Math::is_equal_approx((double)p_keys[idx - 1].time, p_time)) {
+ p_keys.write[idx - 1] = p_value;
+ return idx - 1;
+
+ // Condition for insert.
+ } else if (idx == 0 || p_keys[idx - 1].time < p_time) {
+ p_keys.insert(idx, p_value);
+ return idx;
+ }
+
+ idx--;
+ }
+
+ return -1;
+}
+
template <typename T>
void Animation::_clear(T &p_keys) {
p_keys.clear();
@@ -3163,6 +3214,90 @@ void Animation::track_get_key_indices_in_range(int p_track, double p_time, doubl
}
}
+void Animation::add_marker(const StringName &p_name, double p_time) {
+ int idx = _find(marker_names, p_time);
+
+ if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(p_time, marker_names[idx].time)) {
+ marker_times.erase(marker_names[idx].name);
+ marker_colors.erase(marker_names[idx].name);
+ marker_names.write[idx].name = p_name;
+ marker_times.insert(p_name, p_time);
+ marker_colors.insert(p_name, Color(1, 1, 1));
+ } else {
+ _marker_insert(p_time, marker_names, MarkerKey(p_time, p_name));
+ marker_times.insert(p_name, p_time);
+ marker_colors.insert(p_name, Color(1, 1, 1));
+ }
+}
+
+void Animation::remove_marker(const StringName &p_name) {
+ HashMap<StringName, double>::Iterator E = marker_times.find(p_name);
+ ERR_FAIL_COND(!E);
+ int idx = _find(marker_names, E->value);
+ bool success = idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, E->value);
+ ERR_FAIL_COND(!success);
+ marker_names.remove_at(idx);
+ marker_times.remove(E);
+ marker_colors.erase(p_name);
+}
+
+bool Animation::has_marker(const StringName &p_name) const {
+ return marker_times.has(p_name);
+}
+
+StringName Animation::get_marker_at_time(double p_time) const {
+ int idx = _find(marker_names, p_time);
+
+ if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, p_time)) {
+ return marker_names[idx].name;
+ }
+
+ return StringName();
+}
+
+StringName Animation::get_next_marker(double p_time) const {
+ int idx = _find(marker_names, p_time);
+
+ if (idx >= -1 && idx < marker_names.size() - 1) {
+ // _find ensures that the time at idx is always the closest time to p_time that is also smaller to it.
+ // So we add 1 to get the next marker.
+ return marker_names[idx + 1].name;
+ }
+ return StringName();
+}
+
+StringName Animation::get_prev_marker(double p_time) const {
+ int idx = _find(marker_names, p_time);
+
+ if (idx >= 0 && idx < marker_names.size()) {
+ return marker_names[idx].name;
+ }
+ return StringName();
+}
+
+double Animation::get_marker_time(const StringName &p_name) const {
+ ERR_FAIL_COND_V(!marker_times.has(p_name), -1);
+ return marker_times.get(p_name);
+}
+
+PackedStringArray Animation::get_marker_names() const {
+ PackedStringArray names;
+ // We iterate on marker_names so the result is sorted by time.
+ for (const MarkerKey &marker_name : marker_names) {
+ names.push_back(marker_name.name);
+ }
+ return names;
+}
+
+Color Animation::get_marker_color(const StringName &p_name) const {
+ ERR_FAIL_COND_V(!marker_colors.has(p_name), Color());
+ return marker_colors[p_name];
+}
+
+void Animation::set_marker_color(const StringName &p_name, const Color &p_color) {
+ marker_colors[p_name] = p_color;
+}
+
Vector<Variant> Animation::method_track_get_params(int p_track, int p_key_idx) const {
ERR_FAIL_INDEX_V(p_track, tracks.size(), Vector<Variant>());
Track *t = tracks[p_track];
@@ -3894,6 +4029,17 @@ void Animation::_bind_methods() {
ClassDB::bind_method(D_METHOD("animation_track_set_key_animation", "track_idx", "key_idx", "animation"), &Animation::animation_track_set_key_animation);
ClassDB::bind_method(D_METHOD("animation_track_get_key_animation", "track_idx", "key_idx"), &Animation::animation_track_get_key_animation);
+ ClassDB::bind_method(D_METHOD("add_marker", "name", "time"), &Animation::add_marker);
+ ClassDB::bind_method(D_METHOD("remove_marker", "name"), &Animation::remove_marker);
+ ClassDB::bind_method(D_METHOD("has_marker", "name"), &Animation::has_marker);
+ ClassDB::bind_method(D_METHOD("get_marker_at_time", "time"), &Animation::get_marker_at_time);
+ ClassDB::bind_method(D_METHOD("get_next_marker", "time"), &Animation::get_next_marker);
+ ClassDB::bind_method(D_METHOD("get_prev_marker", "time"), &Animation::get_prev_marker);
+ ClassDB::bind_method(D_METHOD("get_marker_time", "name"), &Animation::get_marker_time);
+ ClassDB::bind_method(D_METHOD("get_marker_names"), &Animation::get_marker_names);
+ ClassDB::bind_method(D_METHOD("get_marker_color", "name"), &Animation::get_marker_color);
+ ClassDB::bind_method(D_METHOD("set_marker_color", "name", "color"), &Animation::set_marker_color);
+
ClassDB::bind_method(D_METHOD("set_length", "time_sec"), &Animation::set_length);
ClassDB::bind_method(D_METHOD("get_length"), &Animation::get_length);
diff --git a/scene/resources/animation.h b/scene/resources/animation.h
index 0c29790ea4..618dc9ca17 100644
--- a/scene/resources/animation.h
+++ b/scene/resources/animation.h
@@ -237,6 +237,20 @@ private:
}
};
+ /* Marker */
+
+ struct MarkerKey {
+ double time;
+ StringName name;
+ MarkerKey(double p_time, const StringName &p_name) :
+ time(p_time), name(p_name) {}
+ MarkerKey() = default;
+ };
+
+ Vector<MarkerKey> marker_names; // time -> name
+ HashMap<StringName, double> marker_times; // name -> time
+ HashMap<StringName, Color> marker_colors; // name -> color
+
Vector<Track *> tracks;
template <typename T>
@@ -245,6 +259,8 @@ private:
template <typename T, typename V>
int _insert(double p_time, T &p_keys, const V &p_value);
+ int _marker_insert(double p_time, Vector<MarkerKey> &p_keys, const MarkerKey &p_value);
+
template <typename K>
inline int _find(const Vector<K> &p_keys, double p_time, bool p_backward = false, bool p_limit = false) const;
@@ -501,6 +517,17 @@ public:
void track_get_key_indices_in_range(int p_track, double p_time, double p_delta, List<int> *p_indices, Animation::LoopedFlag p_looped_flag = Animation::LOOPED_FLAG_NONE) const;
+ void add_marker(const StringName &p_name, double p_time);
+ void remove_marker(const StringName &p_name);
+ bool has_marker(const StringName &p_name) const;
+ StringName get_marker_at_time(double p_time) const;
+ StringName get_next_marker(double p_time) const;
+ StringName get_prev_marker(double p_time) const;
+ double get_marker_time(const StringName &p_time) const;
+ PackedStringArray get_marker_names() const;
+ Color get_marker_color(const StringName &p_name) const;
+ void set_marker_color(const StringName &p_name, const Color &p_color);
+
void set_length(real_t p_length);
real_t get_length() const;
diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp
index ab25aabb81..9bd3f2edce 100644
--- a/scene/resources/material.cpp
+++ b/scene/resources/material.cpp
@@ -946,7 +946,7 @@ uniform vec4 refraction_texture_channel;
code += "uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear_mipmap;\n";
}
- if (proximity_fade_enabled) {
+ if (features[FEATURE_REFRACTION] || proximity_fade_enabled) {
code += "uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;\n";
}
@@ -1627,7 +1627,14 @@ void fragment() {)";
}
code += R"(
float ref_amount = 1.0 - albedo.a * albedo_tex.a;
- EMISSION += textureLod(screen_texture, ref_ofs, ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE;
+
+ float refraction_depth_tex = textureLod(depth_texture, ref_ofs, 0.0).r;
+ vec4 refraction_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, refraction_depth_tex, 1.0);
+ refraction_view_pos.xyz /= refraction_view_pos.w;
+
+ // If the depth buffer is lower then the model's Z position, use the refracted UV, otherwise use the normal screen UV.
+ // At low depth differences, decrease refraction intensity to avoid sudden discontinuities.
+ EMISSION += textureLod(screen_texture, mix(SCREEN_UV, ref_ofs, smoothstep(0.0, 1.0, VERTEX.z - refraction_view_pos.z)), ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE;
ALBEDO *= 1.0 - ref_amount;
// Force transparency on the material (required for refraction).
ALPHA = 1.0;
@@ -1649,10 +1656,10 @@ void fragment() {)";
if (proximity_fade_enabled) {
code += R"(
// Proximity Fade: Enabled
- float depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r;
- vec4 world_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth_tex, 1.0);
- world_pos.xyz /= world_pos.w;
- ALPHA *= clamp(1.0 - smoothstep(world_pos.z + proximity_fade_distance, world_pos.z, VERTEX.z), 0.0, 1.0);
+ float proximity_depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r;
+ vec4 proximity_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, proximity_depth_tex, 1.0);
+ proximity_view_pos.xyz /= proximity_view_pos.w;
+ ALPHA *= clamp(1.0 - smoothstep(proximity_view_pos.z + proximity_fade_distance, proximity_view_pos.z, VERTEX.z), 0.0, 1.0);
)";
}
diff --git a/scene/resources/style_box_flat.cpp b/scene/resources/style_box_flat.cpp
index 60b91ef0cb..15816925c1 100644
--- a/scene/resources/style_box_flat.cpp
+++ b/scene/resources/style_box_flat.cpp
@@ -226,33 +226,16 @@ inline void set_inner_corner_radius(const Rect2 style_rect, const Rect2 inner_re
real_t border_right = style_rect.size.width - inner_rect.size.width - border_left;
real_t border_bottom = style_rect.size.height - inner_rect.size.height - border_top;
- real_t rad;
-
- // Top left.
- rad = MIN(border_top, border_left);
- inner_corner_radius[0] = MAX(corner_radius[0] - rad, 0);
-
- // Top right;
- rad = MIN(border_top, border_right);
- inner_corner_radius[1] = MAX(corner_radius[1] - rad, 0);
-
- // Bottom right.
- rad = MIN(border_bottom, border_right);
- inner_corner_radius[2] = MAX(corner_radius[2] - rad, 0);
-
- // Bottom left.
- rad = MIN(border_bottom, border_left);
- inner_corner_radius[3] = MAX(corner_radius[3] - rad, 0);
+ inner_corner_radius[0] = MAX(corner_radius[0] - MIN(border_top, border_left), 0); // Top left.
+ inner_corner_radius[1] = MAX(corner_radius[1] - MIN(border_top, border_right), 0); // Top right.
+ inner_corner_radius[2] = MAX(corner_radius[2] - MIN(border_bottom, border_right), 0); // Bottom right.
+ inner_corner_radius[3] = MAX(corner_radius[3] - MIN(border_bottom, border_left), 0); // Bottom left.
}
inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices, Vector<Color> &colors, const Rect2 &style_rect, const real_t corner_radius[4],
const Rect2 &ring_rect, const Rect2 &inner_rect, const Color &inner_color, const Color &outer_color, const int corner_detail, const Vector2 &skew, bool is_filled = false) {
int vert_offset = verts.size();
- if (!vert_offset) {
- vert_offset = 0;
- }
-
- int adapted_corner_detail = (corner_radius[0] == 0 && corner_radius[1] == 0 && corner_radius[2] == 0 && corner_radius[3] == 0) ? 1 : corner_detail;
+ int adapted_corner_detail = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0) ? corner_detail : 1;
bool draw_border = !is_filled;
@@ -280,30 +263,44 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices,
// If the center is filled, we do not draw the border and directly use the inner ring as reference. Because all calls to this
// method either draw a ring or a filled rounded rectangle, but not both.
- int max_inner_outer = draw_border ? 2 : 1;
-
- for (int corner_index = 0; corner_index < 4; corner_index++) {
+ real_t quarter_arc_rad = Math_PI / 2.0;
+ Point2 style_rect_center = style_rect.get_center();
+
+ int colors_size = colors.size();
+ int verts_size = verts.size();
+ int new_verts_amount = (adapted_corner_detail + 1) * (draw_border ? 8 : 4);
+ colors.resize(colors_size + new_verts_amount);
+ verts.resize(verts_size + new_verts_amount);
+ Color *colors_ptr = colors.ptrw();
+ Vector2 *verts_ptr = verts.ptrw();
+
+ for (int corner_idx = 0; corner_idx < 4; corner_idx++) {
for (int detail = 0; detail <= adapted_corner_detail; detail++) {
- for (int inner_outer = 0; inner_outer < max_inner_outer; inner_outer++) {
- real_t radius;
- Color color;
- Point2 corner_point;
- if (inner_outer == 0) {
- radius = inner_corner_radius[corner_index];
- color = inner_color;
- corner_point = inner_points[corner_index];
- } else {
- radius = ring_corner_radius[corner_index];
- color = outer_color;
- corner_point = outer_points[corner_index];
- }
+ int idx_ofs = (adapted_corner_detail + 1) * corner_idx + detail;
+ if (draw_border) {
+ idx_ofs *= 2;
+ }
- const real_t x = radius * (real_t)cos((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.x;
- const real_t y = radius * (real_t)sin((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.y;
- const float x_skew = -skew.x * (y - style_rect.get_center().y);
- const float y_skew = -skew.y * (x - style_rect.get_center().x);
- verts.push_back(Vector2(x + x_skew, y + y_skew));
- colors.push_back(color);
+ const real_t pt_angle = (corner_idx + detail / (double)adapted_corner_detail) * quarter_arc_rad + Math_PI;
+ const real_t angle_cosine = cos(pt_angle);
+ const real_t angle_sine = sin(pt_angle);
+
+ {
+ const real_t x = inner_corner_radius[corner_idx] * angle_cosine + inner_points[corner_idx].x;
+ const real_t y = inner_corner_radius[corner_idx] * angle_sine + inner_points[corner_idx].y;
+ const float x_skew = -skew.x * (y - style_rect_center.y);
+ const float y_skew = -skew.y * (x - style_rect_center.x);
+ verts_ptr[verts_size + idx_ofs] = Vector2(x + x_skew, y + y_skew);
+ colors_ptr[colors_size + idx_ofs] = inner_color;
+ }
+
+ if (draw_border) {
+ const real_t x = ring_corner_radius[corner_idx] * angle_cosine + outer_points[corner_idx].x;
+ const real_t y = ring_corner_radius[corner_idx] * angle_sine + outer_points[corner_idx].y;
+ const float x_skew = -skew.x * (y - style_rect_center.y);
+ const float y_skew = -skew.y * (x - style_rect_center.x);
+ verts_ptr[verts_size + idx_ofs + 1] = Vector2(x + x_skew, y + y_skew);
+ colors_ptr[colors_size + idx_ofs + 1] = outer_color;
}
}
}
@@ -313,10 +310,15 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices,
// Fill the indices and the colors for the border.
if (draw_border) {
+ int indices_size = indices.size();
+ indices.resize(indices_size + ring_vert_count * 3);
+ int *indices_ptr = indices.ptrw();
+
for (int i = 0; i < ring_vert_count; i++) {
- indices.push_back(vert_offset + ((i + 0) % ring_vert_count));
- indices.push_back(vert_offset + ((i + 2) % ring_vert_count));
- indices.push_back(vert_offset + ((i + 1) % ring_vert_count));
+ int idx_ofs = indices_size + i * 3;
+ indices_ptr[idx_ofs] = vert_offset + i % ring_vert_count;
+ indices_ptr[idx_ofs + 1] = vert_offset + (i + 2) % ring_vert_count;
+ indices_ptr[idx_ofs + 2] = vert_offset + (i + 1) % ring_vert_count;
}
}
@@ -327,40 +329,30 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices,
int stripes_count = ring_vert_count / 2 - 1;
int last_vert_id = ring_vert_count - 1;
+ int indices_size = indices.size();
+ indices.resize(indices_size + stripes_count * 6);
+ int *indices_ptr = indices.ptrw();
+
for (int i = 0; i < stripes_count; i++) {
+ int idx_ofs = indices_size + i * 6;
// Polygon 1.
- indices.push_back(vert_offset + i);
- indices.push_back(vert_offset + last_vert_id - i - 1);
- indices.push_back(vert_offset + i + 1);
+ indices_ptr[idx_ofs] = vert_offset + i;
+ indices_ptr[idx_ofs + 1] = vert_offset + last_vert_id - i - 1;
+ indices_ptr[idx_ofs + 2] = vert_offset + i + 1;
// Polygon 2.
- indices.push_back(vert_offset + i);
- indices.push_back(vert_offset + last_vert_id - 0 - i);
- indices.push_back(vert_offset + last_vert_id - 1 - i);
+ indices_ptr[idx_ofs + 3] = vert_offset + i;
+ indices_ptr[idx_ofs + 4] = vert_offset + last_vert_id - i;
+ indices_ptr[idx_ofs + 5] = vert_offset + last_vert_id - i - 1;
}
}
}
inline void adapt_values(int p_index_a, int p_index_b, real_t *adapted_values, const real_t *p_values, const real_t p_width, const real_t p_max_a, const real_t p_max_b) {
- if (p_values[p_index_a] + p_values[p_index_b] > p_width) {
- real_t factor;
- real_t new_value;
-
- factor = (real_t)p_width / (real_t)(p_values[p_index_a] + p_values[p_index_b]);
-
- new_value = (p_values[p_index_a] * factor);
- if (new_value < adapted_values[p_index_a]) {
- adapted_values[p_index_a] = new_value;
- }
- new_value = (p_values[p_index_b] * factor);
- if (new_value < adapted_values[p_index_b]) {
- adapted_values[p_index_b] = new_value;
- }
- } else {
- adapted_values[p_index_a] = MIN(p_values[p_index_a], adapted_values[p_index_a]);
- adapted_values[p_index_b] = MIN(p_values[p_index_b], adapted_values[p_index_b]);
- }
- adapted_values[p_index_a] = MIN(p_max_a, adapted_values[p_index_a]);
- adapted_values[p_index_b] = MIN(p_max_b, adapted_values[p_index_b]);
+ real_t value_a = p_values[p_index_a];
+ real_t value_b = p_values[p_index_b];
+ real_t factor = MIN(1.0, p_width / (value_a + value_b));
+ adapted_values[p_index_a] = MIN(MIN(value_a * factor, p_max_a), adapted_values[p_index_a]);
+ adapted_values[p_index_b] = MIN(MIN(value_b * factor, p_max_b), adapted_values[p_index_b]);
}
Rect2 StyleBoxFlat::get_draw_rect(const Rect2 &p_rect) const {
@@ -388,7 +380,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const {
}
const bool rounded_corners = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0);
- // Only enable antialiasing if it is actually needed. This improve performances
+ // Only enable antialiasing if it is actually needed. This improves performance
// and maximizes sharpness for non-skewed StyleBoxes with sharp corners.
const bool aa_on = (rounded_corners || !skew.is_zero_approx()) && anti_aliased;
@@ -428,7 +420,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const {
Vector<Color> colors;
Vector<Point2> uvs;
- // Create shadow
+ // Create shadow.
if (draw_shadow) {
Rect2 shadow_inner_rect = style_rect;
shadow_inner_rect.position += shadow_offset;
@@ -538,9 +530,10 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const {
// Compute UV coordinates.
Rect2 uv_rect = style_rect.grow(aa_on ? aa_size : 0);
uvs.resize(verts.size());
+ Point2 *uvs_ptr = uvs.ptrw();
for (int i = 0; i < verts.size(); i++) {
- uvs.write[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width;
- uvs.write[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height;
+ uvs_ptr[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width;
+ uvs_ptr[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height;
}
// Draw stylebox.
diff --git a/servers/rendering/renderer_rd/shaders/canvas.glsl b/servers/rendering/renderer_rd/shaders/canvas.glsl
index 704aafdfa5..fd0cd5bfad 100644
--- a/servers/rendering/renderer_rd/shaders/canvas.glsl
+++ b/servers/rendering/renderer_rd/shaders/canvas.glsl
@@ -493,7 +493,8 @@ void main() {
#endif
if (bool(draw_data.flags & FLAGS_CLIP_RECT_UV)) {
- uv = clamp(uv, draw_data.src_rect.xy, draw_data.src_rect.xy + abs(draw_data.src_rect.zw));
+ vec2 half_texpixel = draw_data.color_texture_pixel_size * 0.5;
+ uv = clamp(uv, draw_data.src_rect.xy + half_texpixel, draw_data.src_rect.xy + abs(draw_data.src_rect.zw) - half_texpixel);
}
#endif
diff --git a/tests/core/io/test_packet_peer.h b/tests/core/io/test_packet_peer.h
new file mode 100644
index 0000000000..59c8dadad8
--- /dev/null
+++ b/tests/core/io/test_packet_peer.h
@@ -0,0 +1,204 @@
+/**************************************************************************/
+/* test_packet_peer.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_PACKET_PEER_H
+#define TEST_PACKET_PEER_H
+
+#include "core/io/packet_peer.h"
+#include "tests/test_macros.h"
+
+namespace TestPacketPeer {
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Encode buffer max size") {
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+
+ SUBCASE("Default value") {
+ CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
+ }
+
+ SUBCASE("Max encode buffer must be at least 1024 bytes") {
+ ERR_PRINT_OFF;
+ pps->set_encode_buffer_max_size(42);
+ ERR_PRINT_ON;
+
+ CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
+ }
+
+ SUBCASE("Max encode buffer cannot exceed 256 MiB") {
+ ERR_PRINT_OFF;
+ pps->set_encode_buffer_max_size((256 * 1024 * 1024) + 42);
+ ERR_PRINT_ON;
+
+ CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024);
+ }
+
+ SUBCASE("Should be next power of two") {
+ pps->set_encode_buffer_max_size(2000);
+
+ CHECK_EQ(pps->get_encode_buffer_max_size(), 2048);
+ }
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer") {
+ String godot_rules = "Godot Rules!!!";
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ spb->put_var(godot_rules);
+ spb->seek(0);
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ Variant value;
+ CHECK_EQ(pps->get_var(value), Error::OK);
+ CHECK_EQ(String(value), godot_rules);
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer fails") {
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+
+ Variant value;
+ ERR_PRINT_OFF;
+ CHECK_EQ(pps->get_var(value), Error::ERR_UNCONFIGURED);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer") {
+ String godot_rules = "Godot Rules!!!";
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ CHECK_EQ(pps->put_var(godot_rules), Error::OK);
+
+ spb->seek(0);
+ CHECK_EQ(String(spb->get_var()), godot_rules);
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer out of memory failure") {
+ String more_than_1mb = String("*").repeat(1024 + 1);
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+ pps->set_encode_buffer_max_size(1024);
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(pps->put_var(more_than_1mb), Error::ERR_OUT_OF_MEMORY);
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer") {
+ String godot_rules = "Godot Rules!!!";
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ // First 4 bytes are the length of the string.
+ CharString cs = godot_rules.ascii();
+ Vector<uint8_t> buffer = { (uint8_t)(cs.length() + 1), 0, 0, 0 };
+ buffer.resize_zeroed(4 + cs.length() + 1);
+ memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length());
+ spb->set_data_array(buffer);
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ buffer.clear();
+ CHECK_EQ(pps->get_packet_buffer(buffer), Error::OK);
+
+ CHECK_EQ(String(reinterpret_cast<const char *>(buffer.ptr())), godot_rules);
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer from an empty peer") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ Vector<uint8_t> buffer;
+ ERR_PRINT_OFF;
+ CHECK_EQ(pps->get_packet_buffer(buffer), Error::ERR_UNAVAILABLE);
+ ERR_PRINT_ON;
+ CHECK_EQ(buffer.size(), 0);
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer") {
+ String godot_rules = "Godot Rules!!!";
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ CHECK_EQ(pps->put_packet_buffer(godot_rules.to_ascii_buffer()), Error::OK);
+
+ spb->seek(0);
+ CHECK_EQ(spb->get_string(), godot_rules);
+ // First 4 bytes are the length of the string.
+ CharString cs = godot_rules.ascii();
+ Vector<uint8_t> buffer = { (uint8_t)cs.length(), 0, 0, 0 };
+ buffer.resize(4 + cs.length());
+ memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length());
+ CHECK_EQ(spb->get_data_array(), buffer);
+}
+
+TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer when is empty") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ Ref<PacketPeerStream> pps;
+ pps.instantiate();
+ pps->set_stream_peer(spb);
+
+ Vector<uint8_t> buffer;
+ CHECK_EQ(pps->put_packet_buffer(buffer), Error::OK);
+
+ CHECK_EQ(spb->get_size(), 0);
+}
+
+} // namespace TestPacketPeer
+
+#endif // TEST_PACKET_PEER_H
diff --git a/tests/core/io/test_stream_peer.h b/tests/core/io/test_stream_peer.h
new file mode 100644
index 0000000000..31bd69edd0
--- /dev/null
+++ b/tests/core/io/test_stream_peer.h
@@ -0,0 +1,289 @@
+/**************************************************************************/
+/* test_stream_peer.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_STREAM_PEER_H
+#define TEST_STREAM_PEER_H
+
+#include "core/io/stream_peer.h"
+#include "tests/test_macros.h"
+
+namespace TestStreamPeer {
+
+TEST_CASE("[StreamPeer] Initialization through StreamPeerBuffer") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ CHECK_EQ(spb->is_big_endian_enabled(), false);
+}
+
+TEST_CASE("[StreamPeer] Get and sets through StreamPeerBuffer") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ SUBCASE("A int8_t value") {
+ int8_t value = 42;
+
+ spb->clear();
+ spb->put_8(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_8(), value);
+ }
+
+ SUBCASE("A uint8_t value") {
+ uint8_t value = 42;
+
+ spb->clear();
+ spb->put_u8(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u8(), value);
+ }
+
+ SUBCASE("A int16_t value") {
+ int16_t value = 42;
+
+ spb->clear();
+ spb->put_16(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_16(), value);
+ }
+
+ SUBCASE("A uint16_t value") {
+ uint16_t value = 42;
+
+ spb->clear();
+ spb->put_u16(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u16(), value);
+ }
+
+ SUBCASE("A int32_t value") {
+ int32_t value = 42;
+
+ spb->clear();
+ spb->put_32(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_32(), value);
+ }
+
+ SUBCASE("A uint32_t value") {
+ uint32_t value = 42;
+
+ spb->clear();
+ spb->put_u32(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u32(), value);
+ }
+
+ SUBCASE("A int64_t value") {
+ int64_t value = 42;
+
+ spb->clear();
+ spb->put_64(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_64(), value);
+ }
+
+ SUBCASE("A int64_t value") {
+ uint64_t value = 42;
+
+ spb->clear();
+ spb->put_u64(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u64(), value);
+ }
+
+ SUBCASE("A float value") {
+ float value = 42.0f;
+
+ spb->clear();
+ spb->put_float(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_float(), value);
+ }
+
+ SUBCASE("A double value") {
+ double value = 42.0;
+
+ spb->clear();
+ spb->put_double(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_double(), value);
+ }
+
+ SUBCASE("A string value") {
+ String value = "Hello, World!";
+
+ spb->clear();
+ spb->put_string(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_string(), value);
+ }
+
+ SUBCASE("A utf8 string value") {
+ String value = String::utf8("Hello✩, World✩!");
+
+ spb->clear();
+ spb->put_utf8_string(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_utf8_string(), value);
+ }
+
+ SUBCASE("A variant value") {
+ Array value;
+ value.push_front(42);
+ value.push_front("Hello, World!");
+
+ spb->clear();
+ spb->put_var(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_var(), value);
+ }
+}
+
+TEST_CASE("[StreamPeer] Get and sets big endian through StreamPeerBuffer") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ spb->set_big_endian(true);
+
+ SUBCASE("A int16_t value") {
+ int16_t value = 42;
+
+ spb->clear();
+ spb->put_16(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_16(), value);
+ }
+
+ SUBCASE("A uint16_t value") {
+ uint16_t value = 42;
+
+ spb->clear();
+ spb->put_u16(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u16(), value);
+ }
+
+ SUBCASE("A int32_t value") {
+ int32_t value = 42;
+
+ spb->clear();
+ spb->put_32(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_32(), value);
+ }
+
+ SUBCASE("A uint32_t value") {
+ uint32_t value = 42;
+
+ spb->clear();
+ spb->put_u32(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u32(), value);
+ }
+
+ SUBCASE("A int64_t value") {
+ int64_t value = 42;
+
+ spb->clear();
+ spb->put_64(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_64(), value);
+ }
+
+ SUBCASE("A int64_t value") {
+ uint64_t value = 42;
+
+ spb->clear();
+ spb->put_u64(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_u64(), value);
+ }
+
+ SUBCASE("A float value") {
+ float value = 42.0f;
+
+ spb->clear();
+ spb->put_float(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_float(), value);
+ }
+
+ SUBCASE("A double value") {
+ double value = 42.0;
+
+ spb->clear();
+ spb->put_double(value);
+ spb->seek(0);
+
+ CHECK_EQ(spb->get_double(), value);
+ }
+}
+
+TEST_CASE("[StreamPeer] Get string when there is no string") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(spb->get_string(), "");
+ ERR_PRINT_ON;
+}
+
+TEST_CASE("[StreamPeer] Get UTF8 string when there is no string") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+
+ ERR_PRINT_OFF;
+ CHECK_EQ(spb->get_utf8_string(), "");
+ ERR_PRINT_ON;
+}
+
+} // namespace TestStreamPeer
+
+#endif // TEST_STREAM_PEER_H
diff --git a/tests/core/io/test_stream_peer_buffer.h b/tests/core/io/test_stream_peer_buffer.h
new file mode 100644
index 0000000000..8ba9c0a72c
--- /dev/null
+++ b/tests/core/io/test_stream_peer_buffer.h
@@ -0,0 +1,185 @@
+/**************************************************************************/
+/* test_stream_peer_buffer.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_STREAM_PEER_BUFFER_H
+#define TEST_STREAM_PEER_BUFFER_H
+
+#include "core/io/stream_peer.h"
+#include "tests/test_macros.h"
+
+namespace TestStreamPeerBuffer {
+
+TEST_CASE("[StreamPeerBuffer] Initialization") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ CHECK_EQ(spb->get_size(), 0);
+ CHECK_EQ(spb->get_position(), 0);
+ CHECK_EQ(spb->get_available_bytes(), 0);
+}
+
+TEST_CASE("[StreamPeerBuffer] Seek") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ uint8_t first = 5;
+ uint8_t second = 7;
+ uint8_t third = 11;
+
+ spb->put_u8(first);
+ spb->put_u8(second);
+ spb->put_u8(third);
+
+ spb->seek(0);
+ CHECK_EQ(spb->get_u8(), first);
+ CHECK_EQ(spb->get_u8(), second);
+ CHECK_EQ(spb->get_u8(), third);
+
+ spb->seek(1);
+ CHECK_EQ(spb->get_position(), 1);
+ CHECK_EQ(spb->get_u8(), second);
+
+ spb->seek(1);
+ ERR_PRINT_OFF;
+ spb->seek(-1);
+ ERR_PRINT_ON;
+ CHECK_EQ(spb->get_position(), 1);
+ ERR_PRINT_OFF;
+ spb->seek(5);
+ ERR_PRINT_ON;
+ CHECK_EQ(spb->get_position(), 1);
+}
+
+TEST_CASE("[StreamPeerBuffer] Resize") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ CHECK_EQ(spb->get_size(), 0);
+ CHECK_EQ(spb->get_position(), 0);
+ CHECK_EQ(spb->get_available_bytes(), 0);
+
+ spb->resize(42);
+ CHECK_EQ(spb->get_size(), 42);
+ CHECK_EQ(spb->get_position(), 0);
+ CHECK_EQ(spb->get_available_bytes(), 42);
+
+ spb->seek(21);
+ CHECK_EQ(spb->get_size(), 42);
+ CHECK_EQ(spb->get_position(), 21);
+ CHECK_EQ(spb->get_available_bytes(), 21);
+}
+
+TEST_CASE("[StreamPeerBuffer] Get underlying data array") {
+ uint8_t first = 5;
+ uint8_t second = 7;
+ uint8_t third = 11;
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ spb->put_u8(first);
+ spb->put_u8(second);
+ spb->put_u8(third);
+
+ Vector<uint8_t> data_array = spb->get_data_array();
+
+ CHECK_EQ(data_array[0], first);
+ CHECK_EQ(data_array[1], second);
+ CHECK_EQ(data_array[2], third);
+}
+
+TEST_CASE("[StreamPeerBuffer] Set underlying data array") {
+ uint8_t first = 5;
+ uint8_t second = 7;
+ uint8_t third = 11;
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ spb->put_u8(1);
+ spb->put_u8(2);
+ spb->put_u8(3);
+
+ Vector<uint8_t> new_data_array;
+ new_data_array.push_back(first);
+ new_data_array.push_back(second);
+ new_data_array.push_back(third);
+
+ spb->set_data_array(new_data_array);
+
+ CHECK_EQ(spb->get_u8(), first);
+ CHECK_EQ(spb->get_u8(), second);
+ CHECK_EQ(spb->get_u8(), third);
+}
+
+TEST_CASE("[StreamPeerBuffer] Duplicate") {
+ uint8_t first = 5;
+ uint8_t second = 7;
+ uint8_t third = 11;
+
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ spb->put_u8(first);
+ spb->put_u8(second);
+ spb->put_u8(third);
+
+ Ref<StreamPeerBuffer> spb2 = spb->duplicate();
+
+ CHECK_EQ(spb2->get_u8(), first);
+ CHECK_EQ(spb2->get_u8(), second);
+ CHECK_EQ(spb2->get_u8(), third);
+}
+
+TEST_CASE("[StreamPeerBuffer] Put data with size equal to zero does nothing") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ uint8_t data = 42;
+
+ Error error = spb->put_data((const uint8_t *)&data, 0);
+
+ CHECK_EQ(error, OK);
+ CHECK_EQ(spb->get_size(), 0);
+ CHECK_EQ(spb->get_position(), 0);
+ CHECK_EQ(spb->get_available_bytes(), 0);
+}
+
+TEST_CASE("[StreamPeerBuffer] Get data with invalid size returns an error") {
+ Ref<StreamPeerBuffer> spb;
+ spb.instantiate();
+ uint8_t data = 42;
+ spb->put_u8(data);
+ spb->seek(0);
+
+ uint8_t data_out = 0;
+ Error error = spb->get_data(&data_out, 3);
+
+ CHECK_EQ(error, ERR_INVALID_PARAMETER);
+ CHECK_EQ(spb->get_size(), 1);
+ CHECK_EQ(spb->get_position(), 1);
+}
+
+} // namespace TestStreamPeerBuffer
+
+#endif // TEST_STREAM_PEER_BUFFER_H
diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h
index 301771a3de..0131f4c02a 100644
--- a/tests/core/string/test_string.h
+++ b/tests/core/string/test_string.h
@@ -1988,6 +1988,46 @@ TEST_CASE("[String] Variant ptr indexed set") {
CHECK_EQ(s, String("azcd"));
}
+TEST_CASE("[String] parse_url") {
+ String scheme, host, path, fragment;
+ int port;
+
+ SUBCASE("Typical URL") {
+ Error err = String("https://docs.godotengine.org/en/stable/").parse_url(scheme, host, port, path, fragment);
+ REQUIRE(err == OK);
+ CHECK_EQ(scheme, "https://");
+ CHECK_EQ(host, "docs.godotengine.org");
+ CHECK_EQ(port, 0);
+ CHECK_EQ(path, "/en/stable/");
+ CHECK_EQ(fragment, "");
+ }
+
+ SUBCASE("All Elements") {
+ Error err = String("https://www.example.com:8080/path/to/file.html#fragment").parse_url(scheme, host, port, path, fragment);
+ REQUIRE(err == OK);
+ CHECK_EQ(scheme, "https://");
+ CHECK_EQ(host, "www.example.com");
+ CHECK_EQ(port, 8080);
+ CHECK_EQ(path, "/path/to/file.html");
+ CHECK_EQ(fragment, "fragment");
+ }
+
+ SUBCASE("Invalid Scheme") {
+ Error err = String("http_://example.com").parse_url(scheme, host, port, path, fragment);
+ REQUIRE(err == ERR_INVALID_PARAMETER); // Host being empty is an error.
+ }
+
+ SUBCASE("Scheme vs Fragment") {
+ Error err = String("google.com/#goto=http://redirect_url/").parse_url(scheme, host, port, path, fragment);
+ REQUIRE(err == OK);
+ CHECK_EQ(scheme, "");
+ CHECK_EQ(host, "google.com");
+ CHECK_EQ(port, 0);
+ CHECK_EQ(path, "/");
+ CHECK_EQ(fragment, "goto=http://redirect_url/");
+ }
+}
+
TEST_CASE("[Stress][String] Empty via ' == String()'") {
for (int i = 0; i < 100000; ++i) {
String str = "Hello World!";
diff --git a/tests/scene/test_sky.h b/tests/scene/test_sky.h
new file mode 100644
index 0000000000..812ea9b5ad
--- /dev/null
+++ b/tests/scene/test_sky.h
@@ -0,0 +1,141 @@
+/**************************************************************************/
+/* test_sky.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_SKY_H
+#define TEST_SKY_H
+
+#include "scene/resources/sky.h"
+
+#include "tests/test_macros.h"
+
+namespace TestSky {
+
+TEST_CASE("[SceneTree][Sky] Constructor") {
+ Sky *test_sky = memnew(Sky);
+
+ CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_AUTOMATIC);
+ CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_256);
+ CHECK(test_sky->get_material().is_null());
+ memdelete(test_sky);
+}
+
+TEST_CASE("[SceneTree][Sky] Radiance size setter and getter") {
+ Sky *test_sky = memnew(Sky);
+
+ // Check default.
+ CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_256);
+
+ test_sky->set_radiance_size(Sky::RADIANCE_SIZE_1024);
+ CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_1024);
+
+ ERR_PRINT_OFF;
+ // Check setting invalid radiance size.
+ test_sky->set_radiance_size(Sky::RADIANCE_SIZE_MAX);
+ ERR_PRINT_ON;
+
+ CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_1024);
+
+ memdelete(test_sky);
+}
+
+TEST_CASE("[SceneTree][Sky] Process mode setter and getter") {
+ Sky *test_sky = memnew(Sky);
+
+ // Check default.
+ CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_AUTOMATIC);
+
+ test_sky->set_process_mode(Sky::PROCESS_MODE_INCREMENTAL);
+ CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_INCREMENTAL);
+
+ memdelete(test_sky);
+}
+
+TEST_CASE("[SceneTree][Sky] Material setter and getter") {
+ Sky *test_sky = memnew(Sky);
+ Ref<Material> material = memnew(Material);
+
+ SUBCASE("Material passed to the class should remain the same") {
+ test_sky->set_material(material);
+ CHECK(test_sky->get_material() == material);
+ }
+ SUBCASE("Material passed many times to the class should remain the same") {
+ test_sky->set_material(material);
+ test_sky->set_material(material);
+ test_sky->set_material(material);
+ CHECK(test_sky->get_material() == material);
+ }
+ SUBCASE("Material rewrite testing") {
+ Ref<Material> material1 = memnew(Material);
+ Ref<Material> material2 = memnew(Material);
+
+ test_sky->set_material(material1);
+ test_sky->set_material(material2);
+ CHECK_MESSAGE(test_sky->get_material() != material1,
+ "After rewrite, second material should be in class.");
+ CHECK_MESSAGE(test_sky->get_material() == material2,
+ "After rewrite, second material should be in class.");
+ }
+
+ SUBCASE("Assign same material to two skys") {
+ Sky *sky2 = memnew(Sky);
+
+ test_sky->set_material(material);
+ sky2->set_material(material);
+ CHECK_MESSAGE(test_sky->get_material() == sky2->get_material(),
+ "Both skys should have the same material.");
+ memdelete(sky2);
+ }
+
+ SUBCASE("Swapping materials between two skys") {
+ Sky *sky2 = memnew(Sky);
+ Ref<Material> material1 = memnew(Material);
+ Ref<Material> material2 = memnew(Material);
+
+ test_sky->set_material(material1);
+ sky2->set_material(material2);
+ CHECK(test_sky->get_material() == material1);
+ CHECK(sky2->get_material() == material2);
+
+ // Do the swap.
+ Ref<Material> temp = test_sky->get_material();
+ test_sky->set_material(sky2->get_material());
+ sky2->set_material(temp);
+
+ CHECK(test_sky->get_material() == material2);
+ CHECK(sky2->get_material() == material1);
+ memdelete(sky2);
+ }
+
+ memdelete(test_sky);
+}
+
+} // namespace TestSky
+
+#endif // TEST_SKY_H
diff --git a/tests/test_main.cpp b/tests/test_main.cpp
index 12ff3ad4bc..14a7855832 100644
--- a/tests/test_main.cpp
+++ b/tests/test_main.cpp
@@ -50,8 +50,11 @@
#include "tests/core/io/test_json.h"
#include "tests/core/io/test_json_native.h"
#include "tests/core/io/test_marshalls.h"
+#include "tests/core/io/test_packet_peer.h"
#include "tests/core/io/test_pck_packer.h"
#include "tests/core/io/test_resource.h"
+#include "tests/core/io/test_stream_peer.h"
+#include "tests/core/io/test_stream_peer_buffer.h"
#include "tests/core/io/test_xml_parser.h"
#include "tests/core/math/test_aabb.h"
#include "tests/core/math/test_astar.h"
@@ -161,6 +164,7 @@
#include "tests/scene/test_path_follow_3d.h"
#include "tests/scene/test_primitives.h"
#include "tests/scene/test_skeleton_3d.h"
+#include "tests/scene/test_sky.h"
#endif // _3D_DISABLED
#include "modules/modules_tests.gen.h"
diff --git a/thirdparty/README.md b/thirdparty/README.md
index a219839afc..0bd7c47292 100644
--- a/thirdparty/README.md
+++ b/thirdparty/README.md
@@ -652,7 +652,7 @@ Collection of single-file libraries used in Godot components.
- `bcdec.h`
* Upstream: https://github.com/iOrange/bcdec
- * Version: git (026acf98ea271045cb10713daa96ba98528badb7, 2022)
+ * Version: git (3b29f8f44466c7d59852670f82f53905cf627d48, 2024)
* License: MIT
- `clipper.{cpp,hpp}`
* Upstream: https://sourceforge.net/projects/polyclipping
diff --git a/thirdparty/misc/bcdec.h b/thirdparty/misc/bcdec.h
index 78074a0c90..275ee05d94 100644
--- a/thirdparty/misc/bcdec.h
+++ b/thirdparty/misc/bcdec.h
@@ -1,4 +1,4 @@
-/* bcdec.h - v0.96
+/* bcdec.h - v0.97
provides functions to decompress blocks of BC compressed images
written by Sergii "iOrange" Kudlai in 2022
@@ -30,6 +30,11 @@
- Split BC6H decompression function into 'half' and
'float' variants
+ Michael Schmidt (@RunDevelopment) - Found better "magic" coefficients for integer interpolation
+ of reference colors in BC1 color block, that match with
+ the floating point interpolation. This also made it faster
+ than integer division by 3!
+
bugfixes:
@linkmauve
@@ -39,6 +44,9 @@
#ifndef BCDEC_HEADER_INCLUDED
#define BCDEC_HEADER_INCLUDED
+#define BCDEC_VERSION_MAJOR 0
+#define BCDEC_VERSION_MINOR 97
+
/* if BCDEC_STATIC causes problems, try defining BCDECDEF to 'inline' or 'static inline' */
#ifndef BCDECDEF
#ifdef BCDEC_STATIC
@@ -96,6 +104,7 @@ BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBl
BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned);
BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch);
+#endif /* BCDEC_HEADER_INCLUDED */
#ifdef BCDEC_IMPLEMENTATION
@@ -110,35 +119,44 @@ static void bcdec__color_block(const void* compressedBlock, void* decompressedBl
c0 = ((unsigned short*)compressedBlock)[0];
c1 = ((unsigned short*)compressedBlock)[1];
+ /* Unpack 565 ref colors */
+ r0 = (c0 >> 11) & 0x1F;
+ g0 = (c0 >> 5) & 0x3F;
+ b0 = c0 & 0x1F;
+
+ r1 = (c1 >> 11) & 0x1F;
+ g1 = (c1 >> 5) & 0x3F;
+ b1 = c1 & 0x1F;
+
/* Expand 565 ref colors to 888 */
- r0 = (((c0 >> 11) & 0x1F) * 527 + 23) >> 6;
- g0 = (((c0 >> 5) & 0x3F) * 259 + 33) >> 6;
- b0 = ((c0 & 0x1F) * 527 + 23) >> 6;
- refColors[0] = 0xFF000000 | (b0 << 16) | (g0 << 8) | r0;
+ r = (r0 * 527 + 23) >> 6;
+ g = (g0 * 259 + 33) >> 6;
+ b = (b0 * 527 + 23) >> 6;
+ refColors[0] = 0xFF000000 | (b << 16) | (g << 8) | r;
- r1 = (((c1 >> 11) & 0x1F) * 527 + 23) >> 6;
- g1 = (((c1 >> 5) & 0x3F) * 259 + 33) >> 6;
- b1 = ((c1 & 0x1F) * 527 + 23) >> 6;
- refColors[1] = 0xFF000000 | (b1 << 16) | (g1 << 8) | r1;
+ r = (r1 * 527 + 23) >> 6;
+ g = (g1 * 259 + 33) >> 6;
+ b = (b1 * 527 + 23) >> 6;
+ refColors[1] = 0xFF000000 | (b << 16) | (g << 8) | r;
if (c0 > c1 || onlyOpaqueMode) { /* Standard BC1 mode (also BC3 color block uses ONLY this mode) */
/* color_2 = 2/3*color_0 + 1/3*color_1
color_3 = 1/3*color_0 + 2/3*color_1 */
- r = (2 * r0 + r1 + 1) / 3;
- g = (2 * g0 + g1 + 1) / 3;
- b = (2 * b0 + b1 + 1) / 3;
+ r = ((2 * r0 + r1) * 351 + 61) >> 7;
+ g = ((2 * g0 + g1) * 2763 + 1039) >> 11;
+ b = ((2 * b0 + b1) * 351 + 61) >> 7;
refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r;
- r = (r0 + 2 * r1 + 1) / 3;
- g = (g0 + 2 * g1 + 1) / 3;
- b = (b0 + 2 * b1 + 1) / 3;
+ r = ((r0 + r1 * 2) * 351 + 61) >> 7;
+ g = ((g0 + g1 * 2) * 2763 + 1039) >> 11;
+ b = ((b0 + b1 * 2) * 351 + 61) >> 7;
refColors[3] = 0xFF000000 | (b << 16) | (g << 8) | r;
} else { /* Quite rare BC1A mode */
/* color_2 = 1/2*color_0 + 1/2*color_1;
color_3 = 0; */
- r = (r0 + r1 + 1) >> 1;
- g = (g0 + g1 + 1) >> 1;
- b = (b0 + b1 + 1) >> 1;
+ r = ((r0 + r1) * 1053 + 125) >> 8;
+ g = ((g0 + g1) * 4145 + 1019) >> 11;
+ b = ((b0 + b1) * 1053 + 125) >> 8;
refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r;
refColors[3] = 0x00000000;
@@ -1269,8 +1287,6 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in
#endif /* BCDEC_IMPLEMENTATION */
-#endif /* BCDEC_HEADER_INCLUDED */
-
/* LICENSE:
This software is available under 2 licenses -- choose whichever you prefer.
@@ -1326,4 +1342,4 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
-*/ \ No newline at end of file
+*/
diff --git a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch
index 8517b277d0..6b56d60181 100644
--- a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch
+++ b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch
@@ -1,17 +1,13 @@
diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h
-index 9d2af46344..d61dbb1290 100644
+index 8026787ad4..7a54b12a38 100644
--- a/thirdparty/vulkan/vk_enum_string_helper.h
+++ b/thirdparty/vulkan/vk_enum_string_helper.h
-@@ -13,7 +13,11 @@
+@@ -13,7 +13,7 @@
#ifdef __cplusplus
#include <string>
#endif
-#include <vulkan/vulkan.h>
-+#ifdef USE_VOLK
-+ #include <volk.h>
-+#else
-+ #include <vulkan/vulkan.h>
-+#endif
++#include "drivers/vulkan/godot_vulkan.h"
static inline const char* string_VkResult(VkResult input_value) {
switch (input_value) {
case VK_SUCCESS:
diff --git a/thirdparty/vulkan/patches/VMA-use-volk.patch b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch
index e2e5ea5ad4..a6c546e3d8 100644
--- a/thirdparty/vulkan/patches/VMA-use-volk.patch
+++ b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch
@@ -1,17 +1,18 @@
diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h
-index 711f486571..e5eaa80e74 100644
+index 2307325d4e..ecb84094b9 100644
--- a/thirdparty/vulkan/vk_mem_alloc.h
+++ b/thirdparty/vulkan/vk_mem_alloc.h
-@@ -127,7 +127,11 @@ See documentation chapter: \ref statistics.
+@@ -122,12 +122,12 @@ for user-defined purpose without allocating any real GPU memory.
+ See documentation chapter: \ref statistics.
+ */
+
++#include "drivers/vulkan/godot_vulkan.h"
+
+ #ifdef __cplusplus
extern "C" {
#endif
-#include <vulkan/vulkan.h>
-+#ifdef USE_VOLK
-+ #include <volk.h>
-+#else
-+ #include <vulkan/vulkan.h>
-+#endif
#if !defined(VMA_VULKAN_VERSION)
#if defined(VK_VERSION_1_3)
diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h
index 598453e745..7a54b12a38 100644
--- a/thirdparty/vulkan/vk_enum_string_helper.h
+++ b/thirdparty/vulkan/vk_enum_string_helper.h
@@ -13,11 +13,7 @@
#ifdef __cplusplus
#include <string>
#endif
-#ifdef USE_VOLK
- #include <volk.h>
-#else
- #include <vulkan/vulkan.h>
-#endif
+#include "drivers/vulkan/godot_vulkan.h"
static inline const char* string_VkResult(VkResult input_value) {
switch (input_value) {
case VK_SUCCESS:
diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h
index b39b73b17d..ecb84094b9 100644
--- a/thirdparty/vulkan/vk_mem_alloc.h
+++ b/thirdparty/vulkan/vk_mem_alloc.h
@@ -122,16 +122,12 @@ for user-defined purpose without allocating any real GPU memory.
See documentation chapter: \ref statistics.
*/
+#include "drivers/vulkan/godot_vulkan.h"
#ifdef __cplusplus
extern "C" {
#endif
-#ifdef USE_VOLK
- #include <volk.h>
-#else
- #include <vulkan/vulkan.h>
-#endif
#if !defined(VMA_VULKAN_VERSION)
#if defined(VK_VERSION_1_3)