summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/object/worker_thread_pool.cpp178
-rw-r--r--core/object/worker_thread_pool.h28
-rw-r--r--core/register_core_types.cpp8
-rw-r--r--doc/classes/Skeleton3D.xml32
-rw-r--r--editor/add_metadata_dialog.cpp118
-rw-r--r--editor/add_metadata_dialog.h66
-rw-r--r--editor/editor_inspector.cpp100
-rw-r--r--editor/editor_inspector.h4
-rw-r--r--editor/editor_node.cpp120
-rw-r--r--editor/editor_node.h10
-rw-r--r--editor/editor_resource_picker.cpp14
-rw-r--r--editor/export/editor_export_plugin.cpp4
-rw-r--r--editor/export/editor_export_plugin.h1
-rw-r--r--editor/filesystem_dock.cpp169
-rw-r--r--editor/filesystem_dock.h12
-rw-r--r--editor/import_dock.cpp55
-rw-r--r--editor/import_dock.h2
-rw-r--r--editor/plugins/skeleton_3d_editor_plugin.cpp173
-rw-r--r--editor/plugins/skeleton_3d_editor_plugin.h22
-rw-r--r--main/main.cpp18
-rw-r--r--modules/gltf/gltf_document.cpp4
-rw-r--r--modules/gltf/skin_tool.cpp5
-rw-r--r--modules/gltf/tests/test_gltf_extras.h57
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Godot.NET.Sdk.csproj1
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Android.props5
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk/Sdk/Sdk.props1
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs25
-rw-r--r--modules/mono/mono_gd/gd_mono.cpp176
-rw-r--r--modules/mono/mono_gd/gd_mono.h2
-rwxr-xr-xmodules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jarbin0 -> 8552 bytes
-rw-r--r--platform/android/export/export_plugin.cpp24
-rw-r--r--platform/android/java/app/build.gradle40
-rw-r--r--platform/android/java/app/config.gradle8
-rw-r--r--platform/android/java/app/src/com/godot/game/GodotApp.java15
-rw-r--r--platform/android/java/build.gradle40
-rw-r--r--scene/3d/skeleton_3d.cpp65
-rw-r--r--scene/3d/skeleton_3d.h9
-rw-r--r--tests/scene/test_skeleton_3d.h78
-rw-r--r--tests/test_main.cpp1
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
new file mode 100755
index 0000000000..7366030881
--- /dev/null
+++ b/modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar
Binary files differ
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"