diff options
97 files changed, 1469 insertions, 686 deletions
diff --git a/core/os/rw_lock.h b/core/os/rw_lock.h index 7b232600b7..a232fcb1ce 100644 --- a/core/os/rw_lock.h +++ b/core/os/rw_lock.h @@ -31,39 +31,41 @@ #ifndef RW_LOCK_H #define RW_LOCK_H +#include "core/typedefs.h" + #include <shared_mutex> class RWLock { mutable std::shared_timed_mutex mutex; public: - // Lock the rwlock, block if locked by someone else - void read_lock() const { + // Lock the RWLock, block if locked by someone else. + _ALWAYS_INLINE_ void read_lock() const { mutex.lock_shared(); } - // Unlock the rwlock, let other threads continue - void read_unlock() const { + // Unlock the RWLock, let other threads continue. + _ALWAYS_INLINE_ void read_unlock() const { mutex.unlock_shared(); } // Attempt to lock the RWLock for reading. True on success, false means it can't lock. - bool read_try_lock() const { + _ALWAYS_INLINE_ bool read_try_lock() const { return mutex.try_lock_shared(); } - // Lock the rwlock, block if locked by someone else - void write_lock() { + // Lock the RWLock, block if locked by someone else. + _ALWAYS_INLINE_ void write_lock() { mutex.lock(); } - // Unlock the rwlock, let other thwrites continue - void write_unlock() { + // Unlock the RWLock, let other threads continue. + _ALWAYS_INLINE_ void write_unlock() { mutex.unlock(); } // Attempt to lock the RWLock for writing. True on success, false means it can't lock. - bool write_try_lock() { + _ALWAYS_INLINE_ bool write_try_lock() { return mutex.try_lock(); } }; @@ -72,11 +74,11 @@ class RWLockRead { const RWLock &lock; public: - RWLockRead(const RWLock &p_lock) : + _ALWAYS_INLINE_ RWLockRead(const RWLock &p_lock) : lock(p_lock) { lock.read_lock(); } - ~RWLockRead() { + _ALWAYS_INLINE_ ~RWLockRead() { lock.read_unlock(); } }; @@ -85,11 +87,11 @@ class RWLockWrite { RWLock &lock; public: - RWLockWrite(RWLock &p_lock) : + _ALWAYS_INLINE_ RWLockWrite(RWLock &p_lock) : lock(p_lock) { lock.write_lock(); } - ~RWLockWrite() { + _ALWAYS_INLINE_ ~RWLockWrite() { lock.write_unlock(); } }; diff --git a/core/os/semaphore.h b/core/os/semaphore.h index 6a290f21c6..a992a4587d 100644 --- a/core/os/semaphore.h +++ b/core/os/semaphore.h @@ -39,32 +39,33 @@ class Semaphore { private: - mutable std::mutex mutex_; - mutable std::condition_variable condition_; - mutable unsigned long count_ = 0; // Initialized as locked. + mutable std::mutex mutex; + mutable std::condition_variable condition; + mutable uint32_t count = 0; // Initialized as locked. public: _ALWAYS_INLINE_ void post() const { - std::lock_guard<decltype(mutex_)> lock(mutex_); - ++count_; - condition_.notify_one(); + std::lock_guard lock(mutex); + count++; + condition.notify_one(); } _ALWAYS_INLINE_ void wait() const { - std::unique_lock<decltype(mutex_)> lock(mutex_); - while (!count_) { // Handle spurious wake-ups. - condition_.wait(lock); + std::unique_lock lock(mutex); + while (!count) { // Handle spurious wake-ups. + condition.wait(lock); } - --count_; + count--; } _ALWAYS_INLINE_ bool try_wait() const { - std::lock_guard<decltype(mutex_)> lock(mutex_); - if (count_) { - --count_; + std::lock_guard lock(mutex); + if (count) { + count--; return true; + } else { + return false; } - return false; } }; diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index c0626dcfe4..74ee13a3d2 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -104,6 +104,13 @@ [param stream] is the [AudioStream] resource to play. [param start_offset] is the number of seconds cut off at the beginning of the audio stream, while [param end_offset] is at the ending. </description> </method> + <method name="audio_track_is_use_blend" qualifiers="const"> + <return type="bool" /> + <param index="0" name="track_idx" type="int" /> + <description> + Returns [code]true[/code] if the track at [code]idx[/code] will be blended with other animations. + </description> + </method> <method name="audio_track_set_key_end_offset"> <return type="void" /> <param index="0" name="track_idx" type="int" /> @@ -131,6 +138,14 @@ Sets the stream of the key identified by [param key_idx] to value [param stream]. The [param track_idx] must be the index of an Audio Track. </description> </method> + <method name="audio_track_set_use_blend"> + <return type="void" /> + <param index="0" name="track_idx" type="int" /> + <param index="1" name="enable" type="bool" /> + <description> + Sets whether the track will be blended with other animations. If [code]true[/code], the audio playback volume changes depending on the blend value. + </description> + </method> <method name="bezier_track_get_key_in_handle" qualifiers="const"> <return type="Vector2" /> <param index="0" name="track_idx" type="int" /> diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 8982eeedb2..25e4a4549d 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -232,6 +232,10 @@ <member name="assigned_animation" type="String" setter="set_assigned_animation" getter="get_assigned_animation"> If playing, the the current animation's key, otherwise, the animation last played. When set, this changes the animation, but will not play it unless already playing. See also [member current_animation]. </member> + <member name="audio_max_polyphony" type="int" setter="set_audio_max_polyphony" getter="get_audio_max_polyphony" default="32"> + The number of possible simultaneous sounds for each of the assigned AudioStreamPlayers. + For example, if this value is [code]32[/code] and the animation has two audio tracks, the two [AudioStreamPlayer]s assigned can play simultaneously up to [code]32[/code] voices each. + </member> <member name="autoplay" type="String" setter="set_autoplay" getter="get_autoplay" default=""""> The key of the animation to play when the scene loads. </member> diff --git a/doc/classes/AnimationTree.xml b/doc/classes/AnimationTree.xml index 3a3e8bb1fa..86562c340d 100644 --- a/doc/classes/AnimationTree.xml +++ b/doc/classes/AnimationTree.xml @@ -110,6 +110,10 @@ <member name="anim_player" type="NodePath" setter="set_animation_player" getter="get_animation_player" default="NodePath("")"> The path to the [AnimationPlayer] used for animating. </member> + <member name="audio_max_polyphony" type="int" setter="set_audio_max_polyphony" getter="get_audio_max_polyphony" default="32"> + The number of possible simultaneous sounds for each of the assigned AudioStreamPlayers. + For example, if this value is [code]32[/code] and the animation has two audio tracks, the two [AudioStreamPlayer]s assigned can play simultaneously up to [code]32[/code] voices each. + </member> <member name="process_callback" type="int" setter="set_process_callback" getter="get_process_callback" enum="AnimationTree.AnimationProcessCallback" default="1"> The process mode of this [AnimationTree]. See [enum AnimationProcessCallback] for available modes. </member> diff --git a/doc/classes/AudioStreamPlayer.xml b/doc/classes/AudioStreamPlayer.xml index 06e183f4e2..9b3a4f7bba 100644 --- a/doc/classes/AudioStreamPlayer.xml +++ b/doc/classes/AudioStreamPlayer.xml @@ -28,6 +28,12 @@ Returns the [AudioStreamPlayback] object associated with this [AudioStreamPlayer]. </description> </method> + <method name="has_stream_playback"> + <return type="bool" /> + <description> + Returns whether the [AudioStreamPlayer] can return the [AudioStreamPlayback] object or not. + </description> + </method> <method name="play"> <return type="void" /> <param index="0" name="from_position" type="float" default="0.0" /> diff --git a/doc/classes/AudioStreamPlayer2D.xml b/doc/classes/AudioStreamPlayer2D.xml index 81755b580f..18869e87cb 100644 --- a/doc/classes/AudioStreamPlayer2D.xml +++ b/doc/classes/AudioStreamPlayer2D.xml @@ -25,6 +25,12 @@ Returns the [AudioStreamPlayback] object associated with this [AudioStreamPlayer2D]. </description> </method> + <method name="has_stream_playback"> + <return type="bool" /> + <description> + Returns whether the [AudioStreamPlayer] can return the [AudioStreamPlayback] object or not. + </description> + </method> <method name="play"> <return type="void" /> <param index="0" name="from_position" type="float" default="0.0" /> diff --git a/doc/classes/AudioStreamPlayer3D.xml b/doc/classes/AudioStreamPlayer3D.xml index f711210ca1..d5a06dcad6 100644 --- a/doc/classes/AudioStreamPlayer3D.xml +++ b/doc/classes/AudioStreamPlayer3D.xml @@ -25,6 +25,12 @@ Returns the [AudioStreamPlayback] object associated with this [AudioStreamPlayer3D]. </description> </method> + <method name="has_stream_playback"> + <return type="bool" /> + <description> + Returns whether the [AudioStreamPlayer] can return the [AudioStreamPlayback] object or not. + </description> + </method> <method name="play"> <return type="void" /> <param index="0" name="from_position" type="float" default="0.0" /> diff --git a/doc/classes/NavigationServer2D.xml b/doc/classes/NavigationServer2D.xml index 95e3fded36..16f6de5238 100644 --- a/doc/classes/NavigationServer2D.xml +++ b/doc/classes/NavigationServer2D.xml @@ -41,12 +41,10 @@ <method name="agent_set_callback"> <return type="void" /> <param index="0" name="agent" type="RID" /> - <param index="1" name="object_id" type="int" /> - <param index="2" name="method" type="StringName" /> - <param index="3" name="userdata" type="Variant" default="null" /> + <param index="1" name="callback" type="Callable" /> <description> - Sets the callback [param object_id] and [param method] that gets called after each avoidance processing step for the [param agent]. The calculated [code]safe_velocity[/code] will be dispatched with a signal to the object just before the physics calculations. - [b]Note:[/b] Created callbacks are always processed independently of the SceneTree state as long as the agent is on a navigation map and not freed. To disable the dispatch of a callback from an agent use [method agent_set_callback] again with a [code]0[/code] ObjectID as the [param object_id]. + Sets the callback that gets called after each avoidance processing step for the [param agent]. The calculated [code]safe_velocity[/code] will be passed as the first parameter just before the physics calculations. + [b]Note:[/b] Created callbacks are always processed independently of the SceneTree state as long as the agent is on a navigation map and not freed. To disable the dispatch of a callback from an agent use [method agent_set_callback] again with an empty [Callable]. </description> </method> <method name="agent_set_map"> diff --git a/doc/classes/NavigationServer3D.xml b/doc/classes/NavigationServer3D.xml index fd20f780b3..340821d41e 100644 --- a/doc/classes/NavigationServer3D.xml +++ b/doc/classes/NavigationServer3D.xml @@ -41,12 +41,10 @@ <method name="agent_set_callback"> <return type="void" /> <param index="0" name="agent" type="RID" /> - <param index="1" name="object_id" type="int" /> - <param index="2" name="method" type="StringName" /> - <param index="3" name="userdata" type="Variant" default="null" /> + <param index="1" name="callback" type="Callable" /> <description> - Sets the callback [param object_id] and [param method] that gets called after each avoidance processing step for the [param agent]. The calculated [code]safe_velocity[/code] will be dispatched with a signal to the object just before the physics calculations. - [b]Note:[/b] Created callbacks are always processed independently of the SceneTree state as long as the agent is on a navigation map and not freed. To disable the dispatch of a callback from an agent use [method agent_set_callback] again with a [code]0[/code] ObjectID as the [param object_id]. + Sets the callback that gets called after each avoidance processing step for the [param agent]. The calculated [code]safe_velocity[/code] will be passed as the first parameter just before the physics calculations. + [b]Note:[/b] Created callbacks are always processed independently of the SceneTree state as long as the agent is on a navigation map and not freed. To disable the dispatch of a callback from an agent use [method agent_set_callback] again with an empty [Callable]. </description> </method> <method name="agent_set_map"> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 430f1c60fd..fa381adbb3 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -408,8 +408,11 @@ <member name="debug/gdscript/warnings/incompatible_ternary" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a ternary operator may emit values with incompatible types. </member> - <member name="debug/gdscript/warnings/int_assigned_to_enum" type="int" setter="" getter="" default="1"> - When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to assign an integer to a variable that expects an enum value. + <member name="debug/gdscript/warnings/int_as_enum_without_cast" type="int" setter="" getter="" default="1"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to use an integer as an enum without an explicit cast. + </member> + <member name="debug/gdscript/warnings/int_as_enum_without_match" type="int" setter="" getter="" default="1"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when trying to use an integer as an enum when there is no matching enum member for that numeric value. </member> <member name="debug/gdscript/warnings/integer_division" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when dividing an integer by another integer (the decimal part will be discarded). diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index 338a22c070..8dd087451c 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -1954,6 +1954,10 @@ void AnimationTrackEdit::_notification(int p_what) { get_theme_icon(SNAME("TrackDiscrete"), SNAME("EditorIcons")), get_theme_icon(SNAME("TrackCapture"), SNAME("EditorIcons")) }; + Ref<Texture2D> blend_icon[2] = { + get_theme_icon(SNAME("UseBlendEnable"), SNAME("EditorIcons")), + get_theme_icon(SNAME("UseBlendDisable"), SNAME("EditorIcons")), + }; int ofs = get_size().width - timeline->get_buttons_width(); @@ -1982,6 +1986,11 @@ void AnimationTrackEdit::_notification(int p_what) { if (!animation->track_is_compressed(track) && animation->track_get_type(track) == Animation::TYPE_VALUE) { draw_texture(update_icon, update_mode_rect.position); } + if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { + Ref<Texture2D> use_blend_icon = blend_icon[animation->audio_track_is_use_blend(track) ? 0 : 1]; + Vector2 use_blend_icon_pos = update_mode_rect.position + (update_mode_rect.size - use_blend_icon->get_size()) / 2; + draw_texture(use_blend_icon, use_blend_icon_pos); + } // Make it easier to click. update_mode_rect.position.y = 0; update_mode_rect.size.y = get_size().height; @@ -1990,13 +1999,12 @@ void AnimationTrackEdit::_notification(int p_what) { update_mode_rect.size.x += hsep / 2; if (!read_only) { - if (animation->track_get_type(track) == Animation::TYPE_VALUE) { + if (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_AUDIO) { draw_texture(down_icon, Vector2(ofs, int(get_size().height - down_icon->get_height()) / 2)); update_mode_rect.size.x += down_icon->get_width(); } else if (animation->track_get_type(track) == Animation::TYPE_BEZIER) { Ref<Texture2D> bezier_icon = get_theme_icon(SNAME("EditBezier"), SNAME("EditorIcons")); update_mode_rect.size.x += down_icon->get_width(); - update_mode_rect = Rect2(); } else { update_mode_rect = Rect2(); @@ -2439,7 +2447,11 @@ String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const { } if (update_mode_rect.has_point(p_pos)) { - return TTR("Update Mode (How this property is set)"); + if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { + return TTR("Use Blend"); + } else { + return TTR("Update Mode (How this property is set)"); + } } if (interp_mode_rect.has_point(p_pos)) { @@ -2641,9 +2653,14 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { menu->connect("id_pressed", callable_mp(this, &AnimationTrackEdit::_menu_selected)); } menu->clear(); - menu->add_icon_item(get_theme_icon(SNAME("TrackContinuous"), SNAME("EditorIcons")), TTR("Continuous"), MENU_CALL_MODE_CONTINUOUS); - menu->add_icon_item(get_theme_icon(SNAME("TrackDiscrete"), SNAME("EditorIcons")), TTR("Discrete"), MENU_CALL_MODE_DISCRETE); - menu->add_icon_item(get_theme_icon(SNAME("TrackCapture"), SNAME("EditorIcons")), TTR("Capture"), MENU_CALL_MODE_CAPTURE); + if (animation->track_get_type(track) == Animation::TYPE_AUDIO) { + menu->add_icon_item(get_theme_icon(SNAME("UseBlendEnable"), SNAME("EditorIcons")), TTR("Use Blend"), MENU_USE_BLEND_ENABLED); + menu->add_icon_item(get_theme_icon(SNAME("UseBlendDisable"), SNAME("EditorIcons")), TTR("Don't Use Blend"), MENU_USE_BLEND_DISABLED); + } else { + menu->add_icon_item(get_theme_icon(SNAME("TrackContinuous"), SNAME("EditorIcons")), TTR("Continuous"), MENU_CALL_MODE_CONTINUOUS); + menu->add_icon_item(get_theme_icon(SNAME("TrackDiscrete"), SNAME("EditorIcons")), TTR("Discrete"), MENU_CALL_MODE_DISCRETE); + menu->add_icon_item(get_theme_icon(SNAME("TrackCapture"), SNAME("EditorIcons")), TTR("Capture"), MENU_CALL_MODE_CAPTURE); + } menu->reset_size(); Vector2 popup_pos = get_screen_position() + update_mode_rect.position + Vector2(0, update_mode_rect.size.height); @@ -2662,7 +2679,7 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) { menu->add_icon_item(get_theme_icon(SNAME("InterpRaw"), SNAME("EditorIcons")), TTR("Nearest"), MENU_INTERPOLATION_NEAREST); menu->add_icon_item(get_theme_icon(SNAME("InterpLinear"), SNAME("EditorIcons")), TTR("Linear"), MENU_INTERPOLATION_LINEAR); menu->add_icon_item(get_theme_icon(SNAME("InterpCubic"), SNAME("EditorIcons")), TTR("Cubic"), MENU_INTERPOLATION_CUBIC); - // Check is angle property. + // Check whether it is angle property. AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); if (ape) { AnimationPlayer *ap = ape->get_player(); @@ -3055,6 +3072,16 @@ void AnimationTrackEdit::_menu_selected(int p_index) { emit_signal(SNAME("delete_request")); } break; + case MENU_USE_BLEND_ENABLED: + case MENU_USE_BLEND_DISABLED: { + bool use_blend = p_index == MENU_USE_BLEND_ENABLED; + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Change Animation Use Blend")); + undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", track, use_blend); + undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", track, animation->audio_track_is_use_blend(track)); + undo_redo->commit_action(); + queue_redraw(); + } break; } } @@ -3488,6 +3515,9 @@ void AnimationTrackEditor::_animation_track_remove_request(int p_track, Ref<Anim if (p_from_animation->track_get_type(idx) == Animation::TYPE_VALUE) { undo_redo->add_undo_method(p_from_animation.ptr(), "value_track_set_update_mode", idx, p_from_animation->value_track_get_update_mode(idx)); } + if (animation->track_get_type(idx) == Animation::TYPE_AUDIO) { + undo_redo->add_undo_method(animation.ptr(), "audio_track_set_use_blend", idx, animation->audio_track_is_use_blend(idx)); + } undo_redo->commit_action(); } @@ -5618,6 +5648,9 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { if (tc.track_type == Animation::TYPE_VALUE) { tc.update_mode = animation->value_track_get_update_mode(idx); } + if (tc.track_type == Animation::TYPE_AUDIO) { + tc.use_blend = animation->audio_track_is_use_blend(idx); + } tc.loop_wrap = animation->track_get_interpolation_loop_wrap(idx); tc.enabled = animation->track_is_enabled(idx); for (int i = 0; i < animation->track_get_key_count(idx); i++) { @@ -5662,6 +5695,9 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { if (track_clipboard[i].track_type == Animation::TYPE_VALUE) { undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", base_track, track_clipboard[i].update_mode); } + if (track_clipboard[i].track_type == Animation::TYPE_AUDIO) { + undo_redo->add_do_method(animation.ptr(), "audio_track_set_use_blend", base_track, track_clipboard[i].use_blend); + } for (int j = 0; j < track_clipboard[i].keys.size(); j++) { undo_redo->add_do_method(animation.ptr(), "track_insert_key", base_track, track_clipboard[i].keys[j].time, track_clipboard[i].keys[j].value, track_clipboard[i].keys[j].transition); diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index 8506d9b80d..2a59bda2a4 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -220,7 +220,9 @@ class AnimationTrackEdit : public Control { MENU_KEY_INSERT, MENU_KEY_DUPLICATE, MENU_KEY_ADD_RESET, - MENU_KEY_DELETE + MENU_KEY_DELETE, + MENU_USE_BLEND_ENABLED, + MENU_USE_BLEND_DISABLED, }; AnimationTimelineEdit *timeline = nullptr; @@ -566,6 +568,7 @@ class AnimationTrackEditor : public VBoxContainer { Animation::LoopMode loop_mode = Animation::LOOP_PINGPONG; bool loop_wrap = false; bool enabled = false; + bool use_blend = false; struct Key { float time = 0; diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 945a3ef932..32952a367d 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -548,7 +548,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da cpp_cond->set_text(0, "<" + vformat(TTR("%s Error"), source_language_name) + ">"); cpp_cond->set_text(1, oe.error); cpp_cond->set_text_alignment(0, HORIZONTAL_ALIGNMENT_LEFT); - tooltip += vformat(TTR("%s Error:"), source_language_name) + "\n"; + tooltip += vformat(TTR("%s Error:"), source_language_name) + " " + oe.error + "\n"; if (source_is_project_file) { cpp_cond->set_metadata(0, source_meta); } @@ -570,7 +570,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da cpp_source->set_text(0, "<" + vformat(TTR("%s Source"), source_language_name) + ">"); cpp_source->set_text(1, source_txt); cpp_source->set_text_alignment(0, HORIZONTAL_ALIGNMENT_LEFT); - tooltip += vformat(TTR("%s Source:"), source_language_name) + source_txt + "\n"; + tooltip += vformat(TTR("%s Source:"), source_language_name) + " " + source_txt + "\n"; // Set metadata to highlight error line in scripts. if (source_is_project_file) { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 0d5c820a3c..8b28319a1d 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -2096,14 +2096,25 @@ void EditorNode::edit_item(Object *p_object, Object *p_editing_owner) { if (!item_plugins.is_empty()) { ObjectID owner_id = p_editing_owner->get_instance_id(); + List<EditorPlugin *> to_remove; for (EditorPlugin *plugin : active_plugins[owner_id]) { if (!item_plugins.has(plugin)) { + // Remove plugins no longer used by this editing owner. + to_remove.push_back(plugin); plugin->make_visible(false); plugin->edit(nullptr); } } + for (EditorPlugin *plugin : to_remove) { + active_plugins[owner_id].erase(plugin); + } + for (EditorPlugin *plugin : item_plugins) { + if (active_plugins[owner_id].has(plugin)) { + continue; + } + for (KeyValue<ObjectID, HashSet<EditorPlugin *>> &kv : active_plugins) { if (kv.key != owner_id) { EditorPropertyResource *epres = Object::cast_to<EditorPropertyResource>(ObjectDB::get_instance(kv.key)); diff --git a/editor/icons/UseBlendDisable.svg b/editor/icons/UseBlendDisable.svg new file mode 100644 index 0000000000..449dc2d0c8 --- /dev/null +++ b/editor/icons/UseBlendDisable.svg @@ -0,0 +1 @@ +<svg enable-background="new -595.5 420.5 16 16" height="16" viewBox="-595.5 420.5 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m-591 421.5h7v14h-7z" fill="#a3e595" opacity=".5"/><g fill="none" stroke="#a3e595" stroke-linecap="square" stroke-width="2"><path d="m-585 434.5v-12"/><path d="m-590 422.5v12"/><path d="m-581.5 422.5h-12"/></g></svg> diff --git a/editor/icons/UseBlendEnable.svg b/editor/icons/UseBlendEnable.svg new file mode 100644 index 0000000000..5567b2d8c6 --- /dev/null +++ b/editor/icons/UseBlendEnable.svg @@ -0,0 +1 @@ +<svg enable-background="new -595.5 420.5 16 16" height="16" viewBox="-595.5 420.5 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m-587.5 423.244c-3.995 2.354-7 6.775-7 11.256v1h14v-1c0-4.48-3.005-8.901-7-11.256z" fill="#95c6e8" opacity=".5"/><g fill="none" stroke="#95c6e8" stroke-linecap="square" stroke-width="2"><path d="m-581.5 422.5c-6 0-12 6-12 12"/><path d="m-581.5 434.5c0-6-6-12-12-12"/></g></svg> diff --git a/editor/input_event_configuration_dialog.cpp b/editor/input_event_configuration_dialog.cpp index 48e3759e57..37c2fd5f1e 100644 --- a/editor/input_event_configuration_dialog.cpp +++ b/editor/input_event_configuration_dialog.cpp @@ -685,9 +685,9 @@ InputEventConfigurationDialog::InputEventConfigurationDialog() { // Key Mode Selection key_mode = memnew(OptionButton); - key_mode->add_item("Keycode (Latin equvialent)", KEYMODE_KEYCODE); - key_mode->add_item("Physical Keycode (poistion of US QWERTY keyboard)", KEYMODE_PHY_KEYCODE); - key_mode->add_item("Unicode (case-insencetive)", KEYMODE_UNICODE); + key_mode->add_item(TTR("Keycode (Latin Equivalent)"), KEYMODE_KEYCODE); + key_mode->add_item(TTR("Physical Keycode (Position on US QWERTY Keyboard)"), KEYMODE_PHY_KEYCODE); + key_mode->add_item(TTR("Key Label (Unicode, Case-Insensitive)"), KEYMODE_UNICODE); key_mode->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_key_mode_selected)); key_mode->hide(); additional_options_container->add_child(key_mode); diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 1b29999796..ffd9564816 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -383,11 +383,12 @@ void ShaderTextEditor::_code_complete_script(const String &p_code, List<ScriptLa List<ScriptLanguage::CodeCompletionOption> pp_defines; ShaderPreprocessor preprocessor; String code; - complete_from_path = (shader.is_valid() ? shader->get_path() : shader_inc->get_path()).get_base_dir(); + String resource_path = (shader.is_valid() ? shader->get_path() : shader_inc->get_path()); + complete_from_path = resource_path.get_base_dir(); if (!complete_from_path.ends_with("/")) { complete_from_path += "/"; } - preprocessor.preprocess(p_code, "", code, nullptr, nullptr, nullptr, nullptr, &pp_options, &pp_defines, _complete_include_paths); + preprocessor.preprocess(p_code, resource_path, code, nullptr, nullptr, nullptr, nullptr, &pp_options, &pp_defines, _complete_include_paths); complete_from_path = String(); if (pp_options.size()) { for (const ScriptLanguage::CodeCompletionOption &E : pp_options) { diff --git a/editor/project_converter_3_to_4.cpp b/editor/project_converter_3_to_4.cpp index f38c8ee39d..dfbb8e728b 100644 --- a/editor/project_converter_3_to_4.cpp +++ b/editor/project_converter_3_to_4.cpp @@ -1367,6 +1367,10 @@ static const char *project_settings_renames[][2] = { { "rendering/vram_compression/import_etc2", "rendering/textures/vram_compression/import_etc2" }, { "rendering/vram_compression/import_pvrtc", "rendering/textures/vram_compression/import_pvrtc" }, { "rendering/vram_compression/import_s3tc", "rendering/textures/vram_compression/import_s3tc" }, + { "window/size/width", "window/size/viewport_width" }, + { "window/size/height", "window/size/viewport_height" }, + { "window/size/test_width", "window/size/window_width_override" }, + { "window/size/test_height", "window/size/window_height_override" }, { nullptr, nullptr }, }; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index f6385dd132..3ad8f12192 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -160,19 +160,6 @@ static GDScriptParser::DataType make_builtin_meta_type(Variant::Type p_type) { return type; } -static StringName enum_get_value_name(const GDScriptParser::DataType p_type, int64_t p_val) { - // Check that an enum has a given value, not key. - // Make sure that implicit conversion to int64_t is sensible before calling! - HashMap<StringName, int64_t>::ConstIterator i = p_type.enum_values.begin(); - while (i) { - if (i->value == p_val) { - return i->key; - } - ++i; - } - return StringName(); -} - bool GDScriptAnalyzer::has_member_name_conflict_in_script_class(const StringName &p_member_name, const GDScriptParser::ClassNode *p_class, const GDScriptParser::Node *p_member) { if (p_class->members_indices.has(p_member_name)) { int index = p_class->members_indices[p_member_name]; @@ -1596,6 +1583,9 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi } } + if (has_specified_type && p_assignable->initializer->is_constant) { + update_const_expression_builtin_type(p_assignable->initializer, specified_type, "assign"); + } GDScriptParser::DataType initializer_type = p_assignable->initializer->get_datatype(); if (p_assignable->infer_datatype) { @@ -1630,7 +1620,7 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi downgrade_node_type_source(p_assignable->initializer); } } else if (!is_type_compatible(specified_type, initializer_type, true, p_assignable->initializer)) { - if (!is_constant && is_type_compatible(initializer_type, specified_type, true, p_assignable->initializer)) { + if (!is_constant && is_type_compatible(initializer_type, specified_type)) { mark_node_unsafe(p_assignable->initializer); p_assignable->use_conversion_assign = true; } else { @@ -1958,11 +1948,9 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) { GDScriptParser::DataType result; GDScriptParser::DataType expected_type; - bool has_expected_type = false; - - if (parser->current_function != nullptr) { + bool has_expected_type = parser->current_function != nullptr; + if (has_expected_type) { expected_type = parser->current_function->get_datatype(); - has_expected_type = true; } if (p_return->return_value != nullptr) { @@ -1976,6 +1964,9 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) { if (has_expected_type && expected_type.is_hard_type() && expected_type.kind == GDScriptParser::DataType::BUILTIN && expected_type.builtin_type == Variant::NIL) { push_error("A void function cannot return a value.", p_return); } + if (has_expected_type && expected_type.is_hard_type() && p_return->return_value->is_constant) { + update_const_expression_builtin_type(p_return->return_value, expected_type, "return"); + } result = p_return->return_value->get_datatype(); } else { // Return type is null by default. @@ -1985,24 +1976,21 @@ void GDScriptAnalyzer::resolve_return(GDScriptParser::ReturnNode *p_return) { result.is_constant = true; } - if (has_expected_type) { - expected_type.is_meta_type = false; - if (expected_type.is_hard_type()) { - if (!is_type_compatible(expected_type, result)) { - // Try other way. Okay but not safe. - if (!is_type_compatible(result, expected_type)) { - push_error(vformat(R"(Cannot return value of type "%s" because the function return type is "%s".)", result.to_string(), expected_type.to_string()), p_return); - } else { - // TODO: Add warning. - mark_node_unsafe(p_return); - } + if (has_expected_type && !expected_type.is_variant()) { + if (result.is_variant() || !result.is_hard_type()) { + mark_node_unsafe(p_return); + if (!is_type_compatible(expected_type, result, true, p_return)) { + downgrade_node_type_source(p_return); + } + } else if (!is_type_compatible(expected_type, result, true, p_return)) { + mark_node_unsafe(p_return); + if (!is_type_compatible(result, expected_type)) { + push_error(vformat(R"(Cannot return value of type "%s" because the function return type is "%s".)", result.to_string(), expected_type.to_string()), p_return); + } #ifdef DEBUG_ENABLED - } else if (expected_type.builtin_type == Variant::INT && result.builtin_type == Variant::FLOAT) { - parser->push_warning(p_return, GDScriptWarning::NARROWING_CONVERSION); - } else if (result.is_variant()) { - mark_node_unsafe(p_return); + } else if (expected_type.builtin_type == Variant::INT && result.builtin_type == Variant::FLOAT) { + parser->push_warning(p_return, GDScriptWarning::NARROWING_CONVERSION); #endif - } } } @@ -2116,6 +2104,68 @@ void GDScriptAnalyzer::reduce_array(GDScriptParser::ArrayNode *p_array) { p_array->set_datatype(arr_type); } +#ifdef DEBUG_ENABLED +static bool enum_has_value(const GDScriptParser::DataType p_type, int64_t p_value) { + for (const KeyValue<StringName, int64_t> &E : p_type.enum_values) { + if (E.value == p_value) { + return true; + } + } + return false; +} +#endif + +void GDScriptAnalyzer::update_const_expression_builtin_type(GDScriptParser::ExpressionNode *p_expression, const GDScriptParser::DataType &p_type, const char *p_usage, bool p_is_cast) { + if (p_expression->get_datatype() == p_type) { + return; + } + if (p_type.kind != GDScriptParser::DataType::BUILTIN && p_type.kind != GDScriptParser::DataType::ENUM) { + return; + } + + GDScriptParser::DataType expression_type = p_expression->get_datatype(); + bool is_enum_cast = p_is_cast && p_type.kind == GDScriptParser::DataType::ENUM && p_type.is_meta_type == false && expression_type.builtin_type == Variant::INT; + if (!is_enum_cast && !is_type_compatible(p_type, expression_type, true, p_expression)) { + push_error(vformat(R"(Cannot %s a value of type "%s" as "%s".)", p_usage, expression_type.to_string(), p_type.to_string()), p_expression); + return; + } + + GDScriptParser::DataType value_type = type_from_variant(p_expression->reduced_value, p_expression); + if (expression_type.is_variant() && !is_enum_cast && !is_type_compatible(p_type, value_type, true, p_expression)) { + push_error(vformat(R"(Cannot %s a value of type "%s" as "%s".)", p_usage, value_type.to_string(), p_type.to_string()), p_expression); + return; + } + +#ifdef DEBUG_ENABLED + if (p_type.kind == GDScriptParser::DataType::ENUM && value_type.builtin_type == Variant::INT && !enum_has_value(p_type, p_expression->reduced_value)) { + parser->push_warning(p_expression, GDScriptWarning::INT_AS_ENUM_WITHOUT_MATCH, p_usage, p_expression->reduced_value.stringify(), p_type.to_string()); + } +#endif + + if (value_type.builtin_type == p_type.builtin_type) { + p_expression->set_datatype(p_type); + return; + } + + Variant converted_to; + const Variant *converted_from = &p_expression->reduced_value; + Callable::CallError call_error; + Variant::construct(p_type.builtin_type, converted_to, &converted_from, 1, call_error); + if (call_error.error) { + push_error(vformat(R"(Failed to convert a value of type "%s" to "%s".)", value_type.to_string(), p_type.to_string()), p_expression); + return; + } + +#ifdef DEBUG_ENABLED + if (p_type.builtin_type == Variant::INT && value_type.builtin_type == Variant::FLOAT) { + parser->push_warning(p_expression, GDScriptWarning::NARROWING_CONVERSION); + } +#endif + + p_expression->reduced_value = converted_to; + p_expression->set_datatype(p_type); +} + // When an array literal is stored (or passed as function argument) to a typed context, we then assume the array is typed. // This function determines which type is that (if any). void GDScriptAnalyzer::update_array_literal_element_type(const GDScriptParser::DataType &p_base_type, GDScriptParser::ArrayNode *p_array_literal) { @@ -2182,6 +2232,10 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig update_array_literal_element_type(assignee_type, static_cast<GDScriptParser::ArrayNode *>(p_assignment->assigned_value)); } + if (p_assignment->operation == GDScriptParser::AssignmentNode::OP_NONE && assignee_type.is_hard_type() && p_assignment->assigned_value->is_constant) { + update_const_expression_builtin_type(p_assignment->assigned_value, assignee_type, "assign"); + } + GDScriptParser::DataType assigned_value_type = p_assignment->assigned_value->get_datatype(); bool assignee_is_variant = assignee_type.is_variant(); @@ -2242,7 +2296,7 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig // non-variant assignee and incompatible result mark_node_unsafe(p_assignment); if (assignee_is_hard) { - if (is_type_compatible(op_type, assignee_type, true, p_assignment->assigned_value)) { + if (is_type_compatible(op_type, assignee_type)) { // hard non-variant assignee and maybe compatible result p_assignment->use_conversion_assign = true; } else { @@ -2358,7 +2412,7 @@ void GDScriptAnalyzer::reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_o GDScriptParser::DataType test_type = right_type; test_type.is_meta_type = false; - if (!is_type_compatible(test_type, left_type, false)) { + if (!is_type_compatible(test_type, left_type)) { push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)"), p_binary_op->left_operand); p_binary_op->reduced_value = false; } else { @@ -2375,43 +2429,41 @@ void GDScriptAnalyzer::reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_o GDScriptParser::DataType result; - if (left_type.is_variant() || right_type.is_variant()) { - // Cannot infer type because one operand can be anything. - result.kind = GDScriptParser::DataType::VARIANT; - mark_node_unsafe(p_binary_op); - } else { - if (p_binary_op->variant_op < Variant::OP_MAX) { - bool valid = false; - result = get_operation_type(p_binary_op->variant_op, left_type, right_type, valid, p_binary_op); + if (p_binary_op->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST) { + GDScriptParser::DataType test_type = right_type; + test_type.is_meta_type = false; - if (!valid) { - push_error(vformat(R"(Invalid operands "%s" and "%s" for "%s" operator.)", left_type.to_string(), right_type.to_string(), Variant::get_operator_name(p_binary_op->variant_op)), p_binary_op); - } - } else { - if (p_binary_op->operation == GDScriptParser::BinaryOpNode::OP_TYPE_TEST) { - GDScriptParser::DataType test_type = right_type; - test_type.is_meta_type = false; - - if (!is_type_compatible(test_type, left_type, false)) { - // Test reverse as well to consider for subtypes. - if (!is_type_compatible(left_type, test_type, false)) { - if (left_type.is_hard_type()) { - push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)", left_type.to_string(), test_type.to_string()), p_binary_op->left_operand); - } else { - // TODO: Warning. - mark_node_unsafe(p_binary_op); - } - } - } - - // "is" operator is always a boolean anyway. - result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; - result.kind = GDScriptParser::DataType::BUILTIN; - result.builtin_type = Variant::BOOL; + if (!is_type_compatible(test_type, left_type) && !is_type_compatible(left_type, test_type)) { + if (left_type.is_hard_type()) { + push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)", left_type.to_string(), test_type.to_string()), p_binary_op->left_operand); } else { - ERR_PRINT("Parser bug: unknown binary operation."); + // TODO: Warning. + mark_node_unsafe(p_binary_op); } } + + // "is" operator is always a boolean anyway. + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::BOOL; + } else if ((p_binary_op->variant_op == Variant::OP_EQUAL || p_binary_op->variant_op == Variant::OP_NOT_EQUAL) && + ((left_type.kind == GDScriptParser::DataType::BUILTIN && left_type.builtin_type == Variant::NIL) || (right_type.kind == GDScriptParser::DataType::BUILTIN && right_type.builtin_type == Variant::NIL))) { + // "==" and "!=" operators always return a boolean when comparing to null. + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::BOOL; + } else if (left_type.is_variant() || right_type.is_variant()) { + // Cannot infer type because one operand can be anything. + result.kind = GDScriptParser::DataType::VARIANT; + mark_node_unsafe(p_binary_op); + } else if (p_binary_op->variant_op < Variant::OP_MAX) { + bool valid = false; + result = get_operation_type(p_binary_op->variant_op, left_type, right_type, valid, p_binary_op); + if (!valid) { + push_error(vformat(R"(Invalid operands "%s" and "%s" for "%s" operator.)", left_type.to_string(), right_type.to_string(), Variant::get_operator_name(p_binary_op->variant_op)), p_binary_op); + } + } else { + ERR_PRINT("Parser bug: unknown binary operation."); } p_binary_op->set_datatype(result); @@ -2557,6 +2609,11 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a } if (types_match) { + for (int i = 0; i < p_call->arguments.size(); i++) { + if (p_call->arguments[i]->is_constant) { + update_const_expression_builtin_type(p_call->arguments[i], type_from_property(info.arguments[i], true), "pass"); + } + } match = true; call_type = type_from_property(info.return_val); break; @@ -2853,67 +2910,39 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { } p_cast->set_datatype(cast_type); + if (p_cast->operand->is_constant) { + update_const_expression_builtin_type(p_cast->operand, cast_type, "cast", true); + if (cast_type.is_variant() || p_cast->operand->get_datatype() == cast_type) { + p_cast->is_constant = true; + p_cast->reduced_value = p_cast->operand->reduced_value; + } + } if (!cast_type.is_variant()) { GDScriptParser::DataType op_type = p_cast->operand->get_datatype(); - if (!op_type.is_variant()) { + if (op_type.is_variant() || !op_type.is_hard_type()) { + mark_node_unsafe(p_cast); +#ifdef DEBUG_ENABLED + if (op_type.is_variant() && !op_type.is_hard_type()) { + parser->push_warning(p_cast, GDScriptWarning::UNSAFE_CAST, cast_type.to_string()); + } +#endif + } else { bool valid = false; - bool more_informative_error = false; - if (op_type.kind == GDScriptParser::DataType::ENUM && cast_type.kind == GDScriptParser::DataType::ENUM) { - // Enum casts are compatible when value from operand exists in target enum - if (p_cast->operand->is_constant && p_cast->operand->reduced) { - if (enum_get_value_name(cast_type, p_cast->operand->reduced_value) != StringName()) { - valid = true; - } else { - valid = false; - more_informative_error = true; - push_error(vformat(R"(Invalid cast. Enum "%s" does not have value corresponding to "%s.%s" (%d).)", - cast_type.to_string(), op_type.enum_type, - enum_get_value_name(op_type, p_cast->operand->reduced_value), // Can never be null - p_cast->operand->reduced_value.operator uint64_t()), - p_cast->cast_type); - } - } else { - // Can't statically tell whether int has a corresponding enum value. Valid but dangerous! - mark_node_unsafe(p_cast); - valid = true; - } - } else if (op_type.kind == GDScriptParser::DataType::BUILTIN && op_type.builtin_type == Variant::INT && cast_type.kind == GDScriptParser::DataType::ENUM) { - // Int assignment to enum not valid when exact int assigned is known but is not an enum value - if (p_cast->operand->is_constant && p_cast->operand->reduced) { - if (enum_get_value_name(cast_type, p_cast->operand->reduced_value) != StringName()) { - valid = true; - } else { - valid = false; - more_informative_error = true; - push_error(vformat(R"(Invalid cast. Enum "%s" does not have enum value %d.)", cast_type.to_string(), p_cast->operand->reduced_value.operator uint64_t()), p_cast->cast_type); - } - } else { - // Can't statically tell whether int has a corresponding enum value. Valid but dangerous! - mark_node_unsafe(p_cast); - valid = true; - } + if (op_type.builtin_type == Variant::INT && cast_type.kind == GDScriptParser::DataType::ENUM) { + mark_node_unsafe(p_cast); + valid = true; } else if (op_type.kind == GDScriptParser::DataType::BUILTIN && cast_type.kind == GDScriptParser::DataType::BUILTIN) { valid = Variant::can_convert(op_type.builtin_type, cast_type.builtin_type); } else if (op_type.kind != GDScriptParser::DataType::BUILTIN && cast_type.kind != GDScriptParser::DataType::BUILTIN) { valid = is_type_compatible(cast_type, op_type) || is_type_compatible(op_type, cast_type); } - if (!valid && !more_informative_error) { + if (!valid) { push_error(vformat(R"(Invalid cast. Cannot convert from "%s" to "%s".)", op_type.to_string(), cast_type.to_string()), p_cast->cast_type); } } - } else { - mark_node_unsafe(p_cast); - } -#ifdef DEBUG_ENABLED - if (p_cast->operand->get_datatype().is_variant()) { - parser->push_warning(p_cast, GDScriptWarning::UNSAFE_CAST, cast_type.to_string()); - mark_node_unsafe(p_cast); } -#endif - - // TODO: Perform cast on constants. } void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) { @@ -4212,26 +4241,22 @@ bool GDScriptAnalyzer::function_signature_from_info(const MethodInfo &p_info, GD return true; } -bool GDScriptAnalyzer::validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call) { +void GDScriptAnalyzer::validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call) { List<GDScriptParser::DataType> arg_types; for (const PropertyInfo &E : p_method.arguments) { arg_types.push_back(type_from_property(E, true)); } - return validate_call_arg(arg_types, p_method.default_arguments.size(), (p_method.flags & METHOD_FLAG_VARARG) != 0, p_call); + validate_call_arg(arg_types, p_method.default_arguments.size(), (p_method.flags & METHOD_FLAG_VARARG) != 0, p_call); } -bool GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call) { - bool valid = true; - +void GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call) { if (p_call->arguments.size() < p_par_types.size() - p_default_args_count) { push_error(vformat(R"*(Too few arguments for "%s()" call. Expected at least %d but received %d.)*", p_call->function_name, p_par_types.size() - p_default_args_count, p_call->arguments.size()), p_call); - valid = false; } if (!p_is_vararg && p_call->arguments.size() > p_par_types.size()) { push_error(vformat(R"*(Too many arguments for "%s()" call. Expected at most %d but received %d.)*", p_call->function_name, p_par_types.size(), p_call->arguments.size()), p_call->arguments[p_par_types.size()]); - valid = false; } for (int i = 0; i < p_call->arguments.size(); i++) { @@ -4240,9 +4265,13 @@ bool GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p break; } GDScriptParser::DataType par_type = p_par_types[i]; + + if (par_type.is_hard_type() && p_call->arguments[i]->is_constant) { + update_const_expression_builtin_type(p_call->arguments[i], par_type, "pass"); + } GDScriptParser::DataType arg_type = p_call->arguments[i]->get_datatype(); - if (arg_type.is_variant()) { + if ((arg_type.is_variant() || !arg_type.is_hard_type()) && !(par_type.is_hard_type() && par_type.is_variant())) { // Argument can be anything, so this is unsafe. mark_node_unsafe(p_call->arguments[i]); } else if (par_type.is_hard_type() && !is_type_compatible(par_type, arg_type, true)) { @@ -4252,17 +4281,13 @@ bool GDScriptAnalyzer::validate_call_arg(const List<GDScriptParser::DataType> &p push_error(vformat(R"*(Invalid argument for "%s()" function: argument %d should be "%s" but is "%s".)*", p_call->function_name, i + 1, par_type.to_string(), arg_type.to_string()), p_call->arguments[i]); - valid = false; } #ifdef DEBUG_ENABLED - } else { - if (par_type.kind == GDScriptParser::DataType::BUILTIN && par_type.builtin_type == Variant::INT && arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == Variant::FLOAT) { - parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name); - } + } else if (par_type.kind == GDScriptParser::DataType::BUILTIN && par_type.builtin_type == Variant::INT && arg_type.kind == GDScriptParser::DataType::BUILTIN && arg_type.builtin_type == Variant::FLOAT) { + parser->push_warning(p_call, GDScriptWarning::NARROWING_CONVERSION, p_call->function_name); #endif } } - return valid; } #ifdef DEBUG_ENABLED @@ -4414,7 +4439,7 @@ bool GDScriptAnalyzer::is_type_compatible(const GDScriptParser::DataType &p_targ if (p_source.kind == GDScriptParser::DataType::BUILTIN && p_source.builtin_type == Variant::INT) { #ifdef DEBUG_ENABLED if (p_source_node) { - parser->push_warning(p_source_node, GDScriptWarning::INT_ASSIGNED_TO_ENUM); + parser->push_warning(p_source_node, GDScriptWarning::INT_AS_ENUM_WITHOUT_CAST); } #endif return true; diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index 5397be33f0..cd2c4c6569 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -112,10 +112,11 @@ class GDScriptAnalyzer { GDScriptParser::DataType make_global_class_meta_type(const StringName &p_class_name, const GDScriptParser::Node *p_source); bool get_function_signature(GDScriptParser::Node *p_source, bool p_is_constructor, GDScriptParser::DataType base_type, const StringName &p_function, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); bool function_signature_from_info(const MethodInfo &p_info, GDScriptParser::DataType &r_return_type, List<GDScriptParser::DataType> &r_par_types, int &r_default_arg_count, bool &r_static, bool &r_vararg); - bool validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call); - bool validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call); + void validate_call_arg(const List<GDScriptParser::DataType> &p_par_types, int p_default_args_count, bool p_is_vararg, const GDScriptParser::CallNode *p_call); + void validate_call_arg(const MethodInfo &p_method, const GDScriptParser::CallNode *p_call); GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, const GDScriptParser::DataType &p_b, bool &r_valid, const GDScriptParser::Node *p_source); GDScriptParser::DataType get_operation_type(Variant::Operator p_operation, const GDScriptParser::DataType &p_a, bool &r_valid, const GDScriptParser::Node *p_source); + void update_const_expression_builtin_type(GDScriptParser::ExpressionNode *p_expression, const GDScriptParser::DataType &p_type, const char *p_usage, bool p_is_cast = false); void update_array_literal_element_type(const GDScriptParser::DataType &p_base_type, GDScriptParser::ArrayNode *p_array_literal); bool is_type_compatible(const GDScriptParser::DataType &p_target, const GDScriptParser::DataType &p_source, bool p_allow_implicit_conversion = false, const GDScriptParser::Node *p_source_node = nullptr); void push_error(const String &p_message, const GDScriptParser::Node *p_origin = nullptr); diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index a6cbb7f6ae..024fed8517 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -148,9 +148,13 @@ String GDScriptWarning::get_message() const { CHECK_SYMBOLS(3); return vformat(R"(The %s '%s' has the same name as a %s.)", symbols[0], symbols[1], symbols[2]); } - case INT_ASSIGNED_TO_ENUM: { + case INT_AS_ENUM_WITHOUT_CAST: { return "Integer used when an enum value is expected. If this is intended cast the integer to the enum type."; } + case INT_AS_ENUM_WITHOUT_MATCH: { + CHECK_SYMBOLS(3); + return vformat(R"(Cannot %s %s as Enum "%s": no enum member has matching value.)", symbols[0], symbols[1], symbols[2]); + } break; case STATIC_CALLED_ON_INSTANCE: { CHECK_SYMBOLS(2); return vformat(R"(The function '%s()' is a static function but was called from an instance. Instead, it should be directly called from the type: '%s.%s()'.)", symbols[0], symbols[1], symbols[0]); @@ -221,7 +225,8 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "REDUNDANT_AWAIT", "EMPTY_FILE", "SHADOWED_GLOBAL_IDENTIFIER", - "INT_ASSIGNED_TO_ENUM", + "INT_AS_ENUM_WITHOUT_CAST", + "INT_AS_ENUM_WITHOUT_MATCH", "STATIC_CALLED_ON_INSTANCE", "CONFUSABLE_IDENTIFIER", }; diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index b485f02b9c..7492972c1a 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -76,7 +76,8 @@ public: REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine). EMPTY_FILE, // A script file is empty. SHADOWED_GLOBAL_IDENTIFIER, // A global class or function has the same name as variable. - INT_ASSIGNED_TO_ENUM, // An integer value was assigned to an enum-typed variable without casting. + INT_AS_ENUM_WITHOUT_CAST, // An integer value was used as an enum value without casting. + INT_AS_ENUM_WITHOUT_MATCH, // An integer value was used as an enum value without matching enum member. STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. CONFUSABLE_IDENTIFIER, // The identifier contains misleading characters that can be confused. E.g. "usеr" (has Cyrillic "е" instead of Latin "e"). WARNING_MAX, diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.gd b/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.gd new file mode 100644 index 0000000000..1b22d173c7 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.gd @@ -0,0 +1,3 @@ +func test(): + var var_color: String = Color.RED + print('not ok') diff --git a/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.out b/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.out new file mode 100644 index 0000000000..cc4b1d86bf --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/errors/assymetric_assignment_bad.out @@ -0,0 +1,2 @@ +GDTEST_ANALYZER_ERROR +Cannot assign a value of type "Color" as "String". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_enum.out b/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_enum.out deleted file mode 100644 index 3a8d2a205a..0000000000 --- a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_enum.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_ANALYZER_ERROR -Invalid cast. Enum "cast_enum_bad_enum.gd::MyEnum" does not have value corresponding to "MyOtherEnum.OTHER_ENUM_VALUE_3" (2). diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_int.out b/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_int.out deleted file mode 100644 index bc0d8b7834..0000000000 --- a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_int.out +++ /dev/null @@ -1,2 +0,0 @@ -GDTEST_ANALYZER_ERROR -Invalid cast. Enum "cast_enum_bad_int.gd::MyEnum" does not have enum value 2. diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out index 02c4633586..84958f1aa2 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_assign_with_wrong_enum_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Value of type "enum_class_var_assign_with_wrong_enum_type.gd::MyOtherEnum" cannot be assigned to a variable of type "enum_class_var_assign_with_wrong_enum_type.gd::MyEnum". +Cannot assign a value of type "enum_class_var_assign_with_wrong_enum_type.gd::MyOtherEnum" as "enum_class_var_assign_with_wrong_enum_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out index 441cccbf7b..e294f3496a 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_class_var_init_with_wrong_enum_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot assign a value of type enum_class_var_init_with_wrong_enum_type.gd::MyOtherEnum to variable "class_var" with specified type enum_class_var_init_with_wrong_enum_type.gd::MyEnum. +Cannot assign a value of type "enum_class_var_init_with_wrong_enum_type.gd::MyOtherEnum" as "enum_class_var_init_with_wrong_enum_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_function_parameter_wrong_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_function_parameter_wrong_type.out index e85f7d6f9f..a91189e2dd 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_function_parameter_wrong_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_function_parameter_wrong_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Invalid argument for "enum_func()" function: argument 1 should be "enum_function_parameter_wrong_type.gd::MyEnum" but is "enum_function_parameter_wrong_type.gd::MyOtherEnum". +Cannot pass a value of type "enum_function_parameter_wrong_type.gd::MyOtherEnum" as "enum_function_parameter_wrong_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_function_return_wrong_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_function_return_wrong_type.out index f7ea3267fa..6b4eba3740 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_function_return_wrong_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_function_return_wrong_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot return value of type "enum_function_return_wrong_type.gd::MyOtherEnum" because the function return type is "enum_function_return_wrong_type.gd::MyEnum". +Cannot return a value of type "enum_function_return_wrong_type.gd::MyOtherEnum" as "enum_function_return_wrong_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_outer_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_outer_with_wrong_enum_type.out index 38df5a0cd8..616358bb61 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_outer_with_wrong_enum_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_outer_with_wrong_enum_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Value of type "enum_local_var_assign_outer_with_wrong_enum_type.gd::InnerClass::MyEnum" cannot be assigned to a variable of type "enum_local_var_assign_outer_with_wrong_enum_type.gd::MyEnum". +Cannot assign a value of type "enum_local_var_assign_outer_with_wrong_enum_type.gd::InnerClass::MyEnum" as "enum_local_var_assign_outer_with_wrong_enum_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out index 2adcbd9edf..af3dde663f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_assign_with_wrong_enum_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Value of type "enum_local_var_assign_with_wrong_enum_type.gd::MyOtherEnum" cannot be assigned to a variable of type "enum_local_var_assign_with_wrong_enum_type.gd::MyEnum". +Cannot assign a value of type "enum_local_var_assign_with_wrong_enum_type.gd::MyOtherEnum" as "enum_local_var_assign_with_wrong_enum_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out index 331113dd30..781b0bc85f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_local_var_init_with_wrong_enum_type.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot assign a value of type enum_local_var_init_with_wrong_enum_type.gd::MyOtherEnum to variable "local_var" with specified type enum_local_var_init_with_wrong_enum_type.gd::MyEnum. +Cannot assign a value of type "enum_local_var_init_with_wrong_enum_type.gd::MyOtherEnum" as "enum_local_var_init_with_wrong_enum_type.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_preload_unnamed_assign_to_named.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_preload_unnamed_assign_to_named.out index 6298c026b4..e8c7f86c4f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_preload_unnamed_assign_to_named.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_preload_unnamed_assign_to_named.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Value of type "enum_value_from_parent.gd::<anonymous enum>" cannot be assigned to a variable of type "enum_preload_unnamed_assign_to_named.gd::MyEnum". +Cannot assign a value of type "enum_value_from_parent.gd::<anonymous enum>" as "enum_preload_unnamed_assign_to_named.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/enum_unnamed_assign_to_named.out b/modules/gdscript/tests/scripts/analyzer/errors/enum_unnamed_assign_to_named.out index b70121ed81..fb18c94d0b 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/enum_unnamed_assign_to_named.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/enum_unnamed_assign_to_named.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot assign a value of type enum_unnamed_assign_to_named.gd::<anonymous enum> to variable "local_var" with specified type enum_unnamed_assign_to_named.gd::MyEnum. +Cannot assign a value of type "enum_unnamed_assign_to_named.gd::<anonymous enum>" as "enum_unnamed_assign_to_named.gd::MyEnum". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/lambda_wrong_return.out b/modules/gdscript/tests/scripts/analyzer/errors/lambda_wrong_return.out index 53e2b012e6..69af0984f7 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/lambda_wrong_return.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/lambda_wrong_return.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Cannot return value of type "String" because the function return type is "int". +Cannot return a value of type "String" as "int". diff --git a/modules/gdscript/tests/scripts/analyzer/errors/preload_enum_error.out b/modules/gdscript/tests/scripts/analyzer/errors/preload_enum_error.out index 5e3c446bf6..08a973503f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/preload_enum_error.out +++ b/modules/gdscript/tests/scripts/analyzer/errors/preload_enum_error.out @@ -1,2 +1,2 @@ GDTEST_ANALYZER_ERROR -Value of type "enum_from_outer.gd::Named" cannot be assigned to a variable of type "preload_enum_error.gd::LocalNamed". +Cannot assign a value of type "enum_from_outer.gd::Named" as "preload_enum_error.gd::LocalNamed". diff --git a/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd new file mode 100644 index 0000000000..efd8ad6edb --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd @@ -0,0 +1,16 @@ +const const_color: Color = 'red' + +func func_color(arg_color: Color = 'blue') -> bool: + return arg_color == Color.BLUE + +@warning_ignore("assert_always_true") +func test(): + assert(const_color == Color.RED) + + assert(func_color() == true) + assert(func_color('blue') == true) + + var var_color: Color = 'green' + assert(var_color == Color.GREEN) + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.out b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd new file mode 100644 index 0000000000..bed9dd0e96 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd @@ -0,0 +1,24 @@ +const const_float_int: float = 19 +const const_float_plus: float = 12 + 22 +const const_float_cast: float = 76 as float + +const const_packed_empty: PackedFloat64Array = [] +const const_packed_ints: PackedFloat64Array = [52] + +@warning_ignore("assert_always_true") +func test(): + assert(typeof(const_float_int) == TYPE_FLOAT) + assert(str(const_float_int) == '19') + assert(typeof(const_float_plus) == TYPE_FLOAT) + assert(str(const_float_plus) == '34') + assert(typeof(const_float_cast) == TYPE_FLOAT) + assert(str(const_float_cast) == '76') + + assert(typeof(const_packed_empty) == TYPE_PACKED_FLOAT64_ARRAY) + assert(str(const_packed_empty) == '[]') + assert(typeof(const_packed_ints) == TYPE_PACKED_FLOAT64_ARRAY) + assert(str(const_packed_ints) == '[52]') + assert(typeof(const_packed_ints[0]) == TYPE_FLOAT) + assert(str(const_packed_ints[0]) == '52') + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/const_conversions.out b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd new file mode 100644 index 0000000000..48a804ff54 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.gd @@ -0,0 +1,32 @@ +func variant() -> Variant: return null + +var member_weak = variant() +var member_typed: Variant = variant() +var member_inferred := variant() + +func param_weak(param = variant()) -> void: print(param) +func param_typed(param: Variant = variant()) -> void: print(param) +func param_inferred(param := variant()) -> void: print(param) + +func return_untyped(): return variant() +func return_typed() -> Variant: return variant() + +@warning_ignore("unused_variable") +func test() -> void: + var weak = variant() + var typed: Variant = variant() + var inferred := variant() + + weak = variant() + typed = variant() + inferred = variant() + + param_weak(typed) + param_typed(typed) + param_inferred(typed) + + if typed == null: pass + if typed != null: pass + if typed is Node: pass + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/hard_variants.out b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.out new file mode 100644 index 0000000000..08491efa07 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/hard_variants.out @@ -0,0 +1,5 @@ +GDTEST_OK +<null> +<null> +<null> +ok diff --git a/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd new file mode 100644 index 0000000000..0b1576e66e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd @@ -0,0 +1,34 @@ +func convert_literal_int_to_float() -> float: return 76 +func convert_arg_int_to_float(arg: int) -> float: return arg +func convert_var_int_to_float() -> float: var number := 59; return number + +func convert_literal_array_to_packed() -> PackedStringArray: return ['46'] +func convert_arg_array_to_packed(arg: Array) -> PackedStringArray: return arg +func convert_var_array_to_packed() -> PackedStringArray: var array := ['79']; return array + +func test(): + var converted_literal_int := convert_literal_int_to_float() + assert(typeof(converted_literal_int) == TYPE_FLOAT) + assert(converted_literal_int == 76.0) + + var converted_arg_int := convert_arg_int_to_float(36) + assert(typeof(converted_arg_int) == TYPE_FLOAT) + assert(converted_arg_int == 36.0) + + var converted_var_int := convert_var_int_to_float() + assert(typeof(converted_var_int) == TYPE_FLOAT) + assert(converted_var_int == 59.0) + + var converted_literal_array := convert_literal_array_to_packed() + assert(typeof(converted_literal_array) == TYPE_PACKED_STRING_ARRAY) + assert(str(converted_literal_array) == '["46"]') + + var converted_arg_array := convert_arg_array_to_packed(['91']) + assert(typeof(converted_arg_array) == TYPE_PACKED_STRING_ARRAY) + assert(str(converted_arg_array) == '["91"]') + + var converted_var_array := convert_var_array_to_packed() + assert(typeof(converted_var_array) == TYPE_PACKED_STRING_ARRAY) + assert(str(converted_var_array) == '["79"]') + + print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/return_conversions.out b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.out new file mode 100644 index 0000000000..1b47ed10dc --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.out @@ -0,0 +1,2 @@ +GDTEST_OK +ok diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_enum.gd b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_enum.gd index 71616ea3af..71616ea3af 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_enum.gd +++ b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_enum.gd diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_enum.out b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_enum.out new file mode 100644 index 0000000000..6e086a0918 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_enum.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 5 +>> INT_AS_ENUM_WITHOUT_MATCH +>> Cannot cast 2 as Enum "cast_enum_bad_enum.gd::MyEnum": no enum member has matching value. +2 diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_int.gd b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_int.gd index 60a31fb318..60a31fb318 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/cast_enum_bad_int.gd +++ b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_int.gd diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_int.out b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_int.out new file mode 100644 index 0000000000..c19d57f98e --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/cast_enum_bad_int.out @@ -0,0 +1,6 @@ +GDTEST_OK +>> WARNING +>> Line: 4 +>> INT_AS_ENUM_WITHOUT_MATCH +>> Cannot cast 2 as Enum "cast_enum_bad_int.gd::MyEnum": no enum member has matching value. +2 diff --git a/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out index eef13bbff8..b8e243769f 100644 --- a/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out +++ b/modules/gdscript/tests/scripts/parser/warnings/enum_assign_int_without_casting.out @@ -1,19 +1,19 @@ GDTEST_OK >> WARNING >> Line: 5 ->> INT_ASSIGNED_TO_ENUM +>> INT_AS_ENUM_WITHOUT_CAST >> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. >> WARNING >> Line: 9 ->> INT_ASSIGNED_TO_ENUM +>> INT_AS_ENUM_WITHOUT_CAST >> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. >> WARNING >> Line: 12 ->> INT_ASSIGNED_TO_ENUM +>> INT_AS_ENUM_WITHOUT_CAST >> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. >> WARNING >> Line: 14 ->> INT_ASSIGNED_TO_ENUM +>> INT_AS_ENUM_WITHOUT_CAST >> Integer used when an enum value is expected. If this is intended cast the integer to the enum type. 0 1 diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs index 0e92f4331d..af83cc24bf 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs @@ -50,6 +50,15 @@ namespace Godot } /// <summary> + /// The volume of this <see cref="Aabb"/>. + /// See also <see cref="HasVolume"/>. + /// </summary> + public readonly real_t Volume + { + get { return _size.X * _size.Y * _size.Z; } + } + + /// <summary> /// Returns an <see cref="Aabb"/> with equivalent position and size, modified so that /// the most-negative corner is the origin and the size is positive. /// </summary> @@ -312,15 +321,6 @@ namespace Godot } /// <summary> - /// Returns the volume of the <see cref="Aabb"/>. - /// </summary> - /// <returns>The volume.</returns> - public readonly real_t GetVolume() - { - return _size.X * _size.Y * _size.Z; - } - - /// <summary> /// Returns a copy of the <see cref="Aabb"/> grown a given amount of units towards all the sides. /// </summary> /// <param name="by">The amount to grow by.</param> @@ -383,7 +383,7 @@ namespace Godot /// Returns <see langword="true"/> if the <see cref="Aabb"/> has /// area, and <see langword="false"/> if the <see cref="Aabb"/> /// is linear, empty, or has a negative <see cref="Size"/>. - /// See also <see cref="GetVolume"/>. + /// See also <see cref="Volume"/>. /// </summary> /// <returns> /// A <see langword="bool"/> for whether or not the <see cref="Aabb"/> has volume. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs index f4646d1d3c..a61c5403b9 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs @@ -189,10 +189,15 @@ namespace Godot.Collections /// <summary> /// Resizes this <see cref="Array"/> to the given size. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="newSize">The new size of the array.</param> /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns> public Error Resize(int newSize) { + ThrowIfReadOnly(); + var self = (godot_array)NativeValue; return NativeFuncs.godotsharp_array_resize(ref self, newSize); } @@ -200,8 +205,13 @@ namespace Godot.Collections /// <summary> /// Shuffles the contents of this <see cref="Array"/> into a random order. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> public void Shuffle() { + ThrowIfReadOnly(); + var self = (godot_array)NativeValue; NativeFuncs.godotsharp_array_shuffle(ref self); } @@ -240,6 +250,9 @@ namespace Godot.Collections /// <summary> /// Returns the item at the given <paramref name="index"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The property is assigned and the array is read-only. + /// </exception> /// <value>The <see cref="Variant"/> item at the given <paramref name="index"/>.</value> public unsafe Variant this[int index] { @@ -250,8 +263,11 @@ namespace Godot.Collections } set { + ThrowIfReadOnly(); + if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); + var self = (godot_array)NativeValue; godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self); godot_variant* itemPtr = &ptrw[index]; @@ -264,9 +280,14 @@ namespace Godot.Collections /// Adds an item to the end of this <see cref="Array"/>. /// This is the same as <c>append</c> or <c>push_back</c> in GDScript. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="item">The <see cref="Variant"/> item to add.</param> public void Add(Variant item) { + ThrowIfReadOnly(); + godot_variant variantValue = (godot_variant)item.NativeVar; var self = (godot_array)NativeValue; _ = NativeFuncs.godotsharp_array_add(ref self, variantValue); @@ -282,6 +303,9 @@ namespace Godot.Collections /// <summary> /// Erases all items from this <see cref="Array"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> public void Clear() => Resize(0); /// <summary> @@ -303,10 +327,15 @@ namespace Godot.Collections /// or the position at the end of the array. /// Existing items will be moved to the right. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="index">The index to insert at.</param> /// <param name="item">The <see cref="Variant"/> item to insert.</param> public void Insert(int index, Variant item) { + ThrowIfReadOnly(); + if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); @@ -319,9 +348,14 @@ namespace Godot.Collections /// Removes the first occurrence of the specified <paramref name="item"/> /// from this <see cref="Array"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="item">The value to remove.</param> public bool Remove(Variant item) { + ThrowIfReadOnly(); + int index = IndexOf(item); if (index >= 0) { @@ -335,9 +369,14 @@ namespace Godot.Collections /// <summary> /// Removes an element from this <see cref="Array"/> by index. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="index">The index of the element to remove.</param> public void RemoveAt(int index) { + ThrowIfReadOnly(); + if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); @@ -358,7 +397,28 @@ namespace Godot.Collections object ICollection.SyncRoot => false; - bool ICollection<Variant>.IsReadOnly => false; + /// <summary> + /// Returns <see langword="true"/> if the array is read-only. + /// See <see cref="MakeReadOnly"/>. + /// </summary> + public bool IsReadOnly => NativeValue.DangerousSelfRef.IsReadOnly; + + /// <summary> + /// Makes the <see cref="Array"/> read-only, i.e. disabled modying of the + /// array's elements. Does not apply to nested content, e.g. content of + /// nested arrays. + /// </summary> + public void MakeReadOnly() + { + if (IsReadOnly) + { + // Avoid interop call when the array is already read-only. + return; + } + + var self = (godot_array)NativeValue; + NativeFuncs.godotsharp_array_make_read_only(ref self); + } /// <summary> /// Copies the elements of this <see cref="Array"/> to the given @@ -472,6 +532,14 @@ namespace Godot.Collections { elem = NativeValue.DangerousSelfRef.Elements[index]; } + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("Array instance is read-only."); + } + } } internal interface IGenericGodotArray @@ -592,6 +660,9 @@ namespace Godot.Collections /// <summary> /// Resizes this <see cref="Array{T}"/> to the given size. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="newSize">The new size of the array.</param> /// <returns><see cref="Error.Ok"/> if successful, or an error code.</returns> public Error Resize(int newSize) @@ -602,6 +673,9 @@ namespace Godot.Collections /// <summary> /// Shuffles the contents of this <see cref="Array{T}"/> into a random order. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> public void Shuffle() { _underlyingArray.Shuffle(); @@ -634,6 +708,9 @@ namespace Godot.Collections /// <summary> /// Returns the value at the given <paramref name="index"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The property is assigned and the array is read-only. + /// </exception> /// <value>The value at the given <paramref name="index"/>.</value> public unsafe T this[int index] { @@ -644,8 +721,11 @@ namespace Godot.Collections } set { + ThrowIfReadOnly(); + if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); + var self = (godot_array)_underlyingArray.NativeValue; godot_variant* ptrw = NativeFuncs.godotsharp_array_ptrw(ref self); godot_variant* itemPtr = &ptrw[index]; @@ -673,10 +753,15 @@ namespace Godot.Collections /// or the position at the end of the array. /// Existing items will be moved to the right. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="index">The index to insert at.</param> /// <param name="item">The item to insert.</param> public void Insert(int index, T item) { + ThrowIfReadOnly(); + if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); @@ -688,6 +773,9 @@ namespace Godot.Collections /// <summary> /// Removes an element from this <see cref="Array{T}"/> by index. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="index">The index of the element to remove.</param> public void RemoveAt(int index) { @@ -703,16 +791,35 @@ namespace Godot.Collections /// <returns>The number of elements.</returns> public int Count => _underlyingArray.Count; - bool ICollection<T>.IsReadOnly => false; + /// <summary> + /// Returns <see langword="true"/> if the array is read-only. + /// See <see cref="MakeReadOnly"/>. + /// </summary> + public bool IsReadOnly => _underlyingArray.IsReadOnly; + + /// <summary> + /// Makes the <see cref="Array{T}"/> read-only, i.e. disabled modying of the + /// array's elements. Does not apply to nested content, e.g. content of + /// nested arrays. + /// </summary> + public void MakeReadOnly() + { + _underlyingArray.MakeReadOnly(); + } /// <summary> /// Adds an item to the end of this <see cref="Array{T}"/>. /// This is the same as <c>append</c> or <c>push_back</c> in GDScript. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="item">The item to add.</param> /// <returns>The new size after adding the item.</returns> public void Add(T item) { + ThrowIfReadOnly(); + using var variantValue = VariantUtils.CreateFrom(item); var self = (godot_array)_underlyingArray.NativeValue; _ = NativeFuncs.godotsharp_array_add(ref self, variantValue); @@ -721,6 +828,9 @@ namespace Godot.Collections /// <summary> /// Erases all items from this <see cref="Array{T}"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> public void Clear() { _underlyingArray.Clear(); @@ -769,10 +879,15 @@ namespace Godot.Collections /// Removes the first occurrence of the specified value /// from this <see cref="Array{T}"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The array is read-only. + /// </exception> /// <param name="item">The value to remove.</param> /// <returns>A <see langword="bool"/> indicating success or failure.</returns> public bool Remove(T item) { + ThrowIfReadOnly(); + int index = IndexOf(item); if (index >= 0) { @@ -812,5 +927,13 @@ namespace Godot.Collections [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Array<T>(Variant from) => from.AsGodotArray<T>(); + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("Array instance is read-only."); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs index 859b47c276..ec2728140e 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Bridge/ScriptManagerBridge.cs @@ -243,9 +243,33 @@ namespace Godot.Bridge if (wrapperType == null) { - wrapperType = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor")? - .GetType("Godot." + nativeTypeNameStr); + wrapperType = GetTypeByGodotClassAttr(typeof(GodotObject).Assembly, nativeTypeNameStr); + } + + if (wrapperType == null) + { + var editorAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "GodotSharpEditor"); + wrapperType = editorAssembly?.GetType("Godot." + nativeTypeNameStr); + + if (wrapperType == null) + { + wrapperType = GetTypeByGodotClassAttr(editorAssembly, nativeTypeNameStr); + } + } + + static Type? GetTypeByGodotClassAttr(Assembly assembly, string nativeTypeNameStr) + { + var types = assembly.GetTypes(); + foreach (var type in types) + { + var attr = type.GetCustomAttribute<GodotClassNameAttribute>(); + if (attr?.Name == nativeTypeNameStr) + { + return type; + } + } + return null; } static bool IsStatic(Type type) => type.IsAbstract && type.IsSealed; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs index fa304c1b94..923b2adafd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs @@ -93,10 +93,15 @@ namespace Godot.Collections /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/> /// is <see langword="true"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <param name="dictionary">Dictionary to copy entries from.</param> /// <param name="overwrite">If duplicate keys should be copied over as well.</param> public void Merge(Dictionary dictionary, bool overwrite = false) { + ThrowIfReadOnly(); + var self = (godot_dictionary)NativeValue; var other = (godot_dictionary)dictionary.NativeValue; NativeFuncs.godotsharp_dictionary_merge(ref self, in other, overwrite.ToGodotBool()); @@ -175,8 +180,12 @@ namespace Godot.Collections /// <summary> /// Returns the value at the given <paramref name="key"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The property is assigned and the dictionary is read-only. + /// </exception> /// <exception cref="KeyNotFoundException"> - /// An entry for <paramref name="key"/> does not exist in the dictionary. + /// The property is retrieved and an entry for <paramref name="key"/> + /// does not exist in the dictionary. /// </exception> /// <value>The value at the given <paramref name="key"/>.</value> public Variant this[Variant key] @@ -197,6 +206,8 @@ namespace Godot.Collections } set { + ThrowIfReadOnly(); + var self = (godot_dictionary)NativeValue; NativeFuncs.godotsharp_dictionary_set_value(ref self, (godot_variant)key.NativeVar, (godot_variant)value.NativeVar); @@ -207,6 +218,9 @@ namespace Godot.Collections /// Adds an value <paramref name="value"/> at key <paramref name="key"/> /// to this <see cref="Dictionary"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <exception cref="ArgumentException"> /// An entry for <paramref name="key"/> already exists in the dictionary. /// </exception> @@ -214,6 +228,8 @@ namespace Godot.Collections /// <param name="value">The value to add.</param> public void Add(Variant key, Variant value) { + ThrowIfReadOnly(); + var variantKey = (godot_variant)key.NativeVar; var self = (godot_dictionary)NativeValue; @@ -230,8 +246,13 @@ namespace Godot.Collections /// <summary> /// Clears the dictionary, removing all entries from it. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> public void Clear() { + ThrowIfReadOnly(); + var self = (godot_dictionary)NativeValue; NativeFuncs.godotsharp_dictionary_clear(ref self); } @@ -267,15 +288,22 @@ namespace Godot.Collections /// <summary> /// Removes an element from this <see cref="Dictionary"/> by key. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <param name="key">The key of the element to remove.</param> public bool Remove(Variant key) { + ThrowIfReadOnly(); + var self = (godot_dictionary)NativeValue; return NativeFuncs.godotsharp_dictionary_remove_key(ref self, (godot_variant)key.NativeVar).ToBool(); } bool ICollection<KeyValuePair<Variant, Variant>>.Remove(KeyValuePair<Variant, Variant> item) { + ThrowIfReadOnly(); + godot_variant variantKey = (godot_variant)item.Key.NativeVar; var self = (godot_dictionary)NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, @@ -311,7 +339,28 @@ namespace Godot.Collections } } - bool ICollection<KeyValuePair<Variant, Variant>>.IsReadOnly => false; + /// <summary> + /// Returns <see langword="true"/> if the dictionary is read-only. + /// See <see cref="MakeReadOnly"/>. + /// </summary> + public bool IsReadOnly => NativeValue.DangerousSelfRef.IsReadOnly; + + /// <summary> + /// Makes the <see cref="Dictionary"/> read-only, i.e. disabled modying of the + /// dictionary's elements. Does not apply to nested content, e.g. content of + /// nested dictionaries. + /// </summary> + public void MakeReadOnly() + { + if (IsReadOnly) + { + // Avoid interop call when the dictionary is already read-only. + return; + } + + var self = (godot_dictionary)NativeValue; + NativeFuncs.godotsharp_dictionary_make_read_only(ref self); + } /// <summary> /// Gets the value for the given <paramref name="key"/> in the dictionary. @@ -397,6 +446,14 @@ namespace Godot.Collections using (str) return Marshaling.ConvertStringToManaged(str); } + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("Dictionary instance is read-only."); + } + } } internal interface IGenericGodotDictionary @@ -508,6 +565,9 @@ namespace Godot.Collections /// By default, duplicate keys are not copied over, unless <paramref name="overwrite"/> /// is <see langword="true"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <param name="dictionary">Dictionary to copy entries from.</param> /// <param name="overwrite">If duplicate keys should be copied over as well.</param> public void Merge(Dictionary<TKey, TValue> dictionary, bool overwrite = false) @@ -536,6 +596,13 @@ namespace Godot.Collections /// <summary> /// Returns the value at the given <paramref name="key"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The property is assigned and the dictionary is read-only. + /// </exception> + /// <exception cref="KeyNotFoundException"> + /// The property is retrieved and an entry for <paramref name="key"/> + /// does not exist in the dictionary. + /// </exception> /// <value>The value at the given <paramref name="key"/>.</value> public TValue this[TKey key] { @@ -557,6 +624,8 @@ namespace Godot.Collections } set { + ThrowIfReadOnly(); + using var variantKey = VariantUtils.CreateFrom(key); using var variantValue = VariantUtils.CreateFrom(value); var self = (godot_dictionary)_underlyingDict.NativeValue; @@ -616,10 +685,15 @@ namespace Godot.Collections /// Adds an object <paramref name="value"/> at key <paramref name="key"/> /// to this <see cref="Dictionary{TKey, TValue}"/>. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <param name="key">The key at which to add the object.</param> /// <param name="value">The object to add.</param> public void Add(TKey key, TValue value) { + ThrowIfReadOnly(); + using var variantKey = VariantUtils.CreateFrom(key); var self = (godot_dictionary)_underlyingDict.NativeValue; @@ -645,9 +719,14 @@ namespace Godot.Collections /// <summary> /// Removes an element from this <see cref="Dictionary{TKey, TValue}"/> by key. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> /// <param name="key">The key of the element to remove.</param> public bool Remove(TKey key) { + ThrowIfReadOnly(); + using var variantKey = VariantUtils.CreateFrom(key); var self = (godot_dictionary)_underlyingDict.NativeValue; return NativeFuncs.godotsharp_dictionary_remove_key(ref self, variantKey).ToBool(); @@ -683,7 +762,21 @@ namespace Godot.Collections /// <returns>The number of elements.</returns> public int Count => _underlyingDict.Count; - bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly => false; + /// <summary> + /// Returns <see langword="true"/> if the dictionary is read-only. + /// See <see cref="MakeReadOnly"/>. + /// </summary> + public bool IsReadOnly => _underlyingDict.IsReadOnly; + + /// <summary> + /// Makes the <see cref="Dictionary{TKey, TValue}"/> read-only, i.e. disabled + /// modying of the dictionary's elements. Does not apply to nested content, + /// e.g. content of nested dictionaries. + /// </summary> + public void MakeReadOnly() + { + _underlyingDict.MakeReadOnly(); + } void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value); @@ -691,6 +784,9 @@ namespace Godot.Collections /// <summary> /// Clears the dictionary, removing all entries from it. /// </summary> + /// <exception cref="InvalidOperationException"> + /// The dictionary is read-only. + /// </exception> public void Clear() => _underlyingDict.Clear(); bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item) @@ -740,6 +836,8 @@ namespace Godot.Collections bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item) { + ThrowIfReadOnly(); + using var variantKey = VariantUtils.CreateFrom(item.Key); var self = (godot_dictionary)_underlyingDict.NativeValue; bool found = NativeFuncs.godotsharp_dictionary_try_get_value(ref self, @@ -789,5 +887,13 @@ namespace Godot.Collections [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Dictionary<TKey, TValue>(Variant from) => from.AsGodotDictionary<TKey, TValue>(); + + private void ThrowIfReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("Dictionary instance is read-only."); + } + } } } diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs index c295e500de..43e7c7eb9a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs @@ -697,6 +697,9 @@ namespace Godot.NativeInterop private uint _safeRefCount; public VariantVector _arrayVector; + + private unsafe godot_variant* _readOnly; + // There are more fields here, but we don't care as we never store this in C# public readonly int Size @@ -704,6 +707,12 @@ namespace Godot.NativeInterop [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _arrayVector.Size; } + + public readonly unsafe bool IsReadOnly + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _readOnly != null; + } } [StructLayout(LayoutKind.Sequential)] @@ -737,6 +746,12 @@ namespace Godot.NativeInterop get => _p != null ? _p->Size : 0; } + public readonly unsafe bool IsReadOnly + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _p != null && _p->IsReadOnly; + } + public unsafe void Dispose() { if (_p == null) @@ -766,35 +781,59 @@ namespace Godot.NativeInterop // A correctly constructed value needs to call the native default constructor to allocate `_p`. // Don't pass a C# default constructed `godot_dictionary` to native code, unless it's going to // be re-assigned a new value (the copy constructor checks if `_p` is null so that's fine). - [StructLayout(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Explicit)] // ReSharper disable once InconsistentNaming public ref struct godot_dictionary { [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly unsafe godot_dictionary* GetUnsafeAddress() - => (godot_dictionary*)Unsafe.AsPointer(ref Unsafe.AsRef(in _p)); + => (godot_dictionary*)Unsafe.AsPointer(ref Unsafe.AsRef(in _getUnsafeAddressHelper)); - private IntPtr _p; + [FieldOffset(0)] private byte _getUnsafeAddressHelper; - public readonly bool IsAllocated + [FieldOffset(0)] private unsafe DictionaryPrivate* _p; + + [StructLayout(LayoutKind.Sequential)] + private struct DictionaryPrivate + { + private uint _safeRefCount; + + private unsafe godot_variant* _readOnly; + + // There are more fields here, but we don't care as we never store this in C# + + public readonly unsafe bool IsReadOnly + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _readOnly != null; + } + } + + public readonly unsafe bool IsAllocated { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _p != IntPtr.Zero; + get => _p != null; } - public void Dispose() + public readonly unsafe bool IsReadOnly { - if (_p == IntPtr.Zero) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _p != null && _p->IsReadOnly; + } + + public unsafe void Dispose() + { + if (_p == null) return; NativeFuncs.godotsharp_dictionary_destroy(ref this); - _p = IntPtr.Zero; + _p = null; } [StructLayout(LayoutKind.Sequential)] // ReSharper disable once InconsistentNaming internal struct movable { - private IntPtr _p; + private unsafe DictionaryPrivate* _p; public static unsafe explicit operator movable(in godot_dictionary value) => *(movable*)CustomUnsafe.AsPointer(ref CustomUnsafe.AsRef(value)); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index d116284e1c..1e23689c95 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -376,6 +376,8 @@ namespace Godot.NativeInterop public static partial Error godotsharp_array_resize(ref godot_array p_self, int p_new_size); + public static partial void godotsharp_array_make_read_only(ref godot_array p_self); + public static partial void godotsharp_array_shuffle(ref godot_array p_self); public static partial void godotsharp_array_to_string(ref godot_array p_self, out godot_string r_str); @@ -416,6 +418,8 @@ namespace Godot.NativeInterop public static partial godot_bool godotsharp_dictionary_remove_key(ref godot_dictionary p_self, in godot_variant p_key); + public static partial void godotsharp_dictionary_make_read_only(ref godot_dictionary p_self); + public static partial void godotsharp_dictionary_to_string(ref godot_dictionary p_self, out godot_string r_str); // StringExtensions diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs index be4004a0f9..69444f8035 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs @@ -51,11 +51,11 @@ namespace Godot /// <summary> /// The area of this <see cref="Rect2"/>. + /// See also <see cref="HasArea"/>. /// </summary> - /// <value>Equivalent to <see cref="GetArea()"/>.</value> public readonly real_t Area { - get { return GetArea(); } + get { return _size.X * _size.Y; } } /// <summary> @@ -161,15 +161,6 @@ namespace Godot } /// <summary> - /// Returns the area of the <see cref="Rect2"/>. - /// </summary> - /// <returns>The area.</returns> - public readonly real_t GetArea() - { - return _size.X * _size.Y; - } - - /// <summary> /// Returns the center of the <see cref="Rect2"/>, which is equal /// to <see cref="Position"/> + (<see cref="Size"/> / 2). /// </summary> @@ -247,7 +238,7 @@ namespace Godot /// Returns <see langword="true"/> if the <see cref="Rect2"/> has /// area, and <see langword="false"/> if the <see cref="Rect2"/> /// is linear, empty, or has a negative <see cref="Size"/>. - /// See also <see cref="GetArea"/>. + /// See also <see cref="Area"/>. /// </summary> /// <returns> /// A <see langword="bool"/> for whether or not the <see cref="Rect2"/> has area. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs index 5b06101db5..2099d0abca 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs @@ -51,11 +51,11 @@ namespace Godot /// <summary> /// The area of this <see cref="Rect2I"/>. + /// See also <see cref="HasArea"/>. /// </summary> - /// <value>Equivalent to <see cref="GetArea()"/>.</value> public readonly int Area { - get { return GetArea(); } + get { return _size.X * _size.Y; } } /// <summary> @@ -151,15 +151,6 @@ namespace Godot } /// <summary> - /// Returns the area of the <see cref="Rect2I"/>. - /// </summary> - /// <returns>The area.</returns> - public readonly int GetArea() - { - return _size.X * _size.Y; - } - - /// <summary> /// Returns the center of the <see cref="Rect2I"/>, which is equal /// to <see cref="Position"/> + (<see cref="Size"/> / 2). /// If <see cref="Size"/> is an odd number, the returned center @@ -239,7 +230,7 @@ namespace Godot /// Returns <see langword="true"/> if the <see cref="Rect2I"/> has /// area, and <see langword="false"/> if the <see cref="Rect2I"/> /// is linear, empty, or has a negative <see cref="Size"/>. - /// See also <see cref="GetArea"/>. + /// See also <see cref="Area"/>. /// </summary> /// <returns> /// A <see langword="bool"/> for whether or not the <see cref="Rect2I"/> has area. diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index cb9525b49c..df67e075ac 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -69,19 +69,6 @@ namespace Godot } /// <summary> - /// Returns <see langword="true"/> if the strings begins - /// with the given string <paramref name="text"/>. - /// </summary> - /// <param name="instance">The string to check.</param> - /// <param name="text">The beginning string.</param> - /// <returns>If the string begins with the given string.</returns> - [Obsolete("Use string.StartsWith instead.")] - public static bool BeginsWith(this string instance, string text) - { - return instance.StartsWith(text); - } - - /// <summary> /// Returns the bigrams (pairs of consecutive letters) of this string. /// </summary> /// <param name="instance">The string that will be used.</param> @@ -618,7 +605,7 @@ namespace Godot } else { - if (instance.BeginsWith("/")) + if (instance.StartsWith('/')) { rs = instance.Substring(1); directory = "/"; @@ -1199,23 +1186,6 @@ namespace Godot } /// <summary> - /// Returns a copy of the string with characters removed from the left. - /// The <paramref name="chars"/> argument is a string specifying the set of characters - /// to be removed. - /// Note: The <paramref name="chars"/> is not a prefix. See <see cref="TrimPrefix"/> - /// method that will remove a single prefix string rather than a set of characters. - /// </summary> - /// <seealso cref="RStrip(string, string)"/> - /// <param name="instance">The string to remove characters from.</param> - /// <param name="chars">The characters to be removed.</param> - /// <returns>A copy of the string with characters removed from the left.</returns> - [Obsolete("Use string.TrimStart instead.")] - public static string LStrip(this string instance, string chars) - { - return instance.TrimStart(chars.ToCharArray()); - } - - /// <summary> /// Do a simple expression match, where '*' matches zero or more /// arbitrary characters and '?' matches any single character except '.'. /// </summary> @@ -1504,23 +1474,6 @@ namespace Godot } /// <summary> - /// Returns a copy of the string with characters removed from the right. - /// The <paramref name="chars"/> argument is a string specifying the set of characters - /// to be removed. - /// Note: The <paramref name="chars"/> is not a suffix. See <see cref="TrimSuffix"/> - /// method that will remove a single suffix string rather than a set of characters. - /// </summary> - /// <seealso cref="LStrip(string, string)"/> - /// <param name="instance">The string to remove characters from.</param> - /// <param name="chars">The characters to be removed.</param> - /// <returns>A copy of the string with characters removed from the right.</returns> - [Obsolete("Use string.TrimEnd instead.")] - public static string RStrip(this string instance, string chars) - { - return instance.TrimEnd(chars.ToCharArray()); - } - - /// <summary> /// Returns the SHA-1 hash of the string as an array of bytes. /// </summary> /// <seealso cref="Sha1Text(string)"/> diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index e7f50d2caa..d17fe3e75f 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -1012,6 +1012,10 @@ int32_t godotsharp_array_resize(Array *p_self, int32_t p_new_size) { return (int32_t)p_self->resize(p_new_size); } +void godotsharp_array_make_read_only(Array *p_self) { + p_self->make_read_only(); +} + void godotsharp_array_shuffle(Array *p_self) { p_self->shuffle(); } @@ -1081,6 +1085,10 @@ bool godotsharp_dictionary_remove_key(Dictionary *p_self, const Variant *p_key) return p_self->erase(*p_key); } +void godotsharp_dictionary_make_read_only(Dictionary *p_self) { + p_self->make_read_only(); +} + void godotsharp_dictionary_to_string(const Dictionary *p_self, String *r_str) { *r_str = Variant(*p_self).operator String(); } @@ -1439,6 +1447,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_array_insert, (void *)godotsharp_array_remove_at, (void *)godotsharp_array_resize, + (void *)godotsharp_array_make_read_only, (void *)godotsharp_array_shuffle, (void *)godotsharp_array_to_string, (void *)godotsharp_dictionary_try_get_value, @@ -1454,6 +1463,7 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_dictionary_merge, (void *)godotsharp_dictionary_recursive_equal, (void *)godotsharp_dictionary_remove_key, + (void *)godotsharp_dictionary_make_read_only, (void *)godotsharp_dictionary_to_string, (void *)godotsharp_string_simplify_path, (void *)godotsharp_string_to_camel_case, diff --git a/modules/navigation/godot_navigation_server.cpp b/modules/navigation/godot_navigation_server.cpp index 5baf6db2e8..d546c5d3ba 100644 --- a/modules/navigation/godot_navigation_server.cpp +++ b/modules/navigation/godot_navigation_server.cpp @@ -81,36 +81,6 @@ using namespace NavigationUtilities; } \ void GodotNavigationServer::MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1) -#define COMMAND_4(F_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3) \ - struct MERGE(F_NAME, _command) : public SetCommand { \ - T_0 d_0; \ - T_1 d_1; \ - T_2 d_2; \ - T_3 d_3; \ - MERGE(F_NAME, _command) \ - ( \ - T_0 p_d_0, \ - T_1 p_d_1, \ - T_2 p_d_2, \ - T_3 p_d_3) : \ - d_0(p_d_0), \ - d_1(p_d_1), \ - d_2(p_d_2), \ - d_3(p_d_3) {} \ - virtual void exec(GodotNavigationServer *server) override { \ - server->MERGE(_cmd_, F_NAME)(d_0, d_1, d_2, d_3); \ - } \ - }; \ - void GodotNavigationServer::F_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) { \ - auto cmd = memnew(MERGE(F_NAME, _command)( \ - D_0, \ - D_1, \ - D_2, \ - D_3)); \ - add_command(cmd); \ - } \ - void GodotNavigationServer::MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) - GodotNavigationServer::GodotNavigationServer() {} GodotNavigationServer::~GodotNavigationServer() { @@ -711,17 +681,17 @@ bool GodotNavigationServer::agent_is_map_changed(RID p_agent) const { return agent->is_map_changed(); } -COMMAND_4(agent_set_callback, RID, p_agent, ObjectID, p_object_id, StringName, p_method, Variant, p_udata) { +COMMAND_2(agent_set_callback, RID, p_agent, Callable, p_callback) { RvoAgent *agent = agent_owner.get_or_null(p_agent); ERR_FAIL_COND(agent == nullptr); - agent->set_callback(p_object_id, p_method, p_udata); + agent->set_callback(p_callback); if (agent->get_map()) { - if (p_object_id == ObjectID()) { - agent->get_map()->remove_agent_as_controlled(agent); - } else { + if (p_callback.is_valid()) { agent->get_map()->set_agent_as_controlled(agent); + } else { + agent->get_map()->remove_agent_as_controlled(agent); } } } @@ -946,4 +916,3 @@ int GodotNavigationServer::get_process_info(ProcessInfo p_info) const { #undef COMMAND_1 #undef COMMAND_2 -#undef COMMAND_4 diff --git a/modules/navigation/godot_navigation_server.h b/modules/navigation/godot_navigation_server.h index efefa60de8..eea5713c40 100644 --- a/modules/navigation/godot_navigation_server.h +++ b/modules/navigation/godot_navigation_server.h @@ -54,10 +54,6 @@ virtual void F_NAME(T_0 D_0, T_1 D_1) override; \ void MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1) -#define COMMAND_4_DEF(F_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, D_3_DEF) \ - virtual void F_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3 = D_3_DEF) override; \ - void MERGE(_cmd_, F_NAME)(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) - class GodotNavigationServer; struct SetCommand { @@ -182,7 +178,7 @@ public: COMMAND_2(agent_set_position, RID, p_agent, Vector3, p_position); COMMAND_2(agent_set_ignore_y, RID, p_agent, bool, p_ignore); virtual bool agent_is_map_changed(RID p_agent) const override; - COMMAND_4_DEF(agent_set_callback, RID, p_agent, ObjectID, p_object_id, StringName, p_method, Variant, p_udata, Variant()); + COMMAND_2(agent_set_callback, RID, p_agent, Callable, p_callback); COMMAND_1(free, RID, p_object); @@ -198,6 +194,5 @@ public: #undef COMMAND_1 #undef COMMAND_2 -#undef COMMAND_4_DEF #endif // GODOT_NAVIGATION_SERVER_H diff --git a/modules/navigation/rvo_agent.cpp b/modules/navigation/rvo_agent.cpp index 979ef0d917..40f1e925be 100644 --- a/modules/navigation/rvo_agent.cpp +++ b/modules/navigation/rvo_agent.cpp @@ -32,10 +32,6 @@ #include "nav_map.h" -RvoAgent::RvoAgent() { - callback.id = ObjectID(); -} - void RvoAgent::set_map(NavMap *p_map) { map = p_map; } @@ -50,31 +46,25 @@ bool RvoAgent::is_map_changed() { } } -void RvoAgent::set_callback(ObjectID p_id, const StringName p_method, const Variant p_udata) { - callback.id = p_id; - callback.method = p_method; - callback.udata = p_udata; +void RvoAgent::set_callback(Callable p_callback) { + callback = p_callback; } bool RvoAgent::has_callback() const { - return callback.id.is_valid(); + return callback.is_valid(); } void RvoAgent::dispatch_callback() { - if (callback.id.is_null()) { + if (!callback.is_valid()) { return; } - Object *obj = ObjectDB::get_instance(callback.id); - if (!obj) { - callback.id = ObjectID(); - return; - } - - Callable::CallError responseCallError; - callback.new_velocity = Vector3(agent.newVelocity_.x(), agent.newVelocity_.y(), agent.newVelocity_.z()); + Vector3 new_velocity = Vector3(agent.newVelocity_.x(), agent.newVelocity_.y(), agent.newVelocity_.z()); - const Variant *vp[2] = { &callback.new_velocity, &callback.udata }; - int argc = (callback.udata.get_type() == Variant::NIL) ? 1 : 2; - obj->callp(callback.method, vp, argc, responseCallError); + // Invoke the callback with the new velocity. + Variant args[] = { new_velocity }; + const Variant *args_p[] = { &args[0] }; + Variant return_value; + Callable::CallError call_error; + callback.callp(args_p, 1, return_value, call_error); } diff --git a/modules/navigation/rvo_agent.h b/modules/navigation/rvo_agent.h index 7b19907b2b..5f377b6079 100644 --- a/modules/navigation/rvo_agent.h +++ b/modules/navigation/rvo_agent.h @@ -39,21 +39,12 @@ class NavMap; class RvoAgent : public NavRid { - struct AvoidanceComputedCallback { - ObjectID id; - StringName method; - Variant udata; - Variant new_velocity; - }; - NavMap *map = nullptr; RVO::Agent agent; - AvoidanceComputedCallback callback; + Callable callback = Callable(); uint32_t map_update_id = 0; public: - RvoAgent(); - void set_map(NavMap *p_map); NavMap *get_map() { return map; @@ -65,7 +56,7 @@ public: bool is_map_changed(); - void set_callback(ObjectID p_id, const StringName p_method, const Variant p_udata = Variant()); + void set_callback(Callable p_callback); bool has_callback() const; void dispatch_callback(); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 587caf81bf..a23f7d1d02 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -1076,11 +1076,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p Vector<int> feature_versions; if (xr_mode_index == XR_MODE_OPENXR) { - // Set degrees of freedom - feature_names.push_back("android.hardware.vr.headtracking"); - feature_required_list.push_back(true); - feature_versions.push_back(1); - // Check for hand tracking if (hand_tracking_index > XR_HAND_TRACKING_NONE) { feature_names.push_back("oculus.software.handtracking"); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index e450f3edb3..4fdcca68e9 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -259,8 +259,6 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OPENXR; if (uses_xr) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"true\" android:version=\"1\" />\n"; - int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required if (hand_tracking_index == XR_HAND_TRACKING_OPTIONAL) { manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; @@ -279,8 +277,6 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { } String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { - int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); - bool uses_xr = xr_mode_index == XR_MODE_OPENXR; String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " @@ -291,11 +287,6 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { bool_to_string(p_preset->get("package/exclude_from_recents")), orientation, bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); - if (uses_xr) { - manifest_activity_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />\n"; - } else { - manifest_activity_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.vr.focusaware\" />\n"; - } manifest_activity_text += " </activity>\n"; return manifest_activity_text; } @@ -335,8 +326,6 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ hand_tracking_frequency); manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; } - } else { - manifest_application_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n"; } manifest_application_text += _get_activity_tag(p_preset); manifest_application_text += " </application>\n"; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 8c8608cbbb..1969f9c814 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -48,12 +48,6 @@ android:name="xr_hand_tracking_version_name" android:value="xr_hand_tracking_version_value"/> - <!-- Supported Meta devices --> - <!-- This is removed by the exporter if the xr mode is not VR. --> - <meta-data - android:name="com.oculus.supportedDevices" - android:value="all" /> - <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" @@ -66,9 +60,6 @@ android:resizeableActivity="false" tools:ignore="UnusedAttribute" > - <!-- Focus awareness metadata is removed at export time if the xr mode is not VR. --> - <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> - <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/scene/2d/audio_stream_player_2d.cpp b/scene/2d/audio_stream_player_2d.cpp index 7b681eac7a..8c23d33402 100644 --- a/scene/2d/audio_stream_player_2d.cpp +++ b/scene/2d/audio_stream_player_2d.cpp @@ -72,12 +72,10 @@ void AudioStreamPlayer2D::_notification(int p_what) { _update_panning(); } - if (setplay.get() >= 0 && stream.is_valid()) { + if (setplayback.is_valid() && setplay.get() >= 0) { active.set(); - Ref<AudioStreamPlayback> new_playback = stream->instantiate_playback(); - ERR_FAIL_COND_MSG(new_playback.is_null(), "Failed to instantiate playback."); - AudioServer::get_singleton()->start_playback_stream(new_playback, _get_actual_bus(), volume_vector, setplay.get(), pitch_scale); - stream_playbacks.push_back(new_playback); + AudioServer::get_singleton()->start_playback_stream(setplayback, _get_actual_bus(), volume_vector, setplay.get(), pitch_scale); + setplayback.unref(); setplay.set(-1); } @@ -255,7 +253,11 @@ void AudioStreamPlayer2D::play(float p_from_pos) { if (stream->is_monophonic() && is_playing()) { stop(); } + Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback(); + ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback."); + stream_playbacks.push_back(stream_playback); + setplayback = stream_playback; setplay.set(p_from_pos); active.set(); set_physics_process_internal(true); @@ -390,6 +392,10 @@ bool AudioStreamPlayer2D::get_stream_paused() const { return false; } +bool AudioStreamPlayer2D::has_stream_playback() { + return !stream_playbacks.is_empty(); +} + Ref<AudioStreamPlayback> AudioStreamPlayer2D::get_stream_playback() { ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback()."); return stream_playbacks[stream_playbacks.size() - 1]; @@ -458,6 +464,7 @@ void AudioStreamPlayer2D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_panning_strength", "panning_strength"), &AudioStreamPlayer2D::set_panning_strength); ClassDB::bind_method(D_METHOD("get_panning_strength"), &AudioStreamPlayer2D::get_panning_strength); + ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer2D::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer2D::get_stream_playback); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); diff --git a/scene/2d/audio_stream_player_2d.h b/scene/2d/audio_stream_player_2d.h index a5fd584513..79a026fed2 100644 --- a/scene/2d/audio_stream_player_2d.h +++ b/scene/2d/audio_stream_player_2d.h @@ -56,6 +56,7 @@ private: SafeFlag active{ false }; SafeNumeric<float> setplay{ -1.0 }; + Ref<AudioStreamPlayback> setplayback; Vector<AudioFrame> volume_vector; @@ -129,6 +130,7 @@ public: void set_panning_strength(float p_panning_strength); float get_panning_strength() const; + bool has_stream_playback(); Ref<AudioStreamPlayback> get_stream_playback(); AudioStreamPlayer2D(); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 9a3b7c8687..00d13c59b9 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -638,7 +638,7 @@ void GPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "interpolate"), "set_interpolate", "get_interpolate"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fract_delta"), "set_fractional_delta", "get_fractional_delta"); ADD_GROUP("Collision", "collision_"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_base_size", PROPERTY_HINT_RANGE, "0,128,0.01,or_greater,suffix:px"), "set_collision_base_size", "get_collision_base_size"); + 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", ""); ADD_PROPERTY(PropertyInfo(Variant::RECT2, "visibility_rect", PROPERTY_HINT_NONE, "suffix:px"), "set_visibility_rect", "get_visibility_rect"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "local_coords"), "set_use_local_coordinates", "get_use_local_coordinates"); diff --git a/scene/2d/navigation_agent_2d.cpp b/scene/2d/navigation_agent_2d.cpp index 907fb7e6a9..e73b6e7e23 100644 --- a/scene/2d/navigation_agent_2d.cpp +++ b/scene/2d/navigation_agent_2d.cpp @@ -205,9 +205,9 @@ NavigationAgent2D::~NavigationAgent2D() { void NavigationAgent2D::set_avoidance_enabled(bool p_enabled) { avoidance_enabled = p_enabled; if (avoidance_enabled) { - NavigationServer2D::get_singleton()->agent_set_callback(agent, get_instance_id(), "_avoidance_done"); + NavigationServer2D::get_singleton()->agent_set_callback(agent, callable_mp(this, &NavigationAgent2D::_avoidance_done)); } else { - NavigationServer2D::get_singleton()->agent_set_callback(agent, ObjectID(), "_avoidance_done"); + NavigationServer2D::get_singleton()->agent_set_callback(agent, Callable()); } } @@ -217,7 +217,8 @@ bool NavigationAgent2D::get_avoidance_enabled() const { void NavigationAgent2D::set_agent_parent(Node *p_agent_parent) { // remove agent from any avoidance map before changing parent or there will be leftovers on the RVO map - NavigationServer2D::get_singleton()->agent_set_callback(agent, ObjectID(), "_avoidance_done"); + NavigationServer2D::get_singleton()->agent_set_callback(agent, Callable()); + if (Object::cast_to<Node2D>(p_agent_parent) != nullptr) { // place agent on navigation map first or else the RVO agent callback creation fails silently later agent_parent = Object::cast_to<Node2D>(p_agent_parent); @@ -226,6 +227,7 @@ void NavigationAgent2D::set_agent_parent(Node *p_agent_parent) { } else { NavigationServer2D::get_singleton()->agent_set_map(get_rid(), agent_parent->get_world_2d()->get_navigation_map()); } + // create new avoidance callback if enabled set_avoidance_enabled(avoidance_enabled); } else { diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp index 7ed18d2d41..436b936586 100644 --- a/scene/3d/audio_stream_player_3d.cpp +++ b/scene/3d/audio_stream_player_3d.cpp @@ -284,14 +284,12 @@ void AudioStreamPlayer3D::_notification(int p_what) { volume_vector = _update_panning(); } - if (setplay.get() >= 0 && stream.is_valid()) { + if (setplayback.is_valid() && setplay.get() >= 0) { active.set(); - Ref<AudioStreamPlayback> new_playback = stream->instantiate_playback(); - ERR_FAIL_COND_MSG(new_playback.is_null(), "Failed to instantiate playback."); HashMap<StringName, Vector<AudioFrame>> bus_map; bus_map[_get_actual_bus()] = volume_vector; - AudioServer::get_singleton()->start_playback_stream(new_playback, bus_map, setplay.get(), actual_pitch_scale, linear_attenuation, attenuation_filter_cutoff_hz); - stream_playbacks.push_back(new_playback); + AudioServer::get_singleton()->start_playback_stream(setplayback, bus_map, setplay.get(), actual_pitch_scale, linear_attenuation, attenuation_filter_cutoff_hz); + setplayback.unref(); setplay.set(-1); } @@ -580,14 +578,21 @@ void AudioStreamPlayer3D::play(float p_from_pos) { if (stream->is_monophonic() && is_playing()) { stop(); } + Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback(); + ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback."); + + stream_playbacks.push_back(stream_playback); + setplayback = stream_playback; setplay.set(p_from_pos); active.set(); set_physics_process_internal(true); } void AudioStreamPlayer3D::seek(float p_seconds) { - stop(); - play(p_seconds); + if (is_playing()) { + stop(); + play(p_seconds); + } } void AudioStreamPlayer3D::stop() { @@ -783,6 +788,10 @@ bool AudioStreamPlayer3D::get_stream_paused() const { return false; } +bool AudioStreamPlayer3D::has_stream_playback() { + return !stream_playbacks.is_empty(); +} + Ref<AudioStreamPlayback> AudioStreamPlayer3D::get_stream_playback() { ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback()."); return stream_playbacks[stream_playbacks.size() - 1]; @@ -875,6 +884,7 @@ void AudioStreamPlayer3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_panning_strength", "panning_strength"), &AudioStreamPlayer3D::set_panning_strength); ClassDB::bind_method(D_METHOD("get_panning_strength"), &AudioStreamPlayer3D::get_panning_strength); + ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer3D::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer3D::get_stream_playback); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); diff --git a/scene/3d/audio_stream_player_3d.h b/scene/3d/audio_stream_player_3d.h index 67cec91896..bba8f10761 100644 --- a/scene/3d/audio_stream_player_3d.h +++ b/scene/3d/audio_stream_player_3d.h @@ -69,6 +69,7 @@ private: SafeFlag active{ false }; SafeNumeric<float> setplay{ -1.0 }; + Ref<AudioStreamPlayback> setplayback; AttenuationModel attenuation_model = ATTENUATION_INVERSE_DISTANCE; float volume_db = 0.0; @@ -188,6 +189,7 @@ public: void set_panning_strength(float p_panning_strength); float get_panning_strength() const; + bool has_stream_playback(); Ref<AudioStreamPlayback> get_stream_playback(); AudioStreamPlayer3D(); diff --git a/scene/3d/navigation_agent_3d.cpp b/scene/3d/navigation_agent_3d.cpp index fe7ddcb06e..4aa6e61ec5 100644 --- a/scene/3d/navigation_agent_3d.cpp +++ b/scene/3d/navigation_agent_3d.cpp @@ -212,9 +212,9 @@ NavigationAgent3D::~NavigationAgent3D() { void NavigationAgent3D::set_avoidance_enabled(bool p_enabled) { avoidance_enabled = p_enabled; if (avoidance_enabled) { - NavigationServer3D::get_singleton()->agent_set_callback(agent, get_instance_id(), "_avoidance_done"); + NavigationServer3D::get_singleton()->agent_set_callback(agent, callable_mp(this, &NavigationAgent3D::_avoidance_done)); } else { - NavigationServer3D::get_singleton()->agent_set_callback(agent, ObjectID(), "_avoidance_done"); + NavigationServer3D::get_singleton()->agent_set_callback(agent, Callable()); } } @@ -224,7 +224,8 @@ bool NavigationAgent3D::get_avoidance_enabled() const { void NavigationAgent3D::set_agent_parent(Node *p_agent_parent) { // remove agent from any avoidance map before changing parent or there will be leftovers on the RVO map - NavigationServer3D::get_singleton()->agent_set_callback(agent, ObjectID(), "_avoidance_done"); + NavigationServer3D::get_singleton()->agent_set_callback(agent, Callable()); + if (Object::cast_to<Node3D>(p_agent_parent) != nullptr) { // place agent on navigation map first or else the RVO agent callback creation fails silently later agent_parent = Object::cast_to<Node3D>(p_agent_parent); @@ -233,6 +234,7 @@ void NavigationAgent3D::set_agent_parent(Node *p_agent_parent) { } else { NavigationServer3D::get_singleton()->agent_set_map(get_rid(), agent_parent->get_world_3d()->get_navigation_map()); } + // create new avoidance callback if enabled set_avoidance_enabled(avoidance_enabled); } else { diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index 63e0fb6935..1b7615befa 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -431,6 +431,17 @@ void AnimationPlayer::_ensure_node_caches(AnimationData *p_anim, Node *p_root_ov } } + if (a->track_get_type(i) == Animation::TYPE_AUDIO) { + if (!node_cache->audio_anim.has(a->track_get_path(i).get_concatenated_names())) { + TrackNodeCache::AudioAnim aa; + aa.object = (Object *)child; + aa.audio_stream.instantiate(); + aa.audio_stream->set_polyphony(audio_max_polyphony); + + node_cache->audio_anim[a->track_get_path(i).get_concatenated_names()] = aa; + } + } + node_cache->last_setup_pass = setup_pass; } } @@ -820,52 +831,40 @@ void AnimationPlayer::_animation_process_animation(AnimationData *p_anim, double if (!nc->node || is_stopping) { continue; } - - if (p_seeked) { #ifdef TOOLS_ENABLED - if (!can_call) { - continue; // To avoid spamming the preview in editor. - } + if (p_seeked && !can_call) { + continue; // To avoid spamming the preview in editor. + } #endif // TOOLS_ENABLED - int idx = a->track_find_key(i, p_time); - if (idx < 0) { - continue; - } - - Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); - if (!stream.is_valid()) { - nc->node->call(SNAME("stop")); - nc->audio_playing = false; - playing_caches.erase(nc); - } else { - float start_ofs = a->audio_track_get_key_start_offset(i, idx); - start_ofs += p_time - a->track_get_key_time(i, idx); - float end_ofs = a->audio_track_get_key_end_offset(i, idx); - float len = stream->get_length(); - - if (start_ofs > len - end_ofs) { - nc->node->call(SNAME("stop")); - nc->audio_playing = false; - playing_caches.erase(nc); - continue; - } - - nc->node->call(SNAME("set_stream"), stream); - nc->node->call(SNAME("play"), start_ofs); + HashMap<StringName, TrackNodeCache::AudioAnim>::Iterator E = nc->audio_anim.find(a->track_get_path(i).get_concatenated_names()); + ERR_CONTINUE(!E); //should it continue, or create a new one? - nc->audio_playing = true; - playing_caches.insert(nc); - if (len && end_ofs > 0) { //force an end at a time - nc->audio_len = len - start_ofs - end_ofs; - } else { - nc->audio_len = 0; - } + TrackNodeCache::AudioAnim *aa = &E->value; + Node *asp = Object::cast_to<Node>(aa->object); + if (!asp) { + continue; + } + aa->length = a->get_length(); + aa->time = p_time; + aa->loop = a->get_loop_mode() != Animation::LOOP_NONE; + aa->backward = backward; + if (aa->accum_pass != accum_pass) { + ERR_CONTINUE(cache_update_audio_size >= NODE_CACHE_UPDATE_MAX); + cache_update_audio[cache_update_audio_size++] = aa; + aa->accum_pass = accum_pass; + } - nc->audio_start = p_time; + HashMap<int, TrackNodeCache::PlayingAudioStreamInfo> &map = aa->playing_streams; + // Find stream. + int idx = -1; + if (p_seeked) { + idx = a->track_find_key(i, p_time); + // Discard previous stream when seeking. + if (map.has(idx)) { + aa->audio_stream_playback->stop_stream(map[idx].index); + map.erase(idx); } - } else { - //find stuff to play List<int> to_play; if (p_started) { int first_key = a->track_find_key(i, p_prev_time, Animation::FIND_MODE_EXACT); @@ -875,55 +874,47 @@ void AnimationPlayer::_animation_process_animation(AnimationData *p_anim, double } a->track_get_key_indices_in_range(i, p_time, p_delta, &to_play, p_looped_flag); if (to_play.size()) { - int idx = to_play.back()->get(); - - Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); - if (!stream.is_valid()) { - nc->node->call(SNAME("stop")); - nc->audio_playing = false; - playing_caches.erase(nc); - } else { - float start_ofs = a->audio_track_get_key_start_offset(i, idx); - float end_ofs = a->audio_track_get_key_end_offset(i, idx); - float len = stream->get_length(); - - nc->node->call(SNAME("set_stream"), stream); - nc->node->call(SNAME("play"), start_ofs); - - nc->audio_playing = true; - playing_caches.insert(nc); - if (len && end_ofs > 0) { //force an end at a time - nc->audio_len = len - start_ofs - end_ofs; - } else { - nc->audio_len = 0; - } - - nc->audio_start = p_time; - } - } else if (nc->audio_playing) { - bool loop = a->get_loop_mode() != Animation::LOOP_NONE; - - bool stop = false; - - if (!loop) { - if ((p_time < nc->audio_start && !backward) || (p_time > nc->audio_start && backward)) { - stop = true; - } - } else if (nc->audio_len > 0) { - float len = nc->audio_start > p_time ? (a->get_length() - nc->audio_start) + p_time : p_time - nc->audio_start; + idx = to_play.back()->get(); + } + } + if (idx < 0) { + continue; + } - if (len > nc->audio_len) { - stop = true; - } + // Play stream. + Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); + if (stream.is_valid()) { + double start_ofs = a->audio_track_get_key_start_offset(i, idx); + double end_ofs = a->audio_track_get_key_end_offset(i, idx); + double len = stream->get_length(); + + if (aa->object->call(SNAME("get_stream")) != aa->audio_stream) { + aa->object->call(SNAME("set_stream"), aa->audio_stream); + aa->audio_stream_playback.unref(); + if (!playing_audio_stream_players.has(asp)) { + playing_audio_stream_players.push_back(asp); } + } + if (!aa->object->call(SNAME("is_playing"))) { + aa->object->call(SNAME("play")); + } + if (!aa->object->call(SNAME("has_stream_playback"))) { + aa->audio_stream_playback.unref(); + continue; + } + if (aa->audio_stream_playback.is_null()) { + aa->audio_stream_playback = aa->object->call(SNAME("get_stream_playback")); + } - if (stop) { - //time to stop - nc->node->call(SNAME("stop")); - nc->audio_playing = false; - playing_caches.erase(nc); - } + TrackNodeCache::PlayingAudioStreamInfo pasi; + pasi.index = aa->audio_stream_playback->play_stream(stream, start_ofs); + pasi.start = p_time; + if (len && end_ofs > 0) { // Force an end at a time. + pasi.len = len - start_ofs - end_ofs; + } else { + pasi.len = 0; } + map[idx] = pasi; } } break; @@ -1223,6 +1214,53 @@ void AnimationPlayer::_animation_update_transforms() { ERR_CONTINUE(ba->accum_pass != accum_pass); ba->object->set_indexed(ba->bezier_property, ba->bezier_accum); } + + for (int i = 0; i < cache_update_audio_size; i++) { + TrackNodeCache::AudioAnim *aa = cache_update_audio[i]; + + ERR_CONTINUE(aa->accum_pass != accum_pass); + + // Audio ending process. + LocalVector<int> erase_list; + for (const KeyValue<int, TrackNodeCache::PlayingAudioStreamInfo> &K : aa->playing_streams) { + TrackNodeCache::PlayingAudioStreamInfo pasi = K.value; + + bool stop = false; + if (!aa->audio_stream_playback->is_stream_playing(pasi.index)) { + stop = true; + } + if (!aa->loop) { + if (!aa->backward) { + if (aa->time < pasi.start) { + stop = true; + } + } else if (aa->backward) { + if (aa->time > pasi.start) { + stop = true; + } + } + } + if (pasi.len > 0) { + double len = 0.0; + if (!aa->backward) { + len = pasi.start > aa->time ? (aa->length - pasi.start) + aa->time : aa->time - pasi.start; + } else { + len = pasi.start < aa->time ? (aa->length - aa->time) + pasi.start : pasi.start - aa->time; + } + if (len > pasi.len) { + stop = true; + } + } + if (stop) { + // Time to stop. + aa->audio_stream_playback->stop_stream(pasi.index); + erase_list.push_back(K.key); + } + } + for (uint32_t erase_idx = 0; erase_idx < erase_list.size(); erase_idx++) { + aa->playing_streams.erase(erase_list[erase_idx]); + } + } } void AnimationPlayer::_animation_process(double p_delta) { @@ -1238,6 +1276,7 @@ void AnimationPlayer::_animation_process(double p_delta) { cache_update_size = 0; cache_update_prop_size = 0; cache_update_bezier_size = 0; + cache_update_audio_size = 0; AnimationData *prev_from = playback.current.from; _animation_process2(p_delta, started); @@ -1675,6 +1714,7 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa } if (get_current_animation() != p_name) { + _clear_audio_streams(); _stop_playing_caches(false); } @@ -1856,6 +1896,7 @@ void AnimationPlayer::_node_removed(Node *p_node) { } void AnimationPlayer::clear_caches() { + _clear_audio_streams(); _stop_playing_caches(true); node_cache_map.clear(); @@ -1867,10 +1908,19 @@ void AnimationPlayer::clear_caches() { cache_update_size = 0; cache_update_prop_size = 0; cache_update_bezier_size = 0; + cache_update_audio_size = 0; emit_signal(SNAME("caches_cleared")); } +void AnimationPlayer::_clear_audio_streams() { + for (int i = 0; i < playing_audio_stream_players.size(); i++) { + playing_audio_stream_players[i]->call(SNAME("stop")); + playing_audio_stream_players[i]->call(SNAME("set_stream"), Ref<AudioStream>()); + } + playing_audio_stream_players.clear(); +} + void AnimationPlayer::set_active(bool p_active) { if (active == p_active) { return; @@ -1950,6 +2000,15 @@ AnimationPlayer::AnimationMethodCallMode AnimationPlayer::get_method_call_mode() return method_call_mode; } +void AnimationPlayer::set_audio_max_polyphony(int p_audio_max_polyphony) { + ERR_FAIL_COND(p_audio_max_polyphony < 0 || p_audio_max_polyphony > 128); + audio_max_polyphony = p_audio_max_polyphony; +} + +int AnimationPlayer::get_audio_max_polyphony() const { + return audio_max_polyphony; +} + void AnimationPlayer::set_movie_quit_on_finish_enabled(bool p_enabled) { movie_quit_on_finish = p_enabled; } @@ -1978,6 +2037,7 @@ void AnimationPlayer::_set_process(bool p_process, bool p_force) { } void AnimationPlayer::_stop_internal(bool p_reset, bool p_keep_state) { + _clear_audio_streams(); _stop_playing_caches(p_reset); Playback &c = playback; c.blend.clear(); @@ -2198,6 +2258,9 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_method_call_mode", "mode"), &AnimationPlayer::set_method_call_mode); ClassDB::bind_method(D_METHOD("get_method_call_mode"), &AnimationPlayer::get_method_call_mode); + ClassDB::bind_method(D_METHOD("set_audio_max_polyphony", "max_polyphony"), &AnimationPlayer::set_audio_max_polyphony); + ClassDB::bind_method(D_METHOD("get_audio_max_polyphony"), &AnimationPlayer::get_audio_max_polyphony); + ClassDB::bind_method(D_METHOD("set_movie_quit_on_finish_enabled", "enabled"), &AnimationPlayer::set_movie_quit_on_finish_enabled); ClassDB::bind_method(D_METHOD("is_movie_quit_on_finish_enabled"), &AnimationPlayer::is_movie_quit_on_finish_enabled); @@ -2223,6 +2286,7 @@ void AnimationPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playback_active", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_active", "is_active"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "-64,64,0.01"), "set_speed_scale", "get_speed_scale"); ADD_PROPERTY(PropertyInfo(Variant::INT, "method_call_mode", PROPERTY_HINT_ENUM, "Deferred,Immediate"), "set_method_call_mode", "get_method_call_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "audio_max_polyphony", PROPERTY_HINT_RANGE, "1,127,1"), "set_audio_max_polyphony", "get_audio_max_polyphony"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "movie_quit_on_finish"), "set_movie_quit_on_finish_enabled", "is_movie_quit_on_finish_enabled"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index 2901c43dcf..25de278f97 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -37,6 +37,7 @@ #include "scene/3d/skeleton_3d.h" #include "scene/resources/animation.h" #include "scene/resources/animation_library.h" +#include "scene/resources/audio_stream_polyphonic.h" #ifdef TOOLS_ENABLED class AnimatedValuesBackup : public RefCounted { @@ -147,6 +148,26 @@ private: HashMap<StringName, BezierAnim> bezier_anim; + struct PlayingAudioStreamInfo { + int64_t index = -1; + double start = 0.0; + double len = 0.0; + }; + + struct AudioAnim { + Ref<AudioStreamPolyphonic> audio_stream; + Ref<AudioStreamPlaybackPolyphonic> audio_stream_playback; + HashMap<int, PlayingAudioStreamInfo> playing_streams; + Object *object = nullptr; + uint64_t accum_pass = 0; + double length = 0.0; + double time = 0.0; + bool loop = false; + bool backward = false; + }; + + HashMap<StringName, AudioAnim> audio_anim; + uint32_t last_setup_pass = 0; TrackNodeCache() {} }; @@ -187,7 +208,10 @@ private: int cache_update_prop_size = 0; TrackNodeCache::BezierAnim *cache_update_bezier[NODE_CACHE_UPDATE_MAX]; int cache_update_bezier_size = 0; + TrackNodeCache::AudioAnim *cache_update_audio[NODE_CACHE_UPDATE_MAX]; + int cache_update_audio_size = 0; HashSet<TrackNodeCache *> playing_caches; + Vector<Node *> playing_audio_stream_players; uint64_t accum_pass = 1; float speed_scale = 1.0; @@ -263,6 +287,7 @@ private: bool reset_on_save = true; AnimationProcessCallback process_callback = ANIMATION_PROCESS_IDLE; AnimationMethodCallMode method_call_mode = ANIMATION_METHOD_CALL_DEFERRED; + int audio_max_polyphony = 32; bool movie_quit_on_finish = false; bool processing = false; bool active = true; @@ -278,6 +303,7 @@ private: void _animation_process(double p_delta); void _node_removed(Node *p_node); + void _clear_audio_streams(); void _stop_playing_caches(bool p_reset); // bind helpers @@ -377,6 +403,9 @@ public: void set_method_call_mode(AnimationMethodCallMode p_mode); AnimationMethodCallMode get_method_call_mode() const; + void set_audio_max_polyphony(int p_audio_max_polyphony); + int get_audio_max_polyphony() const; + void set_movie_quit_on_finish_enabled(bool p_enabled); bool is_movie_quit_on_finish_enabled() const; diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index 05b6c72cbd..f630660049 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -486,13 +486,7 @@ void AnimationTree::set_active(bool p_active) { } if (!active && is_inside_tree()) { - for (const TrackCache *E : playing_caches) { - if (ObjectDB::get_instance(E->object_id)) { - E->object->call(SNAME("stop")); - } - } - - playing_caches.clear(); + _clear_caches(); } } @@ -531,6 +525,7 @@ bool AnimationTree::_update_caches(AnimationPlayer *player) { if (!player->has_node(player->get_root())) { ERR_PRINT("AnimationTree: AnimationPlayer root is invalid."); set_active(false); + _clear_caches(); return false; } Node *parent = player->get_node(player->get_root()); @@ -763,6 +758,8 @@ bool AnimationTree::_update_caches(AnimationPlayer *player) { track_audio->object = child; track_audio->object_id = track_audio->object->get_instance_id(); + track_audio->audio_stream.instantiate(); + track_audio->audio_stream->set_polyphony(audio_max_polyphony); track = track_audio; @@ -860,14 +857,32 @@ void AnimationTree::_animation_player_changed() { } void AnimationTree::_clear_caches() { + _clear_audio_streams(); + _clear_playing_caches(); for (KeyValue<NodePath, TrackCache *> &K : track_cache) { memdelete(K.value); } - playing_caches.clear(); track_cache.clear(); cache_valid = false; } +void AnimationTree::_clear_audio_streams() { + for (int i = 0; i < playing_audio_stream_players.size(); i++) { + playing_audio_stream_players[i]->call(SNAME("stop")); + playing_audio_stream_players[i]->call(SNAME("set_stream"), Ref<AudioStream>()); + } + playing_audio_stream_players.clear(); +} + +void AnimationTree::_clear_playing_caches() { + for (const TrackCache *E : playing_caches) { + if (ObjectDB::get_instance(E->object_id)) { + E->object->call(SNAME("stop")); + } + } + playing_caches.clear(); +} + static void _call_object(Object *p_object, const StringName &p_method, const Vector<Variant> &p_params, bool p_deferred) { // Separate function to use alloca() more efficiently const Variant **argptrs = (const Variant **)alloca(sizeof(const Variant **) * p_params.size()); @@ -1007,6 +1022,13 @@ void AnimationTree::_process_graph(double p_delta) { TrackCacheBezier *t = static_cast<TrackCacheBezier *>(track); t->value = t->init_value; } break; + case Animation::TYPE_AUDIO: { + TrackCacheAudio *t = static_cast<TrackCacheAudio *>(track); + for (KeyValue<ObjectID, PlayingAudioTrackInfo> &L : t->playing_streams) { + PlayingAudioTrackInfo &track_info = L.value; + track_info.volume = 0.0; + } + } break; default: { } break; } @@ -1026,8 +1048,8 @@ void AnimationTree::_process_graph(double p_delta) { bool seeked = as.seeked; Animation::LoopedFlag looped_flag = as.looped_flag; bool is_external_seeking = as.is_external_seeking; + bool backward = signbit(delta); // This flag is used by the root motion calculates or detecting the end of audio stream. #ifndef _3D_DISABLED - bool backward = signbit(delta); // This flag is required only for the root motion since it calculates the difference between the previous and current frames. bool calc_root = !seeked || is_external_seeking; #endif // _3D_DISABLED @@ -1046,9 +1068,6 @@ void AnimationTree::_process_graph(double p_delta) { int blend_idx = state.track_map[path]; ERR_CONTINUE(blend_idx < 0 || blend_idx >= state.track_count); real_t blend = (*as.track_blends)[blend_idx] * weight; - if (Math::is_zero_approx(blend)) { - continue; // Nothing to blend. - } Animation::TrackType ttype = a->track_get_type(i); if (ttype != Animation::TYPE_POSITION_3D && ttype != Animation::TYPE_ROTATION_3D && ttype != Animation::TYPE_SCALE_3D && track->type != ttype) { @@ -1060,6 +1079,9 @@ void AnimationTree::_process_graph(double p_delta) { switch (ttype) { case Animation::TYPE_POSITION_3D: { #ifndef _3D_DISABLED + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheTransform *t = static_cast<TrackCacheTransform *>(track); if (track->root_motion && calc_root) { double prev_time = time - delta; @@ -1151,6 +1173,9 @@ void AnimationTree::_process_graph(double p_delta) { } break; case Animation::TYPE_ROTATION_3D: { #ifndef _3D_DISABLED + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheTransform *t = static_cast<TrackCacheTransform *>(track); if (track->root_motion && calc_root) { double prev_time = time - delta; @@ -1241,6 +1266,9 @@ void AnimationTree::_process_graph(double p_delta) { } break; case Animation::TYPE_SCALE_3D: { #ifndef _3D_DISABLED + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheTransform *t = static_cast<TrackCacheTransform *>(track); if (track->root_motion && calc_root) { double prev_time = time - delta; @@ -1332,6 +1360,9 @@ void AnimationTree::_process_graph(double p_delta) { } break; case Animation::TYPE_BLEND_SHAPE: { #ifndef _3D_DISABLED + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheBlendShape *t = static_cast<TrackCacheBlendShape *>(track); float value; @@ -1348,6 +1379,9 @@ void AnimationTree::_process_graph(double p_delta) { #endif // _3D_DISABLED } break; case Animation::TYPE_VALUE: { + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheValue *t = static_cast<TrackCacheValue *>(track); Animation::UpdateMode update_mode = a->value_track_get_update_mode(i); @@ -1414,6 +1448,9 @@ void AnimationTree::_process_graph(double p_delta) { continue; } #endif // TOOLS_ENABLED + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheMethod *t = static_cast<TrackCacheMethod *>(track); if (seeked) { @@ -1435,6 +1472,9 @@ void AnimationTree::_process_graph(double p_delta) { } } break; case Animation::TYPE_BEZIER: { + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheBezier *t = static_cast<TrackCacheBezier *>(track); real_t bezier = a->bezier_track_interpolate(i, time); @@ -1445,110 +1485,87 @@ void AnimationTree::_process_graph(double p_delta) { case Animation::TYPE_AUDIO: { TrackCacheAudio *t = static_cast<TrackCacheAudio *>(track); - if (seeked) { - int idx = a->track_find_key(i, time, is_external_seeking ? Animation::FIND_MODE_NEAREST : Animation::FIND_MODE_EXACT); - if (idx < 0) { - continue; - } - - Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); - if (!stream.is_valid()) { - t->object->call(SNAME("stop")); - t->playing = false; - playing_caches.erase(t); - } else { - double start_ofs = a->audio_track_get_key_start_offset(i, idx); - start_ofs += time - a->track_get_key_time(i, idx); - double end_ofs = a->audio_track_get_key_end_offset(i, idx); - double len = stream->get_length(); - - if (start_ofs > len - end_ofs) { - t->object->call(SNAME("stop")); - t->playing = false; - playing_caches.erase(t); - continue; - } - - t->object->call(SNAME("set_stream"), stream); - t->object->call(SNAME("play"), start_ofs); - - t->playing = true; - playing_caches.insert(t); - if (len && end_ofs > 0) { //force an end at a time - t->len = len - start_ofs - end_ofs; - } else { - t->len = 0; - } + Node *asp = Object::cast_to<Node>(t->object); + if (!asp) { + t->playing_streams.clear(); + continue; + } - t->start = time; + ObjectID oid = a->get_instance_id(); + if (!t->playing_streams.has(oid)) { + t->playing_streams[oid] = PlayingAudioTrackInfo(); + } + // The end of audio should be observed even if the blend value is 0, build up the information and store to the cache for that. + PlayingAudioTrackInfo &track_info = t->playing_streams[oid]; + track_info.length = a->get_length(); + track_info.time = time; + track_info.volume += blend; + track_info.loop = a->get_loop_mode() != Animation::LOOP_NONE; + track_info.backward = backward; + track_info.use_blend = a->audio_track_is_use_blend(i); + + HashMap<int, PlayingAudioStreamInfo> &map = track_info.stream_info; + // Find stream. + int idx = -1; + if (seeked) { + idx = a->track_find_key(i, time, is_external_seeking ? Animation::FIND_MODE_NEAREST : Animation::FIND_MODE_EXACT); + // Discard previous stream when seeking. + if (map.has(idx)) { + t->audio_stream_playback->stop_stream(map[idx].index); + map.erase(idx); } - } else { - //find stuff to play List<int> to_play; a->track_get_key_indices_in_range(i, time, delta, &to_play, looped_flag); if (to_play.size()) { - int idx = to_play.back()->get(); - - Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); - if (!stream.is_valid()) { - t->object->call(SNAME("stop")); - t->playing = false; - playing_caches.erase(t); - } else { - double start_ofs = a->audio_track_get_key_start_offset(i, idx); - double end_ofs = a->audio_track_get_key_end_offset(i, idx); - double len = stream->get_length(); - - t->object->call(SNAME("set_stream"), stream); - t->object->call(SNAME("play"), start_ofs); - - t->playing = true; - playing_caches.insert(t); - if (len && end_ofs > 0) { //force an end at a time - t->len = len - start_ofs - end_ofs; - } else { - t->len = 0; - } + idx = to_play.back()->get(); + } + } + if (idx < 0) { + continue; + } - t->start = time; - } - } else if (t->playing) { - bool loop = a->get_loop_mode() != Animation::LOOP_NONE; - - bool stop = false; - - if (!loop) { - if (delta > 0) { - if (time < t->start) { - stop = true; - } - } else if (delta < 0) { - if (time > t->start) { - stop = true; - } - } - } else if (t->len > 0) { - double len = t->start > time ? (a->get_length() - t->start) + time : time - t->start; - - if (len > t->len) { - stop = true; - } + // Play stream. + Ref<AudioStream> stream = a->audio_track_get_key_stream(i, idx); + if (stream.is_valid()) { + double start_ofs = a->audio_track_get_key_start_offset(i, idx); + double end_ofs = a->audio_track_get_key_end_offset(i, idx); + double len = stream->get_length(); + + if (t->object->call(SNAME("get_stream")) != t->audio_stream) { + t->object->call(SNAME("set_stream"), t->audio_stream); + t->audio_stream_playback.unref(); + if (!playing_audio_stream_players.has(asp)) { + playing_audio_stream_players.push_back(asp); } + } + if (!t->object->call(SNAME("is_playing"))) { + t->object->call(SNAME("play")); + } + if (!t->object->call(SNAME("has_stream_playback"))) { + t->audio_stream_playback.unref(); + continue; + } + if (t->audio_stream_playback.is_null()) { + t->audio_stream_playback = t->object->call(SNAME("get_stream_playback")); + } - if (stop) { - //time to stop - t->object->call(SNAME("stop")); - t->playing = false; - playing_caches.erase(t); - } + PlayingAudioStreamInfo pasi; + pasi.index = t->audio_stream_playback->play_stream(stream, start_ofs); + pasi.start = time; + if (len && end_ofs > 0) { // Force an end at a time. + pasi.len = len - start_ofs - end_ofs; + } else { + pasi.len = 0; } + map[idx] = pasi; } - real_t db = Math::linear_to_db(MAX(blend, 0.00001)); - t->object->call(SNAME("set_volume_db"), db); } break; case Animation::TYPE_ANIMATION: { + if (Math::is_zero_approx(blend)) { + continue; // Nothing to blend. + } TrackCacheAnimation *t = static_cast<TrackCacheAnimation *>(track); AnimationPlayer *player2 = Object::cast_to<AnimationPlayer>(t->object); @@ -1694,6 +1711,64 @@ void AnimationTree::_process_graph(double p_delta) { t->object->set_indexed(t->subpath, t->value); } break; + case Animation::TYPE_AUDIO: { + TrackCacheAudio *t = static_cast<TrackCacheAudio *>(track); + + // Audio ending process. + LocalVector<ObjectID> erase_maps; + for (KeyValue<ObjectID, PlayingAudioTrackInfo> &L : t->playing_streams) { + PlayingAudioTrackInfo &track_info = L.value; + float db = Math::linear_to_db(track_info.use_blend ? track_info.volume : 1.0); + LocalVector<int> erase_streams; + HashMap<int, PlayingAudioStreamInfo> &map = track_info.stream_info; + for (const KeyValue<int, PlayingAudioStreamInfo> &M : map) { + PlayingAudioStreamInfo pasi = M.value; + + bool stop = false; + if (!t->audio_stream_playback->is_stream_playing(pasi.index)) { + stop = true; + } + if (!track_info.loop) { + if (!track_info.backward) { + if (track_info.time < pasi.start) { + stop = true; + } + } else if (track_info.backward) { + if (track_info.time > pasi.start) { + stop = true; + } + } + } + if (pasi.len > 0) { + double len = 0.0; + if (!track_info.backward) { + len = pasi.start > track_info.time ? (track_info.length - pasi.start) + track_info.time : track_info.time - pasi.start; + } else { + len = pasi.start < track_info.time ? (track_info.length - track_info.time) + pasi.start : pasi.start - track_info.time; + } + if (len > pasi.len) { + stop = true; + } + } + if (stop) { + // Time to stop. + t->audio_stream_playback->stop_stream(pasi.index); + erase_streams.push_back(M.key); + } else { + t->audio_stream_playback->set_stream_volume(pasi.index, db); + } + } + for (uint32_t erase_idx = 0; erase_idx < erase_streams.size(); erase_idx++) { + map.erase(erase_streams[erase_idx]); + } + if (map.size() == 0) { + erase_maps.push_back(L.key); + } + } + for (uint32_t erase_idx = 0; erase_idx < erase_maps.size(); erase_idx++) { + t->playing_streams.erase(erase_maps[erase_idx]); + } + } break; default: { } //the rest don't matter } @@ -1819,6 +1894,15 @@ NodePath AnimationTree::get_advance_expression_base_node() const { return advance_expression_base_node; } +void AnimationTree::set_audio_max_polyphony(int p_audio_max_polyphony) { + ERR_FAIL_COND(p_audio_max_polyphony < 0 || p_audio_max_polyphony > 128); + audio_max_polyphony = p_audio_max_polyphony; +} + +int AnimationTree::get_audio_max_polyphony() const { + return audio_max_polyphony; +} + bool AnimationTree::is_state_invalid() const { return !state.valid; } @@ -2034,6 +2118,9 @@ void AnimationTree::_bind_methods() { ClassDB::bind_method(D_METHOD("set_root_motion_track", "path"), &AnimationTree::set_root_motion_track); ClassDB::bind_method(D_METHOD("get_root_motion_track"), &AnimationTree::get_root_motion_track); + ClassDB::bind_method(D_METHOD("set_audio_max_polyphony", "max_polyphony"), &AnimationTree::set_audio_max_polyphony); + ClassDB::bind_method(D_METHOD("get_audio_max_polyphony"), &AnimationTree::get_audio_max_polyphony); + ClassDB::bind_method(D_METHOD("get_root_motion_position"), &AnimationTree::get_root_motion_position); ClassDB::bind_method(D_METHOD("get_root_motion_rotation"), &AnimationTree::get_root_motion_rotation); ClassDB::bind_method(D_METHOD("get_root_motion_scale"), &AnimationTree::get_root_motion_scale); @@ -2052,6 +2139,8 @@ void AnimationTree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active"); ADD_PROPERTY(PropertyInfo(Variant::INT, "process_callback", PROPERTY_HINT_ENUM, "Physics,Idle,Manual"), "set_process_callback", "get_process_callback"); + ADD_GROUP("Audio", "audio_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "audio_max_polyphony", PROPERTY_HINT_RANGE, "1,127,1"), "set_audio_max_polyphony", "get_audio_max_polyphony"); ADD_GROUP("Root Motion", "root_motion_"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_motion_track"), "set_root_motion_track", "get_root_motion_track"); diff --git a/scene/animation/animation_tree.h b/scene/animation/animation_tree.h index ab538feb58..394a3a2237 100644 --- a/scene/animation/animation_tree.h +++ b/scene/animation/animation_tree.h @@ -35,6 +35,7 @@ #include "scene/3d/node_3d.h" #include "scene/3d/skeleton_3d.h" #include "scene/resources/animation.h" +#include "scene/resources/audio_stream_polyphonic.h" class AnimationNodeBlendTree; class AnimationNodeStartState; @@ -252,10 +253,26 @@ private: } }; - struct TrackCacheAudio : public TrackCache { - bool playing = false; + struct PlayingAudioStreamInfo { + int64_t index = -1; double start = 0.0; double len = 0.0; + }; + + struct PlayingAudioTrackInfo { + HashMap<int, PlayingAudioStreamInfo> stream_info; + double length = 0.0; + double time = 0.0; + real_t volume = 0.0; + bool loop = false; + bool backward = false; + bool use_blend = false; + }; + + struct TrackCacheAudio : public TrackCache { + Ref<AudioStreamPolyphonic> audio_stream; + Ref<AudioStreamPlaybackPolyphonic> audio_stream_playback; + HashMap<ObjectID, PlayingAudioTrackInfo> playing_streams; // Animation resource RID & AudioTrack key index: PlayingAudioStreamInfo. TrackCacheAudio() { type = Animation::TYPE_AUDIO; @@ -272,6 +289,7 @@ private: HashMap<NodePath, TrackCache *> track_cache; HashSet<TrackCache *> playing_caches; + Vector<Node *> playing_audio_stream_players; Ref<AnimationNode> root; NodePath advance_expression_base_node = NodePath(String(".")); @@ -279,6 +297,7 @@ private: AnimationProcessCallback process_callback = ANIMATION_PROCESS_IDLE; bool active = false; NodePath animation_player; + int audio_max_polyphony = 32; AnimationNode::State state; bool cache_valid = false; @@ -287,6 +306,8 @@ private: void _setup_animation_player(); void _animation_player_changed(); void _clear_caches(); + void _clear_playing_caches(); + void _clear_audio_streams(); bool _update_caches(AnimationPlayer *player); void _process_graph(double p_delta); @@ -348,6 +369,9 @@ public: void set_advance_expression_base_node(const NodePath &p_advance_expression_base_node); NodePath get_advance_expression_base_node() const; + void set_audio_max_polyphony(int p_audio_max_polyphony); + int get_audio_max_polyphony() const; + PackedStringArray get_configuration_warnings() const override; bool is_state_invalid() const; diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp index d40fc10441..7533a56b59 100644 --- a/scene/audio/audio_stream_player.cpp +++ b/scene/audio/audio_stream_player.cpp @@ -307,6 +307,10 @@ void AudioStreamPlayer::_bus_layout_changed() { notify_property_list_changed(); } +bool AudioStreamPlayer::has_stream_playback() { + return !stream_playbacks.is_empty(); +} + Ref<AudioStreamPlayback> AudioStreamPlayer::get_stream_playback() { ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback()."); return stream_playbacks[stream_playbacks.size() - 1]; @@ -347,6 +351,7 @@ void AudioStreamPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_max_polyphony", "max_polyphony"), &AudioStreamPlayer::set_max_polyphony); ClassDB::bind_method(D_METHOD("get_max_polyphony"), &AudioStreamPlayer::get_max_polyphony); + ClassDB::bind_method(D_METHOD("has_stream_playback"), &AudioStreamPlayer::has_stream_playback); ClassDB::bind_method(D_METHOD("get_stream_playback"), &AudioStreamPlayer::get_stream_playback); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); diff --git a/scene/audio/audio_stream_player.h b/scene/audio/audio_stream_player.h index 5368391073..d1f6fca2ee 100644 --- a/scene/audio/audio_stream_player.h +++ b/scene/audio/audio_stream_player.h @@ -107,6 +107,7 @@ public: void set_stream_paused(bool p_pause); bool get_stream_paused() const; + bool has_stream_playback(); Ref<AudioStreamPlayback> get_stream_playback(); AudioStreamPlayer(); diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 3ceb27b3e6..869d12b4df 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -663,18 +663,18 @@ void Window::set_visible(bool p_visible) { return; } - visible = p_visible; - if (!is_inside_tree()) { + visible = p_visible; return; } - if (updating_child_controls) { - _update_child_controls(); - } - ERR_FAIL_COND_MSG(get_parent() == nullptr, "Can't change visibility of main window."); + visible = p_visible; + + // Stop any queued resizing, as the window will be resized right now. + updating_child_controls = false; + Viewport *embedder_vp = _get_embedder(); if (!embedder_vp) { @@ -833,7 +833,7 @@ bool Window::is_visible() const { } void Window::_update_window_size() { - // Force window to respect size limitations of rendering server + // Force window to respect size limitations of rendering server. RenderingServer *rendering_server = RenderingServer::get_singleton(); if (rendering_server) { Size2i max_window_size = rendering_server->get_maximum_viewport_size(); @@ -1130,6 +1130,7 @@ void Window::_notification(int p_what) { case NOTIFICATION_READY: { if (wrap_controls) { + // Finish any resizing immediately so it doesn't interfere on stuff overriding _ready(). _update_child_controls(); } } break; @@ -1138,9 +1139,7 @@ void Window::_notification(int p_what) { _invalidate_theme_cache(); _update_theme_item_cache(); - if (embedder) { - embedder->_sub_window_update(this); - } else if (window_id != DisplayServer::INVALID_WINDOW_ID) { + if (!embedder && window_id != DisplayServer::INVALID_WINDOW_ID) { String tr_title = atr(title); #ifdef DEBUG_ENABLED if (window_id == DisplayServer::MAIN_WINDOW_ID) { @@ -1152,8 +1151,6 @@ void Window::_notification(int p_what) { #endif DisplayServer::get_singleton()->window_set_title(tr_title, window_id); } - - child_controls_changed(); } break; case NOTIFICATION_EXIT_TREE: { @@ -1254,8 +1251,13 @@ Vector<Vector2> Window::get_mouse_passthrough_polygon() const { void Window::set_wrap_controls(bool p_enable) { wrap_controls = p_enable; - if (wrap_controls) { - child_controls_changed(); + + if (!is_inside_tree()) { + return; + } + + if (updating_child_controls) { + _update_child_controls(); } else { _update_window_size(); } @@ -1282,23 +1284,23 @@ Size2 Window::_get_contents_minimum_size() const { return max; } -void Window::_update_child_controls() { - if (!updating_child_controls) { +void Window::child_controls_changed() { + if (!is_inside_tree() || !visible || updating_child_controls) { return; } - _update_window_size(); - - updating_child_controls = false; + updating_child_controls = true; + call_deferred(SNAME("_update_child_controls")); } -void Window::child_controls_changed() { - if (!is_inside_tree() || !visible || updating_child_controls) { +void Window::_update_child_controls() { + if (!updating_child_controls) { return; } - updating_child_controls = true; - call_deferred(SNAME("_update_child_controls")); + _update_window_size(); + + updating_child_controls = false; } bool Window::_can_consume_input_events() const { @@ -1647,7 +1649,24 @@ void Window::_invalidate_theme_cache() { void Window::_update_theme_item_cache() { // Request an update on the next frame to reflect theme changes. // Updating without a delay can cause a lot of lag. - child_controls_changed(); + if (!wrap_controls) { + updating_embedded_window = true; + call_deferred(SNAME("_update_embedded_window")); + } else { + child_controls_changed(); + } +} + +void Window::_update_embedded_window() { + if (!updating_embedded_window) { + return; + } + + if (embedder) { + embedder->_sub_window_update(this); + }; + + updating_embedded_window = false; } void Window::set_theme_type_variation(const StringName &p_theme_type) { @@ -2201,6 +2220,7 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("child_controls_changed"), &Window::child_controls_changed); ClassDB::bind_method(D_METHOD("_update_child_controls"), &Window::_update_child_controls); + ClassDB::bind_method(D_METHOD("_update_embedded_window"), &Window::_update_embedded_window); ClassDB::bind_method(D_METHOD("set_theme", "theme"), &Window::set_theme); ClassDB::bind_method(D_METHOD("get_theme"), &Window::get_theme); diff --git a/scene/main/window.h b/scene/main/window.h index 18841a4f1a..182caf5f0c 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -116,6 +116,7 @@ private: bool exclusive = false; bool wrap_controls = false; bool updating_child_controls = false; + bool updating_embedded_window = false; bool clamp_to_embedder = false; LayoutDirection layout_dir = LAYOUT_DIRECTION_INHERITED; @@ -123,6 +124,7 @@ private: bool auto_translate = true; void _update_child_controls(); + void _update_embedded_window(); Size2i content_scale_size; ContentScaleMode content_scale_mode = CONTENT_SCALE_MODE_DISABLED; diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index cb3c865f05..bfbc92a8d4 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -127,6 +127,10 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) { } } return true; + } else if (what == "use_blend") { + if (track_get_type(track) == TYPE_AUDIO) { + audio_track_set_use_blend(track, p_value); + } } else if (what == "interp") { track_set_interpolation_type(track, InterpolationType(p_value.operator int())); } else if (what == "loop_wrap") { @@ -528,7 +532,10 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const { } return true; - + } else if (what == "use_blend") { + if (track_get_type(track) == TYPE_AUDIO) { + r_ret = audio_track_is_use_blend(track); + } } else if (what == "interp") { r_ret = track_get_interpolation_type(track); } else if (what == "loop_wrap") { @@ -834,6 +841,9 @@ void Animation::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/loop_wrap", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::ARRAY, "tracks/" + itos(i) + "/keys", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); } + if (track_get_type(i) == TYPE_AUDIO) { + p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/use_blend", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); + } } } @@ -3581,6 +3591,27 @@ real_t Animation::audio_track_get_key_end_offset(int p_track, int p_key) const { return at->values[p_key].value.end_offset; } +void Animation::audio_track_set_use_blend(int p_track, bool p_enable) { + ERR_FAIL_INDEX(p_track, tracks.size()); + Track *t = tracks[p_track]; + ERR_FAIL_COND(t->type != TYPE_AUDIO); + + AudioTrack *at = static_cast<AudioTrack *>(t); + + at->use_blend = p_enable; + emit_changed(); +} + +bool Animation::audio_track_is_use_blend(int p_track) const { + ERR_FAIL_INDEX_V(p_track, tracks.size(), false); + Track *t = tracks[p_track]; + ERR_FAIL_COND_V(t->type != TYPE_AUDIO, false); + + AudioTrack *at = static_cast<AudioTrack *>(t); + + return at->use_blend; +} + // int Animation::animation_track_insert_key(int p_track, double p_time, const StringName &p_animation) { @@ -3813,6 +3844,8 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("audio_track_get_key_stream", "track_idx", "key_idx"), &Animation::audio_track_get_key_stream); ClassDB::bind_method(D_METHOD("audio_track_get_key_start_offset", "track_idx", "key_idx"), &Animation::audio_track_get_key_start_offset); ClassDB::bind_method(D_METHOD("audio_track_get_key_end_offset", "track_idx", "key_idx"), &Animation::audio_track_get_key_end_offset); + ClassDB::bind_method(D_METHOD("audio_track_set_use_blend", "track_idx", "enable"), &Animation::audio_track_set_use_blend); + ClassDB::bind_method(D_METHOD("audio_track_is_use_blend", "track_idx"), &Animation::audio_track_is_use_blend); ClassDB::bind_method(D_METHOD("animation_track_insert_key", "track_idx", "time", "animation"), &Animation::animation_track_insert_key); ClassDB::bind_method(D_METHOD("animation_track_set_key_animation", "track_idx", "key_idx", "animation"), &Animation::animation_track_set_key_animation); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 00a0761e0a..2c2ddb7095 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -213,6 +213,7 @@ private: struct AudioTrack : public Track { Vector<TKey<AudioKey>> values; + bool use_blend = true; AudioTrack() { type = TYPE_AUDIO; @@ -447,6 +448,8 @@ public: Ref<Resource> audio_track_get_key_stream(int p_track, int p_key) const; real_t audio_track_get_key_start_offset(int p_track, int p_key) const; real_t audio_track_get_key_end_offset(int p_track, int p_key) const; + void audio_track_set_use_blend(int p_track, bool p_enable); + bool audio_track_is_use_blend(int p_track) const; int animation_track_insert_key(int p_track, double p_time, const StringName &p_animation); void animation_track_set_key_animation(int p_track, int p_key, const StringName &p_animation); diff --git a/scene/resources/shader.cpp b/scene/resources/shader.cpp index a09dbd50cf..fd2be9ba22 100644 --- a/scene/resources/shader.cpp +++ b/scene/resources/shader.cpp @@ -55,6 +55,12 @@ void Shader::set_path(const String &p_path, bool p_take_over) { RS::get_singleton()->shader_set_path_hint(shader, p_path); } +void Shader::set_include_path(const String &p_path) { + // Used only if the shader does not have a resource path set, + // for example during loading stage or when created by code. + include_path = p_path; +} + void Shader::set_code(const String &p_code) { for (Ref<ShaderInclude> E : include_dependencies) { E->disconnect(SNAME("changed"), callable_mp(this, &Shader::_dependency_changed)); @@ -80,11 +86,15 @@ void Shader::set_code(const String &p_code) { HashSet<Ref<ShaderInclude>> new_include_dependencies; { + String path = get_path(); + if (path.is_empty()) { + path = include_path; + } // Preprocessor must run here and not in the server because: // 1) Need to keep track of include dependencies at resource level // 2) Server does not do interaction with Resource filetypes, this is a scene level feature. ShaderPreprocessor preprocessor; - preprocessor.preprocess(p_code, "", pp_code, nullptr, nullptr, nullptr, &new_include_dependencies); + preprocessor.preprocess(p_code, path, pp_code, nullptr, nullptr, nullptr, &new_include_dependencies); } // This ensures previous include resources are not freed and then re-loaded during parse (which would make compiling slower) @@ -231,6 +241,7 @@ Ref<Resource> ResourceFormatLoaderShader::load(const String &p_path, const Strin String str; str.parse_utf8((const char *)buffer.ptr(), buffer.size()); + shader->set_include_path(p_path); shader->set_code(str); if (r_error) { diff --git a/scene/resources/shader.h b/scene/resources/shader.h index 55608b6c11..ca889940ef 100644 --- a/scene/resources/shader.h +++ b/scene/resources/shader.h @@ -56,6 +56,7 @@ private: Mode mode = MODE_SPATIAL; HashSet<Ref<ShaderInclude>> include_dependencies; String code; + String include_path; HashMap<StringName, HashMap<int, Ref<Texture2D>>> default_textures; @@ -72,6 +73,7 @@ public: virtual Mode get_mode() const; virtual void set_path(const String &p_path, bool p_take_over = false) override; + void set_include_path(const String &p_path); void set_code(const String &p_code); String get_code() const; diff --git a/scene/resources/shader_include.cpp b/scene/resources/shader_include.cpp index cd5e9861f7..68137cbec0 100644 --- a/scene/resources/shader_include.cpp +++ b/scene/resources/shader_include.cpp @@ -45,9 +45,14 @@ void ShaderInclude::set_code(const String &p_code) { } { + String path = get_path(); + if (path.is_empty()) { + path = include_path; + } + String pp_code; ShaderPreprocessor preprocessor; - preprocessor.preprocess(p_code, "", pp_code, nullptr, nullptr, nullptr, &new_dependencies); + preprocessor.preprocess(p_code, path, pp_code, nullptr, nullptr, nullptr, &new_dependencies); } // This ensures previous include resources are not freed and then re-loaded during parse (which would make compiling slower) @@ -64,6 +69,10 @@ String ShaderInclude::get_code() const { return code; } +void ShaderInclude::set_include_path(const String &p_path) { + include_path = p_path; +} + void ShaderInclude::_bind_methods() { ClassDB::bind_method(D_METHOD("set_code", "code"), &ShaderInclude::set_code); ClassDB::bind_method(D_METHOD("get_code"), &ShaderInclude::get_code); @@ -86,6 +95,7 @@ Ref<Resource> ResourceFormatLoaderShaderInclude::load(const String &p_path, cons String str; str.parse_utf8((const char *)buffer.ptr(), buffer.size()); + shader_inc->set_include_path(p_path); shader_inc->set_code(str); if (r_error) { diff --git a/scene/resources/shader_include.h b/scene/resources/shader_include.h index 04f4f5cf84..a8949b327e 100644 --- a/scene/resources/shader_include.h +++ b/scene/resources/shader_include.h @@ -42,6 +42,7 @@ class ShaderInclude : public Resource { private: String code; + String include_path; HashSet<Ref<ShaderInclude>> dependencies; void _dependency_changed(); @@ -51,6 +52,8 @@ protected: public: void set_code(const String &p_text); String get_code() const; + + void set_include_path(const String &p_path); }; class ResourceFormatLoaderShaderInclude : public ResourceFormatLoader { diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp index 56efee5ca6..58a638804d 100644 --- a/scene/resources/tile_set.cpp +++ b/scene/resources/tile_set.cpp @@ -2555,6 +2555,11 @@ void TileSet::_compatibility_conversion() { bool flip_v = flags & 2; bool transpose = flags & 4; + Transform2D xform; + xform = flip_h ? xform.scaled(Size2(-1, 1)) : xform; + xform = flip_v ? xform.scaled(Size2(1, -1)) : xform; + xform = transpose ? xform.rotated(Math_PI).scaled(Size2(-1, -1)) : xform; + int alternative_tile = 0; if (!atlas_source->has_tile(coords)) { atlas_source->create_tile(coords); @@ -2591,14 +2596,26 @@ void TileSet::_compatibility_conversion() { if (ctd->occluder.is_valid()) { if (get_occlusion_layers_count() < 1) { add_occlusion_layer(); + }; + Ref<OccluderPolygon2D> occluder = ctd->occluder->duplicate(); + Vector<Vector2> polygon = ctd->occluder->get_polygon(); + for (int index = 0; index < polygon.size(); index++) { + polygon.write[index] = xform.xform(polygon[index] - ctd->region.get_size() / 2.0); } - tile_data->set_occluder(0, ctd->occluder); + occluder->set_polygon(polygon); + tile_data->set_occluder(0, occluder); } if (ctd->navigation.is_valid()) { if (get_navigation_layers_count() < 1) { add_navigation_layer(); } - tile_data->set_navigation_polygon(0, ctd->autotile_navpoly_map[coords]); + Ref<NavigationPolygon> navigation = ctd->navigation->duplicate(); + Vector<Vector2> vertices = navigation->get_vertices(); + for (int index = 0; index < vertices.size(); index++) { + vertices.write[index] = xform.xform(vertices[index] - ctd->region.get_size() / 2.0); + } + navigation->set_vertices(vertices); + tile_data->set_navigation_polygon(0, navigation); } tile_data->set_z_index(ctd->z_index); @@ -2616,7 +2633,7 @@ void TileSet::_compatibility_conversion() { if (convex_shape.is_valid()) { Vector<Vector2> polygon = convex_shape->get_points(); for (int point_index = 0; point_index < polygon.size(); point_index++) { - polygon.write[point_index] = csd.transform.xform(polygon[point_index]); + polygon.write[point_index] = xform.xform(csd.transform.xform(polygon[point_index]) - ctd->region.get_size() / 2.0); } tile_data->set_collision_polygons_count(0, tile_data->get_collision_polygons_count(0) + 1); int index = tile_data->get_collision_polygons_count(0) - 1; @@ -2627,6 +2644,11 @@ void TileSet::_compatibility_conversion() { } } } + // Update the size count. + if (!compatibility_size_count.has(ctd->region.get_size())) { + compatibility_size_count[ctd->region.get_size()] = 0; + } + compatibility_size_count[ctd->region.get_size()]++; } break; case COMPATIBILITY_TILE_MODE_AUTO_TILE: { // Not supported. It would need manual conversion. @@ -2647,6 +2669,11 @@ void TileSet::_compatibility_conversion() { bool flip_v = flags & 2; bool transpose = flags & 4; + Transform2D xform; + xform = flip_h ? xform.scaled(Size2(-1, 1)) : xform; + xform = flip_v ? xform.scaled(Size2(1, -1)) : xform; + xform = transpose ? xform.rotated(Math_PI).scaled(Size2(-1, -1)) : xform; + int alternative_tile = 0; if (!atlas_source->has_tile(coords)) { atlas_source->create_tile(coords); @@ -2684,13 +2711,25 @@ void TileSet::_compatibility_conversion() { if (get_occlusion_layers_count() < 1) { add_occlusion_layer(); } - tile_data->set_occluder(0, ctd->autotile_occluder_map[coords]); + Ref<OccluderPolygon2D> occluder = ctd->autotile_occluder_map[coords]->duplicate(); + Vector<Vector2> polygon = ctd->occluder->get_polygon(); + for (int index = 0; index < polygon.size(); index++) { + polygon.write[index] = xform.xform(polygon[index] - ctd->region.get_size() / 2.0); + } + occluder->set_polygon(polygon); + tile_data->set_occluder(0, occluder); } if (ctd->autotile_navpoly_map.has(coords)) { if (get_navigation_layers_count() < 1) { add_navigation_layer(); } - tile_data->set_navigation_polygon(0, ctd->autotile_navpoly_map[coords]); + Ref<NavigationPolygon> navigation = ctd->autotile_navpoly_map[coords]->duplicate(); + Vector<Vector2> vertices = navigation->get_vertices(); + for (int index = 0; index < vertices.size(); index++) { + vertices.write[index] = xform.xform(vertices[index] - ctd->region.get_size() / 2.0); + } + navigation->set_vertices(vertices); + tile_data->set_navigation_polygon(0, navigation); } if (ctd->autotile_priority_map.has(coords)) { tile_data->set_probability(ctd->autotile_priority_map[coords]); @@ -2712,7 +2751,7 @@ void TileSet::_compatibility_conversion() { if (convex_shape.is_valid()) { Vector<Vector2> polygon = convex_shape->get_points(); for (int point_index = 0; point_index < polygon.size(); point_index++) { - polygon.write[point_index] = csd.transform.xform(polygon[point_index]); + polygon.write[point_index] = xform.xform(csd.transform.xform(polygon[point_index]) - ctd->autotile_tile_size / 2.0); } tile_data->set_collision_polygons_count(0, tile_data->get_collision_polygons_count(0) + 1); int index = tile_data->get_collision_polygons_count(0) - 1; @@ -2735,6 +2774,12 @@ void TileSet::_compatibility_conversion() { } } } + + // Update the size count. + if (!compatibility_size_count.has(ctd->region.get_size())) { + compatibility_size_count[ctd->autotile_tile_size] = 0; + } + compatibility_size_count[ctd->autotile_tile_size] += atlas_size.x * atlas_size.y; } break; } @@ -2751,7 +2796,18 @@ void TileSet::_compatibility_conversion() { } } - // Reset compatibility data + // Update the TileSet tile_size according to the most common size found. + Vector2i max_size = get_tile_size(); + int max_count = 0; + for (KeyValue<Vector2i, int> kv : compatibility_size_count) { + if (kv.value > max_count) { + max_size = kv.key; + max_count = kv.value; + } + } + set_tile_size(max_size); + + // Reset compatibility data (besides the histogram counts) for (const KeyValue<int, CompatibilityTileData *> &E : compatibility_data) { memdelete(E.value); } @@ -2932,6 +2988,10 @@ bool TileSet::_set(const StringName &p_name, const Variant &p_value) { } ctd->shapes.push_back(csd); } + } else if (what == "occluder") { + ctd->occluder = p_value; + } else if (what == "navigation") { + ctd->navigation = p_value; /* // IGNORED FOR NOW, they seem duplicated data compared to the shapes array diff --git a/scene/resources/tile_set.h b/scene/resources/tile_set.h index 044b46a701..ad25629a1c 100644 --- a/scene/resources/tile_set.h +++ b/scene/resources/tile_set.h @@ -198,6 +198,7 @@ private: HashMap<int, CompatibilityTileData *> compatibility_data; HashMap<int, int> compatibility_tilemap_mapping_tile_modes; HashMap<int, RBMap<Array, Array>> compatibility_tilemap_mapping; + HashMap<Vector2i, int> compatibility_size_count; void _compatibility_conversion(); diff --git a/servers/navigation_server_2d.cpp b/servers/navigation_server_2d.cpp index 88178d5c7d..2e4087c1de 100644 --- a/servers/navigation_server_2d.cpp +++ b/servers/navigation_server_2d.cpp @@ -81,35 +81,12 @@ NavigationServer2D *NavigationServer2D::singleton = nullptr; return CONV_R(NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1))); \ } -#define FORWARD_4(FUNC_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, CONV_0, CONV_1, CONV_2, CONV_3) \ - NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) { \ - return NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1), CONV_2(D_2), CONV_3(D_3)); \ - } - -#define FORWARD_4_R_C(CONV_R, FUNC_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, CONV_0, CONV_1, CONV_2, CONV_3) \ - NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) \ - const { \ - return CONV_R(NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1), CONV_2(D_2), CONV_3(D_3))); \ - } - -#define FORWARD_4_C(FUNC_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, CONV_0, CONV_1, CONV_2, CONV_3) \ - NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3) \ - const { \ - return NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1), CONV_2(D_2), CONV_3(D_3)); \ - } - #define FORWARD_5_R_C(CONV_R, FUNC_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, T_4, D_4, CONV_0, CONV_1, CONV_2, CONV_3, CONV_4) \ NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3, T_4 D_4) \ const { \ return CONV_R(NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1), CONV_2(D_2), CONV_3(D_3), CONV_4(D_4))); \ } -#define FORWARD_5_C(FUNC_NAME, T_0, D_0, T_1, D_1, T_2, D_2, T_3, D_3, T_4, D_4, CONV_0, CONV_1, CONV_2, CONV_3, CONV_4) \ - NavigationServer2D::FUNC_NAME(T_0 D_0, T_1 D_1, T_2 D_2, T_3 D_3, T_4 D_4) \ - const { \ - return NavigationServer3D::get_singleton()->FUNC_NAME(CONV_0(D_0), CONV_1(D_1), CONV_2(D_2), CONV_3(D_3), CONV_4(D_4)); \ - } - static RID rid_to_rid(const RID d) { return d; } @@ -155,18 +132,14 @@ static Transform3D trf2_to_trf3(const Transform2D &d) { return Transform3D(b, o); } -static StringName sn_to_sn(const StringName &d) { - return d; -} - -static Variant var_to_var(const Variant &d) { - return d; -} - static ObjectID id_to_id(const ObjectID &id) { return id; } +static Callable callable_to_callable(const Callable &c) { + return c; +} + static Ref<NavigationMesh> poly_to_mesh(Ref<NavigationPolygon> d) { if (d.is_valid()) { return d->get_navigation_mesh(); @@ -308,7 +281,7 @@ void NavigationServer2D::_bind_methods() { ClassDB::bind_method(D_METHOD("agent_set_target_velocity", "agent", "target_velocity"), &NavigationServer2D::agent_set_target_velocity); ClassDB::bind_method(D_METHOD("agent_set_position", "agent", "position"), &NavigationServer2D::agent_set_position); ClassDB::bind_method(D_METHOD("agent_is_map_changed", "agent"), &NavigationServer2D::agent_is_map_changed); - ClassDB::bind_method(D_METHOD("agent_set_callback", "agent", "object_id", "method", "userdata"), &NavigationServer2D::agent_set_callback, DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("agent_set_callback", "agent", "callback"), &NavigationServer2D::agent_set_callback); ClassDB::bind_method(D_METHOD("free_rid", "rid"), &NavigationServer2D::free); @@ -420,7 +393,7 @@ void FORWARD_2(agent_set_target_velocity, RID, p_agent, Vector2, p_velocity, rid void FORWARD_2(agent_set_position, RID, p_agent, Vector2, p_position, rid_to_rid, v2_to_v3); void FORWARD_2(agent_set_ignore_y, RID, p_agent, bool, p_ignore, rid_to_rid, bool_to_bool); bool FORWARD_1_C(agent_is_map_changed, RID, p_agent, rid_to_rid); -void FORWARD_4(agent_set_callback, RID, p_agent, ObjectID, p_object_id, StringName, p_method, Variant, p_udata, rid_to_rid, id_to_id, sn_to_sn, var_to_var); +void FORWARD_2(agent_set_callback, RID, p_agent, Callable, p_callback, rid_to_rid, callable_to_callable); void FORWARD_1(free, RID, p_object, rid_to_rid); diff --git a/servers/navigation_server_2d.h b/servers/navigation_server_2d.h index b50808cddf..cd18476182 100644 --- a/servers/navigation_server_2d.h +++ b/servers/navigation_server_2d.h @@ -223,7 +223,7 @@ public: virtual bool agent_is_map_changed(RID p_agent) const; /// Callback called at the end of the RVO process - virtual void agent_set_callback(RID p_agent, ObjectID p_object_id, StringName p_method, Variant p_udata = Variant()); + virtual void agent_set_callback(RID p_agent, Callable p_callback); virtual void query_path(const Ref<NavigationPathQueryParameters2D> &p_query_parameters, Ref<NavigationPathQueryResult2D> p_query_result) const; diff --git a/servers/navigation_server_3d.cpp b/servers/navigation_server_3d.cpp index 07256badef..5b95317a38 100644 --- a/servers/navigation_server_3d.cpp +++ b/servers/navigation_server_3d.cpp @@ -113,7 +113,7 @@ void NavigationServer3D::_bind_methods() { ClassDB::bind_method(D_METHOD("agent_set_target_velocity", "agent", "target_velocity"), &NavigationServer3D::agent_set_target_velocity); ClassDB::bind_method(D_METHOD("agent_set_position", "agent", "position"), &NavigationServer3D::agent_set_position); ClassDB::bind_method(D_METHOD("agent_is_map_changed", "agent"), &NavigationServer3D::agent_is_map_changed); - ClassDB::bind_method(D_METHOD("agent_set_callback", "agent", "object_id", "method", "userdata"), &NavigationServer3D::agent_set_callback, DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("agent_set_callback", "agent", "callback"), &NavigationServer3D::agent_set_callback); ClassDB::bind_method(D_METHOD("free_rid", "rid"), &NavigationServer3D::free); diff --git a/servers/navigation_server_3d.h b/servers/navigation_server_3d.h index 4d81726fc3..9c468d1b10 100644 --- a/servers/navigation_server_3d.h +++ b/servers/navigation_server_3d.h @@ -238,7 +238,7 @@ public: virtual bool agent_is_map_changed(RID p_agent) const = 0; /// Callback called at the end of the RVO process - virtual void agent_set_callback(RID p_agent, ObjectID p_object_id, StringName p_method, Variant p_udata = Variant()) = 0; + virtual void agent_set_callback(RID p_agent, Callable p_callback) = 0; /// Destroy the `RID` virtual void free(RID p_object) = 0; diff --git a/servers/rendering/shader_preprocessor.cpp b/servers/rendering/shader_preprocessor.cpp index 816d937e9f..b45a7c0db3 100644 --- a/servers/rendering/shader_preprocessor.cpp +++ b/servers/rendering/shader_preprocessor.cpp @@ -674,6 +674,11 @@ void ShaderPreprocessor::process_include(Tokenizer *p_tokenizer) { return; } + path = path.simplify_path(); + if (path.is_relative_path()) { + path = state->current_filename.get_base_dir().path_join(path); + } + Ref<Resource> res = ResourceLoader::load(path); if (res.is_null()) { set_error(RTR("Shader include load failed. Does the shader include exist? Is there a cyclic dependency?"), line); |