diff options
39 files changed, 1406 insertions, 284 deletions
diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp index cf396c2676..08903d6196 100644 --- a/core/object/worker_thread_pool.cpp +++ b/core/object/worker_thread_pool.cpp @@ -180,13 +180,17 @@ void WorkerThreadPool::_process_task(Task *p_task) { void WorkerThreadPool::_thread_function(void *p_user) { ThreadData *thread_data = (ThreadData *)p_user; + while (true) { Task *task_to_process = nullptr; { MutexLock lock(singleton->task_mutex); - if (singleton->exit_threads) { - return; + + bool exit = singleton->_handle_runlevel(thread_data, lock); + if (unlikely(exit)) { + break; } + thread_data->signaled = false; if (singleton->task_queue.first()) { @@ -194,7 +198,6 @@ void WorkerThreadPool::_thread_function(void *p_user) { singleton->task_queue.remove(singleton->task_queue.first()); } else { thread_data->cond_var.wait(lock); - DEV_ASSERT(singleton->exit_threads || thread_data->signaled); } } @@ -204,19 +207,24 @@ void WorkerThreadPool::_thread_function(void *p_user) { } } -void WorkerThreadPool::_post_tasks_and_unlock(Task **p_tasks, uint32_t p_count, bool p_high_priority) { +void WorkerThreadPool::_post_tasks(Task **p_tasks, uint32_t p_count, bool p_high_priority, MutexLock<BinaryMutex> &p_lock) { // Fall back to processing on the calling thread if there are no worker threads. // Separated into its own variable to make it easier to extend this logic // in custom builds. bool process_on_calling_thread = threads.size() == 0; if (process_on_calling_thread) { - task_mutex.unlock(); + p_lock.temp_unlock(); for (uint32_t i = 0; i < p_count; i++) { _process_task(p_tasks[i]); } + p_lock.temp_relock(); return; } + while (runlevel == RUNLEVEL_EXIT_LANGUAGES) { + control_cond_var.wait(p_lock); + } + uint32_t to_process = 0; uint32_t to_promote = 0; @@ -238,8 +246,6 @@ void WorkerThreadPool::_post_tasks_and_unlock(Task **p_tasks, uint32_t p_count, } _notify_threads(caller_pool_thread, to_process, to_promote); - - task_mutex.unlock(); } void WorkerThreadPool::_notify_threads(const ThreadData *p_current_thread_data, uint32_t p_process_count, uint32_t p_promote_count) { @@ -323,9 +329,8 @@ WorkerThreadPool::TaskID WorkerThreadPool::add_native_task(void (*p_func)(void * } WorkerThreadPool::TaskID WorkerThreadPool::_add_task(const Callable &p_callable, void (*p_func)(void *), void *p_userdata, BaseTemplateUserdata *p_template_userdata, bool p_high_priority, const String &p_description) { - ERR_FAIL_COND_V_MSG(threads.is_empty(), INVALID_TASK_ID, "Can't add a task because the WorkerThreadPool is either not initialized yet or already terminated."); + MutexLock<BinaryMutex> lock(task_mutex); - task_mutex.lock(); // Get a free task Task *task = task_allocator.alloc(); TaskID id = last_task++; @@ -337,7 +342,7 @@ WorkerThreadPool::TaskID WorkerThreadPool::_add_task(const Callable &p_callable, task->template_userdata = p_template_userdata; tasks.insert(id, task); - _post_tasks_and_unlock(&task, 1, p_high_priority); + _post_tasks(&task, 1, p_high_priority, lock); return id; } @@ -444,22 +449,34 @@ void WorkerThreadPool::_unlock_unlockable_mutexes() { void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, Task *p_task) { // Keep processing tasks until the condition to stop waiting is met. -#define IS_WAIT_OVER (unlikely(p_task == ThreadData::YIELDING) ? p_caller_pool_thread->yield_is_over : p_task->completed) - while (true) { Task *task_to_process = nullptr; bool relock_unlockables = false; { MutexLock lock(task_mutex); + bool was_signaled = p_caller_pool_thread->signaled; p_caller_pool_thread->signaled = false; - if (IS_WAIT_OVER) { - if (unlikely(p_task == ThreadData::YIELDING)) { + bool exit = _handle_runlevel(p_caller_pool_thread, lock); + if (unlikely(exit)) { + break; + } + + bool wait_is_over = false; + if (unlikely(p_task == ThreadData::YIELDING)) { + if (p_caller_pool_thread->yield_is_over) { p_caller_pool_thread->yield_is_over = false; + wait_is_over = true; } + } else { + if (p_task->completed) { + wait_is_over = true; + } + } - if (!exit_threads && was_signaled) { + if (wait_is_over) { + if (was_signaled) { // This thread was awaken for some additional reason, but it's about to exit. // Let's find out what may be pending and forward the requests. uint32_t to_process = task_queue.first() ? 1 : 0; @@ -474,28 +491,26 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T break; } - if (!exit_threads) { - if (p_caller_pool_thread->current_task->low_priority && low_priority_task_queue.first()) { - if (_try_promote_low_priority_task()) { - _notify_threads(p_caller_pool_thread, 1, 0); - } + if (p_caller_pool_thread->current_task->low_priority && low_priority_task_queue.first()) { + if (_try_promote_low_priority_task()) { + _notify_threads(p_caller_pool_thread, 1, 0); } + } - if (singleton->task_queue.first()) { - task_to_process = task_queue.first()->self(); - task_queue.remove(task_queue.first()); - } + if (singleton->task_queue.first()) { + task_to_process = task_queue.first()->self(); + task_queue.remove(task_queue.first()); + } - if (!task_to_process) { - p_caller_pool_thread->awaited_task = p_task; + if (!task_to_process) { + p_caller_pool_thread->awaited_task = p_task; - _unlock_unlockable_mutexes(); - relock_unlockables = true; - p_caller_pool_thread->cond_var.wait(lock); + _unlock_unlockable_mutexes(); + relock_unlockables = true; - DEV_ASSERT(exit_threads || p_caller_pool_thread->signaled || IS_WAIT_OVER); - p_caller_pool_thread->awaited_task = nullptr; - } + p_caller_pool_thread->cond_var.wait(lock); + + p_caller_pool_thread->awaited_task = nullptr; } } @@ -509,16 +524,65 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T } } +void WorkerThreadPool::_switch_runlevel(Runlevel p_runlevel) { + DEV_ASSERT(p_runlevel > runlevel); + runlevel = p_runlevel; + memset(&runlevel_data, 0, sizeof(runlevel_data)); + for (uint32_t i = 0; i < threads.size(); i++) { + threads[i].cond_var.notify_one(); + threads[i].signaled = true; + } + control_cond_var.notify_all(); +} + +// Returns whether threads have to exit. This may perform the check about handling needed. +bool WorkerThreadPool::_handle_runlevel(ThreadData *p_thread_data, MutexLock<BinaryMutex> &p_lock) { + bool exit = false; + switch (runlevel) { + case RUNLEVEL_NORMAL: { + } break; + case RUNLEVEL_PRE_EXIT_LANGUAGES: { + if (!p_thread_data->pre_exited_languages) { + if (!task_queue.first() && !low_priority_task_queue.first()) { + p_thread_data->pre_exited_languages = true; + runlevel_data.pre_exit_languages.num_idle_threads++; + control_cond_var.notify_all(); + } + } + } break; + case RUNLEVEL_EXIT_LANGUAGES: { + if (!p_thread_data->exited_languages) { + p_lock.temp_unlock(); + ScriptServer::thread_exit(); + p_lock.temp_relock(); + p_thread_data->exited_languages = true; + runlevel_data.exit_languages.num_exited_threads++; + control_cond_var.notify_all(); + } + } break; + case RUNLEVEL_EXIT: { + exit = true; + } break; + } + return exit; +} + void WorkerThreadPool::yield() { int th_index = get_thread_index(); ERR_FAIL_COND_MSG(th_index == -1, "This function can only be called from a worker thread."); _wait_collaboratively(&threads[th_index], ThreadData::YIELDING); - // If this long-lived task started before the scripting server was initialized, - // now is a good time to have scripting languages ready for the current thread. - // Otherwise, such a piece of setup won't happen unless another task has been - // run during the collaborative wait. - ScriptServer::thread_enter(); + task_mutex.lock(); + if (runlevel < RUNLEVEL_EXIT_LANGUAGES) { + // If this long-lived task started before the scripting server was initialized, + // now is a good time to have scripting languages ready for the current thread. + // Otherwise, such a piece of setup won't happen unless another task has been + // run during the collaborative wait. + task_mutex.unlock(); + ScriptServer::thread_enter(); + } else { + task_mutex.unlock(); + } } void WorkerThreadPool::notify_yield_over(TaskID p_task_id) { @@ -543,13 +607,13 @@ void WorkerThreadPool::notify_yield_over(TaskID p_task_id) { } WorkerThreadPool::GroupID WorkerThreadPool::_add_group_task(const Callable &p_callable, void (*p_func)(void *, uint32_t), void *p_userdata, BaseTemplateUserdata *p_template_userdata, int p_elements, int p_tasks, bool p_high_priority, const String &p_description) { - ERR_FAIL_COND_V_MSG(threads.is_empty(), INVALID_TASK_ID, "Can't add a group task because the WorkerThreadPool is either not initialized yet or already terminated."); ERR_FAIL_COND_V(p_elements < 0, INVALID_TASK_ID); if (p_tasks < 0) { p_tasks = MAX(1u, threads.size()); } - task_mutex.lock(); + MutexLock<BinaryMutex> lock(task_mutex); + Group *group = group_allocator.alloc(); GroupID id = last_task++; group->max = p_elements; @@ -584,7 +648,7 @@ WorkerThreadPool::GroupID WorkerThreadPool::_add_group_task(const Callable &p_ca groups[id] = group; - _post_tasks_and_unlock(tasks_posted, p_tasks, p_high_priority); + _post_tasks(tasks_posted, p_tasks, p_high_priority, lock); return id; } @@ -687,6 +751,9 @@ void WorkerThreadPool::thread_exit_unlock_allowance_zone(uint32_t p_zone_id) { void WorkerThreadPool::init(int p_thread_count, float p_low_priority_task_ratio) { ERR_FAIL_COND(threads.size() > 0); + + runlevel = RUNLEVEL_NORMAL; + if (p_thread_count < 0) { p_thread_count = OS::get_singleton()->get_default_thread_pool_size(); } @@ -704,6 +771,26 @@ void WorkerThreadPool::init(int p_thread_count, float p_low_priority_task_ratio) } } +void WorkerThreadPool::exit_languages_threads() { + if (threads.size() == 0) { + return; + } + + MutexLock lock(task_mutex); + + // Wait until all threads are idle. + _switch_runlevel(RUNLEVEL_PRE_EXIT_LANGUAGES); + while (runlevel_data.pre_exit_languages.num_idle_threads != threads.size()) { + control_cond_var.wait(lock); + } + + // Wait until all threads have detached from scripting languages. + _switch_runlevel(RUNLEVEL_EXIT_LANGUAGES); + while (runlevel_data.exit_languages.num_exited_threads != threads.size()) { + control_cond_var.wait(lock); + } +} + void WorkerThreadPool::finish() { if (threads.size() == 0) { return; @@ -716,15 +803,10 @@ void WorkerThreadPool::finish() { print_error("Task waiting was never re-claimed: " + E->self()->description); E = E->next(); } - } - { - MutexLock lock(task_mutex); - exit_threads = true; - } - for (ThreadData &data : threads) { - data.cond_var.notify_one(); + _switch_runlevel(RUNLEVEL_EXIT); } + for (ThreadData &data : threads) { data.thread.wait_to_finish(); } @@ -755,5 +837,5 @@ WorkerThreadPool::WorkerThreadPool() { } WorkerThreadPool::~WorkerThreadPool() { - DEV_ASSERT(threads.size() == 0 && "finish() hasn't been called!"); + finish(); } diff --git a/core/object/worker_thread_pool.h b/core/object/worker_thread_pool.h index 6374dbe8c7..62296ac040 100644 --- a/core/object/worker_thread_pool.h +++ b/core/object/worker_thread_pool.h @@ -114,17 +114,35 @@ private: Thread thread; bool signaled : 1; bool yield_is_over : 1; + bool pre_exited_languages : 1; + bool exited_languages : 1; Task *current_task = nullptr; Task *awaited_task = nullptr; // Null if not awaiting the condition variable, or special value (YIELDING). ConditionVariable cond_var; ThreadData() : signaled(false), - yield_is_over(false) {} + yield_is_over(false), + pre_exited_languages(false), + exited_languages(false) {} }; TightLocalVector<ThreadData> threads; - bool exit_threads = false; + enum Runlevel { + RUNLEVEL_NORMAL, + RUNLEVEL_PRE_EXIT_LANGUAGES, // Block adding new tasks + RUNLEVEL_EXIT_LANGUAGES, // All threads detach from scripting threads. + RUNLEVEL_EXIT, + } runlevel = RUNLEVEL_NORMAL; + union { // Cleared on every runlevel change. + struct { + uint32_t num_idle_threads; + } pre_exit_languages; + struct { + uint32_t num_exited_threads; + } exit_languages; + } runlevel_data; + ConditionVariable control_cond_var; HashMap<Thread::ID, int> thread_ids; HashMap< @@ -152,7 +170,7 @@ private: void _process_task(Task *task); - void _post_tasks_and_unlock(Task **p_tasks, uint32_t p_count, bool p_high_priority); + void _post_tasks(Task **p_tasks, uint32_t p_count, bool p_high_priority, MutexLock<BinaryMutex> &p_lock); void _notify_threads(const ThreadData *p_current_thread_data, uint32_t p_process_count, uint32_t p_promote_count); bool _try_promote_low_priority_task(); @@ -193,6 +211,9 @@ private: void _wait_collaboratively(ThreadData *p_caller_pool_thread, Task *p_task); + void _switch_runlevel(Runlevel p_runlevel); + bool _handle_runlevel(ThreadData *p_thread_data, MutexLock<BinaryMutex> &p_lock); + #ifdef THREADS_ENABLED static uint32_t _thread_enter_unlock_allowance_zone(THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &p_ulock); #endif @@ -256,6 +277,7 @@ public: #endif void init(int p_thread_count = -1, float p_low_priority_task_ratio = 0.3); + void exit_languages_threads(); void finish(); WorkerThreadPool(); ~WorkerThreadPool(); diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 220ed9da31..c866ff0415 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -107,6 +107,8 @@ static Time *_time = nullptr; static core_bind::Geometry2D *_geometry_2d = nullptr; static core_bind::Geometry3D *_geometry_3d = nullptr; +static WorkerThreadPool *worker_thread_pool = nullptr; + extern Mutex _global_mutex; static GDExtensionManager *gdextension_manager = nullptr; @@ -295,6 +297,8 @@ void register_core_types() { GDREGISTER_NATIVE_STRUCT(AudioFrame, "float left;float right"); GDREGISTER_NATIVE_STRUCT(ScriptLanguageExtensionProfilingInfo, "StringName signature;uint64_t call_count;uint64_t total_time;uint64_t self_time"); + worker_thread_pool = memnew(WorkerThreadPool); + OS::get_singleton()->benchmark_end_measure("Core", "Register Types"); } @@ -345,7 +349,7 @@ void register_core_singletons() { Engine::get_singleton()->add_singleton(Engine::Singleton("Time", Time::get_singleton())); Engine::get_singleton()->add_singleton(Engine::Singleton("GDExtensionManager", GDExtensionManager::get_singleton())); Engine::get_singleton()->add_singleton(Engine::Singleton("ResourceUID", ResourceUID::get_singleton())); - Engine::get_singleton()->add_singleton(Engine::Singleton("WorkerThreadPool", WorkerThreadPool::get_singleton())); + Engine::get_singleton()->add_singleton(Engine::Singleton("WorkerThreadPool", worker_thread_pool)); OS::get_singleton()->benchmark_end_measure("Core", "Register Singletons"); } @@ -378,6 +382,8 @@ void unregister_core_types() { // Destroy singletons in reverse order to ensure dependencies are not broken. + memdelete(worker_thread_pool); + memdelete(_engine_debugger); memdelete(_marshalls); memdelete(_classdb); diff --git a/doc/classes/Skeleton3D.xml b/doc/classes/Skeleton3D.xml index cc3f61e1b2..f5b808be8e 100644 --- a/doc/classes/Skeleton3D.xml +++ b/doc/classes/Skeleton3D.xml @@ -99,6 +99,21 @@ Returns the global rest transform for [param bone_idx]. </description> </method> + <method name="get_bone_meta" qualifiers="const"> + <return type="Variant" /> + <param index="0" name="bone_idx" type="int" /> + <param index="1" name="key" type="StringName" /> + <description> + Returns bone metadata for [param bone_idx] with [param key]. + </description> + </method> + <method name="get_bone_meta_list" qualifiers="const"> + <return type="StringName[]" /> + <param index="0" name="bone_idx" type="int" /> + <description> + Returns a list of all metadata keys for [param bone_idx]. + </description> + </method> <method name="get_bone_name" qualifiers="const"> <return type="String" /> <param index="0" name="bone_idx" type="int" /> @@ -171,6 +186,14 @@ Use for invalidating caches in IK solvers and other nodes which process bones. </description> </method> + <method name="has_bone_meta" qualifiers="const"> + <return type="bool" /> + <param index="0" name="bone_idx" type="int" /> + <param index="1" name="key" type="StringName" /> + <description> + Returns whether there exists any bone metadata for [param bone_idx] with key [param key]. + </description> + </method> <method name="is_bone_enabled" qualifiers="const"> <return type="bool" /> <param index="0" name="bone_idx" type="int" /> @@ -263,6 +286,15 @@ [b]Note:[/b] The pose transform needs to be a global pose! To convert a world transform from a [Node3D] to a global bone pose, multiply the [method Transform3D.affine_inverse] of the node's [member Node3D.global_transform] by the desired world transform. </description> </method> + <method name="set_bone_meta"> + <return type="void" /> + <param index="0" name="bone_idx" type="int" /> + <param index="1" name="key" type="StringName" /> + <param index="2" name="value" type="Variant" /> + <description> + Sets bone metadata for [param bone_idx], will set the [param key] meta to [param value]. + </description> + </method> <method name="set_bone_name"> <return type="void" /> <param index="0" name="bone_idx" type="int" /> diff --git a/editor/add_metadata_dialog.cpp b/editor/add_metadata_dialog.cpp new file mode 100644 index 0000000000..0a070e37b6 --- /dev/null +++ b/editor/add_metadata_dialog.cpp @@ -0,0 +1,118 @@ +/**************************************************************************/ +/* add_metadata_dialog.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "add_metadata_dialog.h" + +AddMetadataDialog::AddMetadataDialog() { + VBoxContainer *vbc = memnew(VBoxContainer); + add_child(vbc); + + HBoxContainer *hbc = memnew(HBoxContainer); + vbc->add_child(hbc); + hbc->add_child(memnew(Label(TTR("Name:")))); + + add_meta_name = memnew(LineEdit); + add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1)); + hbc->add_child(add_meta_name); + hbc->add_child(memnew(Label(TTR("Type:")))); + + add_meta_type = memnew(OptionButton); + + hbc->add_child(add_meta_type); + + Control *spacing = memnew(Control); + vbc->add_child(spacing); + spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE)); + + set_ok_button_text(TTR("Add")); + register_text_enter(add_meta_name); + + validation_panel = memnew(EditorValidationPanel); + vbc->add_child(validation_panel); + validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid.")); + validation_panel->set_update_callback(callable_mp(this, &AddMetadataDialog::_check_meta_name)); + validation_panel->set_accept_button(get_ok_button()); + + add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); +} + +void AddMetadataDialog::_complete_init(const StringName &p_title) { + add_meta_name->grab_focus(); + add_meta_name->set_text(""); + validation_panel->update(); + + set_title(vformat(TTR("Add Metadata Property for \"%s\""), p_title)); + + // Skip if we already completed the initialization. + if (add_meta_type->get_item_count()) { + return; + } + + // Theme icons can be retrieved only the Window has been initialized. + for (int i = 0; i < Variant::VARIANT_MAX; i++) { + if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) { + continue; //not editable by inspector. + } + String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i)); + + add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i); + } +} + +void AddMetadataDialog::open(const StringName p_title, List<StringName> &p_existing_metas) { + this->_existing_metas = p_existing_metas; + _complete_init(p_title); + popup_centered(); +} + +StringName AddMetadataDialog::get_meta_name() { + return add_meta_name->get_text(); +} + +Variant AddMetadataDialog::get_meta_defval() { + Variant defval; + Callable::CallError ce; + Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce); + return defval; +} + +void AddMetadataDialog::_check_meta_name() { + const String meta_name = add_meta_name->get_text(); + + if (meta_name.is_empty()) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR); + } else if (!meta_name.is_valid_ascii_identifier()) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR); + } else if (_existing_metas.find(meta_name)) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR); + } else if (meta_name[0] == '_') { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR); + } +} diff --git a/editor/add_metadata_dialog.h b/editor/add_metadata_dialog.h new file mode 100644 index 0000000000..b1a244ddc6 --- /dev/null +++ b/editor/add_metadata_dialog.h @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* add_metadata_dialog.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef ADD_METADATA_DIALOG_H +#define ADD_METADATA_DIALOG_H + +#include "core/object/callable_method_pointer.h" +#include "editor/editor_help.h" +#include "editor/editor_undo_redo_manager.h" +#include "editor/gui/editor_validation_panel.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/button.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/item_list.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/option_button.h" +#include "scene/gui/tree.h" + +class AddMetadataDialog : public ConfirmationDialog { + GDCLASS(AddMetadataDialog, ConfirmationDialog); + +public: + AddMetadataDialog(); + void open(const StringName p_title, List<StringName> &p_existing_metas); + + StringName get_meta_name(); + Variant get_meta_defval(); + +private: + List<StringName> _existing_metas; + + void _check_meta_name(); + void _complete_init(const StringName &p_label); + + LineEdit *add_meta_name = nullptr; + OptionButton *add_meta_type = nullptr; + EditorValidationPanel *validation_panel = nullptr; +}; +#endif // ADD_METADATA_DIALOG_H diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index da50ffc510..4cd0761691 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -32,6 +32,7 @@ #include "editor_inspector.compat.inc" #include "core/os/keyboard.h" +#include "editor/add_metadata_dialog.h" #include "editor/doc_tools.h" #include "editor/editor_feature_profile.h" #include "editor/editor_main_screen.h" @@ -4245,92 +4246,33 @@ Variant EditorInspector::get_property_clipboard() const { return property_clipboard; } -void EditorInspector::_add_meta_confirm() { - String name = add_meta_name->get_text(); - - object->editor_set_section_unfold("metadata", true); // Ensure metadata is unfolded when adding a new metadata. - - Variant defval; - Callable::CallError ce; - Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(vformat(TTR("Add metadata %s"), name)); - undo_redo->add_do_method(object, "set_meta", name, defval); - undo_redo->add_undo_method(object, "remove_meta", name); - undo_redo->commit_action(); -} - -void EditorInspector::_check_meta_name() { - const String meta_name = add_meta_name->get_text(); - - if (meta_name.is_empty()) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR); - } else if (!meta_name.is_valid_ascii_identifier()) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR); - } else if (object->has_meta(meta_name)) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR); - } else if (meta_name[0] == '_') { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR); - } -} - void EditorInspector::_show_add_meta_dialog() { if (!add_meta_dialog) { - add_meta_dialog = memnew(ConfirmationDialog); - - VBoxContainer *vbc = memnew(VBoxContainer); - add_meta_dialog->add_child(vbc); - - HBoxContainer *hbc = memnew(HBoxContainer); - vbc->add_child(hbc); - hbc->add_child(memnew(Label(TTR("Name:")))); - - add_meta_name = memnew(LineEdit); - add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1)); - hbc->add_child(add_meta_name); - hbc->add_child(memnew(Label(TTR("Type:")))); - - add_meta_type = memnew(OptionButton); - for (int i = 0; i < Variant::VARIANT_MAX; i++) { - if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) { - continue; //not editable by inspector. - } - String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i)); - - add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i); - } - hbc->add_child(add_meta_type); - - Control *spacing = memnew(Control); - vbc->add_child(spacing); - spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE)); - - add_meta_dialog->set_ok_button_text(TTR("Add")); - add_child(add_meta_dialog); - add_meta_dialog->register_text_enter(add_meta_name); + add_meta_dialog = memnew(AddMetadataDialog()); add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &EditorInspector::_add_meta_confirm)); - - validation_panel = memnew(EditorValidationPanel); - vbc->add_child(validation_panel); - validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid.")); - validation_panel->set_update_callback(callable_mp(this, &EditorInspector::_check_meta_name)); - validation_panel->set_accept_button(add_meta_dialog->get_ok_button()); - - add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + add_child(add_meta_dialog); } + StringName dialog_title; Node *node = Object::cast_to<Node>(object); - if (node) { - add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), node->get_name())); - } else { - // This should normally be reached when the object is derived from Resource. - add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), object->get_class())); - } + // If object is derived from Node use node name, if derived from Resource use classname. + dialog_title = node ? node->get_name() : StringName(object->get_class()); + + List<StringName> existing_meta_keys; + object->get_meta_list(&existing_meta_keys); + add_meta_dialog->open(dialog_title, existing_meta_keys); +} + +void EditorInspector::_add_meta_confirm() { + // Ensure metadata is unfolded when adding a new metadata. + object->editor_set_section_unfold("metadata", true); - add_meta_dialog->popup_centered(); - add_meta_name->grab_focus(); - add_meta_name->set_text(""); - validation_panel->update(); + String name = add_meta_dialog->get_meta_name(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Add metadata %s"), name)); + undo_redo->add_do_method(object, "set_meta", name, add_meta_dialog->get_meta_defval()); + undo_redo->add_undo_method(object, "remove_meta", name); + undo_redo->commit_action(); } void EditorInspector::_bind_methods() { diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index fda1443000..14b6ff0907 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -31,6 +31,7 @@ #ifndef EDITOR_INSPECTOR_H #define EDITOR_INSPECTOR_H +#include "editor/add_metadata_dialog.h" #include "editor_property_name_processor.h" #include "scene/gui/box_container.h" #include "scene/gui/scroll_container.h" @@ -575,14 +576,13 @@ class EditorInspector : public ScrollContainer { bool _is_property_disabled_by_feature_profile(const StringName &p_property); - ConfirmationDialog *add_meta_dialog = nullptr; + AddMetadataDialog *add_meta_dialog = nullptr; LineEdit *add_meta_name = nullptr; OptionButton *add_meta_type = nullptr; EditorValidationPanel *validation_panel = nullptr; void _add_meta_confirm(); void _show_add_meta_dialog(); - void _check_meta_name(); protected: static void _bind_methods(); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 66fd2cf904..0889415d5a 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -3436,6 +3436,98 @@ void EditorNode::_update_file_menu_closed() { file_menu->set_item_disabled(file_menu->get_item_index(FILE_OPEN_PREV), false); } +void EditorNode::replace_resources_in_object(Object *p_object, const Vector<Ref<Resource>> &p_source_resources, const Vector<Ref<Resource>> &p_target_resource) { + List<PropertyInfo> pi; + p_object->get_property_list(&pi); + + for (const PropertyInfo &E : pi) { + if (!(E.usage & PROPERTY_USAGE_STORAGE)) { + continue; + } + + switch (E.type) { + case Variant::OBJECT: { + if (E.hint == PROPERTY_HINT_RESOURCE_TYPE) { + const Variant &v = p_object->get(E.name); + Ref<Resource> res = v; + + if (res.is_valid()) { + int res_idx = p_source_resources.find(res); + if (res_idx != -1) { + p_object->set(E.name, p_target_resource.get(res_idx)); + } else { + replace_resources_in_object(v, p_source_resources, p_target_resource); + } + } + } + } break; + case Variant::ARRAY: { + Array varray = p_object->get(E.name); + int len = varray.size(); + bool array_requires_updating = false; + for (int i = 0; i < len; i++) { + const Variant &v = varray.get(i); + Ref<Resource> res = v; + + if (res.is_valid()) { + int res_idx = p_source_resources.find(res); + if (res_idx != -1) { + varray.set(i, p_target_resource.get(res_idx)); + array_requires_updating = true; + } else { + replace_resources_in_object(v, p_source_resources, p_target_resource); + } + } + } + if (array_requires_updating) { + p_object->set(E.name, varray); + } + } break; + case Variant::DICTIONARY: { + Dictionary d = p_object->get(E.name); + List<Variant> keys; + bool dictionary_requires_updating = false; + d.get_key_list(&keys); + for (const Variant &F : keys) { + Variant v = d[F]; + Ref<Resource> res = v; + + if (res.is_valid()) { + int res_idx = p_source_resources.find(res); + if (res_idx != -1) { + d[F] = p_target_resource.get(res_idx); + dictionary_requires_updating = true; + } else { + replace_resources_in_object(v, p_source_resources, p_target_resource); + } + } + } + if (dictionary_requires_updating) { + p_object->set(E.name, d); + } + } break; + default: { + } + } + } + + Node *n = Object::cast_to<Node>(p_object); + if (n) { + for (int i = 0; i < n->get_child_count(); i++) { + replace_resources_in_object(n->get_child(i), p_source_resources, p_target_resource); + } + } +} + +void EditorNode::replace_resources_in_scenes(const Vector<Ref<Resource>> &p_source_resources, const Vector<Ref<Resource>> &p_target_resource) { + for (int i = 0; i < editor_data.get_edited_scene_count(); i++) { + Node *edited_scene_root = editor_data.get_edited_scene_root(i); + if (edited_scene_root) { + replace_resources_in_object(edited_scene_root, p_source_resources, p_target_resource); + } + } +} + void EditorNode::add_editor_plugin(EditorPlugin *p_editor, bool p_config_changed) { if (p_editor->has_main_screen()) { singleton->editor_main_screen->add_main_plugin(p_editor); @@ -6350,12 +6442,32 @@ void EditorNode::remove_resource_conversion_plugin(const Ref<EditorResourceConve resource_conversion_plugins.erase(p_plugin); } -Vector<Ref<EditorResourceConversionPlugin>> EditorNode::find_resource_conversion_plugin(const Ref<Resource> &p_for_resource) { +Vector<Ref<EditorResourceConversionPlugin>> EditorNode::find_resource_conversion_plugin_for_resource(const Ref<Resource> &p_for_resource) { + if (p_for_resource.is_null()) { + return Vector<Ref<EditorResourceConversionPlugin>>(); + } + + Vector<Ref<EditorResourceConversionPlugin>> ret; + for (Ref<EditorResourceConversionPlugin> resource_conversion_plugin : resource_conversion_plugins) { + if (resource_conversion_plugin.is_valid() && resource_conversion_plugin->handles(p_for_resource)) { + ret.push_back(resource_conversion_plugin); + } + } + + return ret; +} + +Vector<Ref<EditorResourceConversionPlugin>> EditorNode::find_resource_conversion_plugin_for_type_name(const String &p_type) { Vector<Ref<EditorResourceConversionPlugin>> ret; - for (int i = 0; i < resource_conversion_plugins.size(); i++) { - if (resource_conversion_plugins[i].is_valid() && resource_conversion_plugins[i]->handles(p_for_resource)) { - ret.push_back(resource_conversion_plugins[i]); + if (ClassDB::can_instantiate(p_type)) { + Ref<Resource> temp = Object::cast_to<Resource>(ClassDB::instantiate(p_type)); + if (temp.is_valid()) { + for (Ref<EditorResourceConversionPlugin> resource_conversion_plugin : resource_conversion_plugins) { + if (resource_conversion_plugin.is_valid() && resource_conversion_plugin->handles(temp)) { + ret.push_back(resource_conversion_plugin); + } + } } } diff --git a/editor/editor_node.h b/editor/editor_node.h index 4127dd1539..109cacdf0e 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -754,6 +754,13 @@ public: void push_node_item(Node *p_node); void hide_unused_editors(const Object *p_editing_owner = nullptr); + void replace_resources_in_object( + Object *p_object, + const Vector<Ref<Resource>> &p_source_resources, + const Vector<Ref<Resource>> &p_target_resource); + void replace_resources_in_scenes( + const Vector<Ref<Resource>> &p_source_resources, + const Vector<Ref<Resource>> &p_target_resource); void open_request(const String &p_path); void edit_foreign_resource(Ref<Resource> p_resource); @@ -934,7 +941,8 @@ public: void add_resource_conversion_plugin(const Ref<EditorResourceConversionPlugin> &p_plugin); void remove_resource_conversion_plugin(const Ref<EditorResourceConversionPlugin> &p_plugin); - Vector<Ref<EditorResourceConversionPlugin>> find_resource_conversion_plugin(const Ref<Resource> &p_for_resource); + Vector<Ref<EditorResourceConversionPlugin>> find_resource_conversion_plugin_for_resource(const Ref<Resource> &p_for_resource); + Vector<Ref<EditorResourceConversionPlugin>> find_resource_conversion_plugin_for_type_name(const String &p_type); bool ensure_main_scene(bool p_from_native); }; diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index f20dd992bb..b6b195101c 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -286,12 +286,13 @@ void EditorResourcePicker::_update_menu_items() { // Add options to convert existing resource to another type of resource. if (is_editable() && edited_resource.is_valid()) { - Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource); - if (conversions.size()) { + Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(edited_resource); + if (!conversions.is_empty()) { edit_menu->add_separator(); } - for (int i = 0; i < conversions.size(); i++) { - String what = conversions[i]->converts_to(); + int relative_id = 0; + for (const Ref<EditorResourceConversionPlugin> &conversion : conversions) { + String what = conversion->converts_to(); Ref<Texture2D> icon; if (has_theme_icon(what, EditorStringName(EditorIcons))) { icon = get_editor_theme_icon(what); @@ -299,7 +300,8 @@ void EditorResourcePicker::_update_menu_items() { icon = get_theme_icon(what, SNAME("Resource")); } - edit_menu->add_icon_item(icon, vformat(TTR("Convert to %s"), what), CONVERT_BASE_ID + i); + edit_menu->add_icon_item(icon, vformat(TTR("Convert to %s"), what), CONVERT_BASE_ID + relative_id); + relative_id++; } } } @@ -451,7 +453,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { if (p_which >= CONVERT_BASE_ID) { int to_type = p_which - CONVERT_BASE_ID; - Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin(edited_resource); + Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(edited_resource); ERR_FAIL_INDEX(to_type, conversions.size()); edited_resource = conversions[to_type]->convert(edited_resource); diff --git a/editor/export/editor_export_plugin.cpp b/editor/export/editor_export_plugin.cpp index 3f1b8aa863..5945c02413 100644 --- a/editor/export/editor_export_plugin.cpp +++ b/editor/export/editor_export_plugin.cpp @@ -229,6 +229,10 @@ bool EditorExportPlugin::supports_platform(const Ref<EditorExportPlatform> &p_ex return ret; } +PackedStringArray EditorExportPlugin::get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const { + return _get_export_features(p_export_platform, p_debug); +} + PackedStringArray EditorExportPlugin::get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const { PackedStringArray ret; GDVIRTUAL_CALL(_get_android_dependencies, p_export_platform, p_debug, ret); diff --git a/editor/export/editor_export_plugin.h b/editor/export/editor_export_plugin.h index ae186d4425..4c0107af72 100644 --- a/editor/export/editor_export_plugin.h +++ b/editor/export/editor_export_plugin.h @@ -166,6 +166,7 @@ public: virtual String get_name() const; virtual bool supports_platform(const Ref<EditorExportPlatform> &p_export_platform) const; + PackedStringArray get_export_features(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const; virtual PackedStringArray get_android_dependencies(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const; virtual PackedStringArray get_android_dependencies_maven_repos(const Ref<EditorExportPlatform> &p_export_platform, bool p_debug) const; diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index faaab4aeec..aee41d08ac 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -45,11 +45,13 @@ #include "editor/editor_resource_preview.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" +#include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_dir_dialog.h" #include "editor/gui/editor_scene_tabs.h" #include "editor/import/3d/scene_import_settings.h" #include "editor/import_dock.h" #include "editor/plugins/editor_context_menu_plugin.h" +#include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/plugins/editor_resource_tooltip_plugins.h" #include "editor/scene_create_dialog.h" #include "editor/scene_tree_dock.h" @@ -1188,6 +1190,47 @@ void FileSystemDock::_update_file_list(bool p_keep_selection) { } } +HashSet<String> FileSystemDock::_get_valid_conversions_for_file_paths(const Vector<String> &p_paths) { + HashSet<String> all_valid_conversion_to_targets; + for (const String &fpath : p_paths) { + if (fpath.is_empty() || fpath == "res://" || !FileAccess::exists(fpath) || FileAccess::exists(fpath + ".import")) { + return HashSet<String>(); + } + + Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_type_name(EditorFileSystem::get_singleton()->get_file_type(fpath)); + + if (conversions.is_empty()) { + // This resource can't convert to anything, so return an empty list. + return HashSet<String>(); + } + + // Get a list of all potentional conversion-to targets. + HashSet<String> current_valid_conversion_to_targets; + for (const Ref<EditorResourceConversionPlugin> &E : conversions) { + const String what = E->converts_to(); + current_valid_conversion_to_targets.insert(what); + } + + if (all_valid_conversion_to_targets.is_empty()) { + // If we have no existing valid conversions, this is the first one, so copy them directly. + all_valid_conversion_to_targets = current_valid_conversion_to_targets; + } else { + // Check existing conversion targets and remove any which are not in the current list. + for (const String &S : all_valid_conversion_to_targets) { + if (!current_valid_conversion_to_targets.has(S)) { + all_valid_conversion_to_targets.erase(S); + } + } + // We have no more remaining valid conversions, so break the loop. + if (all_valid_conversion_to_targets.is_empty()) { + break; + } + } + } + + return all_valid_conversion_to_targets; +} + void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorites) { String fpath = p_path; if (fpath.ends_with("/")) { @@ -1917,6 +1960,54 @@ void FileSystemDock::_overwrite_dialog_action(bool p_overwrite) { _move_operation_confirm(to_move_path, to_move_or_copy, p_overwrite ? OVERWRITE_REPLACE : OVERWRITE_RENAME); } +void FileSystemDock::_convert_dialog_action() { + Vector<Ref<Resource>> selected_resources; + for (const String &S : to_convert) { + Ref<Resource> res = ResourceLoader::load(S); + ERR_FAIL_COND(res.is_null()); + selected_resources.push_back(res); + } + + Vector<Ref<Resource>> converted_resources; + HashSet<Ref<Resource>> resources_to_erase_history_for; + for (Ref<Resource> res : selected_resources) { + Vector<Ref<EditorResourceConversionPlugin>> conversions = EditorNode::get_singleton()->find_resource_conversion_plugin_for_resource(res); + for (const Ref<EditorResourceConversionPlugin> &conversion : conversions) { + int conversion_id = 0; + for (const String &target : cached_valid_conversion_targets) { + if (conversion_id == selected_conversion_id && conversion->converts_to() == target) { + Ref<Resource> converted_res = conversion->convert(res); + ERR_FAIL_COND(res.is_null()); + converted_resources.push_back(converted_res); + resources_to_erase_history_for.insert(res); + break; + } + conversion_id++; + } + } + } + + // Clear history for the objects being replaced. + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + for (Ref<Resource> res : resources_to_erase_history_for) { + undo_redo->clear_history(true, undo_redo->get_history_id_for_object(res.ptr())); + } + + // Updates all the resources existing as node properties. + EditorNode::get_singleton()->replace_resources_in_scenes(selected_resources, converted_resources); + + // Overwrite the old resources. + for (int i = 0; i < converted_resources.size(); i++) { + Ref<Resource> original_resource = selected_resources.get(i); + Ref<Resource> new_resource = converted_resources.get(i); + + // Overwrite the path. + new_resource->set_path(original_resource->get_path(), true); + + ResourceSaver::save(new_resource); + } +} + Vector<String> FileSystemDock::_check_existing() { Vector<String> conflicting_items; for (const FileOrFolder &item : to_move) { @@ -2135,6 +2226,16 @@ void FileSystemDock::_file_list_rmb_option(int p_option) { _file_option(p_option, selected); } +void FileSystemDock::_generic_rmb_option_selected(int p_option) { + // Used for submenu commands where we don't know whether we're + // calling from the file_list_rmb menu or the _tree_rmb option. + if (files->has_focus()) { + _file_list_rmb_option(p_option); + } else { + _tree_rmb_option(p_option); + } +} + void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected) { // The first one should be the active item. @@ -2577,9 +2678,31 @@ void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected } break; default: { - if (!EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_option, p_selected)) { - EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, p_option, p_selected); + // Resource conversion commands: + if (p_option >= CONVERT_BASE_ID) { + selected_conversion_id = p_option - CONVERT_BASE_ID; + ERR_FAIL_INDEX(selected_conversion_id, (int)cached_valid_conversion_targets.size()); + + to_convert.clear(); + for (const String &S : p_selected) { + to_convert.push_back(S); + } + + int conversion_id = 0; + for (const String &E : cached_valid_conversion_targets) { + if (conversion_id == selected_conversion_id) { + conversion_dialog->set_text(vformat(TTR("Do you wish to convert these files to %s? (This operation cannot be undone!)"), E)); + conversion_dialog->popup_centered(); + break; + } + conversion_id++; + } + } else { + if (!EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM, p_option, p_selected)) { + EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_FILESYSTEM_CREATE, p_option, p_selected); + } } + break; } } } @@ -3193,7 +3316,7 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vect if (p_paths.size() == 1 && p_display_path_dependent_options) { PopupMenu *new_menu = memnew(PopupMenu); - new_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_tree_rmb_option)); + new_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FileSystemDock::_generic_rmb_option_selected)); p_popup->add_submenu_node_item(TTR("Create New"), new_menu, FILE_NEW); p_popup->set_item_icon(p_popup->get_item_index(FILE_NEW), get_editor_theme_icon(SNAME("Add"))); @@ -3265,6 +3388,41 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vect p_popup->add_icon_item(get_editor_theme_icon(SNAME("NonFavorite")), TTR("Remove from Favorites"), FILE_REMOVE_FAVORITE); } + if (p_paths.size() > 1 || p_paths[0] != "res://") { + cached_valid_conversion_targets = _get_valid_conversions_for_file_paths(p_paths); + + int relative_id = 0; + if (!cached_valid_conversion_targets.is_empty()) { + p_popup->add_separator(); + + // If we have more than one type we can convert into, collapse it into a submenu. + const int CONVERSION_SUBMENU_THRESHOLD = 1; + + PopupMenu *container_menu = p_popup; + String conversion_string_template = "Convert to %s"; + + if (cached_valid_conversion_targets.size() > CONVERSION_SUBMENU_THRESHOLD) { + container_menu = memnew(PopupMenu); + container_menu->connect("id_pressed", callable_mp(this, &FileSystemDock::_generic_rmb_option_selected)); + + p_popup->add_submenu_node_item(TTR("Convert to..."), container_menu, FILE_NEW); + conversion_string_template = "%s"; + } + + for (const String &E : cached_valid_conversion_targets) { + Ref<Texture2D> icon; + if (has_theme_icon(E, SNAME("EditorIcons"))) { + icon = get_editor_theme_icon(E); + } else { + icon = get_editor_theme_icon(SNAME("Resource")); + } + + container_menu->add_icon_item(icon, vformat(TTR(conversion_string_template), E), CONVERT_BASE_ID + relative_id); + relative_id++; + } + } + } + { List<String> resource_extensions; ResourceFormatImporter::get_singleton()->get_recognized_extensions_for_type("Resource", &resource_extensions); @@ -4202,6 +4360,11 @@ FileSystemDock::FileSystemDock() { new_resource_dialog->set_base_type("Resource"); new_resource_dialog->connect("create", callable_mp(this, &FileSystemDock::_resource_created)); + conversion_dialog = memnew(ConfirmationDialog); + add_child(conversion_dialog); + conversion_dialog->set_ok_button_text(TTR("Convert")); + conversion_dialog->connect(SceneStringName(confirmed), callable_mp(this, &FileSystemDock::_convert_dialog_action)); + uncollapsed_paths_before_search = Vector<String>(); tree_update_id = 0; diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index 907f843523..751fbed022 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -140,6 +140,7 @@ private: FILE_NEW_FOLDER, FILE_NEW_SCRIPT, FILE_NEW_SCENE, + CONVERT_BASE_ID = 1000, }; HashMap<String, Color> folder_colors; @@ -201,6 +202,8 @@ private: Label *overwrite_dialog_footer = nullptr; Label *overwrite_dialog_file_list = nullptr; + ConfirmationDialog *conversion_dialog = nullptr; + SceneCreateDialog *make_scene_dialog = nullptr; ScriptCreateDialog *make_script_dialog = nullptr; ShaderCreateDialog *make_shader_dialog = nullptr; @@ -226,6 +229,9 @@ private: String to_move_path; bool to_move_or_copy = false; + Vector<String> to_convert; + int selected_conversion_id = 0; + Vector<String> history; int history_pos; int history_max_size; @@ -245,6 +251,8 @@ private: LocalVector<Ref<EditorResourceTooltipPlugin>> tooltip_plugins; + HashSet<String> cached_valid_conversion_targets; + void _tree_mouse_exited(); void _reselect_items_selected_on_drag_begin(bool reset = false); @@ -256,6 +264,8 @@ private: void _file_list_gui_input(Ref<InputEvent> p_event); void _tree_gui_input(Ref<InputEvent> p_event); + HashSet<String> _get_valid_conversions_for_file_paths(const Vector<String> &p_paths); + void _update_file_list(bool p_keep_selection); void _toggle_file_display(); void _set_file_display(bool p_active); @@ -293,11 +303,13 @@ private: void _rename_operation_confirm(); void _duplicate_operation_confirm(); void _overwrite_dialog_action(bool p_overwrite); + void _convert_dialog_action(); Vector<String> _check_existing(); void _move_operation_confirm(const String &p_to_path, bool p_copy = false, Overwrite p_overwrite = OVERWRITE_UNDECIDED); void _tree_rmb_option(int p_option); void _file_list_rmb_option(int p_option); + void _generic_rmb_option_selected(int p_option); void _file_option(int p_option, const Vector<String> &p_selected); void _fw_history(); diff --git a/editor/import_dock.cpp b/editor/import_dock.cpp index 16f4aeda95..14065abf73 100644 --- a/editor/import_dock.cpp +++ b/editor/import_dock.cpp @@ -606,23 +606,25 @@ void ImportDock::_reimport_and_cleanup() { List<Ref<Resource>> external_resources; ResourceCache::get_cached_resources(&external_resources); + Vector<Ref<Resource>> old_resources_to_replace; + Vector<Ref<Resource>> new_resources_to_replace; for (const String &path : need_cleanup) { Ref<Resource> old_res = old_resources[path]; - Ref<Resource> new_res; if (params->importer.is_valid()) { - new_res = ResourceLoader::load(path); - } - - for (int i = 0; i < EditorNode::get_editor_data().get_edited_scene_count(); i++) { - Node *edited_scene_root = EditorNode::get_editor_data().get_edited_scene_root(i); - if (likely(edited_scene_root)) { - _replace_resource_in_object(edited_scene_root, old_res, new_res); + Ref<Resource> new_res = ResourceLoader::load(path); + if (new_res.is_valid()) { + old_resources_to_replace.append(old_res); + new_resources_to_replace.append(new_res); } } - for (Ref<Resource> res : external_resources) { - _replace_resource_in_object(res.ptr(), old_res, new_res); - } } + + EditorNode::get_singleton()->replace_resources_in_scenes(old_resources_to_replace, new_resources_to_replace); + + for (Ref<Resource> res : external_resources) { + EditorNode::get_singleton()->replace_resources_in_object(res.ptr(), old_resources_to_replace, new_resources_to_replace); + } + need_cleanup.clear(); } @@ -693,37 +695,6 @@ void ImportDock::_reimport() { _set_dirty(false); } -void ImportDock::_replace_resource_in_object(Object *p_object, const Ref<Resource> &old_resource, const Ref<Resource> &new_resource) { - ERR_FAIL_NULL(p_object); - - List<PropertyInfo> props; - p_object->get_property_list(&props); - - for (const PropertyInfo &p : props) { - if (p.type != Variant::OBJECT || p.hint != PROPERTY_HINT_RESOURCE_TYPE) { - continue; - } - - Ref<Resource> res = p_object->get(p.name); - if (res.is_null()) { - continue; - } - - if (res == old_resource) { - p_object->set(p.name, new_resource); - } else { - _replace_resource_in_object(res.ptr(), old_resource, new_resource); - } - } - - Node *n = Object::cast_to<Node>(p_object); - if (n) { - for (int i = 0; i < n->get_child_count(); i++) { - _replace_resource_in_object(n->get_child(i), old_resource, new_resource); - } - } -} - void ImportDock::_notification(int p_what) { switch (p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { diff --git a/editor/import_dock.h b/editor/import_dock.h index c0a1dee7ca..6a6c54f1ce 100644 --- a/editor/import_dock.h +++ b/editor/import_dock.h @@ -81,8 +81,6 @@ class ImportDock : public VBoxContainer { void _reimport_and_cleanup(); void _reimport(); - void _replace_resource_in_object(Object *p_object, const Ref<Resource> &old_resource, const Ref<Resource> &new_resource); - void _advanced_options(); enum { ITEM_SET_AS_DEFAULT = 100, diff --git a/editor/plugins/skeleton_3d_editor_plugin.cpp b/editor/plugins/skeleton_3d_editor_plugin.cpp index 99cb03cdcd..64b9522864 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.cpp +++ b/editor/plugins/skeleton_3d_editor_plugin.cpp @@ -52,7 +52,7 @@ #include "scene/resources/skeleton_profile.h" #include "scene/resources/surface_tool.h" -void BoneTransformEditor::create_editors() { +void BonePropertiesEditor::create_editors() { section = memnew(EditorInspectorSection); section->setup("trf_properties", label, this, Color(0.0f, 0.0f, 0.0f), true); section->unfold(); @@ -61,7 +61,7 @@ void BoneTransformEditor::create_editors() { enabled_checkbox = memnew(EditorPropertyCheck()); enabled_checkbox->set_label("Pose Enabled"); enabled_checkbox->set_selectable(false); - enabled_checkbox->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); + enabled_checkbox->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); section->get_vbox()->add_child(enabled_checkbox); // Position property. @@ -69,8 +69,8 @@ void BoneTransformEditor::create_editors() { position_property->setup(-10000, 10000, 0.001, true); position_property->set_label("Position"); position_property->set_selectable(false); - position_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - position_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + position_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + position_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(position_property); // Rotation property. @@ -78,8 +78,8 @@ void BoneTransformEditor::create_editors() { rotation_property->setup(-10000, 10000, 0.001, true); rotation_property->set_label("Rotation"); rotation_property->set_selectable(false); - rotation_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - rotation_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + rotation_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + rotation_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(rotation_property); // Scale property. @@ -87,8 +87,8 @@ void BoneTransformEditor::create_editors() { scale_property->setup(-10000, 10000, 0.001, true, true); scale_property->set_label("Scale"); scale_property->set_selectable(false); - scale_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - scale_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + scale_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + scale_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(scale_property); // Transform/Matrix section. @@ -102,50 +102,136 @@ void BoneTransformEditor::create_editors() { rest_matrix->set_label("Transform"); rest_matrix->set_selectable(false); rest_section->get_vbox()->add_child(rest_matrix); + + // Bone Metadata property + meta_section = memnew(EditorInspectorSection); + meta_section->setup("bone_meta", TTR("Bone Metadata"), this, Color(.0f, .0f, .0f), true); + section->get_vbox()->add_child(meta_section); + + add_metadata_button = EditorInspector::create_inspector_action_button(TTR("Add Bone Metadata")); + add_metadata_button->connect(SceneStringName(pressed), callable_mp(this, &BonePropertiesEditor::_show_add_meta_dialog)); + section->get_vbox()->add_child(add_metadata_button); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->connect("version_changed", callable_mp(this, &BonePropertiesEditor::_update_properties)); + undo_redo->connect("history_changed", callable_mp(this, &BonePropertiesEditor::_update_properties)); } -void BoneTransformEditor::_notification(int p_what) { +void BonePropertiesEditor::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { const Color section_color = get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)); section->set_bg_color(section_color); rest_section->set_bg_color(section_color); + add_metadata_button->set_icon(get_editor_theme_icon(SNAME("Add"))); } break; } } -void BoneTransformEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { - if (updating) { +void BonePropertiesEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { + if (updating || !skeleton) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property)); + undo_redo->add_do_property(skeleton, p_property, p_value); + + Skeleton3DEditor *se = Skeleton3DEditor::get_singleton(); + if (se) { + undo_redo->add_do_method(se, "update_joint_tree"); + undo_redo->add_undo_method(se, "update_joint_tree"); + } + + undo_redo->commit_action(); +} + +void BonePropertiesEditor::_meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { + if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") { return; } - if (skeleton) { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS); - undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property)); - undo_redo->add_do_property(skeleton, p_property, p_value); - - Skeleton3DEditor *se = Skeleton3DEditor::get_singleton(); - if (se) { - undo_redo->add_do_method(se, "update_joint_tree"); - undo_redo->add_undo_method(se, "update_joint_tree"); - } - undo_redo->commit_action(); + int bone = p_property.get_slicec('/', 1).to_int(); + if (bone >= skeleton->get_bone_count()) { + return; } + + String key = p_property.get_slicec('/', 3); + if (!skeleton->has_bone_meta(1, key)) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Modify metadata '%s' for bone '%s'"), key, skeleton->get_bone_name(bone))); + undo_redo->add_do_property(skeleton, p_property, p_value); + undo_redo->add_do_method(meta_editors[p_property], "update_property"); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key)); + undo_redo->add_undo_method(meta_editors[p_property], "update_property"); + undo_redo->commit_action(); } -BoneTransformEditor::BoneTransformEditor(Skeleton3D *p_skeleton) : +void BonePropertiesEditor::_meta_deleted(const String &p_property) { + if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") { + return; + } + + int bone = p_property.get_slicec('/', 1).to_int(); + if (bone >= skeleton->get_bone_count()) { + return; + } + + String key = p_property.get_slicec('/', 3); + if (!skeleton->has_bone_meta(1, key)) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Remove metadata '%s' from bone '%s'"), key, skeleton->get_bone_name(bone))); + undo_redo->add_do_property(skeleton, p_property, Variant()); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key)); + undo_redo->commit_action(); + + emit_signal(SNAME("property_deleted"), p_property); +} + +void BonePropertiesEditor::_show_add_meta_dialog() { + if (!add_meta_dialog) { + add_meta_dialog = memnew(AddMetadataDialog()); + add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &BonePropertiesEditor::_add_meta_confirm)); + add_child(add_meta_dialog); + } + + int bone = Skeleton3DEditor::get_singleton()->get_selected_bone(); + StringName dialog_title = skeleton->get_bone_name(bone); + + List<StringName> existing_meta_keys; + skeleton->get_bone_meta_list(bone, &existing_meta_keys); + add_meta_dialog->open(dialog_title, existing_meta_keys); +} + +void BonePropertiesEditor::_add_meta_confirm() { + int bone = Skeleton3DEditor::get_singleton()->get_selected_bone(); + String name = add_meta_dialog->get_meta_name(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Add metadata '%s' to bone '%s'"), name, skeleton->get_bone_name(bone))); + undo_redo->add_do_method(skeleton, "set_bone_meta", bone, name, add_meta_dialog->get_meta_defval()); + undo_redo->add_undo_method(skeleton, "set_bone_meta", bone, name, Variant()); + undo_redo->commit_action(); +} + +BonePropertiesEditor::BonePropertiesEditor(Skeleton3D *p_skeleton) : skeleton(p_skeleton) { create_editors(); } -void BoneTransformEditor::set_keyable(const bool p_keyable) { +void BonePropertiesEditor::set_keyable(const bool p_keyable) { position_property->set_keying(p_keyable); rotation_property->set_keying(p_keyable); scale_property->set_keying(p_keyable); } -void BoneTransformEditor::set_target(const String &p_prop) { +void BonePropertiesEditor::set_target(const String &p_prop) { enabled_checkbox->set_object_and_property(skeleton, p_prop + "enabled"); enabled_checkbox->update_property(); @@ -162,7 +248,7 @@ void BoneTransformEditor::set_target(const String &p_prop) { rest_matrix->update_property(); } -void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance) { +void BonePropertiesEditor::_property_keyed(const String &p_path, bool p_advance) { AnimationTrackEditor *te = AnimationPlayerEditor::get_singleton()->get_track_editor(); if (!te || !te->has_keying()) { return; @@ -183,16 +269,17 @@ void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance) } } -void BoneTransformEditor::_update_properties() { +void BonePropertiesEditor::_update_properties() { if (!skeleton) { return; } int selected = Skeleton3DEditor::get_singleton()->get_selected_bone(); List<PropertyInfo> props; + HashSet<StringName> meta_seen; skeleton->get_property_list(&props); for (const PropertyInfo &E : props) { PackedStringArray split = E.name.split("/"); - if (split.size() == 3 && split[0] == "bones") { + if (split.size() >= 3 && split[0] == "bones") { if (split[1].to_int() == selected) { if (split[2] == "enabled") { enabled_checkbox->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); @@ -224,9 +311,35 @@ void BoneTransformEditor::_update_properties() { rest_matrix->update_editor_property_status(); rest_matrix->queue_redraw(); } + if (split[2] == "bone_meta") { + meta_seen.insert(E.name); + if (!meta_editors.find(E.name)) { + EditorProperty *editor = EditorInspectorDefaultPlugin::get_editor_for_property(skeleton, E.type, E.name, PROPERTY_HINT_NONE, "", E.usage); + editor->set_label(split[3]); + editor->set_object_and_property(skeleton, E.name); + editor->set_deletable(true); + editor->set_selectable(false); + editor->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_meta_changed)); + editor->connect("property_deleted", callable_mp(this, &BonePropertiesEditor::_meta_deleted)); + + meta_section->get_vbox()->add_child(editor); + editor->update_property(); + editor->update_editor_property_status(); + editor->queue_redraw(); + + meta_editors[E.name] = editor; + } + } } } } + // UI for any bone metadata prop not seen during the iteration has to be deleted + for (KeyValue<StringName, EditorProperty *> iter : meta_editors) { + if (!meta_seen.has(iter.key)) { + callable_mp((Node *)meta_section->get_vbox(), &Node::remove_child).call_deferred(iter.value); + meta_editors.remove(meta_editors.find(iter.key)); + } + } } Skeleton3DEditor *Skeleton3DEditor::singleton = nullptr; @@ -992,7 +1105,7 @@ void Skeleton3DEditor::create_editors() { SET_DRAG_FORWARDING_GCD(joint_tree, Skeleton3DEditor); s_con->add_child(joint_tree); - pose_editor = memnew(BoneTransformEditor(skeleton)); + pose_editor = memnew(BonePropertiesEditor(skeleton)); pose_editor->set_label(TTR("Bone Transform")); pose_editor->set_visible(false); add_child(pose_editor); diff --git a/editor/plugins/skeleton_3d_editor_plugin.h b/editor/plugins/skeleton_3d_editor_plugin.h index d4dee1f16f..0265183dfa 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.h +++ b/editor/plugins/skeleton_3d_editor_plugin.h @@ -31,6 +31,7 @@ #ifndef SKELETON_3D_EDITOR_PLUGIN_H #define SKELETON_3D_EDITOR_PLUGIN_H +#include "editor/add_metadata_dialog.h" #include "editor/editor_properties.h" #include "editor/gui/editor_file_dialog.h" #include "editor/plugins/editor_plugin.h" @@ -50,8 +51,8 @@ class Tree; class TreeItem; class VSeparator; -class BoneTransformEditor : public VBoxContainer { - GDCLASS(BoneTransformEditor, VBoxContainer); +class BonePropertiesEditor : public VBoxContainer { + GDCLASS(BonePropertiesEditor, VBoxContainer); EditorInspectorSection *section = nullptr; @@ -63,6 +64,10 @@ class BoneTransformEditor : public VBoxContainer { EditorInspectorSection *rest_section = nullptr; EditorPropertyTransform3D *rest_matrix = nullptr; + EditorInspectorSection *meta_section = nullptr; + AddMetadataDialog *add_meta_dialog = nullptr; + Button *add_metadata_button = nullptr; + Rect2 background_rects[5]; Skeleton3D *skeleton = nullptr; @@ -79,11 +84,18 @@ class BoneTransformEditor : public VBoxContainer { void _property_keyed(const String &p_path, bool p_advance); + void _meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing); + void _meta_deleted(const String &p_property); + void _show_add_meta_dialog(); + void _add_meta_confirm(); + + HashMap<StringName, EditorProperty *> meta_editors; + protected: void _notification(int p_what); public: - BoneTransformEditor(Skeleton3D *p_skeleton); + BonePropertiesEditor(Skeleton3D *p_skeleton); // Which transform target to modify. void set_target(const String &p_prop); @@ -123,8 +135,8 @@ class Skeleton3DEditor : public VBoxContainer { }; Tree *joint_tree = nullptr; - BoneTransformEditor *rest_editor = nullptr; - BoneTransformEditor *pose_editor = nullptr; + BonePropertiesEditor *rest_editor = nullptr; + BonePropertiesEditor *pose_editor = nullptr; HBoxContainer *topmenu_bar = nullptr; MenuButton *skeleton_options = nullptr; diff --git a/main/main.cpp b/main/main.cpp index 9c9542325e..f1ee4bf2a6 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -140,7 +140,6 @@ static Engine *engine = nullptr; static ProjectSettings *globals = nullptr; static Input *input = nullptr; static InputMap *input_map = nullptr; -static WorkerThreadPool *worker_thread_pool = nullptr; static TranslationServer *translation_server = nullptr; static Performance *performance = nullptr; static PackedData *packed_data = nullptr; @@ -691,8 +690,6 @@ Error Main::test_setup() { register_core_settings(); // Here globals are present. - worker_thread_pool = memnew(WorkerThreadPool); - translation_server = memnew(TranslationServer); tsman = memnew(TextServerManager); @@ -803,8 +800,6 @@ void Main::test_cleanup() { ResourceSaver::remove_custom_savers(); PropertyListHelper::clear_base_helpers(); - WorkerThreadPool::get_singleton()->finish(); - #ifdef TOOLS_ENABLED GDExtensionManager::get_singleton()->deinitialize_extensions(GDExtension::INITIALIZATION_LEVEL_EDITOR); uninitialize_modules(MODULE_INITIALIZATION_LEVEL_EDITOR); @@ -846,9 +841,6 @@ void Main::test_cleanup() { if (physics_server_2d_manager) { memdelete(physics_server_2d_manager); } - if (worker_thread_pool) { - memdelete(worker_thread_pool); - } if (globals) { memdelete(globals); } @@ -939,7 +931,6 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph register_core_settings(); //here globals are present - worker_thread_pool = memnew(WorkerThreadPool); translation_server = memnew(TranslationServer); performance = memnew(Performance); GDREGISTER_CLASS(Performance); @@ -2629,10 +2620,6 @@ error: if (translation_server) { memdelete(translation_server); } - if (worker_thread_pool) { - worker_thread_pool->finish(); - memdelete(worker_thread_pool); - } if (globals) { memdelete(globals); } @@ -4514,7 +4501,7 @@ void Main::cleanup(bool p_force) { ResourceLoader::clear_translation_remaps(); ResourceLoader::clear_path_remaps(); - WorkerThreadPool::get_singleton()->finish(); + WorkerThreadPool::get_singleton()->exit_languages_threads(); ScriptServer::finish_languages(); @@ -4606,9 +4593,6 @@ void Main::cleanup(bool p_force) { if (physics_server_2d_manager) { memdelete(physics_server_2d_manager); } - if (worker_thread_pool) { - memdelete(worker_thread_pool); - } if (globals) { memdelete(globals); } diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 56dae65831..bd034cbdc5 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -5534,6 +5534,10 @@ void GLTFDocument::_convert_skeleton_to_gltf(Skeleton3D *p_skeleton3d, Ref<GLTFS joint_node->set_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i))); joint_node->transform = skeleton->get_bone_pose(bone_i); joint_node->joint = true; + + if (p_skeleton3d->has_bone_meta(bone_i, "extras")) { + joint_node->set_meta("extras", p_skeleton3d->get_bone_meta(bone_i, "extras")); + } GLTFNodeIndex current_node_i = p_state->nodes.size(); p_state->scene_nodes.insert(current_node_i, skeleton); p_state->nodes.push_back(joint_node); diff --git a/modules/gltf/skin_tool.cpp b/modules/gltf/skin_tool.cpp index a344334d93..1522c0e324 100644 --- a/modules/gltf/skin_tool.cpp +++ b/modules/gltf/skin_tool.cpp @@ -602,6 +602,11 @@ Error SkinTool::_create_skeletons( skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion()); skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale()); + // Store bone-level GLTF extras in skeleton per bone meta. + if (node->has_meta("extras")) { + skeleton->set_bone_meta(bone_index, "extras", node->get_meta("extras")); + } + if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) { const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name()); ERR_FAIL_COND_V(bone_parent < 0, FAILED); diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h index 96aadf3023..37c8f6925c 100644 --- a/modules/gltf/tests/test_gltf_extras.h +++ b/modules/gltf/tests/test_gltf_extras.h @@ -41,6 +41,7 @@ #include "modules/gltf/gltf_document.h" #include "modules/gltf/gltf_state.h" #include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/skeleton_3d.h" #include "scene/main/window.h" #include "scene/resources/3d/primitive_meshes.h" #include "scene/resources/material.h" @@ -158,6 +159,62 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" memdelete(original); memdelete(loaded); } + +TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { + // Setup scene. + Skeleton3D *skeleton = memnew(Skeleton3D); + skeleton->set_name("skeleton"); + Dictionary skeleton_extras; + skeleton_extras["node_type"] = "skeleton"; + skeleton->set_meta("extras", skeleton_extras); + + skeleton->add_bone("parent"); + skeleton->set_bone_rest(0, Transform3D()); + Dictionary parent_bone_extras; + parent_bone_extras["bone"] = "i_am_parent_bone"; + skeleton->set_bone_meta(0, "extras", parent_bone_extras); + + skeleton->add_bone("child"); + skeleton->set_bone_rest(1, Transform3D()); + skeleton->set_bone_parent(1, 0); + Dictionary child_bone_extras; + child_bone_extras["bone"] = "i_am_child_bone"; + skeleton->set_bone_meta(1, "extras", child_bone_extras); + + // We have to have a mesh to link with skeleton or it will not get imported. + Ref<PlaneMesh> meshdata = memnew(PlaneMesh); + meshdata->set_name("planemesh"); + + MeshInstance3D *mesh = memnew(MeshInstance3D); + mesh->set_mesh(meshdata); + mesh->set_name("mesh_instance_3d"); + + Node3D *scene = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(scene); + scene->add_child(skeleton); + scene->add_child(mesh); + scene->set_name("node3d"); + + // Now that both skeleton and mesh are part of scene, link them. + mesh->set_skeleton_path(mesh->get_path_to(skeleton)); + + // Convert to GLFT and back. + String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); + Node *loaded = _gltf_export_then_import(scene, tempfile); + + // Compare the results. + CHECK(loaded->get_name() == "node3d"); + Skeleton3D *result = Object::cast_to<Skeleton3D>(loaded->find_child("Skeleton3D", false, true)); + CHECK(result->get_bone_name(0) == "parent"); + CHECK(Dictionary(result->get_bone_meta(0, "extras"))["bone"] == "i_am_parent_bone"); + CHECK(result->get_bone_name(1) == "child"); + CHECK(Dictionary(result->get_bone_meta(1, "extras"))["bone"] == "i_am_child_bone"); + + memdelete(skeleton); + memdelete(mesh); + memdelete(scene); + memdelete(loaded); +} } // namespace TestGltfExtras #endif // TOOLS_ENABLED diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj index ee624a443d..c5f2dfee4b 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj @@ -30,6 +30,7 @@ <None Include="$(GodotSdkPackageVersionsFilePath)" Pack="true" PackagePath="Sdk"> <Link>Sdk\SdkPackageVersions.props</Link> </None> + <None Include="Sdk\Android.props" Pack="true" PackagePath="Sdk" /> <None Include="Sdk\iOSNativeAOT.props" Pack="true" PackagePath="Sdk" /> <None Include="Sdk\iOSNativeAOT.targets" Pack="true" PackagePath="Sdk" /> </ItemGroup> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props new file mode 100644 index 0000000000..3926a4b22a --- /dev/null +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props @@ -0,0 +1,5 @@ +<Project> + <PropertyGroup> + <UseMonoRuntime Condition=" '$(UseMonoRuntime)' == '' and '$(PublishAot)' != 'true' ">true</UseMonoRuntime> + </PropertyGroup> +</Project> diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props index c4034f1f9f..d10f9ae0ab 100644 --- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props +++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props @@ -112,5 +112,6 @@ <DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants> </PropertyGroup> + <Import Project="$(MSBuildThisFileDirectory)\Android.props" Condition=" '$(GodotTargetPlatform)' == 'android' " /> <Import Project="$(MSBuildThisFileDirectory)\iOSNativeAOT.props" Condition=" '$(GodotTargetPlatform)' == 'ios' " /> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index a5f24fb67b..6fd84d3834 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -245,7 +245,6 @@ namespace GodotTools.Export { publishOutputDir = Path.Combine(GodotSharpDirs.ProjectBaseOutputPath, "godot-publish-dotnet", $"{buildConfig}-{runtimeIdentifier}"); - } outputPaths.Add(publishOutputDir); @@ -322,6 +321,30 @@ namespace GodotTools.Export { if (embedBuildResults) { + if (platform == OS.Platforms.Android) + { + if (IsSharedObject(Path.GetFileName(path))) + { + AddSharedObject(path, tags: new string[] { arch }, + Path.Join(projectDataDirName, + Path.GetRelativePath(publishOutputDir, + Path.GetDirectoryName(path)!))); + + return; + } + + static bool IsSharedObject(string fileName) + { + if (fileName.EndsWith(".so") || fileName.EndsWith(".a") + || fileName.EndsWith(".jar") || fileName.EndsWith(".dex")) + { + return true; + } + + return false; + } + } + string filePath = SanitizeSlashes(Path.GetRelativePath(publishOutputDir, path)); byte[] fileData = File.ReadAllBytes(path); string hash = Convert.ToBase64String(SHA512.HashData(fileData)); diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp index 48caae8523..5293c5c896 100644 --- a/modules/mono/mono_gd/gd_mono.cpp +++ b/modules/mono/mono_gd/gd_mono.cpp @@ -61,6 +61,14 @@ hostfxr_initialize_for_runtime_config_fn hostfxr_initialize_for_runtime_config = hostfxr_get_runtime_delegate_fn hostfxr_get_runtime_delegate = nullptr; hostfxr_close_fn hostfxr_close = nullptr; +#ifndef TOOLS_ENABLED +typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_create_delegate_fn)(void *hostHandle, unsigned int domainId, const char *entryPointAssemblyName, const char *entryPointTypeName, const char *entryPointMethodName, void **delegate); +typedef int(CORECLR_DELEGATE_CALLTYPE *coreclr_initialize_fn)(const char *exePath, const char *appDomainFriendlyName, int propertyCount, const char **propertyKeys, const char **propertyValues, void **hostHandle, unsigned int *domainId); + +coreclr_create_delegate_fn coreclr_create_delegate = nullptr; +coreclr_initialize_fn coreclr_initialize = nullptr; +#endif + #ifdef _WIN32 static_assert(sizeof(char_t) == sizeof(char16_t)); using HostFxrCharString = Char16String; @@ -142,6 +150,56 @@ String find_hostfxr() { #endif } +#ifndef TOOLS_ENABLED +String find_monosgen() { +#if defined(ANDROID_ENABLED) + // Android includes all native libraries in the libs directory of the APK + // so we assume it exists and use only the name to dlopen it. + return "libmonosgen-2.0.so"; +#else +#if defined(WINDOWS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("monosgen-2.0.dll"); +#elif defined(MACOS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libmonosgen-2.0.dylib"); +#elif defined(UNIX_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libmonosgen-2.0.so"); +#else +#error "Platform not supported (yet?)" +#endif + + if (FileAccess::exists(probe_path)) { + return probe_path; + } + + return String(); +#endif +} + +String find_coreclr() { +#if defined(WINDOWS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("coreclr.dll"); +#elif defined(MACOS_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libcoreclr.dylib"); +#elif defined(UNIX_ENABLED) + String probe_path = GodotSharpDirs::get_api_assemblies_dir() + .path_join("libcoreclr.so"); +#else +#error "Platform not supported (yet?)" +#endif + + if (FileAccess::exists(probe_path)) { + return probe_path; + } + + return String(); +} +#endif + bool load_hostfxr(void *&r_hostfxr_dll_handle) { String hostfxr_path = find_hostfxr(); @@ -182,6 +240,47 @@ bool load_hostfxr(void *&r_hostfxr_dll_handle) { hostfxr_close); } +#ifndef TOOLS_ENABLED +bool load_coreclr(void *&r_coreclr_dll_handle) { + String coreclr_path = find_coreclr(); + + bool is_monovm = false; + if (coreclr_path.is_empty()) { + // Fallback to MonoVM (should have the same API as CoreCLR). + coreclr_path = find_monosgen(); + is_monovm = true; + } + + if (coreclr_path.is_empty()) { + return false; + } + + const String coreclr_name = is_monovm ? "monosgen" : "coreclr"; + print_verbose("Found " + coreclr_name + ": " + coreclr_path); + + Error err = OS::get_singleton()->open_dynamic_library(coreclr_path, r_coreclr_dll_handle); + + if (err != OK) { + return false; + } + + void *lib = r_coreclr_dll_handle; + + void *symbol = nullptr; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_initialize", symbol); + ERR_FAIL_COND_V(err != OK, false); + coreclr_initialize = (coreclr_initialize_fn)symbol; + + err = OS::get_singleton()->get_dynamic_library_symbol_handle(lib, "coreclr_create_delegate", symbol); + ERR_FAIL_COND_V(err != OK, false); + coreclr_create_delegate = (coreclr_create_delegate_fn)symbol; + + return (coreclr_initialize && + coreclr_create_delegate); +} +#endif + #ifdef TOOLS_ENABLED load_assembly_and_get_function_pointer_fn initialize_hostfxr_for_config(const char_t *p_config_path) { hostfxr_handle cxt = nullptr; @@ -339,6 +438,56 @@ godot_plugins_initialize_fn try_load_native_aot_library(void *&r_aot_dll_handle) } #endif +#ifndef TOOLS_ENABLED +String make_tpa_list() { + String tpa_list; + +#if defined(WINDOWS_ENABLED) + String separator = ";"; +#else + String separator = ":"; +#endif + + String assemblies_dir = GodotSharpDirs::get_api_assemblies_dir(); + PackedStringArray files = DirAccess::get_files_at(assemblies_dir); + for (const String &file : files) { + tpa_list += assemblies_dir.path_join(file); + tpa_list += separator; + } + + return tpa_list; +} + +godot_plugins_initialize_fn initialize_coreclr_and_godot_plugins(bool &r_runtime_initialized) { + godot_plugins_initialize_fn godot_plugins_initialize = nullptr; + + String assembly_name = path::get_csharp_project_name(); + + String tpa_list = make_tpa_list(); + const char *prop_keys[] = { HOSTFXR_STR("TRUSTED_PLATFORM_ASSEMBLIES") }; + const char *prop_values[] = { get_data(str_to_hostfxr(tpa_list)) }; + int nprops = sizeof(prop_keys) / sizeof(prop_keys[0]); + + void *coreclr_handle = nullptr; + unsigned int domain_id = 0; + int rc = coreclr_initialize(nullptr, nullptr, nprops, (const char **)&prop_keys, (const char **)&prop_values, &coreclr_handle, &domain_id); + ERR_FAIL_COND_V_MSG(rc != 0, nullptr, ".NET: Failed to initialize CoreCLR."); + + r_runtime_initialized = true; + + print_verbose(".NET: CoreCLR initialized"); + + coreclr_create_delegate(coreclr_handle, domain_id, + get_data(str_to_hostfxr(assembly_name)), + HOSTFXR_STR("GodotPlugins.Game.Main"), + HOSTFXR_STR("InitializeFromGameProject"), + (void **)&godot_plugins_initialize); + ERR_FAIL_NULL_V_MSG(godot_plugins_initialize, nullptr, ".NET: Failed to get GodotPlugins initialization function pointer"); + + return godot_plugins_initialize; +} +#endif + } // namespace bool GDMono::should_initialize() { @@ -382,14 +531,21 @@ void GDMono::initialize() { } #endif - if (!load_hostfxr(hostfxr_dll_handle)) { + if (load_hostfxr(hostfxr_dll_handle)) { + godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized); + ERR_FAIL_NULL(godot_plugins_initialize); + } else { #if !defined(TOOLS_ENABLED) - godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle); - - if (godot_plugins_initialize != nullptr) { - is_native_aot = true; - runtime_initialized = true; + if (load_coreclr(coreclr_dll_handle)) { + godot_plugins_initialize = initialize_coreclr_and_godot_plugins(runtime_initialized); } else { + godot_plugins_initialize = try_load_native_aot_library(hostfxr_dll_handle); + if (godot_plugins_initialize != nullptr) { + runtime_initialized = true; + } + } + + if (godot_plugins_initialize == nullptr) { ERR_FAIL_MSG(".NET: Failed to load hostfxr"); } #else @@ -400,11 +556,6 @@ void GDMono::initialize() { #endif } - if (!is_native_aot) { - godot_plugins_initialize = initialize_hostfxr_and_godot_plugins(runtime_initialized); - ERR_FAIL_NULL(godot_plugins_initialize); - } - int32_t interop_funcs_size = 0; const void **interop_funcs = godotsharp::get_runtime_interop_funcs(interop_funcs_size); @@ -553,6 +704,9 @@ GDMono::~GDMono() { if (hostfxr_dll_handle) { OS::get_singleton()->close_dynamic_library(hostfxr_dll_handle); } + if (coreclr_dll_handle) { + OS::get_singleton()->close_dynamic_library(coreclr_dll_handle); + } finalizing_scripts_domain = false; runtime_initialized = false; diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h index 614bfc63fb..fae3421ac9 100644 --- a/modules/mono/mono_gd/gd_mono.h +++ b/modules/mono/mono_gd/gd_mono.h @@ -64,7 +64,7 @@ class GDMono { bool finalizing_scripts_domain = false; void *hostfxr_dll_handle = nullptr; - bool is_native_aot = false; + void *coreclr_dll_handle = nullptr; String project_assembly_path; uint64_t project_assembly_modified_time = 0; diff --git a/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar b/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar Binary files differnew file mode 100755 index 0000000000..7366030881 --- /dev/null +++ b/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index fd07324557..e3d9807af7 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -2381,19 +2381,6 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito #ifdef MODULE_MONO_ENABLED // Android export is still a work in progress, keep a message as a warning. err += TTR("Exporting to Android when using C#/.NET is experimental.") + "\n"; - - bool unsupported_arch = false; - Vector<ABI> enabled_abis = get_enabled_abis(p_preset); - for (ABI abi : enabled_abis) { - if (abi.arch != "arm64" && abi.arch != "x86_64") { - err += vformat(TTR("Android architecture %s not supported in C# projects."), abi.arch) + "\n"; - unsupported_arch = true; - } - } - if (unsupported_arch) { - r_error = err; - return false; - } #endif // Look for export templates (first official, and if defined custom templates). @@ -3201,6 +3188,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP PluginConfigAndroid::get_plugins_custom_maven_repos(enabled_plugins, android_dependencies_maven_repos); #endif // DISABLE_DEPRECATED + bool has_dotnet_project = false; Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { if (export_plugins[i]->supports_platform(Ref<EditorExportPlatform>(this))) { @@ -3218,6 +3206,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP PackedStringArray export_plugin_android_dependencies_maven_repos = export_plugins[i]->get_android_dependencies_maven_repos(Ref<EditorExportPlatform>(this), p_debug); android_dependencies_maven_repos.append_array(export_plugin_android_dependencies_maven_repos); } + + PackedStringArray features = export_plugins[i]->get_export_features(Ref<EditorExportPlatform>(this), p_debug); + if (features.has("dotnet")) { + has_dotnet_project = true; + } } bool clean_build_required = _is_clean_build_required(p_preset); @@ -3231,12 +3224,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back("clean"); } + String edition = has_dotnet_project ? "Mono" : "Standard"; String build_type = p_debug ? "Debug" : "Release"; if (export_format == EXPORT_FORMAT_AAB) { String bundle_build_command = vformat("bundle%s", build_type); cmdline.push_back(bundle_build_command); } else if (export_format == EXPORT_FORMAT_APK) { - String apk_build_command = vformat("assemble%s", build_type); + String apk_build_command = vformat("assemble%s%s", edition, build_type); cmdline.push_back(apk_build_command); } @@ -3319,6 +3313,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP copy_args.push_back("-p"); // argument to specify the start directory. copy_args.push_back(build_path); // start directory. + copy_args.push_back("-Pexport_edition=" + edition.to_lower()); + copy_args.push_back("-Pexport_build_type=" + build_type.to_lower()); String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk"; diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index b9d15deec9..fdc5753798 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -29,6 +29,8 @@ allprojects { configurations { // Initializes a placeholder for the devImplementation dependency configuration. devImplementation {} + // Initializes a placeholder for the monoImplementation dependency configuration. + monoImplementation {} } dependencies { @@ -42,9 +44,9 @@ dependencies { } else { // Godot gradle build mode. In this scenario this project is the only one around and the Godot // library is available through the pre-generated godot-lib.*.aar android archive files. - debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) - devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar']) - releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) + debugImplementation fileTree(dir: 'libs/debug', include: ['**/*.jar', '*.aar']) + devImplementation fileTree(dir: 'libs/dev', include: ['**/*.jar', '*.aar']) + releaseImplementation fileTree(dir: 'libs/release', include: ['**/*.jar', '*.aar']) } // Godot user plugins remote dependencies @@ -60,6 +62,12 @@ dependencies { if (pluginsBinaries != null && pluginsBinaries.size() > 0) { implementation files(pluginsBinaries) } + + // .NET dependencies + String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar' + if (file(jar).exists()) { + monoImplementation files(jar) + } } android { @@ -155,6 +163,10 @@ android { } } + buildFeatures { + buildConfig = true + } + buildTypes { debug { @@ -192,6 +204,13 @@ android { } } + flavorDimensions 'edition' + + productFlavors { + standard {} + mono {} + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' @@ -207,7 +226,8 @@ android { applicationVariants.all { variant -> variant.outputs.all { output -> - output.outputFileName = "android_${variant.name}.apk" + String filenameSuffix = variant.flavorName == "mono" ? variant.name : variant.buildType.name + output.outputFileName = "android_${filenameSuffix}.apk" } } } @@ -220,12 +240,20 @@ task copyAndRenameBinary(type: Copy) { String exportPath = getExportPath() String exportFilename = getExportFilename() + String exportEdition = getExportEdition() String exportBuildType = getExportBuildType() + String exportBuildTypeCapitalized = exportBuildType.capitalize() String exportFormat = getExportFormat() boolean isAab = exportFormat == "aab" - String sourceFilepath = isAab ? "$buildDir/outputs/bundle/$exportBuildType/build-${exportBuildType}.aab" : "$buildDir/outputs/apk/$exportBuildType/android_${exportBuildType}.apk" - String sourceFilename = isAab ? "build-${exportBuildType}.aab" : "android_${exportBuildType}.apk" + boolean isMono = exportEdition == "mono" + String filenameSuffix = exportBuildType + if (isMono) { + filenameSuffix = isAab ? "${exportEdition}-${exportBuildType}" : "${exportEdition}${exportBuildTypeCapitalized}" + } + + String sourceFilename = isAab ? "build-${filenameSuffix}.aab" : "android_${filenameSuffix}.apk" + String sourceFilepath = isAab ? "$buildDir/outputs/bundle/${exportEdition}${exportBuildTypeCapitalized}/$sourceFilename" : "$buildDir/outputs/apk/$exportEdition/$exportBuildType/$sourceFilename" from sourceFilepath into exportPath diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 611a9c4a40..597a4d5c14 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -224,6 +224,14 @@ ext.getExportFilename = { return exportFilename } +ext.getExportEdition = { + String exportEdition = project.hasProperty("export_edition") ? project.property("export_edition") : "" + if (exportEdition == null || exportEdition.isEmpty()) { + exportEdition = "standard" + } + return exportEdition +} + ext.getExportBuildType = { String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : "" if (exportBuildType == null || exportBuildType.isEmpty()) { diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 22e617f6e7..9d4991e120 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -33,14 +33,29 @@ package com.godot.game; import org.godotengine.godot.GodotActivity; import android.os.Bundle; +import android.util.Log; import androidx.core.splashscreen.SplashScreen; +import com.godot.game.BuildConfig; + /** * Template activity for Godot Android builds. * Feel free to extend and modify this class for your custom logic. */ public class GodotApp extends GodotActivity { + static { + // .NET libraries. + if (BuildConfig.FLAVOR.equals("mono")) { + try { + Log.v("GODOT", "Loading System.Security.Cryptography.Native.Android library"); + System.loadLibrary("System.Security.Cryptography.Native.Android"); + } catch (UnsatisfiedLinkError e) { + Log.e("GODOT", "Unable to load System.Security.Cryptography.Native.Android library"); + } + } + } + @Override public void onCreate(Bundle savedInstanceState) { SplashScreen.installSplashScreen(this); diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index d60f97e3e7..9184e8c5d5 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -30,6 +30,7 @@ ext { "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] ] + supportedEditions = ["standard", "mono"] // Used by gradle to specify which architecture to build for by default when running // `./gradlew build` (this command is usually used by Android Studio). @@ -53,7 +54,7 @@ def getSconsTaskName(String flavor, String buildType, String abi) { * The zip file also includes some gradle tools to enable gradle builds from the Godot Editor. */ task zipGradleBuild(type: Zip) { - onlyIf { generateGodotTemplates.state.executed || generateDevTemplate.state.executed } + onlyIf { generateGodotTemplates.state.executed || generateGodotMonoTemplates.state.executed || generateDevTemplate.state.executed } doFirst { logger.lifecycle("Generating Godot gradle build template") } @@ -94,15 +95,22 @@ def templateExcludedBuildTask() { /** * Generates the build tasks for the given flavor * @param flavor Must be one of the supported flavors ('template' / 'editor') + * @param edition Must be one of the supported editions ('standard' / 'mono') * @param androidDistro Must be one of the supported Android distributions ('android' / 'horizonos') */ -def generateBuildTasks(String flavor = "template", String androidDistro = "android") { +def generateBuildTasks(String flavor = "template", String edition = "standard", String androidDistro = "android") { if (!supportedFlavors.contains(flavor)) { throw new GradleException("Invalid build flavor: $flavor") } if (!supportedAndroidDistributions.contains(androidDistro)) { throw new GradleException("Invalid Android distribution: $androidDistro") } + if (!supportedEditions.contains(edition)) { + throw new GradleException("Invalid build edition: $edition") + } + if (edition == "mono" && flavor != "template") { + throw new GradleException("'mono' edition only supports the 'template' flavor.") + } String capitalizedAndroidDistro = androidDistro.capitalize() def buildTasks = [] @@ -126,6 +134,7 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro && targetLibs.listFiles().length > 0)) { String capitalizedTarget = target.capitalize() + String capitalizedEdition = edition.capitalize() if (isTemplate) { // Copy the Godot android library archive file into the app module libs directory. // Depends on the library build task to ensure the AAR file is generated prior to copying. @@ -157,15 +166,16 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro // Copy the generated binary template into the Godot bin directory. // Depends on the app build task to ensure the binary is generated prior to copying. - String copyBinaryTaskName = "copy${capitalizedTarget}BinaryToBin" + String copyBinaryTaskName = "copy${capitalizedEdition}${capitalizedTarget}BinaryToBin" if (tasks.findByName(copyBinaryTaskName) != null) { buildTasks += tasks.getByName(copyBinaryTaskName) } else { buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) { - dependsOn ":app:assemble${capitalizedTarget}" - from("app/build/outputs/apk/${target}") + String filenameSuffix = edition == "mono" ? "${edition}${capitalizedTarget}" : target + dependsOn ":app:assemble${capitalizedEdition}${capitalizedTarget}" + from("app/build/outputs/apk/${edition}/${target}") into(binDir) - include("android_${target}.apk") + include("android_${filenameSuffix}.apk") } } } else { @@ -212,7 +222,7 @@ def generateBuildTasks(String flavor = "template", String androidDistro = "andro */ task generateGodotEditor { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = generateBuildTasks("editor", "android") + dependsOn = generateBuildTasks("editor", "standard", "android") } /** @@ -224,7 +234,7 @@ task generateGodotEditor { */ task generateGodotHorizonOSEditor { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = generateBuildTasks("editor", "horizonos") + dependsOn = generateBuildTasks("editor", "standard", "horizonos") } /** @@ -238,6 +248,17 @@ task generateGodotTemplates { } /** + * Master task used to coordinate the tasks defined above to generate the set of Godot templates + * for the 'mono' edition of the engine. + */ +task generateGodotMonoTemplates { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + dependsOn = generateBuildTasks("template", "mono") + + finalizedBy 'zipGradleBuild' +} + +/** * Generates the same output as generateGodotTemplates but with dev symbols */ task generateDevTemplate { @@ -295,6 +316,9 @@ task cleanGodotTemplates(type: Delete) { delete("$binDir/android_debug.apk") delete("$binDir/android_dev.apk") delete("$binDir/android_release.apk") + delete("$binDir/android_monoDebug.apk") + delete("$binDir/android_monoDev.apk") + delete("$binDir/android_monoRelease.apk") delete("$binDir/android_source.zip") delete("$binDir/godot-lib.template_debug.aar") delete("$binDir/godot-lib.template_debug.dev.aar") diff --git a/scene/3d/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index c6ece84cdd..db9c4db30d 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -103,6 +103,8 @@ bool Skeleton3D::_set(const StringName &p_path, const Variant &p_value) { set_bone_pose_rotation(which, p_value); } else if (what == "scale") { set_bone_pose_scale(which, p_value); + } else if (what == "bone_meta") { + set_bone_meta(which, path.get_slicec('/', 3), p_value); #ifndef DISABLE_DEPRECATED } else if (what == "pose" || what == "bound_children") { // Kept for compatibility from 3.x to 4.x. @@ -170,6 +172,8 @@ bool Skeleton3D::_get(const StringName &p_path, Variant &r_ret) const { r_ret = get_bone_pose_rotation(which); } else if (what == "scale") { r_ret = get_bone_pose_scale(which); + } else if (what == "bone_meta") { + r_ret = get_bone_meta(which, path.get_slicec('/', 3)); } else { return false; } @@ -187,6 +191,11 @@ void Skeleton3D::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("position"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); p_list->push_back(PropertyInfo(Variant::QUATERNION, prep + PNAME("rotation"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("scale"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + + for (const KeyValue<StringName, Variant> &K : bones[i].metadata) { + PropertyInfo pi = PropertyInfo(bones[i].metadata[K.key].get_type(), prep + PNAME("bone_meta/") + K.key, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR); + p_list->push_back(pi); + } } for (PropertyInfo &E : *p_list) { @@ -531,6 +540,57 @@ void Skeleton3D::set_bone_name(int p_bone, const String &p_name) { version++; } +Variant Skeleton3D::get_bone_meta(int p_bone, const StringName &p_key) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, Variant()); + + if (!bones[p_bone].metadata.has(p_key)) { + return Variant(); + } + return bones[p_bone].metadata[p_key]; +} + +TypedArray<StringName> Skeleton3D::_get_bone_meta_list_bind(int p_bone) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, TypedArray<StringName>()); + + TypedArray<StringName> _metaret; + for (const KeyValue<StringName, Variant> &K : bones[p_bone].metadata) { + _metaret.push_back(K.key); + } + return _metaret; +} + +void Skeleton3D::get_bone_meta_list(int p_bone, List<StringName> *p_list) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX(p_bone, bone_size); + + for (const KeyValue<StringName, Variant> &K : bones[p_bone].metadata) { + p_list->push_back(K.key); + } +} + +bool Skeleton3D::has_bone_meta(int p_bone, const StringName &p_key) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, false); + + return bones[p_bone].metadata.has(p_key); +} + +void Skeleton3D::set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value) { + const int bone_size = bones.size(); + ERR_FAIL_INDEX(p_bone, bone_size); + + if (p_value.get_type() == Variant::NIL) { + if (bones.write[p_bone].metadata.has(p_key)) { + bones.write[p_bone].metadata.erase(p_key); + } + return; + } + + bones.write[p_bone].metadata.insert(p_key, p_value, false); +} + bool Skeleton3D::is_bone_parent_of(int p_bone, int p_parent_bone_id) const { int parent_of_bone = get_bone_parent(p_bone); @@ -1014,6 +1074,11 @@ void Skeleton3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_bone_name", "bone_idx"), &Skeleton3D::get_bone_name); ClassDB::bind_method(D_METHOD("set_bone_name", "bone_idx", "name"), &Skeleton3D::set_bone_name); + ClassDB::bind_method(D_METHOD("get_bone_meta", "bone_idx", "key"), &Skeleton3D::get_bone_meta); + ClassDB::bind_method(D_METHOD("get_bone_meta_list", "bone_idx"), &Skeleton3D::_get_bone_meta_list_bind); + ClassDB::bind_method(D_METHOD("has_bone_meta", "bone_idx", "key"), &Skeleton3D::has_bone_meta); + ClassDB::bind_method(D_METHOD("set_bone_meta", "bone_idx", "key", "value"), &Skeleton3D::set_bone_meta); + ClassDB::bind_method(D_METHOD("get_concatenated_bone_names"), &Skeleton3D::get_concatenated_bone_names); ClassDB::bind_method(D_METHOD("get_bone_parent", "bone_idx"), &Skeleton3D::get_bone_parent); diff --git a/scene/3d/skeleton_3d.h b/scene/3d/skeleton_3d.h index a009383f45..07bdeccf2f 100644 --- a/scene/3d/skeleton_3d.h +++ b/scene/3d/skeleton_3d.h @@ -116,6 +116,8 @@ private: } } + HashMap<StringName, Variant> metadata; + #ifndef DISABLE_DEPRECATED Transform3D pose_global_no_override; real_t global_pose_override_amount = 0.0; @@ -193,6 +195,7 @@ protected: void _get_property_list(List<PropertyInfo> *p_list) const; void _validate_property(PropertyInfo &p_property) const; void _notification(int p_what); + TypedArray<StringName> _get_bone_meta_list_bind(int p_bone) const; static void _bind_methods(); virtual void add_child_notify(Node *p_child) override; @@ -238,6 +241,12 @@ public: void set_motion_scale(float p_motion_scale); float get_motion_scale() const; + // bone metadata + Variant get_bone_meta(int p_bone, const StringName &p_key) const; + void get_bone_meta_list(int p_bone, List<StringName> *p_list) const; + bool has_bone_meta(int p_bone, const StringName &p_key) const; + void set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value); + // Posing API Transform3D get_bone_pose(int p_bone) const; Vector3 get_bone_pose_position(int p_bone) const; diff --git a/tests/scene/test_skeleton_3d.h b/tests/scene/test_skeleton_3d.h new file mode 100644 index 0000000000..b5cf49c4eb --- /dev/null +++ b/tests/scene/test_skeleton_3d.h @@ -0,0 +1,78 @@ +/**************************************************************************/ +/* test_skeleton_3d.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SKELETON_3D_H +#define TEST_SKELETON_3D_H + +#include "tests/test_macros.h" + +#include "scene/3d/skeleton_3d.h" + +namespace TestSkeleton3D { + +TEST_CASE("[Skeleton3D] Test per-bone meta") { + Skeleton3D *skeleton = memnew(Skeleton3D); + skeleton->add_bone("root"); + skeleton->set_bone_rest(0, Transform3D()); + + // Adding meta to bone. + skeleton->set_bone_meta(0, "key1", "value1"); + skeleton->set_bone_meta(0, "key2", 12345); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing."); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing."); + + // Rename bone and check if meta persists. + skeleton->set_bone_name(0, "renamed_root"); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing."); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing."); + + // Retrieve list of keys. + List<StringName> keys; + skeleton->get_bone_meta_list(0, &keys); + CHECK_MESSAGE(keys.size() == 2, "Wrong number of bone meta keys."); + CHECK_MESSAGE(keys.find("key1"), "key1 not found in bone meta list"); + CHECK_MESSAGE(keys.find("key2"), "key2 not found in bone meta list"); + + // Removing meta. + skeleton->set_bone_meta(0, "key1", Variant()); + skeleton->set_bone_meta(0, "key2", Variant()); + CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key1"), "Bone meta key1 should be deleted."); + CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key2"), "Bone meta key2 should be deleted."); + List<StringName> should_be_empty_keys; + skeleton->get_bone_meta_list(0, &should_be_empty_keys); + CHECK_MESSAGE(should_be_empty_keys.size() == 0, "Wrong number of bone meta keys."); + + // Deleting non-existing key should succeed. + skeleton->set_bone_meta(0, "non-existing-key", Variant()); + memdelete(skeleton); +} +} // namespace TestSkeleton3D + +#endif // TEST_SKELETON_3D_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 2b6461e9ca..949e4f0b33 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -160,6 +160,7 @@ #include "tests/scene/test_path_3d.h" #include "tests/scene/test_path_follow_3d.h" #include "tests/scene/test_primitives.h" +#include "tests/scene/test_skeleton_3d.h" #endif // _3D_DISABLED #include "modules/modules_tests.gen.h" |