diff options
137 files changed, 3826 insertions, 1232 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0820ab175d..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -patreon: godotengine -custom: https://godotengine.org/donate diff --git a/.github/workflows/android_builds.yml b/.github/workflows/android_builds.yml index 6b16c10b3e..1c9a8f7f03 100644 --- a/.github/workflows/android_builds.yml +++ b/.github/workflows/android_builds.yml @@ -1,4 +1,4 @@ -name: Android Builds +name: 🤖 Android Builds on: [push, pull_request] # Global Cache Settings diff --git a/.github/workflows/ios_builds.yml b/.github/workflows/ios_builds.yml index 0657a6cb18..e6770d384a 100644 --- a/.github/workflows/ios_builds.yml +++ b/.github/workflows/ios_builds.yml @@ -1,4 +1,4 @@ -name: iOS Builds +name: 🍏 iOS Builds on: [push, pull_request] # Global Cache Settings diff --git a/.github/workflows/javascript_builds.disabled b/.github/workflows/javascript_builds.disabled index 09c4c41d53..c3c51209bd 100644 --- a/.github/workflows/javascript_builds.disabled +++ b/.github/workflows/javascript_builds.disabled @@ -1,4 +1,4 @@ -name: JavaScript Builds +name: 🌐 JavaScript Builds on: [push, pull_request] # Global Cache Settings diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index 087b2d15de..436671dd95 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -1,4 +1,4 @@ -name: Linux Builds +name: 🐧 Linux Builds on: [push, pull_request] # Global Cache Settings diff --git a/.github/workflows/macos_builds.yml b/.github/workflows/macos_builds.yml index bd3340111e..74b57adaeb 100644 --- a/.github/workflows/macos_builds.yml +++ b/.github/workflows/macos_builds.yml @@ -1,4 +1,4 @@ -name: macOS Builds +name: 🍎 macOS Builds on: [push, pull_request] # Global Cache Settings diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml index ee01dba2ba..c2af0ac549 100644 --- a/.github/workflows/static_checks.yml +++ b/.github/workflows/static_checks.yml @@ -1,4 +1,4 @@ -name: Static Checks +name: 📊 Static Checks on: [push, pull_request] jobs: diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 0d3c78b45d..a2fa4cc7c8 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -1,4 +1,4 @@ -name: Windows Builds +name: 🏁 Windows Builds on: [push, pull_request] # Global Cache Settings diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f10438769d..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -# Code of Conduct - -By participating in this repository, you agree to abide by the -[Godot Engine Code of Conduct](https://godotengine.org/code-of-conduct). diff --git a/core/SCsub b/core/SCsub index d08f17c60a..40ee78d3ea 100644 --- a/core/SCsub +++ b/core/SCsub @@ -162,13 +162,17 @@ env.CommandNoCache( # Authors env.Depends("#core/authors.gen.h", "../AUTHORS.md") env.CommandNoCache( - "#core/authors.gen.h", "../AUTHORS.md", env.Run(core_builders.make_authors_header, "Generating authors header."), + "#core/authors.gen.h", + "../AUTHORS.md", + env.Run(core_builders.make_authors_header, "Generating authors header."), ) # Donors env.Depends("#core/donors.gen.h", "../DONORS.md") env.CommandNoCache( - "#core/donors.gen.h", "../DONORS.md", env.Run(core_builders.make_donors_header, "Generating donors header."), + "#core/donors.gen.h", + "../DONORS.md", + env.Run(core_builders.make_donors_header, "Generating donors header."), ) # License diff --git a/core/bind/core_bind.cpp b/core/bind/core_bind.cpp index 045d7d5872..489ff762c9 100644 --- a/core/bind/core_bind.cpp +++ b/core/bind/core_bind.cpp @@ -31,6 +31,7 @@ #include "core_bind.h" #include "core/crypto/crypto_core.h" +#include "core/debugger/engine_debugger.h" #include "core/io/file_access_compressed.h" #include "core/io/file_access_encrypted.h" #include "core/io/json.h" @@ -2467,3 +2468,154 @@ Ref<JSONParseResult> _JSON::parse(const String &p_json) { } _JSON *_JSON::singleton = nullptr; + +////// _EngineDebugger ////// + +void _EngineDebugger::_bind_methods() { + ClassDB::bind_method(D_METHOD("is_active"), &_EngineDebugger::is_active); + + ClassDB::bind_method(D_METHOD("register_profiler", "name", "toggle", "add", "tick"), &_EngineDebugger::register_profiler); + ClassDB::bind_method(D_METHOD("unregister_profiler", "name"), &_EngineDebugger::unregister_profiler); + ClassDB::bind_method(D_METHOD("is_profiling", "name"), &_EngineDebugger::is_profiling); + ClassDB::bind_method(D_METHOD("has_profiler", "name"), &_EngineDebugger::has_profiler); + + ClassDB::bind_method(D_METHOD("profiler_add_frame_data", "name", "data"), &_EngineDebugger::profiler_add_frame_data); + ClassDB::bind_method(D_METHOD("profiler_enable", "name", "enable", "arguments"), &_EngineDebugger::profiler_enable, DEFVAL(Array())); + + ClassDB::bind_method(D_METHOD("register_message_capture", "name", "callable"), &_EngineDebugger::register_message_capture); + ClassDB::bind_method(D_METHOD("unregister_message_capture", "name"), &_EngineDebugger::unregister_message_capture); + ClassDB::bind_method(D_METHOD("has_capture", "name"), &_EngineDebugger::has_capture); + + ClassDB::bind_method(D_METHOD("send_message", "message", "data"), &_EngineDebugger::send_message); +} + +bool _EngineDebugger::is_active() { + return EngineDebugger::is_active(); +} + +void _EngineDebugger::register_profiler(const StringName &p_name, const Callable &p_toggle, const Callable &p_add, const Callable &p_tick) { + ERR_FAIL_COND_MSG(profilers.has(p_name) || has_profiler(p_name), "Profiler already registered: " + p_name); + profilers.insert(p_name, ProfilerCallable(p_toggle, p_add, p_tick)); + ProfilerCallable &p = profilers[p_name]; + EngineDebugger::Profiler profiler( + &p, + &_EngineDebugger::call_toggle, + &_EngineDebugger::call_add, + &_EngineDebugger::call_tick); + EngineDebugger::register_profiler(p_name, profiler); +} + +void _EngineDebugger::unregister_profiler(const StringName &p_name) { + ERR_FAIL_COND_MSG(!profilers.has(p_name), "Profiler not registered: " + p_name); + EngineDebugger::unregister_profiler(p_name); + profilers.erase(p_name); +} + +bool _EngineDebugger::_EngineDebugger::is_profiling(const StringName &p_name) { + return EngineDebugger::is_profiling(p_name); +} + +bool _EngineDebugger::has_profiler(const StringName &p_name) { + return EngineDebugger::has_profiler(p_name); +} + +void _EngineDebugger::profiler_add_frame_data(const StringName &p_name, const Array &p_data) { + EngineDebugger::profiler_add_frame_data(p_name, p_data); +} + +void _EngineDebugger::profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts) { + if (EngineDebugger::get_singleton()) { + EngineDebugger::get_singleton()->profiler_enable(p_name, p_enabled, p_opts); + } +} + +void _EngineDebugger::register_message_capture(const StringName &p_name, const Callable &p_callable) { + ERR_FAIL_COND_MSG(captures.has(p_name) || has_capture(p_name), "Capture already registered: " + p_name); + captures.insert(p_name, p_callable); + Callable &c = captures[p_name]; + EngineDebugger::Capture capture(&c, &_EngineDebugger::call_capture); + EngineDebugger::register_message_capture(p_name, capture); +} + +void _EngineDebugger::unregister_message_capture(const StringName &p_name) { + ERR_FAIL_COND_MSG(!captures.has(p_name), "Capture not registered: " + p_name); + EngineDebugger::unregister_message_capture(p_name); + captures.erase(p_name); +} + +bool _EngineDebugger::has_capture(const StringName &p_name) { + return EngineDebugger::has_capture(p_name); +} + +void _EngineDebugger::send_message(const String &p_msg, const Array &p_data) { + ERR_FAIL_COND_MSG(!EngineDebugger::is_active(), "Can't send message. No active debugger"); + EngineDebugger::get_singleton()->send_message(p_msg, p_data); +} + +void _EngineDebugger::call_toggle(void *p_user, bool p_enable, const Array &p_opts) { + Callable &toggle = ((ProfilerCallable *)p_user)->callable_toggle; + if (toggle.is_null()) { + return; + } + Variant enable = p_enable, opts = p_opts; + const Variant *args[2] = { &enable, &opts }; + Variant retval; + Callable::CallError err; + toggle.call(args, 2, retval, err); + ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'toggle' to callable: " + Variant::get_callable_error_text(toggle, args, 2, err)); +} + +void _EngineDebugger::call_add(void *p_user, const Array &p_data) { + Callable &add = ((ProfilerCallable *)p_user)->callable_add; + if (add.is_null()) { + return; + } + Variant data = p_data; + const Variant *args[1] = { &data }; + Variant retval; + Callable::CallError err; + add.call(args, 1, retval, err); + ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'add' to callable: " + Variant::get_callable_error_text(add, args, 1, err)); +} + +void _EngineDebugger::call_tick(void *p_user, float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time) { + Callable &tick = ((ProfilerCallable *)p_user)->callable_tick; + if (tick.is_null()) { + return; + } + Variant frame_time = p_frame_time, idle_time = p_idle_time, physics_time = p_physics_time, physics_frame_time = p_physics_frame_time; + const Variant *args[4] = { &frame_time, &idle_time, &physics_time, &physics_frame_time }; + Variant retval; + Callable::CallError err; + tick.call(args, 4, retval, err); + ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'tick' to callable: " + Variant::get_callable_error_text(tick, args, 4, err)); +} + +Error _EngineDebugger::call_capture(void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) { + Callable &capture = *(Callable *)p_user; + if (capture.is_null()) { + return FAILED; + } + Variant cmd = p_cmd, data = p_data; + const Variant *args[2] = { &cmd, &data }; + Variant retval; + Callable::CallError err; + capture.call(args, 2, retval, err); + ERR_FAIL_COND_V_MSG(err.error != Callable::CallError::CALL_OK, FAILED, "Error calling 'capture' to callable: " + Variant::get_callable_error_text(capture, args, 2, err)); + ERR_FAIL_COND_V_MSG(retval.get_type() != Variant::BOOL, FAILED, "Error calling 'capture' to callable: " + String(capture) + ". Return type is not bool."); + r_captured = retval; + return OK; +} + +_EngineDebugger::~_EngineDebugger() { + for (Map<StringName, Callable>::Element *E = captures.front(); E; E = E->next()) { + EngineDebugger::unregister_message_capture(E->key()); + } + captures.clear(); + for (Map<StringName, ProfilerCallable>::Element *E = profilers.front(); E; E = E->next()) { + EngineDebugger::unregister_profiler(E->key()); + } + profilers.clear(); +} + +_EngineDebugger *_EngineDebugger::singleton = nullptr; diff --git a/core/bind/core_bind.h b/core/bind/core_bind.h index a1fedf1bb8..a59fcda60c 100644 --- a/core/bind/core_bind.h +++ b/core/bind/core_bind.h @@ -715,4 +715,58 @@ public: _JSON() { singleton = this; } }; +class _EngineDebugger : public Object { + GDCLASS(_EngineDebugger, Object); + + class ProfilerCallable { + friend class _EngineDebugger; + + Callable callable_toggle; + Callable callable_add; + Callable callable_tick; + + public: + ProfilerCallable() {} + + ProfilerCallable(const Callable &p_toggle, const Callable &p_add, const Callable &p_tick) { + callable_toggle = p_toggle; + callable_add = p_add; + callable_tick = p_tick; + } + }; + + Map<StringName, Callable> captures; + Map<StringName, ProfilerCallable> profilers; + +protected: + static void _bind_methods(); + static _EngineDebugger *singleton; + +public: + static _EngineDebugger *get_singleton() { return singleton; } + + bool is_active(); + + void register_profiler(const StringName &p_name, const Callable &p_toggle, const Callable &p_add, const Callable &p_tick); + void unregister_profiler(const StringName &p_name); + bool is_profiling(const StringName &p_name); + bool has_profiler(const StringName &p_name); + void profiler_add_frame_data(const StringName &p_name, const Array &p_data); + void profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts = Array()); + + void register_message_capture(const StringName &p_name, const Callable &p_callable); + void unregister_message_capture(const StringName &p_name); + bool has_capture(const StringName &p_name); + + void send_message(const String &p_msg, const Array &p_data); + + static void call_toggle(void *p_user, bool p_enable, const Array &p_opts); + static void call_add(void *p_user, const Array &p_data); + static void call_tick(void *p_user, float p_frame_time, float p_idle_time, float p_physics_time, float p_physics_frame_time); + static Error call_capture(void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured); + + _EngineDebugger() { singleton = this; } + ~_EngineDebugger(); +}; + #endif // CORE_BIND_H diff --git a/core/compressed_translation.cpp b/core/compressed_translation.cpp index a66997aa52..a92275565d 100644 --- a/core/compressed_translation.cpp +++ b/core/compressed_translation.cpp @@ -43,6 +43,8 @@ struct _PHashTranslationCmp { }; void PHashTranslation::generate(const Ref<Translation> &p_from) { + // This method compresses a Translation instance. + // Right now it doesn't handle context or plurals, so Translation subclasses using plurals or context (i.e TranslationPO) shouldn't be compressed. #ifdef TOOLS_ENABLED List<StringName> keys; p_from->get_message_list(&keys); @@ -212,7 +214,9 @@ bool PHashTranslation::_get(const StringName &p_name, Variant &r_ret) const { return true; } -StringName PHashTranslation::get_message(const StringName &p_src_text) const { +StringName PHashTranslation::get_message(const StringName &p_src_text, const StringName &p_context) const { + // p_context passed in is ignore. The use of context is not yet supported in PHashTranslation. + int htsize = hash_table.size(); if (htsize == 0) { @@ -267,6 +271,11 @@ StringName PHashTranslation::get_message(const StringName &p_src_text) const { } } +StringName PHashTranslation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { + // The use of plurals translation is not yet supported in PHashTranslation. + return get_message(p_src_text, p_context); +} + void PHashTranslation::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "hash_table")); p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "bucket_table")); diff --git a/core/compressed_translation.h b/core/compressed_translation.h index 4f9c422e1e..c8b3cd2330 100644 --- a/core/compressed_translation.h +++ b/core/compressed_translation.h @@ -79,7 +79,8 @@ protected: static void _bind_methods(); public: - virtual StringName get_message(const StringName &p_src_text) const override; //overridable for other implementations + virtual StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override; //overridable for other implementations + virtual StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override; void generate(const Ref<Translation> &p_from); PHashTranslation() {} diff --git a/core/io/translation_loader_po.cpp b/core/io/translation_loader_po.cpp index 11aeddee09..d8ddb213c3 100644 --- a/core/io/translation_loader_po.cpp +++ b/core/io/translation_loader_po.cpp @@ -32,26 +32,34 @@ #include "core/os/file_access.h" #include "core/translation.h" +#include "core/translation_po.h" RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { enum Status { STATUS_NONE, STATUS_READING_ID, STATUS_READING_STRING, + STATUS_READING_CONTEXT, + STATUS_READING_PLURAL, }; Status status = STATUS_NONE; String msg_id; String msg_str; + String msg_context; + Vector<String> msgs_plural; String config; if (r_error) { *r_error = ERR_FILE_CORRUPT; } - Ref<Translation> translation = Ref<Translation>(memnew(Translation)); + Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO)); int line = 1; + int plural_forms = 0; + int plural_index = -1; + bool entered_context = false; bool skip_this = false; bool skip_next = false; bool is_eof = false; @@ -63,40 +71,107 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { // If we reached last line and it's not a content line, break, otherwise let processing that last loop if (is_eof && l.empty()) { - if (status == STATUS_READING_ID) { + if (status == STATUS_READING_ID || status == STATUS_READING_CONTEXT || (status == STATUS_READING_PLURAL && plural_index != plural_forms - 1)) { memdelete(f); - ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading 'msgid' at: " + path + ":" + itos(line)); + ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading PO file at: " + path + ":" + itos(line)); } else { break; } } - if (l.begins_with("msgid")) { + if (l.begins_with("msgctxt")) { + if (status != STATUS_READING_STRING && status != STATUS_READING_PLURAL) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgctxt', was expecting 'msgid_plural' or 'msgstr' before 'msgctxt' while parsing: " + path + ":" + itos(line)); + } + + // In PO file, "msgctxt" appears before "msgid". If we encounter a "msgctxt", we add what we have read + // and set "entered_context" to true to prevent adding twice. + if (!skip_this && msg_id != "") { + if (status == STATUS_READING_STRING) { + translation->add_message(msg_id, msg_str, msg_context); + } else if (status == STATUS_READING_PLURAL) { + if (plural_index != plural_forms - 1) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line)); + } + translation->add_plural_message(msg_id, msgs_plural, msg_context); + } + } + msg_context = ""; + l = l.substr(7, l.length()).strip_edges(); + status = STATUS_READING_CONTEXT; + entered_context = true; + } + + if (l.begins_with("msgid_plural")) { + if (plural_forms == 0) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "PO file uses 'msgid_plural' but 'Plural-Forms' is invalid or missing in header: " + path + ":" + itos(line)); + } else if (status != STATUS_READING_ID) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid_plural', was expecting 'msgid' before 'msgid_plural' while parsing: " + path + ":" + itos(line)); + } + // We don't record the message in "msgid_plural" itself as tr_n(), TTRN(), RTRN() interfaces provide the plural string already. + // We just have to reset variables related to plurals for "msgstr[]" later on. + l = l.substr(12, l.length()).strip_edges(); + plural_index = -1; + msgs_plural.clear(); + msgs_plural.resize(plural_forms); + status = STATUS_READING_PLURAL; + } else if (l.begins_with("msgid")) { if (status == STATUS_READING_ID) { memdelete(f); ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid', was expecting 'msgstr' while parsing: " + path + ":" + itos(line)); } if (msg_id != "") { - if (!skip_this) { - translation->add_message(msg_id, msg_str); + if (!skip_this && !entered_context) { + if (status == STATUS_READING_STRING) { + translation->add_message(msg_id, msg_str, msg_context); + } else if (status == STATUS_READING_PLURAL) { + if (plural_index != plural_forms - 1) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line)); + } + translation->add_plural_message(msg_id, msgs_plural, msg_context); + } } } else if (config == "") { config = msg_str; + // Record plural rule. + int p_start = config.find("Plural-Forms"); + if (p_start != -1) { + int p_end = config.find("\n", p_start); + translation->set_plural_rule(config.substr(p_start, p_end - p_start)); + plural_forms = translation->get_plural_forms(); + } } l = l.substr(5, l.length()).strip_edges(); status = STATUS_READING_ID; + // If we did not encounter msgctxt, we reset context to empty to reset it. + if (!entered_context) { + msg_context = ""; + } msg_id = ""; msg_str = ""; skip_this = skip_next; skip_next = false; + entered_context = false; } - if (l.begins_with("msgstr")) { + if (l.begins_with("msgstr[")) { + if (status != STATUS_READING_PLURAL) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr[]', was expecting 'msgid_plural' before 'msgstr[]' while parsing: " + path + ":" + itos(line)); + } + plural_index++; // Increment to add to the next slot in vector msgs_plural. + l = l.substr(9, l.length()).strip_edges(); + } else if (l.begins_with("msgstr")) { if (status != STATUS_READING_ID) { memdelete(f); - ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' while parsing: " + path + ":" + itos(line)); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' before 'msgstr' while parsing: " + path + ":" + itos(line)); } l = l.substr(6, l.length()).strip_edges(); @@ -108,7 +183,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { skip_next = true; } line++; - continue; //nothing to read or comment + continue; // Nothing to read or comment. } if (!l.begins_with("\"") || status == STATUS_NONE) { @@ -146,8 +221,12 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { if (status == STATUS_READING_ID) { msg_id += l; - } else { + } else if (status == STATUS_READING_STRING) { msg_str += l; + } else if (status == STATUS_READING_CONTEXT) { + msg_context += l; + } else if (status == STATUS_READING_PLURAL && plural_index >= 0) { + msgs_plural.write[plural_index] = msgs_plural[plural_index] + l; } line++; @@ -155,14 +234,23 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { memdelete(f); + // Add the last set of data from last iteration. if (status == STATUS_READING_STRING) { if (msg_id != "") { if (!skip_this) { - translation->add_message(msg_id, msg_str); + translation->add_message(msg_id, msg_str, msg_context); } } else if (config == "") { config = msg_str; } + } else if (status == STATUS_READING_PLURAL) { + if (!skip_this && msg_id != "") { + if (plural_index != plural_forms - 1) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line)); + } + translation->add_plural_message(msg_id, msgs_plural, msg_context); + } } ERR_FAIL_COND_V_MSG(config == "", RES(), "No config found in file: " + path + "."); diff --git a/core/object.cpp b/core/object.cpp index ff6d4a666f..67c605c39b 100644 --- a/core/object.cpp +++ b/core/object.cpp @@ -1432,12 +1432,22 @@ void Object::initialize_class() { initialized = true; } -StringName Object::tr(const StringName &p_message) const { +String Object::tr(const StringName &p_message, const StringName &p_context) const { if (!_can_translate || !TranslationServer::get_singleton()) { return p_message; } + return TranslationServer::get_singleton()->translate(p_message, p_context); +} - return TranslationServer::get_singleton()->translate(p_message); +String Object::tr_n(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { + if (!_can_translate || !TranslationServer::get_singleton()) { + // Return message based on English plural rule if translation is not possible. + if (p_n == 1) { + return p_message; + } + return p_message_plural; + } + return TranslationServer::get_singleton()->translate_plural(p_message, p_message_plural, p_n, p_context); } void Object::_clear_internal_resource_paths(const Variant &p_var) { @@ -1578,7 +1588,8 @@ void Object::_bind_methods() { ClassDB::bind_method(D_METHOD("set_message_translation", "enable"), &Object::set_message_translation); ClassDB::bind_method(D_METHOD("can_translate_messages"), &Object::can_translate_messages); - ClassDB::bind_method(D_METHOD("tr", "message"), &Object::tr); + ClassDB::bind_method(D_METHOD("tr", "message", "context"), &Object::tr, DEFVAL("")); + ClassDB::bind_method(D_METHOD("tr_n", "message", "plural_message", "n", "context"), &Object::tr_n, DEFVAL("")); ClassDB::bind_method(D_METHOD("is_queued_for_deletion"), &Object::is_queued_for_deletion); diff --git a/core/object.h b/core/object.h index d9847d10aa..f9a12da8f6 100644 --- a/core/object.h +++ b/core/object.h @@ -719,7 +719,8 @@ public: virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const; - StringName tr(const StringName &p_message) const; // translate message (internationalization) + String tr(const StringName &p_message, const StringName &p_context = "") const; // translate message (internationalization) + String tr_n(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; bool _is_queued_for_deletion = false; // set to true by SceneTree::queue_delete() bool is_queued_for_deletion() const; diff --git a/core/project_settings.h b/core/project_settings.h index 686f6f3873..70b54ec854 100644 --- a/core/project_settings.h +++ b/core/project_settings.h @@ -76,8 +76,8 @@ protected: }; bool registering_order = true; - int last_order = 0; - int last_builtin_order = NO_BUILTIN_ORDER_BASE; + int last_order = NO_BUILTIN_ORDER_BASE; + int last_builtin_order = 0; Map<StringName, VariantContainer> props; String resource_path; Map<StringName, PropertyInfo> custom_prop_info; diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 5dac42cacb..4f094dd6c6 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -86,6 +86,7 @@ static _Engine *_engine = nullptr; static _ClassDB *_classdb = nullptr; static _Marshalls *_marshalls = nullptr; static _JSON *_json = nullptr; +static _EngineDebugger *_engine_debugger = nullptr; static IP *ip = nullptr; @@ -227,6 +228,7 @@ void register_core_types() { _classdb = memnew(_ClassDB); _marshalls = memnew(_Marshalls); _json = memnew(_JSON); + _engine_debugger = memnew(_EngineDebugger); } void register_core_settings() { @@ -256,6 +258,7 @@ void register_core_singletons() { ClassDB::register_class<InputMap>(); ClassDB::register_class<_JSON>(); ClassDB::register_class<Expression>(); + ClassDB::register_class<_EngineDebugger>(); Engine::get_singleton()->add_singleton(Engine::Singleton("ProjectSettings", ProjectSettings::get_singleton())); Engine::get_singleton()->add_singleton(Engine::Singleton("IP", IP::get_singleton())); @@ -271,6 +274,7 @@ void register_core_singletons() { Engine::get_singleton()->add_singleton(Engine::Singleton("Input", Input::get_singleton())); Engine::get_singleton()->add_singleton(Engine::Singleton("InputMap", InputMap::get_singleton())); Engine::get_singleton()->add_singleton(Engine::Singleton("JSON", _JSON::get_singleton())); + Engine::get_singleton()->add_singleton(Engine::Singleton("EngineDebugger", _EngineDebugger::get_singleton())); } void unregister_core_types() { @@ -281,6 +285,7 @@ void unregister_core_types() { memdelete(_classdb); memdelete(_marshalls); memdelete(_json); + memdelete(_engine_debugger); memdelete(_geometry_2d); memdelete(_geometry_3d); diff --git a/core/rid_owner.h b/core/rid_owner.h index 2489475c68..30f1e41733 100644 --- a/core/rid_owner.h +++ b/core/rid_owner.h @@ -236,7 +236,7 @@ public: } _FORCE_INLINE_ T *get_ptr_by_index(uint32_t p_index) { - ERR_FAIL_INDEX_V(p_index, alloc_count, nullptr); + ERR_FAIL_UNSIGNED_INDEX_V(p_index, alloc_count, nullptr); if (THREAD_SAFE) { spin_lock.lock(); } diff --git a/core/script_language.cpp b/core/script_language.cpp index 360995ea15..b63aeb952c 100644 --- a/core/script_language.cpp +++ b/core/script_language.cpp @@ -33,8 +33,6 @@ #include "core/core_string_names.h" #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" -#include "core/io/resource_loader.h" -#include "core/os/file_access.h" #include "core/project_settings.h" #include <stdint.h> @@ -164,7 +162,7 @@ void ScriptServer::init_languages() { for (int i = 0; i < script_classes.size(); i++) { Dictionary c = script_classes[i]; - if (!c.has("class") || !c.has("language") || !c.has("path") || !FileAccess::exists(ResourceLoader::path_remap(c["path"])) || !c.has("base")) { + if (!c.has("class") || !c.has("language") || !c.has("path") || !c.has("base")) { continue; } add_global_class(c["class"], c["base"], c["language"], c["path"]); diff --git a/core/translation.cpp b/core/translation.cpp index 4f835bd7b4..8c8ca06740 100644 --- a/core/translation.cpp +++ b/core/translation.cpp @@ -794,17 +794,12 @@ static const char *locale_renames[][2] = { /////////////////////////////////////////////// -Vector<String> Translation::_get_messages() const { - Vector<String> msgs; - msgs.resize(translation_map.size() * 2); - int idx = 0; +Dictionary Translation::_get_messages() const { + Dictionary d; for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) { - msgs.set(idx + 0, E->key()); - msgs.set(idx + 1, E->get()); - idx += 2; + d[E->key()] = E->value(); } - - return msgs; + return d; } Vector<String> Translation::_get_message_list() const { @@ -819,14 +814,11 @@ Vector<String> Translation::_get_message_list() const { return msgs; } -void Translation::_set_messages(const Vector<String> &p_messages) { - int msg_count = p_messages.size(); - ERR_FAIL_COND(msg_count % 2); - - const String *r = p_messages.ptr(); - - for (int i = 0; i < msg_count; i += 2) { - add_message(r[i + 0], r[i + 1]); +void Translation::_set_messages(const Dictionary &p_messages) { + List<Variant> keys; + p_messages.get_key_list(&keys); + for (auto E = keys.front(); E; E = E->next()) { + translation_map[E->get()] = p_messages[E->get()]; } } @@ -848,11 +840,21 @@ void Translation::set_locale(const String &p_locale) { } } -void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text) { +void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { translation_map[p_src_text] = p_xlated_text; } -StringName Translation::get_message(const StringName &p_src_text) const { +void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) { + WARN_PRINT("Translation class doesn't handle plural messages. Calling add_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class"); + ERR_FAIL_COND_MSG(p_plural_xlated_texts.empty(), "Parameter vector p_plural_xlated_texts passed in is empty."); + translation_map[p_src_text] = p_plural_xlated_texts[0]; +} + +StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const { + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context. Using context in get_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class"); + } + const Map<StringName, StringName>::Element *E = translation_map.find(p_src_text); if (!E) { return StringName(); @@ -861,7 +863,16 @@ StringName Translation::get_message(const StringName &p_src_text) const { return E->get(); } -void Translation::erase_message(const StringName &p_src_text) { +StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { + WARN_PRINT("Translation class doesn't handle plural messages. Calling get_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class"); + return get_message(p_src_text); +} + +void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) { + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context. Using context in erase_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class"); + } + translation_map.erase(p_src_text); } @@ -878,15 +889,17 @@ int Translation::get_message_count() const { void Translation::_bind_methods() { ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale); ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale); - ClassDB::bind_method(D_METHOD("add_message", "src_message", "xlated_message"), &Translation::add_message); - ClassDB::bind_method(D_METHOD("get_message", "src_message"), &Translation::get_message); - ClassDB::bind_method(D_METHOD("erase_message", "src_message"), &Translation::erase_message); + ClassDB::bind_method(D_METHOD("add_message", "src_message", "xlated_message", "context"), &Translation::add_message, DEFVAL("")); + ClassDB::bind_method(D_METHOD("add_plural_message", "src_message", "xlated_messages", "context"), &Translation::add_plural_message, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_message", "src_message", "context"), &Translation::get_message, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_plural_message", "src_message", "src_plural_message", "n", "context"), &Translation::get_plural_message, DEFVAL("")); + ClassDB::bind_method(D_METHOD("erase_message", "src_message", "context"), &Translation::erase_message, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_message_list"), &Translation::_get_message_list); ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count); ClassDB::bind_method(D_METHOD("_set_messages"), &Translation::_set_messages); ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages); - ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale"); } @@ -1020,11 +1033,35 @@ void TranslationServer::remove_translation(const Ref<Translation> &p_translation translations.erase(p_translation); } +Ref<Translation> TranslationServer::get_translation_object(const String &p_locale) { + Ref<Translation> res; + String lang = get_language_code(p_locale); + bool near_match_found = false; + + for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) { + const Ref<Translation> &t = E->get(); + ERR_FAIL_COND_V(t.is_null(), nullptr); + String l = t->get_locale(); + + // Exact match. + if (l == p_locale) { + return t; + } + + // If near match found, keep that match, but keep looking to try to look for perfect match. + if (get_language_code(l) == lang && !near_match_found) { + res = t; + near_match_found = true; + } + } + return res; +} + void TranslationServer::clear() { translations.clear(); } -StringName TranslationServer::translate(const StringName &p_message) const { +StringName TranslationServer::translate(const StringName &p_message, const StringName &p_context) const { // Match given message against the translation catalog for the project locale. if (!enabled) { @@ -1033,6 +1070,46 @@ StringName TranslationServer::translate(const StringName &p_message) const { ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid."); + StringName res = _get_message_from_translations(p_message, p_context, locale, false); + + if (!res && fallback.length() >= 2) { + res = _get_message_from_translations(p_message, p_context, fallback, false); + } + + if (!res) { + return p_message; + } + + return res; +} + +StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { + if (!enabled) { + if (p_n == 1) { + return p_message; + } + return p_message_plural; + } + + ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid."); + + StringName res = _get_message_from_translations(p_message, p_context, locale, true, p_message_plural, p_n); + + if (!res && fallback.length() >= 2) { + res = _get_message_from_translations(p_message, p_context, fallback, true, p_message_plural, p_n); + } + + if (!res) { + if (p_n == 1) { + return p_message; + } + return p_message_plural; + } + + return res; +} + +StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural, int p_n) const { // Locale can be of the form 'll_CC', i.e. language code and regional code, // e.g. 'en_US', 'en_GB', etc. It might also be simply 'll', e.g. 'en'. // To find the relevant translation, we look for those with locale starting @@ -1044,7 +1121,7 @@ StringName TranslationServer::translate(const StringName &p_message) const { // logic, so be sure to propagate changes there when changing things here. StringName res; - String lang = get_language_code(locale); + String lang = get_language_code(p_locale); bool near_match = false; for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) { @@ -1052,7 +1129,7 @@ StringName TranslationServer::translate(const StringName &p_message) const { ERR_FAIL_COND_V(t.is_null(), p_message); String l = t->get_locale(); - bool exact_match = (l == locale); + bool exact_match = (l == p_locale); if (!exact_match) { if (near_match) { continue; // Only near-match once, but keep looking for exact matches. @@ -1062,7 +1139,13 @@ StringName TranslationServer::translate(const StringName &p_message) const { } } - StringName r = t->get_message(p_message); + StringName r; + if (!plural) { + r = t->get_message(p_message, p_context); + } else { + r = t->get_plural_message(p_message, p_message_plural, p_n, p_context); + } + if (!r) { continue; } @@ -1075,44 +1158,6 @@ StringName TranslationServer::translate(const StringName &p_message) const { } } - if (!res && fallback.length() >= 2) { - // Try again with the fallback locale. - String fallback_lang = get_language_code(fallback); - near_match = false; - - for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) { - const Ref<Translation> &t = E->get(); - ERR_FAIL_COND_V(t.is_null(), p_message); - String l = t->get_locale(); - - bool exact_match = (l == fallback); - if (!exact_match) { - if (near_match) { - continue; // Only near-match once, but keep looking for exact matches. - } - if (get_language_code(l) != fallback_lang) { - continue; // Language code does not match. - } - } - - StringName r = t->get_message(p_message); - if (!r) { - continue; - } - res = r; - - if (exact_match) { - break; - } else { - near_match = true; - } - } - } - - if (!res) { - return p_message; - } - return res; } @@ -1169,9 +1214,9 @@ void TranslationServer::set_tool_translation(const Ref<Translation> &p_translati tool_translation = p_translation; } -StringName TranslationServer::tool_translate(const StringName &p_message) const { +StringName TranslationServer::tool_translate(const StringName &p_message, const StringName &p_context) const { if (tool_translation.is_valid()) { - StringName r = tool_translation->get_message(p_message); + StringName r = tool_translation->get_message(p_message, p_context); if (r) { return r; } @@ -1179,13 +1224,27 @@ StringName TranslationServer::tool_translate(const StringName &p_message) const return p_message; } +StringName TranslationServer::tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { + if (tool_translation.is_valid()) { + StringName r = tool_translation->get_plural_message(p_message, p_message_plural, p_n, p_context); + if (r) { + return r; + } + } + + if (p_n == 1) { + return p_message; + } + return p_message_plural; +} + void TranslationServer::set_doc_translation(const Ref<Translation> &p_translation) { doc_translation = p_translation; } -StringName TranslationServer::doc_translate(const StringName &p_message) const { +StringName TranslationServer::doc_translate(const StringName &p_message, const StringName &p_context) const { if (doc_translation.is_valid()) { - StringName r = doc_translation->get_message(p_message); + StringName r = doc_translation->get_message(p_message, p_context); if (r) { return r; } @@ -1193,16 +1252,32 @@ StringName TranslationServer::doc_translate(const StringName &p_message) const { return p_message; } +StringName TranslationServer::doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { + if (doc_translation.is_valid()) { + StringName r = doc_translation->get_plural_message(p_message, p_message_plural, p_n, p_context); + if (r) { + return r; + } + } + + if (p_n == 1) { + return p_message; + } + return p_message_plural; +} + void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_locale", "locale"), &TranslationServer::set_locale); ClassDB::bind_method(D_METHOD("get_locale"), &TranslationServer::get_locale); ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name); - ClassDB::bind_method(D_METHOD("translate", "message"), &TranslationServer::translate); + ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationServer::translate, DEFVAL("")); + ClassDB::bind_method(D_METHOD("translate_plural", "message", "plural_message", "n", "context"), &TranslationServer::translate_plural, DEFVAL("")); ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationServer::add_translation); ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation); + ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationServer::get_translation_object); ClassDB::bind_method(D_METHOD("clear"), &TranslationServer::clear); diff --git a/core/translation.h b/core/translation.h index 4f50a1a4bc..cba25a434f 100644 --- a/core/translation.h +++ b/core/translation.h @@ -41,10 +41,9 @@ class Translation : public Resource { String locale = "en"; Map<StringName, StringName> translation_map; - Vector<String> _get_message_list() const; - - Vector<String> _get_messages() const; - void _set_messages(const Vector<String> &p_messages); + virtual Vector<String> _get_message_list() const; + virtual Dictionary _get_messages() const; + virtual void _set_messages(const Dictionary &p_messages); protected: static void _bind_methods(); @@ -53,12 +52,13 @@ public: void set_locale(const String &p_locale); _FORCE_INLINE_ String get_locale() const { return locale; } - void add_message(const StringName &p_src_text, const StringName &p_xlated_text); - virtual StringName get_message(const StringName &p_src_text) const; //overridable for other implementations - void erase_message(const StringName &p_src_text); - - void get_message_list(List<StringName> *r_messages) const; - int get_message_count() const; + virtual void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = ""); + virtual void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context = ""); + virtual StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const; //overridable for other implementations + virtual StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const; + virtual void erase_message(const StringName &p_src_text, const StringName &p_context = ""); + virtual void get_message_list(List<StringName> *r_messages) const; + virtual int get_message_count() const; Translation() {} }; @@ -80,6 +80,8 @@ class TranslationServer : public Object { static TranslationServer *singleton; bool _load_translations(const String &p_from); + StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural = "", int p_n = 0) const; + static void _bind_methods(); public: @@ -90,6 +92,7 @@ public: void set_locale(const String &p_locale); String get_locale() const; + Ref<Translation> get_translation_object(const String &p_locale); String get_locale_name(const String &p_locale) const; @@ -98,7 +101,8 @@ public: void add_translation(const Ref<Translation> &p_translation); void remove_translation(const Ref<Translation> &p_translation); - StringName translate(const StringName &p_message) const; + StringName translate(const StringName &p_message, const StringName &p_context = "") const; + StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; static Vector<String> get_all_locales(); static Vector<String> get_all_locale_names(); @@ -107,9 +111,11 @@ public: static String get_language_code(const String &p_locale); void set_tool_translation(const Ref<Translation> &p_translation); - StringName tool_translate(const StringName &p_message) const; + StringName tool_translate(const StringName &p_message, const StringName &p_context = "") const; + StringName tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; void set_doc_translation(const Ref<Translation> &p_translation); - StringName doc_translate(const StringName &p_message) const; + StringName doc_translate(const StringName &p_message, const StringName &p_context = "") const; + StringName doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; void setup(); diff --git a/core/translation_po.cpp b/core/translation_po.cpp new file mode 100644 index 0000000000..203f29026b --- /dev/null +++ b/core/translation_po.cpp @@ -0,0 +1,312 @@ +/*************************************************************************/ +/* translation_po.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "translation_po.h" + +#include "core/os/file_access.h" + +#ifdef DEBUG_TRANSLATION_PO +void TranslationPO::print_translation_map() { + Error err; + FileAccess *file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err); + if (err != OK) { + ERR_PRINT("Failed to open translation_map_print_test.txt"); + return; + } + + file->store_line("NPlural : " + String::num_int64(this->get_plural_forms())); + file->store_line("Plural rule : " + this->get_plural_rule()); + file->store_line(""); + + List<StringName> context_l; + translation_map.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== "); + const HashMap<StringName, Vector<StringName>> &inner_map = translation_map[ctx]; + + List<StringName> id_l; + inner_map.get_key_list(&id_l); + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + file->store_line("msgid: " + String::utf8(String(id).utf8())); + for (int i = 0; i < inner_map[id].size(); i++) { + file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8())); + } + file->store_line(""); + } + } + file->close(); +} +#endif + +Dictionary TranslationPO::_get_messages() const { + // Return translation_map as a Dictionary. + + Dictionary d; + + List<StringName> context_l; + translation_map.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + const HashMap<StringName, Vector<StringName>> &id_str_map = translation_map[ctx]; + + Dictionary d2; + List<StringName> id_l; + id_str_map.get_key_list(&id_l); + // Save list of id and strs associated with a context in a temporary dictionary. + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + d2[id] = id_str_map[id]; + } + + d[ctx] = d2; + } + + return d; +} + +void TranslationPO::_set_messages(const Dictionary &p_messages) { + // Construct translation_map from a Dictionary. + + List<Variant> context_l; + p_messages.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + const Dictionary &id_str_map = p_messages[ctx]; + + HashMap<StringName, Vector<StringName>> temp_map; + List<Variant> id_l; + id_str_map.get_key_list(&id_l); + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + temp_map[id] = id_str_map[id]; + } + + translation_map[ctx] = temp_map; + } +} + +Vector<String> TranslationPO::_get_message_list() const { + // Return all keys in translation_map. + + List<StringName> msgs; + get_message_list(&msgs); + + Vector<String> v; + for (auto E = msgs.front(); E; E = E->next()) { + v.push_back(E->get()); + } + + return v; +} + +int TranslationPO::_get_plural_index(int p_n) const { + // Get a number between [0;number of plural forms). + + input_val.clear(); + input_val.push_back(p_n); + + Variant result; + for (int i = 0; i < equi_tests.size(); i++) { + Error err = expr->parse(equi_tests[i], input_name); + ERR_FAIL_COND_V_MSG(err != OK, 0, "Cannot parse expression. Error: " + expr->get_error_text()); + + result = expr->execute(input_val); + ERR_FAIL_COND_V_MSG(expr->has_execute_failed(), 0, "Cannot evaluate expression."); + + // Last expression. Variant result will either map to a bool or an integer, in both cases returning it will give the correct plural index. + if (i + 1 == equi_tests.size()) { + return result; + } + + if (bool(result)) { + return i; + } + } + + ERR_FAIL_V_MSG(0, "Unexpected. Function should have returned. Please report this bug."); +} + +void TranslationPO::_cache_plural_tests(const String &p_plural_rule) { + // Some examples of p_plural_rule passed in can have the form: + // "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic) + // "n >= 2" (French) // When evaluating the last, esp careful with this one. + // "n != 1" (English) + int first_ques_mark = p_plural_rule.find("?"); + if (first_ques_mark == -1) { + equi_tests.push_back(p_plural_rule.strip_edges()); + return; + } + + String equi_test = p_plural_rule.substr(0, first_ques_mark).strip_edges(); + equi_tests.push_back(equi_test); + + String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length()); + _cache_plural_tests(after_colon); +} + +void TranslationPO::set_plural_rule(const String &p_plural_rule) { + // Set plural_forms and plural_rule. + // p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);". + + int first_semi_col = p_plural_rule.find(";"); + plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int(); + + int expression_start = p_plural_rule.find("=", first_semi_col) + 1; + int second_semi_col = p_plural_rule.rfind(";"); + plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start); + + // Setup the cache to make evaluating plural rule faster later on. + plural_rule = plural_rule.replacen("(", ""); + plural_rule = plural_rule.replacen(")", ""); + _cache_plural_tests(plural_rule); + expr.instance(); + input_name.push_back("n"); +} + +void TranslationPO::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { + HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context]; + + if (map_id_str.has(p_src_text)) { + WARN_PRINT("Double translations for \"" + String(p_src_text) + "\" under the same context \"" + String(p_context) + "\" for locale \"" + get_locale() + "\".\nThere should only be one unique translation for a given string under the same context."); + map_id_str[p_src_text].set(0, p_xlated_text); + } else { + map_id_str[p_src_text].push_back(p_xlated_text); + } +} + +void TranslationPO::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) { + ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != plural_forms, "Trying to add plural texts that don't match the required number of plural forms for locale \"" + get_locale() + "\""); + + HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context]; + + if (map_id_str.has(p_src_text)) { + WARN_PRINT("Double translations for \"" + p_src_text + "\" under the same context \"" + p_context + "\" for locale " + get_locale() + ".\nThere should only be one unique translation for a given string under the same context."); + map_id_str[p_src_text].clear(); + } + + for (int i = 0; i < p_plural_xlated_texts.size(); i++) { + map_id_str[p_src_text].push_back(p_plural_xlated_texts[i]); + } +} + +int TranslationPO::get_plural_forms() const { + return plural_forms; +} + +String TranslationPO::get_plural_rule() const { + return plural_rule; +} + +StringName TranslationPO::get_message(const StringName &p_src_text, const StringName &p_context) const { + if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { + return StringName(); + } + ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug."); + + return translation_map[p_context][p_src_text][0]; +} + +StringName TranslationPO::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { + ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers."); + + // If the query is the same as last time, return the cached result. + if (p_n == last_plural_n && p_context == last_plural_context && p_src_text == last_plural_key) { + return translation_map[p_context][p_src_text][last_plural_mapped_index]; + } + + if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { + return StringName(); + } + ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug."); + + if (translation_map[p_context][p_src_text].size() == 1) { + WARN_PRINT("Source string \"" + String(p_src_text) + "\" doesn't have plural translations. Use singular translation API for such as tr(), TTR() to translate \"" + String(p_src_text) + "\""); + return translation_map[p_context][p_src_text][0]; + } + + int plural_index = _get_plural_index(p_n); + ERR_FAIL_COND_V_MSG(plural_index < 0 || translation_map[p_context][p_src_text].size() < plural_index + 1, StringName(), "Plural index returned or number of plural translations is not valid. Please report this bug."); + + // Cache result so that if the next entry is the same, we can return directly. + // _get_plural_index(p_n) can get very costly, especially when evaluating long plural-rule (Arabic) + last_plural_key = p_src_text; + last_plural_context = p_context; + last_plural_n = p_n; + last_plural_mapped_index = plural_index; + + return translation_map[p_context][p_src_text][plural_index]; +} + +void TranslationPO::erase_message(const StringName &p_src_text, const StringName &p_context) { + if (!translation_map.has(p_context)) { + return; + } + + translation_map[p_context].erase(p_src_text); +} + +void TranslationPO::get_message_list(List<StringName> *r_messages) const { + // PHashTranslation uses this function to get the list of msgid. + // Return all the keys of translation_map under "" context. + + List<StringName> context_l; + translation_map.get_key_list(&context_l); + + for (auto E = context_l.front(); E; E = E->next()) { + if (String(E->get()) != "") { + continue; + } + + List<StringName> msgid_l; + translation_map[E->get()].get_key_list(&msgid_l); + + for (auto E2 = msgid_l.front(); E2; E2 = E2->next()) { + r_messages->push_back(E2->get()); + } + } +} + +int TranslationPO::get_message_count() const { + List<StringName> context_l; + translation_map.get_key_list(&context_l); + + int count = 0; + for (auto E = context_l.front(); E; E = E->next()) { + count += translation_map[E->get()].size(); + } + return count; +} + +void TranslationPO::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_plural_forms"), &TranslationPO::get_plural_forms); + ClassDB::bind_method(D_METHOD("get_plural_rule"), &TranslationPO::get_plural_rule); +} diff --git a/core/translation_po.h b/core/translation_po.h new file mode 100644 index 0000000000..730635f63d --- /dev/null +++ b/core/translation_po.h @@ -0,0 +1,92 @@ +/*************************************************************************/ +/* translation_po.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 TRANSLATION_PO_H +#define TRANSLATION_PO_H + +//#define DEBUG_TRANSLATION_PO + +#include "core/math/expression.h" +#include "core/translation.h" + +class TranslationPO : public Translation { + GDCLASS(TranslationPO, Translation); + + // TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr. + // The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string. + // The value Vector<StringName> in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals. + // Otherwise index 0 mathes to msgstr in a singular translation. + // Strings without context have "" as first key. + HashMap<StringName, HashMap<StringName, Vector<StringName>>> translation_map; + + int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1. + String plural_rule; + + // Cache temporary variables related to _get_plural_index() to make it faster + Vector<String> equi_tests; + Vector<String> input_name; + mutable Ref<Expression> expr; + mutable Array input_val; + mutable StringName last_plural_key; + mutable StringName last_plural_context; + mutable int last_plural_n = -1; // Set it to an impossible value at the beginning. + mutable int last_plural_mapped_index = 0; + + void _cache_plural_tests(const String &p_plural_rule); + int _get_plural_index(int p_n) const; + + Vector<String> _get_message_list() const override; + Dictionary _get_messages() const override; + void _set_messages(const Dictionary &p_messages) override; + +protected: + static void _bind_methods(); + +public: + void get_message_list(List<StringName> *r_messages) const override; + int get_message_count() const override; + void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "") override; + void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context = "") override; + StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override; + StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override; + void erase_message(const StringName &p_src_text, const StringName &p_context = "") override; + + void set_plural_rule(const String &p_plural_rule); + int get_plural_forms() const; + String get_plural_rule() const; + +#ifdef DEBUG_TRANSLATION_PO + void print_translation_map(); +#endif + + TranslationPO() {} +}; + +#endif // TRANSLATION_PO_H diff --git a/core/ustring.cpp b/core/ustring.cpp index 957caf1015..9d2d938eaf 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -4269,31 +4269,58 @@ String String::unquote() const { } #ifdef TOOLS_ENABLED -String TTR(const String &p_text) { +String TTR(const String &p_text, const String &p_context) { if (TranslationServer::get_singleton()) { - return TranslationServer::get_singleton()->tool_translate(p_text); + return TranslationServer::get_singleton()->tool_translate(p_text, p_context); } return p_text; } -String DTR(const String &p_text) { +String TTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) { + if (TranslationServer::get_singleton()) { + return TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context); + } + + // Return message based on English plural rule if translation is not possible. + if (p_n == 1) { + return p_text; + } + return p_text_plural; +} + +String DTR(const String &p_text, const String &p_context) { // Comes straight from the XML, so remove indentation and any trailing whitespace. const String text = p_text.dedent().strip_edges(); if (TranslationServer::get_singleton()) { - return TranslationServer::get_singleton()->doc_translate(text); + return TranslationServer::get_singleton()->doc_translate(text, p_context); } return text; } + +String DTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) { + const String text = p_text.dedent().strip_edges(); + const String text_plural = p_text_plural.dedent().strip_edges(); + + if (TranslationServer::get_singleton()) { + return TranslationServer::get_singleton()->doc_translate_plural(text, text_plural, p_n, p_context); + } + + // Return message based on English plural rule if translation is not possible. + if (p_n == 1) { + return text; + } + return text_plural; +} #endif -String RTR(const String &p_text) { +String RTR(const String &p_text, const String &p_context) { if (TranslationServer::get_singleton()) { - String rtr = TranslationServer::get_singleton()->tool_translate(p_text); + String rtr = TranslationServer::get_singleton()->tool_translate(p_text, p_context); if (rtr == String() || rtr == p_text) { - return TranslationServer::get_singleton()->translate(p_text); + return TranslationServer::get_singleton()->translate(p_text, p_context); } else { return rtr; } @@ -4301,3 +4328,20 @@ String RTR(const String &p_text) { return p_text; } + +String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) { + if (TranslationServer::get_singleton()) { + String rtr = TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context); + if (rtr == String() || rtr == p_text || rtr == p_text_plural) { + return TranslationServer::get_singleton()->translate_plural(p_text, p_text_plural, p_n, p_context); + } else { + return rtr; + } + } + + // Return message based on English plural rule if translation is not possible. + if (p_n == 1) { + return p_text; + } + return p_text_plural; +} diff --git a/core/ustring.h b/core/ustring.h index d37346fbd6..7a1c1a5232 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -410,8 +410,10 @@ _FORCE_INLINE_ bool is_str_less(const L *l_ptr, const R *r_ptr) { // and doc translate for the class reference (DTR). #ifdef TOOLS_ENABLED // Gets parsed. -String TTR(const String &); -String DTR(const String &); +String TTR(const String &p_text, const String &p_context = ""); +String TTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = ""); +String DTR(const String &p_text, const String &p_context = ""); +String DTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = ""); // Use for C strings. #define TTRC(m_value) (m_value) // Use to avoid parsing (for use later with C strings). @@ -419,13 +421,16 @@ String DTR(const String &); #else #define TTR(m_value) (String()) +#define TTRN(m_value) (String()) #define DTR(m_value) (String()) +#define DTRN(m_value) (String()) #define TTRC(m_value) (m_value) #define TTRGET(m_value) (m_value) #endif // Runtime translate for the public node API. -String RTR(const String &); +String RTR(const String &p_text, const String &p_context = ""); +String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = ""); bool is_symbol(CharType c); bool select_word(const String &p_s, int p_col, int &r_beg, int &r_end); diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 7f7df33471..2b1770f12b 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -27,6 +27,9 @@ <member name="Engine" type="Engine" setter="" getter=""> The [Engine] singleton. </member> + <member name="EngineDebugger" type="EngineDebugger" setter="" getter=""> + The [EngineDebugger] singleton. + </member> <member name="Geometry2D" type="Geometry2D" setter="" getter=""> The [Geometry2D] singleton. </member> @@ -353,16 +356,16 @@ Right Direction key. </constant> <constant name="KEY_BACK" value="16777280" enum="KeyList"> - Back key. + Media back key. Not to be confused with the Back button on an Android device. </constant> <constant name="KEY_FORWARD" value="16777281" enum="KeyList"> - Forward key. + Media forward key. </constant> <constant name="KEY_STOP" value="16777282" enum="KeyList"> - Stop key. + Media stop key. </constant> <constant name="KEY_REFRESH" value="16777283" enum="KeyList"> - Refresh key. + Media refresh key. </constant> <constant name="KEY_VOLUMEDOWN" value="16777284" enum="KeyList"> Volume down key. diff --git a/doc/classes/AnimatedSprite2D.xml b/doc/classes/AnimatedSprite2D.xml index 8d0534ccd2..7a992d0c03 100644 --- a/doc/classes/AnimatedSprite2D.xml +++ b/doc/classes/AnimatedSprite2D.xml @@ -7,6 +7,7 @@ Animations are created using a [SpriteFrames] resource, which can be configured in the editor via the SpriteFrames panel. </description> <tutorials> + <link title="2D Sprite animation">https://docs.godotengine.org/en/latest/tutorials/2d/2d_sprite_animation.html</link> </tutorials> <methods> <method name="is_playing" qualifiers="const"> diff --git a/doc/classes/AnimatedSprite3D.xml b/doc/classes/AnimatedSprite3D.xml index ad9706a52a..e1fb78e5b5 100644 --- a/doc/classes/AnimatedSprite3D.xml +++ b/doc/classes/AnimatedSprite3D.xml @@ -7,6 +7,7 @@ Animations are created using a [SpriteFrames] resource, which can be configured in the editor via the SpriteFrames panel. </description> <tutorials> + <link title="2D Sprite animation (also applies to 3D)">https://docs.godotengine.org/en/latest/tutorials/2d/2d_sprite_animation.html</link> </tutorials> <methods> <method name="is_playing" qualifiers="const"> diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 1420b1bf64..6267a5a0a9 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -10,6 +10,7 @@ </description> <tutorials> <link>https://docs.godotengine.org/en/latest/getting_started/step_by_step/animations.html</link> + <link title="2D Sprite animation">https://docs.godotengine.org/en/latest/tutorials/2d/2d_sprite_animation.html</link> <link>https://docs.godotengine.org/en/latest/tutorials/animation/index.html</link> </tutorials> <methods> diff --git a/doc/classes/AudioStreamPlayer3D.xml b/doc/classes/AudioStreamPlayer3D.xml index bd90e3bd1a..98100370b6 100644 --- a/doc/classes/AudioStreamPlayer3D.xml +++ b/doc/classes/AudioStreamPlayer3D.xml @@ -5,6 +5,7 @@ </brief_description> <description> Plays a sound effect with directed sound effects, dampens with distance if needed, generates effect of hearable position in space. + By default, audio is heard from the camera position. This can be changed by adding a [Listener3D] node to the scene and enabling it by calling [method Listener3D.make_current] on it. </description> <tutorials> <link>https://docs.godotengine.org/en/latest/tutorials/audio/audio_streams.html</link> diff --git a/doc/classes/CollisionShape2D.xml b/doc/classes/CollisionShape2D.xml index e32ce9c9f9..d2676e55d0 100644 --- a/doc/classes/CollisionShape2D.xml +++ b/doc/classes/CollisionShape2D.xml @@ -13,7 +13,7 @@ </methods> <members> <member name="disabled" type="bool" setter="set_disabled" getter="is_disabled" default="false"> - A disabled collision shape has no effect in the world. + A disabled collision shape has no effect in the world. This property should be changed with [method Object.set_deferred]. </member> <member name="one_way_collision" type="bool" setter="set_one_way_collision" getter="is_one_way_collision_enabled" default="false"> Sets whether this collision shape should only detect collision on one side (top or bottom). diff --git a/doc/classes/EditorDebuggerPlugin.xml b/doc/classes/EditorDebuggerPlugin.xml new file mode 100644 index 0000000000..b97933e582 --- /dev/null +++ b/doc/classes/EditorDebuggerPlugin.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorDebuggerPlugin" inherits="Control" version="4.0"> + <brief_description> + A base class to implement debugger plugins. + </brief_description> + <description> + All debugger plugin scripts must extend [EditorDebuggerPlugin]. It provides functions related to editor side of debugger. + You don't need to instantiate this class. That is handled by the debugger itself. [Control] nodes can be added as child nodes to provide a GUI front-end for the plugin. + Do not queue_free/reparent it's instance otherwise the instance becomes unusable. + </description> + <tutorials> + </tutorials> + <methods> + <method name="has_capture"> + <return type="bool"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Returns [code]true[/code] if a message capture with given name is present otherwise [code]false[/code]. + </description> + </method> + <method name="is_breaked"> + <return type="bool"> + </return> + <description> + Returns [code]true[/code] if the game is in break state otherwise [code]false[/code]. + </description> + </method> + <method name="is_debuggable"> + <return type="bool"> + </return> + <description> + Returns [code]true[/code] if the game can be debugged otherwise [code]false[/code]. + </description> + </method> + <method name="is_session_active"> + <return type="bool"> + </return> + <description> + Returns [code]true[/code] if there is an instance of the game running with the attached debugger otherwise [code]false[/code]. + </description> + </method> + <method name="register_message_capture"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <argument index="1" name="callable" type="Callable"> + </argument> + <description> + Registers a message capture with given [code]name[/code]. If [code]name[/code] is "my_message" then messages starting with "my_message:" will be called with the given callable. + Callable must accept a message string and a data array as argument. If the message and data are valid then callable must return [code]true[/code] otherwise [code]false[/code]. + </description> + </method> + <method name="send_message"> + <return type="void"> + </return> + <argument index="0" name="message" type="String"> + </argument> + <argument index="1" name="data" type="Array"> + </argument> + <description> + Sends a message with given [code]message[/code] and [code]data[/code] array. + </description> + </method> + <method name="unregister_message_capture"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Unregisters the message capture with given name. + </description> + </method> + </methods> + <signals> + <signal name="breaked"> + <argument index="0" name="can_debug" type="bool"> + </argument> + <description> + Emitted when the game enters a break state. + </description> + </signal> + <signal name="continued"> + <description> + Emitted when the game exists a break state. + </description> + </signal> + <signal name="started"> + <description> + Emitted when the debugging starts. + </description> + </signal> + <signal name="stopped"> + <description> + Emitted when the debugging stops. + </description> + </signal> + </signals> + <constants> + </constants> +</class> diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 8cfd3b63d6..6bcaabc39e 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -76,6 +76,18 @@ <argument index="0" name="path" type="String"> </argument> <description> + Adds a static library (*.a) or dynamic library (*.dylib, *.framework) to Linking Phase in iOS's Xcode project. + </description> + </method> + <method name="add_ios_embedded_framework"> + <return type="void"> + </return> + <argument index="0" name="path" type="String"> + </argument> + <description> + Adds a dynamic library (*.dylib, *.framework) to Linking Phase in iOS's Xcode project and embeds it into resulting binary. + [b]Note:[/b] For static libraries (*.a) works in same way as [code]add_ios_framework[/code]. + This method should not be used for System libraries as they are already present on the device. </description> </method> <method name="add_ios_linker_flags"> diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 99fe9b4bb5..36076d8909 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -76,6 +76,15 @@ During run-time, this will be a simple object with a script so this function does not need to be called then. </description> </method> + <method name="add_debugger_plugin"> + <return type="void"> + </return> + <argument index="0" name="script" type="Script"> + </argument> + <description> + Adds a [Script] as debugger plugin to the Debugger. The script must extend [EditorDebuggerPlugin]. + </description> + </method> <method name="add_export_plugin"> <return type="void"> </return> @@ -424,6 +433,15 @@ Removes a custom type added by [method add_custom_type]. </description> </method> + <method name="remove_debugger_plugin"> + <return type="void"> + </return> + <argument index="0" name="script" type="Script"> + </argument> + <description> + Removes the debugger plugin with given script fromm the Debugger. + </description> + </method> <method name="remove_export_plugin"> <return type="void"> </return> diff --git a/doc/classes/EditorTranslationParserPlugin.xml b/doc/classes/EditorTranslationParserPlugin.xml index d40fc558de..f5204e7bab 100644 --- a/doc/classes/EditorTranslationParserPlugin.xml +++ b/doc/classes/EditorTranslationParserPlugin.xml @@ -5,30 +5,41 @@ </brief_description> <description> Plugins are registered via [method EditorPlugin.add_translation_parser_plugin] method. To define the parsing and string extraction logic, override the [method parse_file] method in script. + Add the extracted strings to argument [code]msgids[/code] or [code]msgids_context_plural[/code] if context or plural is used. + When adding to [code]msgids_context_plural[/code], you must add the data using the format [code]["A", "B", "C"][/code], where [code]A[/code] represents the extracted string, [code]B[/code] represents the context, and [code]C[/code] represents the plural version of the extracted string. If you want to add only context but not plural, put [code]""[/code] for the plural slot. The idea is the same if you only want to add plural but not context. See the code below for concrete examples. The extracted strings will be written into a POT file selected by user under "POT Generation" in "Localization" tab in "Project Settings" menu. - Below shows an example of a custom parser that extracts strings in a CSV file to write into a POT. + Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT. [codeblock] tool extends EditorTranslationParserPlugin - func parse_file(path, extracted_strings): + func parse_file(path, msgids, msgids_context_plural): var file = File.new() file.open(path, File.READ) var text = file.get_as_text() var split_strs = text.split(",", false, 0) for s in split_strs: - extracted_strings.append(s) + msgids.append(s) #print("Extracted string: " + s) func get_recognized_extensions(): return ["csv"] [/codeblock] + To add a translatable string associated with context or plural, add it to [code]msgids_context_plural[/code]: + [codeblock] + # This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals". + msgids_context_plural.append(["Test 1", "context", "test 1 plurals"]) + # This will add a message with msgid "A test without context" and msgid_plural "plurals". + msgids_context_plural.append(["A test without context", "", "plurals"]) + # This will add a message with msgid "Only with context" and msgctxt "a friendly context". + msgids_context_plural.append(["Only with context", "a friendly context", ""]) + [/codeblock] [b]Note:[/b] If you override parsing logic for standard script types (GDScript, C#, etc.), it would be better to load the [code]path[/code] argument using [method ResourceLoader.load]. This is because built-in scripts are loaded as [Resource] type, not [File] type. For example: [codeblock] - func parse_file(path, extracted_strings): + func parse_file(path, msgids, msgids_context_plural): var res = ResourceLoader.load(path, "Script") var text = res.get_source_code() # Parsing logic. @@ -53,7 +64,9 @@ </return> <argument index="0" name="path" type="String"> </argument> - <argument index="1" name="extracted_strings" type="Array"> + <argument index="1" name="msgids" type="Array"> + </argument> + <argument index="2" name="msgids_context_plural" type="Array"> </argument> <description> Override this method to define a custom parsing logic to extract the translatable strings. diff --git a/doc/classes/EngineDebugger.xml b/doc/classes/EngineDebugger.xml new file mode 100644 index 0000000000..7db36b89d0 --- /dev/null +++ b/doc/classes/EngineDebugger.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EngineDebugger" inherits="Object" version="4.0"> + <brief_description> + Exposes the internal debugger. + </brief_description> + <description> + [EngineDebugger] handles the communication between the editor and the running game. It is active in the running game. Messages can be sent/received through it. It also manages the profilers. + </description> + <tutorials> + </tutorials> + <methods> + <method name="has_capture"> + <return type="bool"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Returns [code]true[/code] if a capture with the given name is present otherwise [code]false[/code]. + </description> + </method> + <method name="has_profiler"> + <return type="bool"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Returns [code]true[/code] if a profiler with the given name is present otherwise [code]false[/code]. + </description> + </method> + <method name="is_active"> + <return type="bool"> + </return> + <description> + Returns [code]true[/code] if the debugger is active otherwise [code]false[/code]. + </description> + </method> + <method name="is_profiling"> + <return type="bool"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Returns [code]true[/code] if a profiler with the given name is present and active otherwise [code]false[/code]. + </description> + </method> + <method name="profiler_add_frame_data"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <argument index="1" name="data" type="Array"> + </argument> + <description> + Calls the [code]add[/code] callable of the profiler with given [code]name[/code] and [code]data[/code]. + </description> + </method> + <method name="profiler_enable"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <argument index="1" name="enable" type="bool"> + </argument> + <argument index="2" name="arguments" type="Array" default="[ ]"> + </argument> + <description> + Calls the [code]toggle[/code] callable of the profiler with given [code]name[/code] and [code]arguments[/code]. Enables/Disables the same profiler depending on [code]enable[/code] argument. + </description> + </method> + <method name="register_message_capture"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <argument index="1" name="callable" type="Callable"> + </argument> + <description> + Registers a message capture with given [code]name[/code]. If [code]name[/code] is "my_message" then messages starting with "my_message:" will be called with the given callable. + Callable must accept a message string and a data array as argument. If the message and data are valid then callable must return [code]true[/code] otherwise [code]false[/code]. + </description> + </method> + <method name="register_profiler"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <argument index="1" name="toggle" type="Callable"> + </argument> + <argument index="2" name="add" type="Callable"> + </argument> + <argument index="3" name="tick" type="Callable"> + </argument> + <description> + Registers a profiler with the given [code]name[/code]. + [code]toggle[/code] callable is called when the profiler is enabled/disabled. It must take an argument array as an argument. + [code]add[/code] callable is called when data is added to profiler using [method EngineDebugger.profiler_add_frame_data]. It must take a data array as argument. + [code]tick[/code] callable is called at every active profiler iteration. It must take frame time, idle time, physics time, and physics idle time as arguments. + </description> + </method> + <method name="send_message"> + <return type="void"> + </return> + <argument index="0" name="message" type="String"> + </argument> + <argument index="1" name="data" type="Array"> + </argument> + <description> + Sends a message with given [code]message[/code] and [code]data[/code] array. + </description> + </method> + <method name="unregister_message_capture"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Unregisters the message capture with given [code]name[/code]. + </description> + </method> + <method name="unregister_profiler"> + <return type="void"> + </return> + <argument index="0" name="name" type="StringName"> + </argument> + <description> + Unregisters a profiler with given [code]name[/code]. + </description> + </method> + </methods> + <constants> + </constants> +</class> diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml index fc3c3776ce..fbc9dfa564 100644 --- a/doc/classes/Input.xml +++ b/doc/classes/Input.xml @@ -417,7 +417,8 @@ Makes the mouse cursor hidden if it is visible. </constant> <constant name="MOUSE_MODE_CAPTURED" value="2" enum="MouseMode"> - Captures the mouse. The mouse will be hidden and unable to leave the game window, but it will still register movement and mouse button presses. On Windows and Linux, the mouse will use raw input mode, which means the reported movement will be unaffected by the OS' mouse acceleration settings. + Captures the mouse. The mouse will be hidden and its position locked at the center of the screen. + [b]Note:[/b] If you want to process the mouse's movement in this mode, you need to use [member InputEventMouseMotion.relative]. </constant> <constant name="MOUSE_MODE_CONFINED" value="3" enum="MouseMode"> Makes the mouse cursor visible but confines it to the game window. diff --git a/doc/classes/Line2D.xml b/doc/classes/Line2D.xml index cfd23b28bd..22fe4f6194 100644 --- a/doc/classes/Line2D.xml +++ b/doc/classes/Line2D.xml @@ -85,7 +85,7 @@ The style for the points between the start and the end. </member> <member name="points" type="PackedVector2Array" setter="set_points" getter="get_points" default="PackedVector2Array( )"> - The points that form the lines. The line is drawn between every point set in this array. + The points that form the lines. The line is drawn between every point set in this array. Points are interpreted as local vectors. </member> <member name="round_precision" type="int" setter="set_round_precision" getter="get_round_precision" default="8"> The smoothness of the rounded joints and caps. This is only used if a cap or joint is set as round. diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml index ca6b624359..077067a0f4 100644 --- a/doc/classes/Object.xml +++ b/doc/classes/Object.xml @@ -486,13 +486,35 @@ </description> </method> <method name="tr" qualifiers="const"> - <return type="StringName"> + <return type="String"> </return> <argument index="0" name="message" type="StringName"> </argument> + <argument index="1" name="context" type="StringName" default=""""> + </argument> <description> - Translates a message using translation catalogs configured in the Project Settings. + Translates a message using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context. Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] unchanged. See [method set_message_translation]. + See <link>https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html</link> for examples of the usage of this method. + </description> + </method> + <method name="tr_n" qualifiers="const"> + <return type="String"> + </return> + <argument index="0" name="message" type="StringName"> + </argument> + <argument index="1" name="plural_message" type="StringName"> + </argument> + <argument index="2" name="n" type="int"> + </argument> + <argument index="3" name="context" type="StringName" default=""""> + </argument> + <description> + Translates a message involving plurals using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context. + Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] or [code]plural_message[/code] unchanged. See [method set_message_translation]. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + [b]Note:[/b] Negative and floating-point values usually represent physical entities for which singular and plural don't clearly apply. In such cases, use [method tr]. + See <link>https://docs.godotengine.org/en/latest/tutorials/i18n/localization_using_gettext.html</link> for examples of the usage of this method. </description> </method> </methods> diff --git a/doc/classes/PhysicsServer2D.xml b/doc/classes/PhysicsServer2D.xml index b2904c6538..6a1508b0e3 100644 --- a/doc/classes/PhysicsServer2D.xml +++ b/doc/classes/PhysicsServer2D.xml @@ -804,6 +804,7 @@ </argument> <description> Sets a body state using one of the [enum BodyState] constants. + Note that the method doesn't take effect immediately. The state will change on the next physics frame. </description> </method> <method name="body_test_motion"> diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index c44df72878..a1c4fcc53c 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -6,6 +6,7 @@ <description> Rich text can contain custom text, fonts, images and some basic formatting. The label manages these as an internal tag stack. It also adapts itself to given width/heights. [b]Note:[/b] Assignments to [member bbcode_text] clear the tag stack and reconstruct it from the property's contents. Any edits made to [member bbcode_text] will erase previous edits made from other manual sources such as [method append_bbcode] and the [code]push_*[/code] / [method pop] methods. + [b]Note:[/b] Unlike [Label], RichTextLabel doesn't have a [i]property[/i] to horizontally align text to the center. Instead, enable [member bbcode_enabled] and surround the text in a [code][center][/code] tag as follows: [code][center]Example[/center][/code]. There is currently no built-in way to vertically align text either, but this can be emulated by relying on anchors/containers and the [member fit_content_height] property. </description> <tutorials> <link>https://docs.godotengine.org/en/latest/tutorials/gui/bbcode_in_richtextlabel.html</link> diff --git a/doc/classes/RigidBody2D.xml b/doc/classes/RigidBody2D.xml index a7efba518c..1fbcccdbb5 100644 --- a/doc/classes/RigidBody2D.xml +++ b/doc/classes/RigidBody2D.xml @@ -9,6 +9,7 @@ [b]Note:[/b] You should not change a RigidBody2D's [code]position[/code] or [code]linear_velocity[/code] every frame or even very often. If you need to directly affect the body's state, use [method _integrate_forces], which allows you to directly access the physics state. Please also keep in mind that physics bodies manage their own transform which overwrites the ones you set. So any direct or indirect transformation (including scaling of the node or its parent) will be visible in the editor only, and immediately reset at runtime. If you need to override the default physics behavior or add a transformation at runtime, you can write a custom force integration. See [member custom_integrator]. + The center of mass is always located at the node's origin without taking into account the [CollisionShape2D] centroid offsets. </description> <tutorials> </tutorials> diff --git a/doc/classes/RigidBody3D.xml b/doc/classes/RigidBody3D.xml index 933885ba77..d53d6b001c 100644 --- a/doc/classes/RigidBody3D.xml +++ b/doc/classes/RigidBody3D.xml @@ -8,6 +8,7 @@ A RigidBody3D has 4 behavior [member mode]s: Rigid, Static, Character, and Kinematic. [b]Note:[/b] Don't change a RigidBody3D's position every frame or very often. Sporadic changes work fine, but physics runs at a different granularity (fixed Hz) than usual rendering (process callback) and maybe even in a separate thread, so changing this from a process loop may result in strange behavior. If you need to directly affect the body's state, use [method _integrate_forces], which allows you to directly access the physics state. If you need to override the default physics behavior, you can write a custom force integration function. See [member custom_integrator]. + With Bullet physics (the default), the center of mass is the RigidBody3D center. With GodotPhysics, the center of mass is the average of the [CollisionShape3D] centers. </description> <tutorials> <link>https://docs.godotengine.org/en/latest/tutorials/physics/physics_introduction.html</link> diff --git a/doc/classes/Thread.xml b/doc/classes/Thread.xml index 5c63d01322..46377ecf20 100644 --- a/doc/classes/Thread.xml +++ b/doc/classes/Thread.xml @@ -5,6 +5,7 @@ </brief_description> <description> A unit of execution in a process. Can run methods on [Object]s simultaneously. The use of synchronization via [Mutex] or [Semaphore] is advised if working with shared objects. + [b]Note:[/b] Breakpoints won't break on code if it's running in a thread. This is a current limitation of the GDScript debugger. </description> <tutorials> <link title="Using multiple threads">https://docs.godotengine.org/en/latest/tutorials/threads/using_multiple_threads.html</link> @@ -15,7 +16,7 @@ <return type="String"> </return> <description> - Returns the current [Thread]'s ID, uniquely identifying it among all threads. + Returns the current [Thread]'s ID, uniquely identifying it among all threads. If the [Thread] is not running this returns an empty string. </description> </method> <method name="is_active" qualifiers="const"> diff --git a/doc/classes/TouchScreenButton.xml b/doc/classes/TouchScreenButton.xml index c7f886b3f2..355804f2a3 100644 --- a/doc/classes/TouchScreenButton.xml +++ b/doc/classes/TouchScreenButton.xml @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="UTF-8" ?> <class name="TouchScreenButton" inherits="Node2D" version="4.0"> <brief_description> - Button for touch screen devices. + Button for touch screen devices for gameplay use. </brief_description> <description> - Button for touch screen devices. You can set it to be visible on all screens, or only on touch devices. + TouchScreenButton allows you to create on-screen buttons for touch devices. It's intended for gameplay use, such as a unit you have to touch to move. + This node inherits from [Node2D]. Unlike with [Control] nodes, you cannot set anchors on it. If you want to create menus or user interfaces, you may want to use [Button] nodes instead. To make button nodes react to touch events, you can enable the Emulate Mouse option in the Project Settings. + You can configure TouchScreenButton to be visible only on touch devices, helping you develop your game both for desktop and mobile devices. </description> <tutorials> </tutorials> diff --git a/doc/classes/Translation.xml b/doc/classes/Translation.xml index 11245195bf..1989a63362 100644 --- a/doc/classes/Translation.xml +++ b/doc/classes/Translation.xml @@ -18,8 +18,25 @@ </argument> <argument index="1" name="xlated_message" type="StringName"> </argument> + <argument index="2" name="context" type="StringName" default=""""> + </argument> <description> Adds a message if nonexistent, followed by its translation. + An additional context could be used to specify the translation context or differentiate polysemic words. + </description> + </method> + <method name="add_plural_message"> + <return type="void"> + </return> + <argument index="0" name="src_message" type="StringName"> + </argument> + <argument index="1" name="xlated_messages" type="PackedStringArray"> + </argument> + <argument index="2" name="context" type="StringName" default=""""> + </argument> + <description> + Adds a message involving plural translation if nonexistent, followed by its translation. + An additional context could be used to specify the translation context or differentiate polysemic words. </description> </method> <method name="erase_message"> @@ -27,6 +44,8 @@ </return> <argument index="0" name="src_message" type="StringName"> </argument> + <argument index="1" name="context" type="StringName" default=""""> + </argument> <description> Erases a message. </description> @@ -36,6 +55,8 @@ </return> <argument index="0" name="src_message" type="StringName"> </argument> + <argument index="1" name="context" type="StringName" default=""""> + </argument> <description> Returns a message's translation. </description> @@ -54,6 +75,22 @@ Returns all the messages (keys). </description> </method> + <method name="get_plural_message" qualifiers="const"> + <return type="StringName"> + </return> + <argument index="0" name="src_message" type="StringName"> + </argument> + <argument index="1" name="src_plural_message" type="StringName"> + </argument> + <argument index="2" name="n" type="int"> + </argument> + <argument index="3" name="context" type="StringName" default=""""> + </argument> + <description> + Returns a message's translation involving plurals. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + </description> + </method> </methods> <members> <member name="locale" type="String" setter="set_locale" getter="get_locale" default=""en""> diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index aaf7a4d160..3369663af6 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -50,6 +50,16 @@ Returns a locale's language and its variant (e.g. [code]"en_US"[/code] would return [code]"English (United States)"[/code]). </description> </method> + <method name="get_translation_object"> + <return type="Translation"> + </return> + <argument index="0" name="locale" type="String"> + </argument> + <description> + Returns the [Translation] instance based on the [code]locale[/code] passed in. + It will return a [code]nullptr[/code] if there is no [Translation] instance that matches the [code]locale[/code]. + </description> + </method> <method name="remove_translation"> <return type="void"> </return> @@ -73,8 +83,26 @@ </return> <argument index="0" name="message" type="StringName"> </argument> + <argument index="1" name="context" type="StringName" default=""""> + </argument> + <description> + Returns the current locale's translation for the given message (key) and context. + </description> + </method> + <method name="translate_plural" qualifiers="const"> + <return type="StringName"> + </return> + <argument index="0" name="message" type="StringName"> + </argument> + <argument index="1" name="plural_message" type="StringName"> + </argument> + <argument index="2" name="n" type="int"> + </argument> + <argument index="3" name="context" type="StringName" default=""""> + </argument> <description> - Returns the current locale's translation for the given message (key). + Returns the current locale's translation for the given message (key), plural_message and context. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. </description> </method> </methods> diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index a9c18138d8..b461ac4f35 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -34,6 +34,7 @@ #include "editor/debugger/script_editor_debugger.h" #include "editor/editor_log.h" #include "editor/editor_node.h" +#include "editor/plugins/editor_debugger_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "scene/gui/menu_button.h" #include "scene/gui/tab_container.h" @@ -114,6 +115,12 @@ ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() { tabs->add_theme_style_override("panel", EditorNode::get_singleton()->get_gui_base()->get_theme_stylebox("DebuggerPanel", "EditorStyles")); } + if (!debugger_plugins.empty()) { + for (Set<Ref<Script>>::Element *i = debugger_plugins.front(); i; i = i->next()) { + node->add_debugger_plugin(i->get()); + } + } + return node; } @@ -618,3 +625,23 @@ void EditorDebuggerNode::live_debug_reparent_node(const NodePath &p_at, const No dbg->live_debug_reparent_node(p_at, p_new_place, p_new_name, p_at_pos); }); } + +void EditorDebuggerNode::add_debugger_plugin(const Ref<Script> &p_script) { + ERR_FAIL_COND_MSG(debugger_plugins.has(p_script), "Debugger plugin already exists."); + ERR_FAIL_COND_MSG(p_script.is_null(), "Debugger plugin script is null"); + ERR_FAIL_COND_MSG(String(p_script->get_instance_base_type()) == "", "Debugger plugin script has error."); + ERR_FAIL_COND_MSG(String(p_script->get_instance_base_type()) != "EditorDebuggerPlugin", "Base type of debugger plugin is not 'EditorDebuggerPlugin'."); + ERR_FAIL_COND_MSG(!p_script->is_tool(), "Debugger plugin script is not in tool mode."); + debugger_plugins.insert(p_script); + for (int i = 0; get_debugger(i); i++) { + get_debugger(i)->add_debugger_plugin(p_script); + } +} + +void EditorDebuggerNode::remove_debugger_plugin(const Ref<Script> &p_script) { + ERR_FAIL_COND_MSG(!debugger_plugins.has(p_script), "Debugger plugin doesn't exists."); + debugger_plugins.erase(p_script); + for (int i = 0; get_debugger(i); i++) { + get_debugger(i)->remove_debugger_plugin(p_script); + } +} diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h index ff9601c026..8d70a7f961 100644 --- a/editor/debugger/editor_debugger_node.h +++ b/editor/debugger/editor_debugger_node.h @@ -103,6 +103,8 @@ private: CameraOverride camera_override = OVERRIDE_NONE; Map<Breakpoint, bool> breakpoints; + Set<Ref<Script>> debugger_plugins; + ScriptEditorDebugger *_add_debugger(); EditorDebuggerRemoteObject *get_inspected_remote_object(); @@ -186,5 +188,8 @@ public: Error start(const String &p_protocol = "tcp://"); void stop(); + + void add_debugger_plugin(const Ref<Script> &p_script); + void remove_debugger_plugin(const Ref<Script> &p_script); }; #endif // EDITOR_DEBUGGER_NODE_H diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 49bf068be7..1fca95b6da 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -44,6 +44,7 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/plugins/canvas_item_editor_plugin.h" +#include "editor/plugins/editor_debugger_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "editor/property_editor.h" #include "main/performance.h" @@ -701,7 +702,28 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da performance_profiler->update_monitors(monitors); } else { - WARN_PRINT("unknown message " + p_msg); + int colon_index = p_msg.find_char(':'); + ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received"); + + bool parsed = false; + const String cap = p_msg.substr(0, colon_index); + Map<StringName, Callable>::Element *element = captures.find(cap); + if (element) { + Callable &c = element->value(); + ERR_FAIL_COND_MSG(c.is_null(), "Invalid callable registered: " + cap); + Variant cmd = p_msg.substr(colon_index + 1), data = p_data; + const Variant *args[2] = { &cmd, &data }; + Variant retval; + Callable::CallError err; + c.call(args, 2, retval, err); + ERR_FAIL_COND_MSG(err.error != Callable::CallError::CALL_OK, "Error calling 'capture' to callable: " + Variant::get_callable_error_text(c, args, 2, err)); + ERR_FAIL_COND_MSG(retval.get_type() != Variant::BOOL, "Error calling 'capture' to callable: " + String(c) + ". Return type is not bool."); + parsed = retval; + } + + if (!parsed) { + WARN_PRINT("unknown message " + p_msg); + } } } @@ -847,6 +869,7 @@ void ScriptEditorDebugger::start(Ref<RemoteDebuggerPeer> p_peer) { tabs->set_current_tab(0); _set_reason_text(TTR("Debug session started."), MESSAGE_SUCCESS); _update_buttons_state(); + emit_signal("started"); } void ScriptEditorDebugger::_update_buttons_state() { @@ -1395,6 +1418,7 @@ void ScriptEditorDebugger::_bind_methods() { ClassDB::bind_method(D_METHOD("request_remote_object", "id"), &ScriptEditorDebugger::request_remote_object); ClassDB::bind_method(D_METHOD("update_remote_object", "id", "property", "value"), &ScriptEditorDebugger::update_remote_object); + ADD_SIGNAL(MethodInfo("started")); ADD_SIGNAL(MethodInfo("stopped")); ADD_SIGNAL(MethodInfo("stop_requested")); ADD_SIGNAL(MethodInfo("stack_frame_selected", PropertyInfo(Variant::INT, "frame"))); @@ -1408,6 +1432,43 @@ void ScriptEditorDebugger::_bind_methods() { ADD_SIGNAL(MethodInfo("remote_tree_updated")); } +void ScriptEditorDebugger::add_debugger_plugin(const Ref<Script> &p_script) { + if (!debugger_plugins.has(p_script)) { + EditorDebuggerPlugin *plugin = memnew(EditorDebuggerPlugin()); + plugin->attach_debugger(this); + plugin->set_script(p_script); + tabs->add_child(plugin); + debugger_plugins.insert(p_script, plugin); + } +} + +void ScriptEditorDebugger::remove_debugger_plugin(const Ref<Script> &p_script) { + if (debugger_plugins.has(p_script)) { + tabs->remove_child(debugger_plugins[p_script]); + debugger_plugins[p_script]->detach_debugger(false); + memdelete(debugger_plugins[p_script]); + debugger_plugins.erase(p_script); + } +} + +void ScriptEditorDebugger::send_message(const String &p_message, const Array &p_args) { + _put_msg(p_message, p_args); +} + +void ScriptEditorDebugger::register_message_capture(const StringName &p_name, const Callable &p_callable) { + ERR_FAIL_COND_MSG(has_capture(p_name), "Capture already registered: " + p_name); + captures.insert(p_name, p_callable); +} + +void ScriptEditorDebugger::unregister_message_capture(const StringName &p_name) { + ERR_FAIL_COND_MSG(!has_capture(p_name), "Capture not registered: " + p_name); + captures.erase(p_name); +} + +bool ScriptEditorDebugger::has_capture(const StringName &p_name) { + return captures.has(p_name); +} + ScriptEditorDebugger::ScriptEditorDebugger(EditorNode *p_editor) { editor = p_editor; diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index 6e5699e929..56b34e8e8c 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -54,6 +54,7 @@ class EditorVisualProfiler; class EditorNetworkProfiler; class EditorPerformanceProfiler; class SceneDebuggerTree; +class EditorDebuggerPlugin; class ScriptEditorDebugger : public MarginContainer { GDCLASS(ScriptEditorDebugger, MarginContainer); @@ -146,6 +147,10 @@ private: EditorDebuggerNode::CameraOverride camera_override; + Map<Ref<Script>, EditorDebuggerPlugin *> debugger_plugins; + + Map<StringName, Callable> captures; + void _stack_dump_frame_selected(); void _file_selected(const String &p_file); @@ -253,6 +258,16 @@ public: bool is_skip_breakpoints(); virtual Size2 get_minimum_size() const override; + + void add_debugger_plugin(const Ref<Script> &p_script); + void remove_debugger_plugin(const Ref<Script> &p_script); + + void send_message(const String &p_message, const Array &p_args); + + void register_message_capture(const StringName &p_name, const Callable &p_callable); + void unregister_message_capture(const StringName &p_name); + bool has_capture(const StringName &p_name); + ScriptEditorDebugger(EditorNode *p_editor = nullptr); ~ScriptEditorDebugger(); }; diff --git a/editor/editor_export.cpp b/editor/editor_export.cpp index 951bec2c83..16e69734d3 100644 --- a/editor/editor_export.cpp +++ b/editor/editor_export.cpp @@ -512,10 +512,18 @@ void EditorExportPlugin::add_ios_framework(const String &p_path) { ios_frameworks.push_back(p_path); } +void EditorExportPlugin::add_ios_embedded_framework(const String &p_path) { + ios_embedded_frameworks.push_back(p_path); +} + Vector<String> EditorExportPlugin::get_ios_frameworks() const { return ios_frameworks; } +Vector<String> EditorExportPlugin::get_ios_embedded_frameworks() const { + return ios_embedded_frameworks; +} + void EditorExportPlugin::add_ios_plist_content(const String &p_plist_content) { ios_plist_content += p_plist_content + "\n"; } @@ -592,6 +600,7 @@ void EditorExportPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("add_ios_project_static_lib", "path"), &EditorExportPlugin::add_ios_project_static_lib); ClassDB::bind_method(D_METHOD("add_file", "path", "file", "remap"), &EditorExportPlugin::add_file); ClassDB::bind_method(D_METHOD("add_ios_framework", "path"), &EditorExportPlugin::add_ios_framework); + ClassDB::bind_method(D_METHOD("add_ios_embedded_framework", "path"), &EditorExportPlugin::add_ios_embedded_framework); ClassDB::bind_method(D_METHOD("add_ios_plist_content", "plist_content"), &EditorExportPlugin::add_ios_plist_content); ClassDB::bind_method(D_METHOD("add_ios_linker_flags", "flags"), &EditorExportPlugin::add_ios_linker_flags); ClassDB::bind_method(D_METHOD("add_ios_bundle_file", "path"), &EditorExportPlugin::add_ios_bundle_file); diff --git a/editor/editor_export.h b/editor/editor_export.h index e31b53ad67..bb701b94ec 100644 --- a/editor/editor_export.h +++ b/editor/editor_export.h @@ -290,6 +290,7 @@ class EditorExportPlugin : public Reference { bool skipped; Vector<String> ios_frameworks; + Vector<String> ios_embedded_frameworks; Vector<String> ios_project_static_libs; String ios_plist_content; String ios_linker_flags; @@ -304,6 +305,7 @@ class EditorExportPlugin : public Reference { _FORCE_INLINE_ void _export_end() { ios_frameworks.clear(); + ios_embedded_frameworks.clear(); ios_bundle_files.clear(); ios_plist_content = ""; ios_linker_flags = ""; @@ -322,6 +324,7 @@ protected: void add_shared_object(const String &p_path, const Vector<String> &tags); void add_ios_framework(const String &p_path); + void add_ios_embedded_framework(const String &p_path); void add_ios_project_static_lib(const String &p_path); void add_ios_plist_content(const String &p_plist_content); void add_ios_linker_flags(const String &p_flags); @@ -337,6 +340,7 @@ protected: public: Vector<String> get_ios_frameworks() const; + Vector<String> get_ios_embedded_frameworks() const; Vector<String> get_ios_project_static_libs() const; String get_ios_plist_content() const; String get_ios_linker_flags() const; diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index cf32ffb4e0..2e716a636e 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -1174,6 +1174,47 @@ void EditorInspectorSection::_notification(int p_what) { if (arrow.is_valid()) { draw_texture(arrow, Point2(Math::round(arrow_margin * EDSCALE), (h - arrow->get_height()) / 2).floor()); } + + if (dropping && !vbox->is_visible_in_tree()) { + Color accent_color = get_theme_color("accent_color", "Editor"); + draw_rect(Rect2(Point2(), get_size()), accent_color, false); + } + } + + if (p_what == NOTIFICATION_DRAG_BEGIN) { + Dictionary dd = get_viewport()->gui_get_drag_data(); + + // Only allow dropping if the section contains properties which can take the dragged data. + bool children_can_drop = false; + for (int child_idx = 0; child_idx < vbox->get_child_count(); child_idx++) { + Control *editor_property = Object::cast_to<Control>(vbox->get_child(child_idx)); + + // Test can_drop_data and can_drop_data_fw, since can_drop_data only works if set up with forwarding or if script attached. + if (editor_property && (editor_property->can_drop_data(Point2(), dd) || editor_property->call("can_drop_data_fw", Point2(), dd, this))) { + children_can_drop = true; + break; + } + } + + dropping = children_can_drop; + update(); + } + + if (p_what == NOTIFICATION_DRAG_END) { + dropping = false; + update(); + } + + if (p_what == NOTIFICATION_MOUSE_ENTER) { + if (dropping) { + dropping_unfold_timer->start(); + } + } + + if (p_what == NOTIFICATION_MOUSE_EXIT) { + if (dropping) { + dropping_unfold_timer->stop(); + } } } @@ -1236,14 +1277,11 @@ void EditorInspectorSection::_gui_input(const Ref<InputEvent> &p_event) { return; } - _test_unfold(); - - bool unfold = !object->editor_is_section_unfolded(section); - object->editor_set_section_unfold(section, unfold); - if (unfold) { - vbox->show(); + bool should_unfold = !object->editor_is_section_unfolded(section); + if (should_unfold) { + unfold(); } else { - vbox->hide(); + fold(); } } } @@ -1291,6 +1329,13 @@ EditorInspectorSection::EditorInspectorSection() { foldable = false; vbox = memnew(VBoxContainer); vbox_added = false; + + dropping = false; + dropping_unfold_timer = memnew(Timer); + dropping_unfold_timer->set_wait_time(0.6); + dropping_unfold_timer->set_one_shot(true); + add_child(dropping_unfold_timer); + dropping_unfold_timer->connect("timeout", callable_mp(this, &EditorInspectorSection::unfold)); } EditorInspectorSection::~EditorInspectorSection() { diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index 95072fd703..36b80a7dd4 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -231,6 +231,9 @@ class EditorInspectorSection : public Container { Color bg_color; bool foldable; + Timer *dropping_unfold_timer; + bool dropping; + void _test_unfold(); protected: diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 381ff88890..8544bb8856 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -123,6 +123,7 @@ #include "editor/plugins/cpu_particles_3d_editor_plugin.h" #include "editor/plugins/curve_editor_plugin.h" #include "editor/plugins/debugger_editor_plugin.h" +#include "editor/plugins/editor_debugger_plugin.h" #include "editor/plugins/editor_preview_plugins.h" #include "editor/plugins/gi_probe_editor_plugin.h" #include "editor/plugins/gpu_particles_2d_editor_plugin.h" @@ -456,8 +457,6 @@ void EditorNode::_notification(int p_what) { editor_selection->update(); - //scene_root->set_size_override(true, Size2(ProjectSettings::get_singleton()->get("display/window/size/width"), ProjectSettings::get_singleton()->get("display/window/size/height"))); - { //TODO should only happen on settings changed int current_filter = GLOBAL_GET("rendering/canvas_textures/default_texture_filter"); if (current_filter != scene_root->get_default_canvas_item_texture_filter()) { @@ -3622,6 +3621,7 @@ void EditorNode::register_editor_types() { // FIXME: Is this stuff obsolete, or should it be ported to new APIs? ClassDB::register_class<EditorScenePostImport>(); //ClassDB::register_type<EditorImportExport>(); + ClassDB::register_class<EditorDebuggerPlugin>(); } void EditorNode::unregister_editor_types() { diff --git a/editor/editor_plugin.cpp b/editor/editor_plugin.cpp index da0a0827d2..bce46b719a 100644 --- a/editor/editor_plugin.cpp +++ b/editor/editor_plugin.cpp @@ -811,6 +811,14 @@ ScriptCreateDialog *EditorPlugin::get_script_create_dialog() { return EditorNode::get_singleton()->get_script_create_dialog(); } +void EditorPlugin::add_debugger_plugin(const Ref<Script> &p_script) { + EditorDebuggerNode::get_singleton()->add_debugger_plugin(p_script); +} + +void EditorPlugin::remove_debugger_plugin(const Ref<Script> &p_script) { + EditorDebuggerNode::get_singleton()->remove_debugger_plugin(p_script); +} + void EditorPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("add_control_to_container", "container", "control"), &EditorPlugin::add_control_to_container); ClassDB::bind_method(D_METHOD("add_control_to_bottom_panel", "control", "title"), &EditorPlugin::add_control_to_bottom_panel); @@ -851,6 +859,8 @@ void EditorPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("get_editor_interface"), &EditorPlugin::get_editor_interface); ClassDB::bind_method(D_METHOD("get_script_create_dialog"), &EditorPlugin::get_script_create_dialog); + ClassDB::bind_method(D_METHOD("add_debugger_plugin", "script"), &EditorPlugin::add_debugger_plugin); + ClassDB::bind_method(D_METHOD("remove_debugger_plugin", "script"), &EditorPlugin::remove_debugger_plugin); ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "forward_canvas_gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); ClassDB::add_virtual_method(get_class_static(), MethodInfo("forward_canvas_draw_over_viewport", PropertyInfo(Variant::OBJECT, "overlay", PROPERTY_HINT_RESOURCE_TYPE, "Control"))); diff --git a/editor/editor_plugin.h b/editor/editor_plugin.h index 685f69bf3f..c7803f73c9 100644 --- a/editor/editor_plugin.h +++ b/editor/editor_plugin.h @@ -33,6 +33,7 @@ #include "core/io/config_file.h" #include "core/undo_redo.h" +#include "editor/debugger/editor_debugger_node.h" #include "editor/editor_inspector.h" #include "editor/editor_translation_parser.h" #include "editor/import/editor_import_plugin.h" @@ -249,6 +250,9 @@ public: void add_autoload_singleton(const String &p_name, const String &p_path); void remove_autoload_singleton(const String &p_name); + void add_debugger_plugin(const Ref<Script> &p_script); + void remove_debugger_plugin(const Ref<Script> &p_script); + void enable_plugin(); void disable_plugin(); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 34d553b5f9..4c8af615b4 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -36,6 +36,7 @@ #include "editor_properties_array_dict.h" #include "editor_scale.h" #include "scene/main/window.h" +#include "scene/resources/dynamic_font.h" ///////////////////// NULL ///////////////////////// @@ -2947,11 +2948,9 @@ void EditorPropertyResource::_notification(int p_what) { } if (p_what == NOTIFICATION_DRAG_BEGIN) { - if (is_visible_in_tree()) { - if (_is_drop_valid(get_viewport()->gui_get_drag_data())) { - dropping = true; - assign->update(); - } + if (_is_drop_valid(get_viewport()->gui_get_drag_data())) { + dropping = true; + assign->update(); } } @@ -3016,6 +3015,8 @@ bool EditorPropertyResource::_is_drop_valid(const Dictionary &p_drag_data) const allowed_types.append("Texture2D"); } else if (at == "ShaderMaterial") { allowed_types.append("Shader"); + } else if (at == "Font") { + allowed_types.append("DynamicFontData"); } } @@ -3113,6 +3114,13 @@ void EditorPropertyResource::drop_data_fw(const Point2 &p_point, const Variant & res = mat; break; } + + if (at == "Font" && ClassDB::is_parent_class(res->get_class(), "DynamicFontData")) { + Ref<DynamicFont> font = memnew(DynamicFont); + font->set_font_data(res); + res = font; + break; + } } } diff --git a/editor/editor_translation_parser.cpp b/editor/editor_translation_parser.cpp index da191fbc92..7a90d20000 100644 --- a/editor/editor_translation_parser.cpp +++ b/editor/editor_translation_parser.cpp @@ -37,15 +37,30 @@ EditorTranslationParser *EditorTranslationParser::singleton = nullptr; -Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) { +Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) { if (!get_script_instance()) return ERR_UNAVAILABLE; if (get_script_instance()->has_method("parse_file")) { - Array extracted_strings; - get_script_instance()->call("parse_file", p_path, extracted_strings); - for (int i = 0; i < extracted_strings.size(); i++) { - r_extracted_strings->append(extracted_strings[i]); + Array ids; + Array ids_ctx_plural; + get_script_instance()->call("parse_file", p_path, ids, ids_ctx_plural); + + // Add user's extracted translatable messages. + for (int i = 0; i < ids.size(); i++) { + r_ids->append(ids[i]); + } + + // Add user's collected translatable messages with context or plurals. + for (int i = 0; i < ids_ctx_plural.size(); i++) { + Array arr = ids_ctx_plural[i]; + ERR_FAIL_COND_V_MSG(arr.size() != 3, ERR_INVALID_DATA, "Array entries written into `msgids_context_plural` in `parse_file()` method should have the form [\"message\", \"context\", \"plural message\"]"); + + Vector<String> id_ctx_plural; + id_ctx_plural.push_back(arr[0]); + id_ctx_plural.push_back(arr[1]); + id_ctx_plural.push_back(arr[2]); + r_ids_ctx_plural->append(id_ctx_plural); } return OK; } else { @@ -69,7 +84,7 @@ void EditorTranslationParserPlugin::get_recognized_extensions(List<String> *r_ex } void EditorTranslationParserPlugin::_bind_methods() { - ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::NIL, "parse_file", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::ARRAY, "extracted_strings"))); + ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::NIL, "parse_file", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::ARRAY, "msgids"), PropertyInfo(Variant::ARRAY, "msgids_context_plural"))); ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::ARRAY, "get_recognized_extensions")); } diff --git a/editor/editor_translation_parser.h b/editor/editor_translation_parser.h index fb8aa6ec9b..18f49b3803 100644 --- a/editor/editor_translation_parser.h +++ b/editor/editor_translation_parser.h @@ -41,7 +41,7 @@ protected: static void _bind_methods(); public: - virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings); + virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural); virtual void get_recognized_extensions(List<String> *r_extensions) const; }; diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index f3508cedbd..3f9f159d7f 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -2599,6 +2599,11 @@ void CanvasItemEditor::_gui_input_viewport(const Ref<InputEvent> &p_event) { void CanvasItemEditor::_update_cursor() { CursorShape c = CURSOR_ARROW; + bool should_switch = false; + if (drag_selection.size() != 0) { + float angle = drag_selection[0]->_edit_get_rotation(); + should_switch = abs(Math::cos(angle)) < Math_SQRT12; + } switch (drag_type) { case DRAG_NONE: switch (tool) { @@ -2621,21 +2626,37 @@ void CanvasItemEditor::_update_cursor() { case DRAG_LEFT: case DRAG_RIGHT: case DRAG_V_GUIDE: - c = CURSOR_HSIZE; + if (should_switch) { + c = CURSOR_VSIZE; + } else { + c = CURSOR_HSIZE; + } break; case DRAG_TOP: case DRAG_BOTTOM: case DRAG_H_GUIDE: - c = CURSOR_VSIZE; + if (should_switch) { + c = CURSOR_HSIZE; + } else { + c = CURSOR_VSIZE; + } break; case DRAG_TOP_LEFT: case DRAG_BOTTOM_RIGHT: case DRAG_DOUBLE_GUIDE: - c = CURSOR_FDIAGSIZE; + if (should_switch) { + c = CURSOR_BDIAGSIZE; + } else { + c = CURSOR_FDIAGSIZE; + } break; case DRAG_TOP_RIGHT: case DRAG_BOTTOM_LEFT: - c = CURSOR_BDIAGSIZE; + if (should_switch) { + c = CURSOR_FDIAGSIZE; + } else { + c = CURSOR_BDIAGSIZE; + } break; case DRAG_MOVE: c = CURSOR_MOVE; diff --git a/editor/plugins/editor_debugger_plugin.cpp b/editor/plugins/editor_debugger_plugin.cpp new file mode 100644 index 0000000000..b775e871e2 --- /dev/null +++ b/editor/plugins/editor_debugger_plugin.cpp @@ -0,0 +1,124 @@ +/*************************************************************************/ +/* editor_debugger_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor_debugger_plugin.h" + +#include "editor/debugger/script_editor_debugger.h" + +void EditorDebuggerPlugin::_breaked(bool p_really_did, bool p_can_debug) { + if (p_really_did) { + emit_signal("breaked", p_can_debug); + } else { + emit_signal("continued"); + } +} + +void EditorDebuggerPlugin::_started() { + emit_signal("started"); +} + +void EditorDebuggerPlugin::_stopped() { + emit_signal("stopped"); +} + +void EditorDebuggerPlugin::_bind_methods() { + ClassDB::bind_method(D_METHOD("send_message", "message", "data"), &EditorDebuggerPlugin::send_message); + ClassDB::bind_method(D_METHOD("register_message_capture", "name", "callable"), &EditorDebuggerPlugin::register_message_capture); + ClassDB::bind_method(D_METHOD("unregister_message_capture", "name"), &EditorDebuggerPlugin::unregister_message_capture); + ClassDB::bind_method(D_METHOD("has_capture", "name"), &EditorDebuggerPlugin::has_capture); + ClassDB::bind_method(D_METHOD("is_breaked"), &EditorDebuggerPlugin::is_breaked); + ClassDB::bind_method(D_METHOD("is_debuggable"), &EditorDebuggerPlugin::is_debuggable); + ClassDB::bind_method(D_METHOD("is_session_active"), &EditorDebuggerPlugin::is_session_active); + + ADD_SIGNAL(MethodInfo("started")); + ADD_SIGNAL(MethodInfo("stopped")); + ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "can_debug"))); + ADD_SIGNAL(MethodInfo("continued")); +} + +void EditorDebuggerPlugin::attach_debugger(ScriptEditorDebugger *p_debugger) { + debugger = p_debugger; + if (debugger) { + debugger->connect("started", callable_mp(this, &EditorDebuggerPlugin::_started)); + debugger->connect("stopped", callable_mp(this, &EditorDebuggerPlugin::_stopped)); + debugger->connect("breaked", callable_mp(this, &EditorDebuggerPlugin::_breaked)); + } +} + +void EditorDebuggerPlugin::detach_debugger(bool p_call_debugger) { + if (debugger) { + debugger->disconnect("started", callable_mp(this, &EditorDebuggerPlugin::_started)); + debugger->disconnect("stopped", callable_mp(this, &EditorDebuggerPlugin::_stopped)); + debugger->disconnect("breaked", callable_mp(this, &EditorDebuggerPlugin::_breaked)); + if (p_call_debugger && get_script_instance()) { + debugger->remove_debugger_plugin(get_script_instance()->get_script()); + } + debugger = nullptr; + } +} + +void EditorDebuggerPlugin::send_message(const String &p_message, const Array &p_args) { + ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger"); + debugger->send_message(p_message, p_args); +} + +void EditorDebuggerPlugin::register_message_capture(const StringName &p_name, const Callable &p_callable) { + ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger"); + debugger->register_message_capture(p_name, p_callable); +} + +void EditorDebuggerPlugin::unregister_message_capture(const StringName &p_name) { + ERR_FAIL_COND_MSG(!debugger, "Plugin is not attached to debugger"); + debugger->unregister_message_capture(p_name); +} + +bool EditorDebuggerPlugin::has_capture(const StringName &p_name) { + ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger"); + return debugger->has_capture(p_name); +} + +bool EditorDebuggerPlugin::is_breaked() { + ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger"); + return debugger->is_breaked(); +} + +bool EditorDebuggerPlugin::is_debuggable() { + ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger"); + return debugger->is_debuggable(); +} + +bool EditorDebuggerPlugin::is_session_active() { + ERR_FAIL_COND_V_MSG(!debugger, false, "Plugin is not attached to debugger"); + return debugger->is_session_active(); +} + +EditorDebuggerPlugin::~EditorDebuggerPlugin() { + detach_debugger(true); +} diff --git a/editor/plugins/editor_debugger_plugin.h b/editor/plugins/editor_debugger_plugin.h new file mode 100644 index 0000000000..5536f2daa2 --- /dev/null +++ b/editor/plugins/editor_debugger_plugin.h @@ -0,0 +1,64 @@ +/*************************************************************************/ +/* editor_debugger_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EDITOR_DEBUGGER_PLUGIN_H +#define EDITOR_DEBUGGER_PLUGIN_H + +#include "scene/gui/control.h" + +class ScriptEditorDebugger; + +class EditorDebuggerPlugin : public Control { + GDCLASS(EditorDebuggerPlugin, Control); + +private: + ScriptEditorDebugger *debugger; + + void _breaked(bool p_really_did, bool p_can_debug); + void _started(); + void _stopped(); + +protected: + static void _bind_methods(); + +public: + void attach_debugger(ScriptEditorDebugger *p_debugger); + void detach_debugger(bool p_call_debugger); + void send_message(const String &p_message, const Array &p_args); + void register_message_capture(const StringName &p_name, const Callable &p_callable); + void unregister_message_capture(const StringName &p_name); + bool has_capture(const StringName &p_name); + bool is_breaked(); + bool is_debuggable(); + bool is_session_active(); + ~EditorDebuggerPlugin(); +}; + +#endif // EDITOR_DEBUGGER_PLUGIN_H diff --git a/editor/plugins/packed_scene_translation_parser_plugin.cpp b/editor/plugins/packed_scene_translation_parser_plugin.cpp index 52af0008b7..608b5c3104 100644 --- a/editor/plugins/packed_scene_translation_parser_plugin.cpp +++ b/editor/plugins/packed_scene_translation_parser_plugin.cpp @@ -37,7 +37,7 @@ void PackedSceneEditorTranslationParserPlugin::get_recognized_extensions(List<St ResourceLoader::get_recognized_extensions_for_type("PackedScene", r_extensions); } -Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) { +Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) { // Parse specific scene Node's properties (see in constructor) that are auto-translated by the engine when set. E.g Label's text property. // These properties are translated with the tr() function in the C++ code when being set or updated. @@ -71,8 +71,10 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, String extension = s->get_language()->get_extension(); if (EditorTranslationParser::get_singleton()->can_parse(extension)) { Vector<String> temp; - EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(s->get_path(), &temp); + Vector<Vector<String>> ids_context_plural; + EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(s->get_path(), &temp, &ids_context_plural); parsed_strings.append_array(temp); + r_ids_ctx_plural->append_array(ids_context_plural); } } else if (property_name == "filters") { // Extract FileDialog's filters property with values in format "*.png ; PNG Images","*.gd ; GDScript Files". @@ -93,7 +95,7 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, } } - r_extracted_strings->append_array(parsed_strings); + r_ids->append_array(parsed_strings); return OK; } diff --git a/editor/plugins/packed_scene_translation_parser_plugin.h b/editor/plugins/packed_scene_translation_parser_plugin.h index 2bd4dae995..a0ffdf692c 100644 --- a/editor/plugins/packed_scene_translation_parser_plugin.h +++ b/editor/plugins/packed_scene_translation_parser_plugin.h @@ -40,7 +40,7 @@ class PackedSceneEditorTranslationParserPlugin : public EditorTranslationParserP Set<String> lookup_properties; public: - virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings) override; + virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override; virtual void get_recognized_extensions(List<String> *r_extensions) const override; PackedSceneEditorTranslationParserPlugin(); diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 18a107ff75..76dd6e38e0 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -629,7 +629,7 @@ ThemeEditor::ThemeEditor() { ScrollContainer *scroll = memnew(ScrollContainer); add_child(scroll); scroll->set_enable_v_scroll(true); - scroll->set_enable_h_scroll(false); + scroll->set_enable_h_scroll(true); scroll->set_v_size_flags(SIZE_EXPAND_FILL); MarginContainer *root_container = memnew(MarginContainer); diff --git a/editor/pot_generator.cpp b/editor/pot_generator.cpp index f9b8722aad..f09750efdc 100644 --- a/editor/pot_generator.cpp +++ b/editor/pot_generator.cpp @@ -31,23 +31,25 @@ #include "pot_generator.h" #include "core/error_macros.h" -#include "core/os/file_access.h" #include "core/project_settings.h" #include "editor_translation_parser.h" #include "plugins/packed_scene_translation_parser_plugin.h" POTGenerator *POTGenerator::singleton = nullptr; -//#define DEBUG_POT - #ifdef DEBUG_POT -void _print_all_translation_strings(const OrderedHashMap<String, Set<String>> &p_all_translation_strings) { - for (auto E_pair = p_all_translation_strings.front(); E_pair; E_pair = E_pair.next()) { - String msg = static_cast<String>(E_pair.key()) + " : "; - for (Set<String>::Element *E = E_pair.value().front(); E; E = E->next()) { - msg += E->get() + " "; +void POTGenerator::_print_all_translation_strings() { + for (auto E = all_translation_strings.front(); E; E = E.next()) { + Vector<MsgidData> v_md = all_translation_strings[E.key()]; + for (int i = 0; i < v_md.size(); i++) { + print_line("++++++"); + print_line("msgid: " + E.key()); + print_line("context: " + v_md[i].ctx); + print_line("msgid_plural: " + v_md[i].plural); + for (Set<String>::Element *E = v_md[i].locations.front(); E; E = E->next()) { + print_line("location: " + E->get()); + } } - print_line(msg); } } #endif @@ -65,27 +67,27 @@ void POTGenerator::generate_pot(const String &p_file) { // Collect all translatable strings according to files order in "POT Generation" setting. for (int i = 0; i < files.size(); i++) { - Vector<String> translation_strings; + Vector<String> msgids; + Vector<Vector<String>> msgids_context_plural; String file_path = files[i]; String file_extension = file_path.get_extension(); if (EditorTranslationParser::get_singleton()->can_parse(file_extension)) { - EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &translation_strings); + EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &msgids, &msgids_context_plural); } else { ERR_PRINT("Unrecognized file extension " + file_extension + " in generate_pot()"); return; } - // Store translation strings parsed in this iteration along with their corresponding source file - to write into POT later on. - for (int j = 0; j < translation_strings.size(); j++) { - all_translation_strings[translation_strings[j]].insert(file_path); + for (int j = 0; j < msgids_context_plural.size(); j++) { + Vector<String> entry = msgids_context_plural[j]; + _add_new_msgid(entry[0], entry[1], entry[2], file_path); + } + for (int j = 0; j < msgids.size(); j++) { + _add_new_msgid(msgids[j], "", "", file_path); } } -#ifdef DEBUG_POT - _print_all_translation_strings(all_translation_strings); -#endif - _write_to_pot(p_file); } @@ -119,35 +121,86 @@ void POTGenerator::_write_to_pot(const String &p_file) { file->store_string(header); - for (OrderedHashMap<String, Set<String>>::Element E_pair = all_translation_strings.front(); E_pair; E_pair = E_pair.next()) { - String msg = E_pair.key(); + for (OrderedHashMap<String, Vector<MsgidData>>::Element E_pair = all_translation_strings.front(); E_pair; E_pair = E_pair.next()) { + String msgid = E_pair.key(); + Vector<MsgidData> v_msgid_data = E_pair.value(); + for (int i = 0; i < v_msgid_data.size(); i++) { + String context = v_msgid_data[i].ctx; + String plural = v_msgid_data[i].plural; + const Set<String> &locations = v_msgid_data[i].locations; + + // Write file locations. + for (Set<String>::Element *E = locations.front(); E; E = E->next()) { + file->store_line("#: " + E->get().trim_prefix("res://")); + } - // Write file locations. - for (Set<String>::Element *E = E_pair.value().front(); E; E = E->next()) { - file->store_line("#: " + E->get().trim_prefix("res://")); - } + // Write context. + if (!context.empty()) { + file->store_line("msgctxt \"" + context + "\""); + } - // Split \\n and \n. - Vector<String> temp = msg.split("\\n"); - Vector<String> msg_lines; - for (int i = 0; i < temp.size(); i++) { - msg_lines.append_array(temp[i].split("\n")); - if (i < temp.size() - 1) { - // Add \n. - msg_lines.set(msg_lines.size() - 1, msg_lines[msg_lines.size() - 1] + "\\n"); + // Write msgid. + _write_msgid(file, msgid, false); + + // Write msgid_plural + if (!plural.empty()) { + _write_msgid(file, plural, true); + file->store_line("msgstr[0] \"\""); + file->store_line("msgstr[1] \"\"\n"); + } else { + file->store_line("msgstr \"\"\n"); } } + } - // Write msgid. - file->store_string("msgid "); - for (int i = 0; i < msg_lines.size(); i++) { - file->store_line("\"" + msg_lines[i] + "\""); + file->close(); +} + +void POTGenerator::_write_msgid(FileAccess *r_file, const String &p_id, bool p_plural) { + // Split \\n and \n. + Vector<String> temp = p_id.split("\\n"); + Vector<String> msg_lines; + for (int i = 0; i < temp.size(); i++) { + msg_lines.append_array(temp[i].split("\n")); + if (i < temp.size() - 1) { + // Add \n. + msg_lines.set(msg_lines.size() - 1, msg_lines[msg_lines.size() - 1] + "\\n"); } + } - file->store_line("msgstr \"\"\n"); + if (p_plural) { + r_file->store_string("msgid_plural "); + } else { + r_file->store_string("msgid "); } - file->close(); + for (int i = 0; i < msg_lines.size(); i++) { + r_file->store_line("\"" + msg_lines[i] + "\""); + } +} + +void POTGenerator::_add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location) { + // Insert new location if msgid under same context exists already. + if (all_translation_strings.has(p_msgid)) { + Vector<MsgidData> &v_mdata = all_translation_strings[p_msgid]; + for (int i = 0; i < v_mdata.size(); i++) { + if (v_mdata[i].ctx == p_context) { + if (!v_mdata[i].plural.empty() && !p_plural.empty() && v_mdata[i].plural != p_plural) { + WARN_PRINT("Redefinition of plural message (msgid_plural), under the same message (msgid) and context (msgctxt)"); + } + v_mdata.write[i].locations.insert(p_location); + return; + } + } + } + + // Add a new entry of msgid, context, plural and location - context and plural might be empty if the inserted msgid doesn't associated + // context or plurals. + MsgidData mdata; + mdata.ctx = p_context; + mdata.plural = p_plural; + mdata.locations.insert(p_location); + all_translation_strings[p_msgid].push_back(mdata); } POTGenerator *POTGenerator::get_singleton() { diff --git a/editor/pot_generator.h b/editor/pot_generator.h index abe1a21d41..8853b784ed 100644 --- a/editor/pot_generator.h +++ b/editor/pot_generator.h @@ -32,14 +32,29 @@ #define POT_GENERATOR_H #include "core/ordered_hash_map.h" +#include "core/os/file_access.h" #include "core/set.h" +//#define DEBUG_POT + class POTGenerator { static POTGenerator *singleton; - // Stores all translatable strings and the source files containing them. - OrderedHashMap<String, Set<String>> all_translation_strings; + + struct MsgidData { + String ctx; + String plural; + Set<String> locations; + }; + // Store msgid as key and the additional data around the msgid - if it's under a context, has plurals and its file locations. + OrderedHashMap<String, Vector<MsgidData>> all_translation_strings; void _write_to_pot(const String &p_file); + void _write_msgid(FileAccess *r_file, const String &p_id, bool p_plural); + void _add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location); + +#ifdef DEBUG_POT + void _print_all_translation_strings(); +#endif public: static POTGenerator *get_singleton(); diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 35311b32eb..1a02390a6d 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -330,6 +330,7 @@ private: return; } } + String sp = p.simplify_path(); project_path->set_text(sp); _path_text_changed(sp); @@ -1012,7 +1013,7 @@ public: void update_dock_menu(); void load_projects(); void set_search_term(String p_search_term); - void set_order_option(ProjectListFilter::FilterOption p_option); + void set_order_option(int p_option); void sort_projects(); int get_project_count() const; void select_project(int p_index); @@ -1045,7 +1046,7 @@ private: static void load_project_data(const String &p_property_key, Item &p_item, bool p_favorite); String _search_term; - ProjectListFilter::FilterOption _order_option; + FilterOption _order_option; Set<String> _selected_project_keys; String _last_clicked; // Project key VBoxContainer *_scroll_children; @@ -1055,7 +1056,7 @@ private: }; struct ProjectListComparator { - ProjectListFilter::FilterOption order_option; + FilterOption order_option; // operator< _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const { @@ -1066,9 +1067,9 @@ struct ProjectListComparator { return false; } switch (order_option) { - case ProjectListFilter::FILTER_PATH: + case FILTER_PATH: return a.project_key < b.project_key; - case ProjectListFilter::FILTER_EDIT_DATE: + case FILTER_EDIT_DATE: return a.last_edited > b.last_edited; default: return a.project_name < b.project_name; @@ -1077,7 +1078,7 @@ struct ProjectListComparator { }; ProjectList::ProjectList() { - _order_option = ProjectListFilter::FILTER_EDIT_DATE; + _order_option = FILTER_EDIT_DATE; _scroll_children = memnew(VBoxContainer); _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -1391,9 +1392,9 @@ void ProjectList::set_search_term(String p_search_term) { _search_term = p_search_term; } -void ProjectList::set_order_option(ProjectListFilter::FilterOption p_option) { +void ProjectList::set_order_option(int p_option) { if (_order_option != p_option) { - _order_option = p_option; + _order_option = (FilterOption)p_option; EditorSettings::get_singleton()->set("project_manager/sorting_order", (int)_order_option); EditorSettings::get_singleton()->save(); } @@ -1798,6 +1799,8 @@ void ProjectList::_bind_methods() { void ProjectManager::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { + search_box->set_right_icon(get_theme_icon("Search", "EditorIcons")); + search_box->set_clear_button_enabled(true); Engine::get_singleton()->set_editor_hint(false); } break; case NOTIFICATION_RESIZED: { @@ -1813,7 +1816,7 @@ void ProjectManager::_notification(int p_what) { if (_project_list->get_project_count() >= 1) { // Focus on the search box immediately to allow the user // to search without having to reach for their mouse - project_filter->search_box->grab_focus(); + search_box->grab_focus(); } } break; case NOTIFICATION_VISIBILITY_CHANGED: { @@ -1833,7 +1836,7 @@ void ProjectManager::_dim_window() { // No transition is applied, as the effect needs to be visible immediately float c = 0.5f; Color dim_color = Color(c, c, c); - gui_base->set_modulate(dim_color); + set_modulate(dim_color); } void ProjectManager::_update_project_buttons() { @@ -1930,7 +1933,7 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { } break; case KEY_F: { if (k->get_command()) { - this->project_filter->search_box->grab_focus(); + this->search_box->grab_focus(); } else { keycode_handled = false; } @@ -1947,8 +1950,8 @@ void ProjectManager::_unhandled_input(const Ref<InputEvent> &p_ev) { } void ProjectManager::_load_recent_projects() { - _project_list->set_order_option(project_order_filter->get_filter_option()); - _project_list->set_search_term(project_filter->get_search_term()); + _project_list->set_order_option(filter_option->get_selected()); + _project_list->set_search_term(search_box->get_text().strip_edges()); _project_list->load_projects(); _update_project_buttons(); @@ -1970,7 +1973,7 @@ void ProjectManager::_on_projects_updated() { } void ProjectManager::_on_project_created(const String &dir) { - project_filter->clear(); + search_box->clear(); int i = _project_list->refresh_project(dir); _project_list->select_project(i); _project_list->ensure_project_visible(i); @@ -2113,7 +2116,6 @@ void ProjectManager::_run_project_confirm() { } } -// When you press the "Run" button void ProjectManager::_run_project() { const Set<String> &selected_list = _project_list->get_selected_project_keys(); @@ -2226,8 +2228,6 @@ void ProjectManager::_erase_missing_projects() { void ProjectManager::_language_selected(int p_id) { String lang = language_btn->get_item_metadata(p_id); EditorSettings::get_singleton()->set("interface/editor/editor_language", lang); - language_btn->set_text(lang); - language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager.")); language_restart_ask->popup_centered(); @@ -2304,13 +2304,19 @@ void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) { } } -void ProjectManager::_on_order_option_changed() { - _project_list->set_order_option(project_order_filter->get_filter_option()); - _project_list->sort_projects(); +void ProjectManager::_on_order_option_changed(int p_idx) { + FilterOption selected = (FilterOption)(p_idx); + if (_current_filter != selected) { + _current_filter = selected; + if (is_inside_tree()) { + _project_list->set_order_option(p_idx); + _project_list->sort_projects(); + } + } } -void ProjectManager::_on_filter_option_changed() { - _project_list->set_search_term(project_filter->get_search_term()); +void ProjectManager::_on_search_term_changed(const String &p_term) { + _project_list->set_search_term(p_term); _project_list->sort_projects(); // Select the first visible project in the list. @@ -2341,7 +2347,6 @@ ProjectManager::ProjectManager() { { int display_scale = EditorSettings::get_singleton()->get("interface/editor/display_scale"); - float custom_display_scale = EditorSettings::get_singleton()->get("interface/editor/custom_display_scale"); switch (display_scale) { case 0: { @@ -2372,9 +2377,8 @@ ProjectManager::ProjectManager() { case 6: editor_set_scale(2.0); break; - default: { - editor_set_scale(custom_display_scale); + editor_set_scale(EditorSettings::get_singleton()->get("interface/editor/custom_display_scale")); } break; } @@ -2385,28 +2389,26 @@ ProjectManager::ProjectManager() { DisplayServer::get_singleton()->window_set_size(DisplayServer::get_singleton()->window_get_size() * MAX(1, EDSCALE)); } + String cp; + cp += 0xA9; + DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager") + " - " + cp + " 2007-2020 Juan Linietsky, Ariel Manzur & Godot Contributors"); + FileDialog::set_default_show_hidden_files(EditorSettings::get_singleton()->get("filesystem/file_dialog/show_hidden_files")); set_anchors_and_margins_preset(Control::PRESET_WIDE); set_theme(create_custom_theme()); - gui_base = memnew(Control); - add_child(gui_base); - gui_base->set_anchors_and_margins_preset(Control::PRESET_WIDE); + set_anchors_and_margins_preset(Control::PRESET_WIDE); Panel *panel = memnew(Panel); - gui_base->add_child(panel); + add_child(panel); panel->set_anchors_and_margins_preset(Control::PRESET_WIDE); - panel->add_theme_style_override("panel", gui_base->get_theme_stylebox("Background", "EditorStyles")); + panel->add_theme_style_override("panel", get_theme_stylebox("Background", "EditorStyles")); VBoxContainer *vb = memnew(VBoxContainer); panel->add_child(vb); vb->set_anchors_and_margins_preset(Control::PRESET_WIDE, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE); - String cp; - cp += 0xA9; - DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager") + " - " + cp + " 2007-2020 Juan Linietsky, Ariel Manzur & Godot Contributors"); - Control *center_box = memnew(Control); center_box->set_v_size_flags(Control::SIZE_EXPAND_FILL); vb->add_child(center_box); @@ -2416,322 +2418,248 @@ ProjectManager::ProjectManager() { tabs->set_anchors_and_margins_preset(Control::PRESET_WIDE); tabs->set_tab_align(TabContainer::ALIGN_LEFT); - HBoxContainer *tree_hb = memnew(HBoxContainer); - projects_hb = tree_hb; - + HBoxContainer *projects_hb = memnew(HBoxContainer); projects_hb->set_name(TTR("Projects")); + tabs->add_child(projects_hb); - tabs->add_child(tree_hb); - - VBoxContainer *search_tree_vb = memnew(VBoxContainer); - tree_hb->add_child(search_tree_vb); - search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); - - HBoxContainer *sort_filters = memnew(HBoxContainer); - Label *sort_label = memnew(Label); - sort_label->set_text(TTR("Sort:")); - sort_filters->add_child(sort_label); - Vector<String> sort_filter_titles; - sort_filter_titles.push_back(TTR("Name")); - sort_filter_titles.push_back(TTR("Path")); - sort_filter_titles.push_back(TTR("Last Edited")); - project_order_filter = memnew(ProjectListFilter); - project_order_filter->add_filter_option(); - project_order_filter->_setup_filters(sort_filter_titles); - project_order_filter->set_filter_size(150); - sort_filters->add_child(project_order_filter); - project_order_filter->connect("filter_changed", callable_mp(this, &ProjectManager::_on_order_option_changed)); - project_order_filter->set_custom_minimum_size(Size2(180, 10) * EDSCALE); - - int projects_sorting_order = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); - project_order_filter->set_filter_option((ProjectListFilter::FilterOption)projects_sorting_order); - - sort_filters->add_spacer(true); - - project_filter = memnew(ProjectListFilter); - project_filter->add_search_box(); - project_filter->connect("filter_changed", callable_mp(this, &ProjectManager::_on_filter_option_changed)); - project_filter->set_custom_minimum_size(Size2(280, 10) * EDSCALE); - sort_filters->add_child(project_filter); - - search_tree_vb->add_child(sort_filters); - - PanelContainer *pc = memnew(PanelContainer); - pc->add_theme_style_override("panel", gui_base->get_theme_stylebox("bg", "Tree")); - search_tree_vb->add_child(pc); - pc->set_v_size_flags(Control::SIZE_EXPAND_FILL); - - _project_list = memnew(ProjectList); - _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); - _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); - pc->add_child(_project_list); - _project_list->set_enable_h_scroll(false); - - VBoxContainer *tree_vb = memnew(VBoxContainer); - tree_hb->add_child(tree_vb); - - Button *open = memnew(Button); - open->set_text(TTR("Edit")); - tree_vb->add_child(open); - open->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask)); - open_btn = open; - - Button *run = memnew(Button); - run->set_text(TTR("Run")); - tree_vb->add_child(run); - run->connect("pressed", callable_mp(this, &ProjectManager::_run_project)); - run_btn = run; - - tree_vb->add_child(memnew(HSeparator)); - - Button *scan = memnew(Button); - scan->set_text(TTR("Scan")); - tree_vb->add_child(scan); - scan->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects)); - - tree_vb->add_child(memnew(HSeparator)); - - scan_dir = memnew(FileDialog); - scan_dir->set_access(FileDialog::ACCESS_FILESYSTEM); - scan_dir->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); - scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden - scan_dir->set_current_dir(EditorSettings::get_singleton()->get("filesystem/directories/default_project_path")); - gui_base->add_child(scan_dir); - scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin)); - - Button *create = memnew(Button); - create->set_text(TTR("New Project")); - tree_vb->add_child(create); - create->connect("pressed", callable_mp(this, &ProjectManager::_new_project)); - - Button *import = memnew(Button); - import->set_text(TTR("Import")); - tree_vb->add_child(import); - import->connect("pressed", callable_mp(this, &ProjectManager::_import_project)); - - Button *rename = memnew(Button); - rename->set_text(TTR("Rename")); - tree_vb->add_child(rename); - rename->connect("pressed", callable_mp(this, &ProjectManager::_rename_project)); - rename_btn = rename; - - Button *erase = memnew(Button); - erase->set_text(TTR("Remove")); - tree_vb->add_child(erase); - erase->connect("pressed", callable_mp(this, &ProjectManager::_erase_project)); - erase_btn = erase; - - Button *erase_missing = memnew(Button); - erase_missing->set_text(TTR("Remove Missing")); - tree_vb->add_child(erase_missing); - erase_missing->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects)); - erase_missing_btn = erase_missing; - - tree_vb->add_spacer(); + { + // Projects + search bar + VBoxContainer *search_tree_vb = memnew(VBoxContainer); + projects_hb->add_child(search_tree_vb); + search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL); - if (StreamPeerSSL::is_available()) { - asset_library = memnew(EditorAssetLibrary(true)); - asset_library->set_name(TTR("Templates")); - tabs->add_child(asset_library); - asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project)); - } else { - WARN_PRINT("Asset Library not available, as it requires SSL to work."); - } + HBoxContainer *hb = memnew(HBoxContainer); + hb->set_h_size_flags(Control::SIZE_EXPAND_FILL); + search_tree_vb->add_child(hb); - HBoxContainer *settings_hb = memnew(HBoxContainer); - settings_hb->set_alignment(BoxContainer::ALIGN_END); - settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN); + search_box = memnew(LineEdit); + search_box->set_placeholder(TTR("Search")); + search_box->set_tooltip(TTR("The search box filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character.")); + search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed)); + search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); + hb->add_child(search_box); - Label *version_label = memnew(Label); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = "." + hash.left(9); - } - version_label->set_text("v" VERSION_FULL_BUILD "" + hash); - // Fade out the version label to be less prominent, but still readable - version_label->set_self_modulate(Color(1, 1, 1, 0.6)); - version_label->set_align(Label::ALIGN_CENTER); - settings_hb->add_child(version_label); + hb->add_spacer(); - language_btn = memnew(OptionButton); - language_btn->set_flat(true); - language_btn->set_focus_mode(Control::FOCUS_NONE); + Label *sort_label = memnew(Label); + sort_label->set_text(TTR("Sort:")); + hb->add_child(sort_label); - Vector<String> editor_languages; - List<PropertyInfo> editor_settings_properties; - EditorSettings::get_singleton()->get_property_list(&editor_settings_properties); - for (List<PropertyInfo>::Element *E = editor_settings_properties.front(); E; E = E->next()) { - PropertyInfo &pi = E->get(); - if (pi.name == "interface/editor/editor_language") { - editor_languages = pi.hint_string.split(","); - } - } - String current_lang = EditorSettings::get_singleton()->get("interface/editor/editor_language"); - for (int i = 0; i < editor_languages.size(); i++) { - String lang = editor_languages[i]; - String lang_name = TranslationServer::get_singleton()->get_locale_name(lang); - language_btn->add_item(lang_name + " [" + lang + "]", i); - language_btn->set_item_metadata(i, lang); - if (current_lang == lang) { - language_btn->select(i); - language_btn->set_text(lang); - } - } - language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); + _current_filter = FilterOption::FILTER_NAME; + int default_sorting = (int)EditorSettings::get_singleton()->get("project_manager/sorting_order"); - settings_hb->add_child(language_btn); - language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected)); + filter_option = memnew(OptionButton); + filter_option->set_clip_text(true); + filter_option->set_custom_minimum_size(Size2(150 * EDSCALE, 10 * EDSCALE)); + filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed)); + filter_option->select(default_sorting); + hb->add_child(filter_option); - center_box->add_child(settings_hb); - settings_hb->set_anchors_and_margins_preset(Control::PRESET_TOP_RIGHT); + Vector<String> sort_filter_titles; + sort_filter_titles.push_back(TTR("Name")); + sort_filter_titles.push_back(TTR("Path")); + sort_filter_titles.push_back(TTR("Last Edited")); - ////////////////////////////////////////////////////////////// + for (int i = 0; i < sort_filter_titles.size(); i++) { + filter_option->add_item(sort_filter_titles[i]); + } - language_restart_ask = memnew(ConfirmationDialog); - language_restart_ask->get_ok()->set_text(TTR("Restart Now")); - language_restart_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm)); - language_restart_ask->get_cancel()->set_text(TTR("Continue")); - gui_base->add_child(language_restart_ask); + PanelContainer *pc = memnew(PanelContainer); + pc->add_theme_style_override("panel", get_theme_stylebox("bg", "Tree")); + pc->set_v_size_flags(Control::SIZE_EXPAND_FILL); + search_tree_vb->add_child(pc); - erase_missing_ask = memnew(ConfirmationDialog); - erase_missing_ask->get_ok()->set_text(TTR("Remove All")); - erase_missing_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm)); - gui_base->add_child(erase_missing_ask); + _project_list = memnew(ProjectList); + _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons)); + _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask)); + _project_list->set_enable_h_scroll(false); + pc->add_child(_project_list); + } - erase_ask = memnew(ConfirmationDialog); - erase_ask->get_ok()->set_text(TTR("Remove")); - erase_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm)); - gui_base->add_child(erase_ask); + { + // Project tab side bar + VBoxContainer *tree_vb = memnew(VBoxContainer); + projects_hb->add_child(tree_vb); - multi_open_ask = memnew(ConfirmationDialog); - multi_open_ask->get_ok()->set_text(TTR("Edit")); - multi_open_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects)); - gui_base->add_child(multi_open_ask); + Button *create = memnew(Button); + create->set_text(TTR("New Project")); + create->connect("pressed", callable_mp(this, &ProjectManager::_new_project)); + tree_vb->add_child(create); - multi_run_ask = memnew(ConfirmationDialog); - multi_run_ask->get_ok()->set_text(TTR("Run")); - multi_run_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm)); - gui_base->add_child(multi_run_ask); + Button *import = memnew(Button); + import->set_text(TTR("Import")); + import->connect("pressed", callable_mp(this, &ProjectManager::_import_project)); + tree_vb->add_child(import); - multi_scan_ask = memnew(ConfirmationDialog); - multi_scan_ask->get_ok()->set_text(TTR("Scan")); - gui_base->add_child(multi_scan_ask); + Button *scan = memnew(Button); + scan->set_text(TTR("Scan")); + scan->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects)); + tree_vb->add_child(scan); - ask_update_settings = memnew(ConfirmationDialog); - ask_update_settings->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings)); - gui_base->add_child(ask_update_settings); + tree_vb->add_child(memnew(HSeparator)); - OS::get_singleton()->set_low_processor_usage_mode(true); + open_btn = memnew(Button); + open_btn->set_text(TTR("Edit")); + open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask)); + tree_vb->add_child(open_btn); - npdialog = memnew(ProjectDialog); - gui_base->add_child(npdialog); + run_btn = memnew(Button); + run_btn->set_text(TTR("Run")); + run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project)); + tree_vb->add_child(run_btn); - npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated)); - npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created)); + rename_btn = memnew(Button); + rename_btn->set_text(TTR("Rename")); + rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project)); + tree_vb->add_child(rename_btn); - _load_recent_projects(); + erase_btn = memnew(Button); + erase_btn->set_text(TTR("Remove")); + erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project)); + tree_vb->add_child(erase_btn); - if (EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")) { - _scan_begin(EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")); + erase_missing_btn = memnew(Button); + erase_missing_btn->set_text(TTR("Remove Missing")); + erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects)); + tree_vb->add_child(erase_missing_btn); } - SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped)); - - run_error_diag = memnew(AcceptDialog); - gui_base->add_child(run_error_diag); - run_error_diag->set_title(TTR("Can't run project")); + { + // Version info and language options + HBoxContainer *settings_hb = memnew(HBoxContainer); + settings_hb->set_alignment(BoxContainer::ALIGN_END); + settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN); + + Label *version_label = memnew(Label); + String hash = String(VERSION_HASH); + if (hash.length() != 0) { + hash = "." + hash.left(9); + } + version_label->set_text("v" VERSION_FULL_BUILD "" + hash); + version_label->set_self_modulate(Color(1, 1, 1, 0.6)); + version_label->set_align(Label::ALIGN_CENTER); + settings_hb->add_child(version_label); + + language_btn = memnew(OptionButton); + language_btn->set_flat(true); + language_btn->set_icon(get_theme_icon("Environment", "EditorIcons")); + language_btn->set_focus_mode(Control::FOCUS_NONE); + language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected)); + + Vector<String> editor_languages; + List<PropertyInfo> editor_settings_properties; + EditorSettings::get_singleton()->get_property_list(&editor_settings_properties); + for (List<PropertyInfo>::Element *E = editor_settings_properties.front(); E; E = E->next()) { + PropertyInfo &pi = E->get(); + if (pi.name == "interface/editor/editor_language") { + editor_languages = pi.hint_string.split(","); + break; + } + } - dialog_error = memnew(AcceptDialog); - gui_base->add_child(dialog_error); + String current_lang = EditorSettings::get_singleton()->get("interface/editor/editor_language"); + language_btn->set_text(current_lang); - open_templates = memnew(ConfirmationDialog); - open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?")); - open_templates->get_ok()->set_text(TTR("Open Asset Library")); - open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library)); - add_child(open_templates); -} + for (int i = 0; i < editor_languages.size(); i++) { + String lang = editor_languages[i]; + String lang_name = TranslationServer::get_singleton()->get_locale_name(lang); + language_btn->add_item(lang_name + " [" + lang + "]", i); + language_btn->set_item_metadata(i, lang); + if (current_lang == lang) { + language_btn->select(i); + } + } -ProjectManager::~ProjectManager() { - if (EditorSettings::get_singleton()) { - EditorSettings::destroy(); + settings_hb->add_child(language_btn); + center_box->add_child(settings_hb); + settings_hb->set_anchors_and_margins_preset(Control::PRESET_TOP_RIGHT); } -} -void ProjectListFilter::_setup_filters(Vector<String> options) { - filter_option->clear(); - for (int i = 0; i < options.size(); i++) { - filter_option->add_item(options[i]); + if (StreamPeerSSL::is_available()) { + asset_library = memnew(EditorAssetLibrary(true)); + asset_library->set_name(TTR("Templates")); + tabs->add_child(asset_library); + asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project)); + } else { + WARN_PRINT("Asset Library not available, as it requires SSL to work."); } -} - -void ProjectListFilter::_search_text_changed(const String &p_newtext) { - emit_signal("filter_changed"); -} - -String ProjectListFilter::get_search_term() { - return search_box->get_text().strip_edges(); -} -ProjectListFilter::FilterOption ProjectListFilter::get_filter_option() { - return _current_filter; -} - -void ProjectListFilter::set_filter_option(FilterOption option) { - filter_option->select((int)option); - _filter_option_selected(0); -} + { + // Dialogs + language_restart_ask = memnew(ConfirmationDialog); + language_restart_ask->get_ok()->set_text(TTR("Restart Now")); + language_restart_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm)); + language_restart_ask->get_cancel()->set_text(TTR("Continue")); + add_child(language_restart_ask); + + scan_dir = memnew(FileDialog); + scan_dir->set_access(FileDialog::ACCESS_FILESYSTEM); + scan_dir->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); + scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden + scan_dir->set_current_dir(EditorSettings::get_singleton()->get("filesystem/directories/default_project_path")); + add_child(scan_dir); + scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin)); + + erase_missing_ask = memnew(ConfirmationDialog); + erase_missing_ask->get_ok()->set_text(TTR("Remove All")); + erase_missing_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm)); + add_child(erase_missing_ask); + + erase_ask = memnew(ConfirmationDialog); + erase_ask->get_ok()->set_text(TTR("Remove")); + erase_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm)); + add_child(erase_ask); + + multi_open_ask = memnew(ConfirmationDialog); + multi_open_ask->get_ok()->set_text(TTR("Edit")); + multi_open_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects)); + add_child(multi_open_ask); + + multi_run_ask = memnew(ConfirmationDialog); + multi_run_ask->get_ok()->set_text(TTR("Run")); + multi_run_ask->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm)); + add_child(multi_run_ask); + + multi_scan_ask = memnew(ConfirmationDialog); + multi_scan_ask->get_ok()->set_text(TTR("Scan")); + add_child(multi_scan_ask); + + ask_update_settings = memnew(ConfirmationDialog); + ask_update_settings->get_ok()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings)); + add_child(ask_update_settings); + + npdialog = memnew(ProjectDialog); + npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated)); + npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created)); + add_child(npdialog); + + run_error_diag = memnew(AcceptDialog); + run_error_diag->set_title(TTR("Can't run project")); + add_child(run_error_diag); -void ProjectListFilter::_filter_option_selected(int p_idx) { - FilterOption selected = (FilterOption)(filter_option->get_selected()); - if (_current_filter != selected) { - _current_filter = selected; - if (is_inside_tree()) { - emit_signal("filter_changed"); - } - } -} + dialog_error = memnew(AcceptDialog); + add_child(dialog_error); -void ProjectListFilter::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE && has_search_box) { - search_box->set_right_icon(get_theme_icon("Search", "EditorIcons")); - search_box->set_clear_button_enabled(true); + open_templates = memnew(ConfirmationDialog); + open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?")); + open_templates->get_ok()->set_text(TTR("Open Asset Library")); + open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library)); + add_child(open_templates); } -} - -void ProjectListFilter::_bind_methods() { - ADD_SIGNAL(MethodInfo("filter_changed")); -} - -void ProjectListFilter::add_filter_option() { - filter_option = memnew(OptionButton); - filter_option->set_clip_text(true); - filter_option->connect("item_selected", callable_mp(this, &ProjectListFilter::_filter_option_selected)); - add_child(filter_option); -} -void ProjectListFilter::add_search_box() { - search_box = memnew(LineEdit); - search_box->set_placeholder(TTR("Search")); - search_box->set_tooltip( - TTR("The search box filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character.")); - search_box->connect("text_changed", callable_mp(this, &ProjectListFilter::_search_text_changed)); - search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); - add_child(search_box); + _load_recent_projects(); - has_search_box = true; -} + if (EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")) { + _scan_begin(EditorSettings::get_singleton()->get("filesystem/directories/autoscan_project_path")); + } -void ProjectListFilter::set_filter_size(int h_size) { - filter_option->set_custom_minimum_size(Size2(h_size * EDSCALE, 10 * EDSCALE)); -} + SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped)); -ProjectListFilter::ProjectListFilter() { - _current_filter = FILTER_NAME; - has_search_box = false; + OS::get_singleton()->set_low_processor_usage_mode(true); } -void ProjectListFilter::clear() { - if (has_search_box) { - search_box->clear(); +ProjectManager::~ProjectManager() { + if (EditorSettings::get_singleton()) { + EditorSettings::destroy(); } } diff --git a/editor/project_manager.h b/editor/project_manager.h index 66b38d0746..63ba25e37c 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -39,22 +39,32 @@ class ProjectDialog; class ProjectList; -class ProjectListFilter; + +enum FilterOption { + FILTER_NAME, + FILTER_PATH, + FILTER_EDIT_DATE, +}; class ProjectManager : public Control { GDCLASS(ProjectManager, Control); - Button *erase_btn; - Button *erase_missing_btn; + TabContainer *tabs; + + ProjectList *_project_list; + + LineEdit *search_box; + OptionButton *filter_option; + FilterOption _current_filter; + + Button *run_btn; Button *open_btn; Button *rename_btn; - Button *run_btn; + Button *erase_btn; + Button *erase_missing_btn; EditorAssetLibrary *asset_library; - ProjectListFilter *project_filter; - ProjectListFilter *project_order_filter; - FileDialog *scan_dir; ConfirmationDialog *language_restart_ask; ConfirmationDialog *erase_ask; @@ -64,18 +74,12 @@ class ProjectManager : public Control { ConfirmationDialog *multi_scan_ask; ConfirmationDialog *ask_update_settings; ConfirmationDialog *open_templates; + AcceptDialog *run_error_diag; AcceptDialog *dialog_error; ProjectDialog *npdialog; - HBoxContainer *projects_hb; - TabContainer *tabs; - ProjectList *_project_list; - OptionButton *language_btn; - Control *gui_base; - - bool importing; void _open_asset_library(); void _scan_projects(); @@ -94,14 +98,13 @@ class ProjectManager : public Control { void _language_selected(int p_id); void _restart_confirm(); void _exit_dialog(); - void _scan_begin(const String &p_base); - void _confirm_update_settings(); void _load_recent_projects(); void _on_project_created(const String &dir); void _on_projects_updated(); - void _update_scroll_position(const String &dir); + void _scan_multiple_folders(PackedStringArray p_files); + void _scan_begin(const String &p_base); void _scan_dir(const String &path, List<String> *r_projects); void _install_project(const String &p_zip_path, const String &p_title); @@ -109,10 +112,9 @@ class ProjectManager : public Control { void _dim_window(); void _unhandled_input(const Ref<InputEvent> &p_ev); void _files_dropped(PackedStringArray p_files, int p_screen); - void _scan_multiple_folders(PackedStringArray p_files); - void _on_order_option_changed(); - void _on_filter_option_changed(); + void _on_order_option_changed(int p_idx); + void _on_search_term_changed(const String &p_term); protected: void _notification(int p_what); @@ -123,41 +125,4 @@ public: ~ProjectManager(); }; -class ProjectListFilter : public HBoxContainer { - GDCLASS(ProjectListFilter, HBoxContainer); - -public: - enum FilterOption { - FILTER_NAME, - FILTER_PATH, - FILTER_EDIT_DATE, - }; - -private: - friend class ProjectManager; - - OptionButton *filter_option; - LineEdit *search_box; - bool has_search_box; - FilterOption _current_filter; - - void _search_text_changed(const String &p_newtext); - void _filter_option_selected(int p_idx); - -protected: - void _notification(int p_what); - static void _bind_methods(); - -public: - void _setup_filters(Vector<String> options); - void add_filter_option(); - void add_search_box(); - void set_filter_size(int h_size); - String get_search_term(); - FilterOption get_filter_option(); - void set_filter_option(FilterOption); - ProjectListFilter(); - void clear(); -}; - #endif // PROJECT_MANAGER_H diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 89feb436d8..b6621d0d1e 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -113,11 +113,17 @@ void ProjectSettingsEditor::_add_setting() { inspector->set_current_section(setting.get_slice("/", 1)); } -void ProjectSettingsEditor::_delete_setting() { +void ProjectSettingsEditor::_delete_setting(bool p_confirmed) { String setting = _get_setting_name(); Variant value = ps->get(setting); int order = ps->get_order(setting); + if (!p_confirmed) { + del_confirmation->set_text(vformat(TTR("Are you sure you want to delete '%s'?"), setting)); + del_confirmation->popup_centered(); + return; + } + undo_redo->create_action(TTR("Delete Item")); undo_redo->add_do_method(ps, "clear", setting); @@ -327,6 +333,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { header->add_child(search_bar); search_box = memnew(LineEdit); + search_box->set_placeholder(TTR("Search")); search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); search_bar->add_child(search_box); @@ -394,7 +401,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { del_button = memnew(Button); del_button->set_flat(true); - del_button->connect("pressed", callable_mp(this, &ProjectSettingsEditor::_delete_setting)); + del_button->connect("pressed", callable_mp(this, &ProjectSettingsEditor::_delete_setting), varray(false)); hbc->add_child(del_button); error_label = memnew(Label); @@ -466,6 +473,10 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { timer->set_one_shot(true); add_child(timer); + del_confirmation = memnew(ConfirmationDialog); + del_confirmation->connect("confirmed", callable_mp(this, &ProjectSettingsEditor::_delete_setting), varray(true)); + add_child(del_confirmation); + get_ok()->set_text(TTR("Close")); set_hide_on_ok(true); } diff --git a/editor/project_settings_editor.h b/editor/project_settings_editor.h index 0d7e19b242..4ecd28e514 100644 --- a/editor/project_settings_editor.h +++ b/editor/project_settings_editor.h @@ -77,6 +77,8 @@ class ProjectSettingsEditor : public AcceptDialog { OptionButton *feature_override; Label *error_label; + ConfirmationDialog *del_confirmation; + Label *restart_label; TextureRect *restart_icon; PanelContainer *restart_container; @@ -94,7 +96,7 @@ class ProjectSettingsEditor : public AcceptDialog { void _setting_edited(const String &p_name); void _setting_selected(const String &p_path); void _add_setting(); - void _delete_setting(); + void _delete_setting(bool p_confirmed); void _editor_restart_request(); void _editor_restart(); diff --git a/editor/property_selector.cpp b/editor/property_selector.cpp index c6c93fae83..27b11e4fb5 100644 --- a/editor/property_selector.cpp +++ b/editor/property_selector.cpp @@ -84,6 +84,9 @@ void PropertySelector::_update_search() { TreeItem *root = search_options->create_item(); + // Allow using spaces in place of underscores in the search string (makes the search more fault-tolerant). + const String search_text = search_box->get_text().replace(" ", "_"); + if (properties) { List<PropertyInfo> props; @@ -167,7 +170,7 @@ void PropertySelector::_update_search() { continue; } - if (search_box->get_text() != String() && E->get().name.find(search_box->get_text()) == -1) { + if (search_box->get_text() != String() && E->get().name.findn(search_text) == -1) { continue; } @@ -180,7 +183,7 @@ void PropertySelector::_update_search() { item->set_metadata(0, E->get().name); item->set_icon(0, type_icons[E->get().type]); - if (!found && search_box->get_text() != String() && E->get().name.find(search_box->get_text()) != -1) { + if (!found && search_box->get_text() != String() && E->get().name.findn(search_text) != -1) { item->select(0); found = true; } @@ -255,7 +258,7 @@ void PropertySelector::_update_search() { continue; } - if (search_box->get_text() != String() && name.find(search_box->get_text()) == -1) { + if (search_box->get_text() != String() && name.findn(search_text) == -1) { continue; } @@ -270,29 +273,29 @@ void PropertySelector::_update_search() { } else if (mi.return_val.type != Variant::NIL) { desc = Variant::get_type_name(mi.return_val.type); } else { - desc = "void "; + desc = "void"; } - desc += " " + mi.name + " ( "; + desc += vformat(" %s(", mi.name); for (int i = 0; i < mi.arguments.size(); i++) { if (i > 0) { desc += ", "; } + desc += mi.arguments[i].name; + if (mi.arguments[i].type == Variant::NIL) { - desc += "var "; + desc += ": Variant"; } else if (mi.arguments[i].name.find(":") != -1) { - desc += mi.arguments[i].name.get_slice(":", 1) + " "; + desc += vformat(": %s", mi.arguments[i].name.get_slice(":", 1)); mi.arguments[i].name = mi.arguments[i].name.get_slice(":", 0); } else { - desc += Variant::get_type_name(mi.arguments[i].type) + " "; + desc += vformat(": %s", Variant::get_type_name(mi.arguments[i].type)); } - - desc += mi.arguments[i].name; } - desc += " )"; + desc += ")"; if (E->get().flags & METHOD_FLAG_CONST) { desc += " const"; @@ -306,7 +309,7 @@ void PropertySelector::_update_search() { item->set_metadata(0, name); item->set_selectable(0, true); - if (!found && search_box->get_text() != String() && name.find(search_box->get_text()) != -1) { + if (!found && search_box->get_text() != String() && name.findn(search_text) != -1) { item->select(0); found = true; } diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index 7404c9779b..a62448169d 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -258,27 +258,35 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { int num_connections = p_node->get_persistent_signal_connection_count(); int num_groups = p_node->get_persistent_group_count(); + String msg_temp; + if (num_connections >= 1) { + Array arr; + arr.push_back(num_connections); + msg_temp += TTRN("Node has one connection.", "Node has {num} connections.", num_connections).format(arr, "{num}"); + msg_temp += " "; + } + if (num_groups >= 1) { + Array arr; + arr.push_back(num_groups); + msg_temp += TTRN("Node is in one group.", "Node is in {num} groups.", num_groups).format(arr, "{num}"); + } + if (num_connections >= 1 || num_groups >= 1) { + msg_temp += "\n" + TTR("Click to show signals dock."); + } + + Ref<Texture2D> icon_temp; + auto signal_temp = BUTTON_SIGNALS; if (num_connections >= 1 && num_groups >= 1) { - item->add_button( - 0, - get_theme_icon("SignalsAndGroups", "EditorIcons"), - BUTTON_SIGNALS, - false, - vformat(TTR("Node has %s connection(s) and %s group(s).\nClick to show signals dock."), num_connections, num_groups)); + icon_temp = get_theme_icon("SignalsAndGroups", "EditorIcons"); } else if (num_connections >= 1) { - item->add_button( - 0, - get_theme_icon("Signals", "EditorIcons"), - BUTTON_SIGNALS, - false, - vformat(TTR("Node has %s connection(s).\nClick to show signals dock."), num_connections)); + icon_temp = get_theme_icon("Signals", "EditorIcons"); } else if (num_groups >= 1) { - item->add_button( - 0, - get_theme_icon("Groups", "EditorIcons"), - BUTTON_GROUPS, - false, - vformat(TTR("Node is in %s group(s).\nClick to show groups dock."), num_groups)); + icon_temp = get_theme_icon("Groups", "EditorIcons"); + signal_temp = BUTTON_GROUPS; + } + + if (num_connections >= 1 || num_groups >= 1) { + item->add_button(0, icon_temp, signal_temp, false, msg_temp); } } diff --git a/editor/settings_config_dialog.cpp b/editor/settings_config_dialog.cpp index 9f286bd8f6..35610ef71b 100644 --- a/editor/settings_config_dialog.cpp +++ b/editor/settings_config_dialog.cpp @@ -405,6 +405,7 @@ EditorSettingsDialog::EditorSettingsDialog() { tab_general->add_child(hbc); search_box = memnew(LineEdit); + search_box->set_placeholder(TTR("Search")); search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); hbc->add_child(search_box); @@ -449,6 +450,7 @@ EditorSettingsDialog::EditorSettingsDialog() { tab_shortcuts->add_child(hbc); shortcut_search_box = memnew(LineEdit); + shortcut_search_box->set_placeholder(TTR("Search")); shortcut_search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); hbc->add_child(shortcut_search_box); shortcut_search_box->connect("text_changed", callable_mp(this, &EditorSettingsDialog::_filter_shortcuts)); diff --git a/editor/translations/extract.py b/editor/translations/extract.py index 749bad5fff..02ed65131f 100755 --- a/editor/translations/extract.py +++ b/editor/translations/extract.py @@ -33,6 +33,7 @@ matches.sort() unique_str = [] unique_loc = {} +ctx_group = {} # Store msgctx, msg, and locations. main_po = """ # LANGUAGE translation of the Godot Engine editor. # Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. @@ -52,6 +53,34 @@ msgstr "" """ +def _write_message(msgctx, msg, msg_plural, location): + global main_po + main_po += "\n#: " + location + "\n" + if msgctx != "": + main_po += 'msgctxt "' + msgctx + '"\n' + main_po += 'msgid "' + msg + '"\n' + if msg_plural != "": + main_po += 'msgid_plural "' + msg_plural + '"\n' + main_po += 'msgstr[0] ""\n' + main_po += 'msgstr[1] ""\n' + else: + main_po += 'msgstr ""\n' + + +def _add_additional_location(msgctx, msg, location): + global main_po + # Add additional location to previous occurrence + msg_pos = -1 + if msgctx != "": + msg_pos = main_po.find('\nmsgctxt "' + msgctx + '"\nmsgid "' + msg + '"') + else: + msg_pos = main_po.find('\nmsgid "' + msg + '"') + + if msg_pos == -1: + print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.") + main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:] + + def process_file(f, fname): global main_po, unique_str, unique_loc @@ -60,10 +89,11 @@ def process_file(f, fname): lc = 1 while l: - patterns = ['RTR("', 'TTR("', 'TTRC("'] + patterns = ['RTR("', 'TTR("', 'TTRC("', 'TTRN("', 'RTRN("'] idx = 0 pos = 0 while pos >= 0: + # Loop until a pattern is found. If not, next line. pos = l.find(patterns[idx], pos) if pos == -1: if idx < len(patterns) - 1: @@ -72,29 +102,64 @@ def process_file(f, fname): continue pos += len(patterns[idx]) + # Read msg until " msg = "" while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"): msg += l[pos] pos += 1 + # Read plural. + msg_plural = "" + if patterns[idx] in ['TTRN("', 'RTRN("']: + pos = l.find('"', pos + 1) + pos += 1 + while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"): + msg_plural += l[pos] + pos += 1 + + # Read context. + msgctx = "" + pos += 1 + read_ctx = False + while pos < len(l): + if l[pos] == ")": + break + elif l[pos] == '"': + read_ctx = True + break + pos += 1 + + pos += 1 + if read_ctx: + while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"): + msgctx += l[pos] + pos += 1 + + # File location. location = os.path.relpath(fname).replace("\\", "/") if line_nb: location += ":" + str(lc) - if not msg in unique_str: - main_po += "\n#: " + location + "\n" - main_po += 'msgid "' + msg + '"\n' - main_po += 'msgstr ""\n' - unique_str.append(msg) - unique_loc[msg] = [location] - elif not location in unique_loc[msg]: - # Add additional location to previous occurrence too - msg_pos = main_po.find('\nmsgid "' + msg + '"') - if msg_pos == -1: - print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.") - main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:] - unique_loc[msg].append(location) - + if msgctx != "": + # If it's a new context or a new message within an existing context, then write new msgid. + # Else add location to existing msgid. + if not msgctx in ctx_group: + _write_message(msgctx, msg, msg_plural, location) + ctx_group[msgctx] = {msg: [location]} + elif not msg in ctx_group[msgctx]: + _write_message(msgctx, msg, msg_plural, location) + ctx_group[msgctx][msg] = [location] + elif not location in ctx_group[msgctx][msg]: + _add_additional_location(msgctx, msg, location) + ctx_group[msgctx][msg].append(location) + else: + if not msg in unique_str: + _write_message(msgctx, msg, msg_plural, location) + unique_str.append(msg) + unique_loc[msg] = [location] + elif not location in unique_loc[msg]: + _add_additional_location(msgctx, msg, location) + unique_loc[msg].append(location) l = f.readline() lc += 1 @@ -102,7 +167,7 @@ def process_file(f, fname): print("Updating the editor.pot template...") for fname in matches: - with open(fname, "r") as f: + with open(fname, "r", encoding="utf8") as f: process_file(f, fname) with open("editor.pot", "w") as f: diff --git a/main/SCsub b/main/SCsub index ebadefd450..87d64e48f9 100644 --- a/main/SCsub +++ b/main/SCsub @@ -15,7 +15,9 @@ if env["tests"]: env_main.Depends("#main/splash.gen.h", "#main/splash.png") env_main.CommandNoCache( - "#main/splash.gen.h", "#main/splash.png", env.Run(main_builders.make_splash, "Building splash screen header."), + "#main/splash.gen.h", + "#main/splash.png", + env.Run(main_builders.make_splash, "Building splash screen header."), ) env_main.Depends("#main/splash_editor.gen.h", "#main/splash_editor.png") @@ -27,7 +29,9 @@ env_main.CommandNoCache( env_main.Depends("#main/app_icon.gen.h", "#main/app_icon.png") env_main.CommandNoCache( - "#main/app_icon.gen.h", "#main/app_icon.png", env.Run(main_builders.make_app_icon, "Building application icon."), + "#main/app_icon.gen.h", + "#main/app_icon.png", + env.Run(main_builders.make_app_icon, "Building application icon."), ) lib = env_main.add_library("main", env.main_sources) diff --git a/methods.py b/methods.py index 65b0e2dd6b..44e381014d 100644 --- a/methods.py +++ b/methods.py @@ -555,18 +555,15 @@ def generate_vs_project(env, num_jobs): # last double quote off, confusing MSBuild env["MSVSBUILDCOM"] = build_commandline( "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" platform=windows progress=no target=$(Configuration)" - " tools=!tools! -j" - + str(num_jobs) + " tools=!tools! -j" + str(num_jobs) ) env["MSVSREBUILDCOM"] = build_commandline( "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" platform=windows progress=no target=$(Configuration)" - " tools=!tools! vsproj=yes -j" - + str(num_jobs) + " tools=!tools! vsproj=yes -j" + str(num_jobs) ) env["MSVSCLEANCOM"] = build_commandline( "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" --clean platform=windows progress=no" - " target=$(Configuration) tools=!tools! -j" - + str(num_jobs) + " target=$(Configuration) tools=!tools! -j" + str(num_jobs) ) # This version information (Win32, x64, Debug, Release, Release_Debug seems to be diff --git a/misc/dist/osx_tools.app/Contents/Info.plist b/misc/dist/osx_tools.app/Contents/Info.plist index fd62bc0ef5..508586904c 100755 --- a/misc/dist/osx_tools.app/Contents/Info.plist +++ b/misc/dist/osx_tools.app/Contents/Info.plist @@ -111,7 +111,7 @@ <string>GDScript.icns</string> <key>UTTypeConformsTo</key> <array> - <string>public.data</string> + <string>public.script</string> </array> <key>UTTypeTagSpecification</key> <dict> diff --git a/modules/assimp/editor_scene_importer_assimp.cpp b/modules/assimp/editor_scene_importer_assimp.cpp index aedc4b690a..e5becfd559 100644 --- a/modules/assimp/editor_scene_importer_assimp.cpp +++ b/modules/assimp/editor_scene_importer_assimp.cpp @@ -441,7 +441,6 @@ EditorSceneImporterAssimp::_generate_scene(const String &p_path, aiScene *scene, Transform pform = AssimpUtils::assimp_matrix_transform(bone->mNode->mTransformation); skeleton->add_bone(bone_name); skeleton->set_bone_rest(boneIdx, pform); - skeleton->set_bone_pose(boneIdx, pform); if (parent_node != nullptr) { int parent_bone_id = skeleton->find_bone(AssimpUtils::get_anim_string_from_assimp(parent_node->mName)); @@ -612,7 +611,7 @@ void EditorSceneImporterAssimp::_insert_animation_track(ImportState &scene, cons xform.basis.set_quat_scale(rot, scale); xform.origin = pos; - xform = skeleton->get_bone_pose(skeleton_bone).inverse() * xform; + xform = skeleton->get_bone_rest(skeleton_bone).inverse() * xform; rot = xform.basis.get_rotation_quat(); rot.normalize(); diff --git a/modules/bmp/image_loader_bmp.cpp b/modules/bmp/image_loader_bmp.cpp index ac4e534983..757afeb9e3 100644 --- a/modules/bmp/image_loader_bmp.cpp +++ b/modules/bmp/image_loader_bmp.cpp @@ -51,16 +51,20 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, if (bits_per_pixel == 1) { // Requires bit unpacking... - ERR_FAIL_COND_V(width % 8 != 0, ERR_UNAVAILABLE); - ERR_FAIL_COND_V(height % 8 != 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V_MSG(width % 8 != 0, ERR_UNAVAILABLE, + vformat("1-bpp BMP images must have a width that is a multiple of 8, but the imported BMP is %d pixels wide.", int(width))); + ERR_FAIL_COND_V_MSG(height % 8 != 0, ERR_UNAVAILABLE, + vformat("1-bpp BMP images must have a height that is a multiple of 8, but the imported BMP is %d pixels tall.", int(height))); } else if (bits_per_pixel == 4) { // Requires bit unpacking... - ERR_FAIL_COND_V(width % 2 != 0, ERR_UNAVAILABLE); - ERR_FAIL_COND_V(height % 2 != 0, ERR_UNAVAILABLE); + ERR_FAIL_COND_V_MSG(width % 2 != 0, ERR_UNAVAILABLE, + vformat("4-bpp BMP images must have a width that is a multiple of 2, but the imported BMP is %d pixels wide.", int(width))); + ERR_FAIL_COND_V_MSG(height % 2 != 0, ERR_UNAVAILABLE, + vformat("4-bpp BMP images must have a height that is a multiple of 2, but the imported BMP is %d pixels tall.", int(height))); } else if (bits_per_pixel == 16) { - ERR_FAIL_V(ERR_UNAVAILABLE); + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "16-bpp BMP images are not supported."); } // Image data (might be indexed) @@ -72,7 +76,7 @@ Error ImageLoaderBMP::convert_to_image(Ref<Image> p_image, } else { // color data_len = width * height * 4; } - ERR_FAIL_COND_V(data_len == 0, ERR_BUG); + ERR_FAIL_COND_V_MSG(data_len == 0, ERR_BUG, "Couldn't parse the BMP image data."); err = data.resize(data_len); uint8_t *data_w = data.ptrw(); @@ -215,13 +219,15 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, // Info Header bmp_header.bmp_info_header.bmp_header_size = f->get_32(); - ERR_FAIL_COND_V(bmp_header.bmp_info_header.bmp_header_size < BITMAP_INFO_HEADER_MIN_SIZE, ERR_FILE_CORRUPT); + ERR_FAIL_COND_V_MSG(bmp_header.bmp_info_header.bmp_header_size < BITMAP_INFO_HEADER_MIN_SIZE, ERR_FILE_CORRUPT, + vformat("Couldn't parse the BMP info header. The file is likely corrupt: %s", f->get_path())); bmp_header.bmp_info_header.bmp_width = f->get_32(); bmp_header.bmp_info_header.bmp_height = f->get_32(); bmp_header.bmp_info_header.bmp_planes = f->get_16(); - ERR_FAIL_COND_V(bmp_header.bmp_info_header.bmp_planes != 1, ERR_FILE_CORRUPT); + ERR_FAIL_COND_V_MSG(bmp_header.bmp_info_header.bmp_planes != 1, ERR_FILE_CORRUPT, + vformat("Couldn't parse the BMP planes. The file is likely corrupt: %s", f->get_path())); bmp_header.bmp_info_header.bmp_bit_count = f->get_16(); bmp_header.bmp_info_header.bmp_compression = f->get_32(); @@ -236,10 +242,10 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, case BI_RLE4: case BI_CMYKRLE8: case BI_CMYKRLE4: { - // Stop parsing - String bmp_path = f->get_path(); + // Stop parsing. f->close(); - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Compressed BMP files are not supported: " + bmp_path + "."); + ERR_FAIL_V_MSG(ERR_UNAVAILABLE, + vformat("Compressed BMP files are not supported: %s", f->get_path())); } break; } // Don't rely on sizeof(bmp_file_header) as structure padding @@ -255,7 +261,8 @@ Error ImageLoaderBMP::load_image(Ref<Image> p_image, FileAccess *f, if (bmp_header.bmp_info_header.bmp_bit_count <= 8) { // Support 256 colors max color_table_size = 1 << bmp_header.bmp_info_header.bmp_bit_count; - ERR_FAIL_COND_V(color_table_size == 0, ERR_BUG); + ERR_FAIL_COND_V_MSG(color_table_size == 0, ERR_BUG, + vformat("Couldn't parse the BMP color table: %s", f->get_path())); } Vector<uint8_t> bmp_color_table; diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 9e40a69712..e528fc6623 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -167,6 +167,7 @@ i = ceil(1.45) # i is 2 i = ceil(1.001) # i is 2 [/codeblock] + See also [method floor], [method round], and [method stepify]. </description> </method> <method name="char"> @@ -338,6 +339,7 @@ # a is -3.0 a = floor(-2.99) [/codeblock] + See also [method ceil], [method round], and [method stepify]. [b]Note:[/b] This method returns a float. If you need an integer, you can use [code]int(s)[/code] directly. </description> </method> @@ -1043,6 +1045,7 @@ [codeblock] round(2.6) # Returns 3 [/codeblock] + See also [method floor], [method ceil], and [method stepify]. </description> </method> <method name="seed"> @@ -1161,6 +1164,7 @@ stepify(100, 32) # Returns 96 stepify(3.14159, 0.01) # Returns 3.14 [/codeblock] + See also [method ceil], [method floor], and [method round]. </description> </method> <method name="str" qualifiers="vararg"> diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp index 6d454e43f2..944ed859f5 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.cpp @@ -37,9 +37,11 @@ void GDScriptEditorTranslationParserPlugin::get_recognized_extensions(List<Strin GDScriptLanguage::get_singleton()->get_recognized_extensions(r_extensions); } -Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) { - // Parse and match all GDScript function API that involves translation string. - // E.g get_node("Label").text = "something", var test = tr("something"), "something" will be matched and collected. +Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) { + // Extract all translatable strings using the parsed tree from GDSriptParser. + // The strategy is to find all ExpressionNode and AssignmentNode from the tree and extract strings if relevant, i.e + // Search strings in ExpressionNode -> CallNode -> tr(), set_text(), set_placeholder() etc. + // Search strings in AssignmentNode -> text = "__", hint_tooltip = "__" etc. Error err; RES loaded_res = ResourceLoader::load(p_path, "", false, &err); @@ -48,108 +50,302 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve return err; } + ids = r_ids; + ids_ctx_plural = r_ids_ctx_plural; Ref<GDScript> gdscript = loaded_res; String source_code = gdscript->get_source_code(); - Vector<String> parsed_strings; - - // Search translation strings with RegEx. - regex.clear(); - regex.compile(String("|").join(patterns)); - Array results = regex.search_all(source_code); - _get_captured_strings(results, &parsed_strings); - - // Special handling for FileDialog. - Vector<String> temp; - _parse_file_dialog(source_code, &temp); - parsed_strings.append_array(temp); - - // Filter out / and + - String filter = "(?:\\\\\\n|\"[\\s\\\\]*\\+\\s*\")"; - regex.clear(); - regex.compile(filter); - for (int i = 0; i < parsed_strings.size(); i++) { - parsed_strings.set(i, regex.sub(parsed_strings[i], "", true)); + + GDScriptParser parser; + err = parser.parse(source_code, p_path, false); + if (err != OK) { + ERR_PRINT("Failed to parse with GDScript with GDScriptParser."); + return err; } - r_extracted_strings->append_array(parsed_strings); + // Traverse through the parsed tree from GDScriptParser. + GDScriptParser::ClassNode *c = parser.get_tree(); + _traverse_class(c); return OK; } -void GDScriptEditorTranslationParserPlugin::_parse_file_dialog(const String &p_source_code, Vector<String> *r_output) { - // FileDialog API has the form .filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]). - // First filter: Get "*.png ; PNG Images", "*.gd ; GDScript Files" from PackedStringArray. - regex.clear(); - regex.compile(String("|").join(file_dialog_patterns)); - Array results = regex.search_all(p_source_code); - - Vector<String> temp; - _get_captured_strings(results, &temp); - String captured_strings = String(",").join(temp); - - // Second filter: Get the texts after semicolon from "*.png ; PNG Images","*.gd ; GDScript Files". - String second_filter = "\"[^;]+;" + text + "\""; - regex.clear(); - regex.compile(second_filter); - results = regex.search_all(captured_strings); - _get_captured_strings(results, r_output); - for (int i = 0; i < r_output->size(); i++) { - r_output->set(i, r_output->get(i).strip_edges()); +void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) { + for (int i = 0; i < p_class->members.size(); i++) { + const GDScriptParser::ClassNode::Member &m = p_class->members[i]; + // There are 7 types of Member, but only class, function and variable can contain translatable strings. + switch (m.type) { + case GDScriptParser::ClassNode::Member::CLASS: + _traverse_class(m.m_class); + break; + case GDScriptParser::ClassNode::Member::FUNCTION: + _traverse_function(m.function); + break; + case GDScriptParser::ClassNode::Member::VARIABLE: + _read_variable(m.variable); + break; + default: + break; + } + } +} + +void GDScriptEditorTranslationParserPlugin::_traverse_function(const GDScriptParser::FunctionNode *p_func) { + _traverse_block(p_func->body); +} + +void GDScriptEditorTranslationParserPlugin::_read_variable(const GDScriptParser::VariableNode *p_var) { + _assess_expression(p_var->initializer); +} + +void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser::SuiteNode *p_suite) { + if (!p_suite) { + return; + } + + const Vector<GDScriptParser::Node *> &statements = p_suite->statements; + for (int i = 0; i < statements.size(); i++) { + GDScriptParser::Node *statement = statements[i]; + + // Statements with Node type constant, break, continue, pass, breakpoint are skipped because they can't contain translatable strings. + switch (statement->type) { + case GDScriptParser::Node::VARIABLE: + _assess_expression(static_cast<GDScriptParser::VariableNode *>(statement)->initializer); + break; + case GDScriptParser::Node::IF: { + GDScriptParser::IfNode *if_node = static_cast<GDScriptParser::IfNode *>(statement); + _assess_expression(if_node->condition); + //FIXME : if the elif logic is changed in GDScriptParser, then this probably will have to change as well. See GDScriptParser::TreePrinter::print_if(). + _traverse_block(if_node->true_block); + _traverse_block(if_node->false_block); + break; + } + case GDScriptParser::Node::FOR: { + GDScriptParser::ForNode *for_node = static_cast<GDScriptParser::ForNode *>(statement); + _assess_expression(for_node->list); + _traverse_block(for_node->loop); + break; + } + case GDScriptParser::Node::WHILE: { + GDScriptParser::WhileNode *while_node = static_cast<GDScriptParser::WhileNode *>(statement); + _assess_expression(while_node->condition); + _traverse_block(while_node->loop); + break; + } + case GDScriptParser::Node::MATCH: { + GDScriptParser::MatchNode *match_node = static_cast<GDScriptParser::MatchNode *>(statement); + _assess_expression(match_node->test); + for (int j = 0; j < match_node->branches.size(); j++) { + _traverse_block(match_node->branches[j]->block); + } + break; + } + case GDScriptParser::Node::RETURN: + _assess_expression(static_cast<GDScriptParser::ReturnNode *>(statement)->return_value); + break; + case GDScriptParser::Node::ASSERT: + _assess_expression((static_cast<GDScriptParser::AssertNode *>(statement))->condition); + break; + case GDScriptParser::Node::ASSIGNMENT: + _assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(statement)); + break; + default: + if (statement->is_expression()) { + _assess_expression(static_cast<GDScriptParser::ExpressionNode *>(statement)); + } + break; + } + } +} + +void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::ExpressionNode *p_expression) { + // Explore all ExpressionNodes to find CallNodes which contain translation strings, such as tr(), set_text() etc. + // tr() can be embedded quite deep within multiple ExpressionNodes so need to dig down to search through all ExpressionNodes. + if (!p_expression) { + return; + } + + // ExpressionNode of type await, cast, get_node, identifier, literal, preload, self, subscript, unary are ignored as they can't be CallNode + // containing translation strings. + switch (p_expression->type) { + case GDScriptParser::Node::ARRAY: { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(p_expression); + for (int i = 0; i < array_node->elements.size(); i++) { + _assess_expression(array_node->elements[i]); + } + break; + } + case GDScriptParser::Node::ASSIGNMENT: + _assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(p_expression)); + break; + case GDScriptParser::Node::BINARY_OPERATOR: { + GDScriptParser::BinaryOpNode *binary_op_node = static_cast<GDScriptParser::BinaryOpNode *>(p_expression); + _assess_expression(binary_op_node->left_operand); + _assess_expression(binary_op_node->right_operand); + break; + } + case GDScriptParser::Node::CALL: { + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_expression); + _extract_from_call(call_node); + for (int i = 0; i < call_node->arguments.size(); i++) { + _assess_expression(call_node->arguments[i]); + } + } break; + case GDScriptParser::Node::DICTIONARY: { + GDScriptParser::DictionaryNode *dict_node = static_cast<GDScriptParser::DictionaryNode *>(p_expression); + for (int i = 0; i < dict_node->elements.size(); i++) { + _assess_expression(dict_node->elements[i].key); + _assess_expression(dict_node->elements[i].value); + } + break; + } + case GDScriptParser::Node::TERNARY_OPERATOR: { + GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<GDScriptParser::TernaryOpNode *>(p_expression); + _assess_expression(ternary_op_node->condition); + _assess_expression(ternary_op_node->true_expr); + _assess_expression(ternary_op_node->false_expr); + break; + } + default: + break; + } +} + +void GDScriptEditorTranslationParserPlugin::_assess_assignment(GDScriptParser::AssignmentNode *p_assignment) { + // Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____" + + StringName assignee_name; + if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) { + assignee_name = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name; + } else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) { + assignee_name = static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee)->attribute->name; + } + + if (assignment_patterns.has(assignee_name) && p_assignment->assigned_value->type == GDScriptParser::Node::LITERAL) { + // If the assignment is towards one of the extract patterns (text, hint_tooltip etc.), and the value is a string literal, we collect the string. + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_assignment->assigned_value)->value); + } else if (assignee_name == fd_filters && p_assignment->assigned_value->type == GDScriptParser::Node::CALL) { + // FileDialog.filters accepts assignment in the form of PackedStringArray. For example, + // get_node("FileDialog").filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]). + + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_assignment->assigned_value); + if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]); + + // Extract the name in "extension ; name" of PackedStringArray. + for (int i = 0; i < array_node->elements.size(); i++) { + _extract_fd_literals(array_node->elements[i]); + } + } + } else { + // If the assignee is not in extract patterns or the assigned_value is not Literal type, try to see if the assigned_value contains tr(). + _assess_expression(p_assignment->assigned_value); } } -void GDScriptEditorTranslationParserPlugin::_get_captured_strings(const Array &p_results, Vector<String> *r_output) { - Ref<RegExMatch> result; - for (int i = 0; i < p_results.size(); i++) { - result = p_results[i]; - for (int j = 0; j < result->get_group_count(); j++) { - String s = result->get_string(j + 1); - // Prevent reading text with only spaces. - if (!s.strip_edges().empty()) { - r_output->push_back(s); +void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::CallNode *p_call) { + // Extract the translatable strings coming from function calls. For example: + // tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____"). + + StringName function_name = p_call->function_name; + + // Variables for extracting tr() and tr_n(). + Vector<String> id_ctx_plural; + id_ctx_plural.resize(3); + bool extract_id_ctx_plural = true; + + if (function_name == tr_func) { + // Extract from tr(id, ctx). + for (int i = 0; i < p_call->arguments.size(); i++) { + if (p_call->arguments[i]->type == GDScriptParser::Node::LITERAL) { + id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[i])->value; + } else { + // Avoid adding something like tr("Flying dragon", var_context_level_1). We want to extract both id and context together. + extract_id_ctx_plural = false; + } + } + if (extract_id_ctx_plural) { + ids_ctx_plural->push_back(id_ctx_plural); + } + } else if (function_name == trn_func) { + // Extract from tr_n(id, plural, n, ctx). + Vector<int> indices; + indices.push_back(0); + indices.push_back(3); + indices.push_back(1); + for (int i = 0; i < indices.size(); i++) { + if (indices[i] >= p_call->arguments.size()) { + continue; + } + + if (p_call->arguments[indices[i]]->type == GDScriptParser::Node::LITERAL) { + id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[indices[i]])->value; + } else { + extract_id_ctx_plural = false; + } + } + if (extract_id_ctx_plural) { + ids_ctx_plural->push_back(id_ctx_plural); + } + } else if (first_arg_patterns.has(function_name)) { + // Extracting argument with only string literals. In other words, not extracting something like set_text("hello " + some_var). + if (p_call->arguments[0]->type == GDScriptParser::Node::LITERAL) { + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[0])->value); + } + } else if (second_arg_patterns.has(function_name)) { + if (p_call->arguments[1]->type == GDScriptParser::Node::LITERAL) { + ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[1])->value); + } + } else if (function_name == fd_add_filter) { + // Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images"). + _extract_fd_literals(p_call->arguments[0]); + + } else if (function_name == fd_set_filter && p_call->arguments[0]->type == GDScriptParser::Node::CALL) { + // FileDialog.set_filters() accepts assignment in the form of PackedStringArray. For example, + // get_node("FileDialog").set_filters( PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])). + + GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_call->arguments[0]); + if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) { + GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]); + for (int i = 0; i < array_node->elements.size(); i++) { + _extract_fd_literals(array_node->elements[i]); } } } } +void GDScriptEditorTranslationParserPlugin::_extract_fd_literals(GDScriptParser::ExpressionNode *p_expression) { + // Extract the name in "extension ; name". + + if (p_expression->type == GDScriptParser::Node::LITERAL) { + String arg_val = String(static_cast<GDScriptParser::LiteralNode *>(p_expression)->value); + PackedStringArray arr = arg_val.split(";", true); + if (arr.size() != 2) { + ERR_PRINT("Argument for setting FileDialog has bad format."); + return; + } + ids->push_back(arr[1].strip_edges()); + } +} + GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() { - // Regex search pattern templates. - // The extra complication in the regex pattern is to ensure that the matching works when users write over multiple lines, use tabs etc. - const String dot = "\\.[\\s\\\\]*"; - const String str_assign_template = "[\\s\\\\]*=[\\s\\\\]*\"" + text + "\""; - const String first_arg_template = "[\\s\\\\]*\\([\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)"; - const String second_arg_template = "[\\s\\\\]*\\([\\s\\S]+?,[\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)"; - - // Common patterns. - patterns.push_back("tr" + first_arg_template); - patterns.push_back(dot + "text" + str_assign_template); - patterns.push_back(dot + "placeholder_text" + str_assign_template); - patterns.push_back(dot + "hint_tooltip" + str_assign_template); - patterns.push_back(dot + "set_text" + first_arg_template); - patterns.push_back(dot + "set_tooltip" + first_arg_template); - patterns.push_back(dot + "set_placeholder" + first_arg_template); - - // Tabs and TabContainer API. - patterns.push_back(dot + "set_tab_title" + second_arg_template); - patterns.push_back(dot + "add_tab" + first_arg_template); - - // PopupMenu API. - patterns.push_back(dot + "add_check_item" + first_arg_template); - patterns.push_back(dot + "add_icon_check_item" + second_arg_template); - patterns.push_back(dot + "add_icon_item" + second_arg_template); - patterns.push_back(dot + "add_icon_radio_check_item" + second_arg_template); - patterns.push_back(dot + "add_item" + first_arg_template); - patterns.push_back(dot + "add_multistate_item" + first_arg_template); - patterns.push_back(dot + "add_radio_check_item" + first_arg_template); - patterns.push_back(dot + "add_separator" + first_arg_template); - patterns.push_back(dot + "add_submenu_item" + first_arg_template); - patterns.push_back(dot + "set_item_text" + second_arg_template); - //patterns.push_back(dot + "set_item_tooltip" + second_arg_template); //no tr() behind this function. might be bug. - - // FileDialog API - special case. - const String fd_text = "((?:[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*\"[\\s\\\\]*,?)*)"; - const String packed_string_array = "[\\s\\\\]*PackedStringArray[\\s\\\\]*\\([\\s\\\\]*\\[" + fd_text + "\\][\\s\\\\]*\\)"; - file_dialog_patterns.push_back(dot + "add_filter[\\s\\\\]*\\(" + fd_text + "[\\s\\\\]*\\)"); - file_dialog_patterns.push_back(dot + "filters[\\s\\\\]*=" + packed_string_array); - file_dialog_patterns.push_back(dot + "set_filters[\\s\\\\]*\\(" + packed_string_array + "[\\s\\\\]*\\)"); + assignment_patterns.insert("text"); + assignment_patterns.insert("placeholder_text"); + assignment_patterns.insert("hint_tooltip"); + + first_arg_patterns.insert("set_text"); + first_arg_patterns.insert("set_tooltip"); + first_arg_patterns.insert("set_placeholder"); + first_arg_patterns.insert("add_tab"); + first_arg_patterns.insert("add_check_item"); + first_arg_patterns.insert("add_item"); + first_arg_patterns.insert("add_multistate_item"); + first_arg_patterns.insert("add_radio_check_item"); + first_arg_patterns.insert("add_separator"); + first_arg_patterns.insert("add_submenu_item"); + + second_arg_patterns.insert("set_tab_title"); + second_arg_patterns.insert("add_icon_check_item"); + second_arg_patterns.insert("add_icon_item"); + second_arg_patterns.insert("add_icon_radio_check_item"); + second_arg_patterns.insert("set_item_text"); } diff --git a/modules/gdscript/editor/gdscript_translation_parser_plugin.h b/modules/gdscript/editor/gdscript_translation_parser_plugin.h index 9fa4b69f01..5ea416d4cc 100644 --- a/modules/gdscript/editor/gdscript_translation_parser_plugin.h +++ b/modules/gdscript/editor/gdscript_translation_parser_plugin.h @@ -31,23 +31,40 @@ #ifndef GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H #define GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H +#include "core/set.h" #include "editor/editor_translation_parser.h" +#include "modules/gdscript/gdscript_parser.h" #include "modules/regex/regex.h" class GDScriptEditorTranslationParserPlugin : public EditorTranslationParserPlugin { GDCLASS(GDScriptEditorTranslationParserPlugin, EditorTranslationParserPlugin); - // Regex and search patterns that are used to match translation strings. - const String text = "((?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*)"; - RegEx regex; - Vector<String> patterns; - Vector<String> file_dialog_patterns; + Vector<String> *ids; + Vector<Vector<String>> *ids_ctx_plural; - void _parse_file_dialog(const String &p_source_code, Vector<String> *r_output); - void _get_captured_strings(const Array &p_results, Vector<String> *r_output); + // List of patterns used for extracting translation strings. + StringName tr_func = "tr"; + StringName trn_func = "tr_n"; + Set<StringName> assignment_patterns; + Set<StringName> first_arg_patterns; + Set<StringName> second_arg_patterns; + // FileDialog patterns. + StringName fd_add_filter = "add_filter"; + StringName fd_set_filter = "set_filters"; + StringName fd_filters = "filters"; + + void _traverse_class(const GDScriptParser::ClassNode *p_class); + void _traverse_function(const GDScriptParser::FunctionNode *p_func); + void _traverse_block(const GDScriptParser::SuiteNode *p_suite); + + void _read_variable(const GDScriptParser::VariableNode *p_var); + void _assess_expression(GDScriptParser::ExpressionNode *p_expression); + void _assess_assignment(GDScriptParser::AssignmentNode *p_assignment); + void _extract_from_call(GDScriptParser::CallNode *p_call); + void _extract_fd_literals(GDScriptParser::ExpressionNode *p_expression); public: - virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings) override; + virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override; virtual void get_recognized_extensions(List<String> *r_extensions) const override; GDScriptEditorTranslationParserPlugin(); diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 9170255c02..0263e32c5b 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -596,6 +596,19 @@ Error GDScript::reload(bool p_keep_state) { return OK; } + { + String source_path = path; + if (source_path.empty()) { + source_path = get_path(); + } + if (!source_path.empty()) { + MutexLock lock(GDScriptCache::singleton->lock); + if (!GDScriptCache::singleton->shallow_gdscript_cache.has(source_path)) { + GDScriptCache::singleton->shallow_gdscript_cache[source_path] = this; + } + } + } + valid = false; GDScriptParser parser; Error err = parser.parse(source, path, false); diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 9906b4014d..79317ff846 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -69,6 +69,7 @@ class GDScript : public Script { friend class GDScriptInstance; friend class GDScriptFunction; + friend class GDScriptAnalyzer; friend class GDScriptCompiler; friend class GDScriptFunctions; friend class GDScriptLanguage; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 561cdbbda4..cabe096579 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -33,6 +33,7 @@ #include "core/class_db.h" #include "core/hash_map.h" #include "core/io/resource_loader.h" +#include "core/os/file_access.h" #include "core/project_settings.h" #include "core/script_language.h" #include "gdscript.h" @@ -181,7 +182,7 @@ Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, return ERR_PARSE_ERROR; } - Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + Error err = parser->raise_status(GDScriptParserRef::INTERFACE_SOLVED); if (err != OK) { push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", p_class->extends_path), p_class); return err; @@ -207,7 +208,7 @@ Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, return ERR_PARSE_ERROR; } - Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + Error err = parser->raise_status(GDScriptParserRef::INTERFACE_SOLVED); if (err != OK) { push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), p_class); return err; @@ -226,7 +227,7 @@ Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, return ERR_PARSE_ERROR; } - Error err = parser->raise_status(GDScriptParserRef::INHERITANCE_SOLVED); + Error err = parser->raise_status(GDScriptParserRef::INTERFACE_SOLVED); if (err != OK) { push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), p_class); return err; @@ -301,6 +302,16 @@ Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, return ERR_PARSE_ERROR; } + // Check for cyclic inheritance. + const GDScriptParser::ClassNode *base_class = result.class_type; + while (base_class) { + if (base_class->fqcn == p_class->fqcn) { + push_error("Cyclic inheritance.", p_class); + return ERR_PARSE_ERROR; + } + base_class = base_class->base_type.class_type; + } + p_class->base_type = result; class_type.native_type = result.native_type; p_class->set_datatype(class_type); @@ -308,7 +319,10 @@ Error GDScriptAnalyzer::resolve_inheritance(GDScriptParser::ClassNode *p_class, if (p_recursive) { for (int i = 0; i < p_class->members.size(); i++) { if (p_class->members[i].type == GDScriptParser::ClassNode::Member::CLASS) { - resolve_inheritance(p_class->members[i].m_class, true); + Error err = resolve_inheritance(p_class->members[i].m_class, true); + if (err) { + return err; + } } } } @@ -498,7 +512,13 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas if (member.variable->initializer != nullptr) { if (!is_type_compatible(datatype, member.variable->initializer->get_datatype(), true)) { - push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", member.variable->initializer->get_datatype().to_string(), datatype.to_string()), member.variable->initializer); + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(member.variable->initializer->get_datatype(), datatype, true)) { + push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", member.variable->initializer->get_datatype().to_string(), datatype.to_string()), member.variable->initializer); + } else { + // TODO: Add warning. + mark_node_unsafe(member.variable->initializer); + } } else if (datatype.builtin_type == Variant::INT && member.variable->initializer->get_datatype().builtin_type == Variant::FLOAT) { #ifdef DEBUG_ENABLED parser->push_warning(member.variable->initializer, GDScriptWarning::NARROWING_CONVERSION); @@ -533,6 +553,7 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas break; case GDScriptParser::DataType::NATIVE: if (ClassDB::is_parent_class(get_real_class_name(datatype.native_type), "Resource")) { + member.variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE; member.variable->export_info.hint_string = get_real_class_name(datatype.native_type); } else { push_error(R"(Export type can only be built-in or a resource.)", member.variable); @@ -596,17 +617,67 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas enum_type.is_meta_type = true; enum_type.is_constant = true; + // Enums can't be nested, so we can safely override this. + current_enum = member.m_enum; + for (int j = 0; j < member.m_enum->values.size(); j++) { - enum_type.enum_values[member.m_enum->values[j].identifier->name] = member.m_enum->values[j].value; + GDScriptParser::EnumNode::Value &element = member.m_enum->values.write[j]; + + if (element.custom_value) { + reduce_expression(element.custom_value); + if (!element.custom_value->is_constant) { + push_error(R"(Enum values must be constant.)", element.custom_value); + } else if (element.custom_value->reduced_value.get_type() != Variant::INT) { + push_error(R"(Enum values must be integers.)", element.custom_value); + } else { + element.value = element.custom_value->reduced_value; + element.resolved = true; + } + } else { + if (element.index > 0) { + element.value = element.parent_enum->values[element.index - 1].value + 1; + } else { + element.value = 0; + } + element.resolved = true; + } + + enum_type.enum_values[element.identifier->name] = element.value; } + current_enum = nullptr; + member.m_enum->set_datatype(enum_type); } break; case GDScriptParser::ClassNode::Member::FUNCTION: resolve_function_signature(member.function); break; - case GDScriptParser::ClassNode::Member::ENUM_VALUE: - break; // Nothing to do, type and value set in parser. + case GDScriptParser::ClassNode::Member::ENUM_VALUE: { + if (member.enum_value.custom_value) { + current_enum = member.enum_value.parent_enum; + reduce_expression(member.enum_value.custom_value); + current_enum = nullptr; + + if (!member.enum_value.custom_value->is_constant) { + push_error(R"(Enum values must be constant.)", member.enum_value.custom_value); + } else if (member.enum_value.custom_value->reduced_value.get_type() != Variant::INT) { + push_error(R"(Enum values must be integers.)", member.enum_value.custom_value); + } else { + member.enum_value.value = member.enum_value.custom_value->reduced_value; + member.enum_value.resolved = true; + } + } else { + if (member.enum_value.index > 0) { + member.enum_value.value = member.enum_value.parent_enum->values[member.enum_value.index - 1].value + 1; + } else { + member.enum_value.value = 0; + } + member.enum_value.resolved = true; + } + // Also update the original references. + member.enum_value.parent_enum->values.write[member.enum_value.index] = member.enum_value; + p_class->members.write[i].enum_value = member.enum_value; + } break; case GDScriptParser::ClassNode::Member::CLASS: break; // Done later. case GDScriptParser::ClassNode::Member::UNDEFINED: @@ -994,7 +1065,13 @@ void GDScriptAnalyzer::resolve_variable(GDScriptParser::VariableNode *p_variable if (p_variable->initializer != nullptr) { if (!is_type_compatible(type, p_variable->initializer->get_datatype(), true)) { - push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", p_variable->initializer->get_datatype().to_string(), type.to_string()), p_variable->initializer); + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(p_variable->initializer->get_datatype(), type, true)) { + push_error(vformat(R"(Value of type "%s" cannot be assigned to a variable of type "%s".)", p_variable->initializer->get_datatype().to_string(), type.to_string()), p_variable->initializer); + } else { + // TODO: Add warning. + mark_node_unsafe(p_variable->initializer); + } #ifdef DEBUG_ENABLED } else if (type.builtin_type == Variant::INT && p_variable->initializer->get_datatype().builtin_type == Variant::FLOAT) { parser->push_warning(p_variable->initializer, GDScriptWarning::NARROWING_CONVERSION); @@ -1385,9 +1462,16 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig compatible = is_type_compatible(p_assignment->assignee->get_datatype(), op_type, true); if (!compatible) { if (p_assignment->assignee->get_datatype().is_hard_type()) { - push_error(vformat(R"(Cannot assign a value of type "%s" to a target of type "%s".)", p_assignment->assigned_value->get_datatype().to_string(), p_assignment->assignee->get_datatype().to_string()), p_assignment->assigned_value); + // Try reverse test since it can be a masked subtype. + if (!is_type_compatible(op_type, p_assignment->assignee->get_datatype(), true)) { + push_error(vformat(R"(Cannot assign a value of type "%s" to a target of type "%s".)", p_assignment->assigned_value->get_datatype().to_string(), p_assignment->assignee->get_datatype().to_string()), p_assignment->assigned_value); + } else { + // TODO: Add warning. + mark_node_unsafe(p_assignment); + } } else { // TODO: Warning in this case. + mark_node_unsafe(p_assignment); } } } else { @@ -1634,6 +1718,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool is_awa } signature += p_call->arguments[i]->get_datatype().to_string(); } + signature += ")"; push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call->callee); } break; case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS: @@ -1684,7 +1769,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool is_awa for (int i = 0; i < p_call->arguments.size(); i++) { GDScriptParser::DataType par_type = type_from_property(info.arguments[i]); - if (!is_type_compatible(par_type, p_call->arguments[i]->get_datatype())) { + if (!is_type_compatible(par_type, p_call->arguments[i]->get_datatype(), true)) { types_match = false; break; #ifdef DEBUG_ENABLED @@ -1711,6 +1796,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool is_awa } signature += p_call->arguments[i]->get_datatype().to_string(); } + signature += ")"; push_error(vformat(R"(No constructor of "%s" matches the signature "%s".)", Variant::get_type_name(builtin_type), signature), p_call); } } @@ -1889,6 +1975,8 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) { void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dictionary) { bool all_is_constant = true; + HashMap<Variant, GDScriptParser::ExpressionNode *, VariantHasher, VariantComparator> elements; + for (int i = 0; i < p_dictionary->elements.size(); i++) { const GDScriptParser::DictionaryNode::Pair &element = p_dictionary->elements[i]; if (p_dictionary->style == GDScriptParser::DictionaryNode::PYTHON_DICT) { @@ -1896,6 +1984,14 @@ void GDScriptAnalyzer::reduce_dictionary(GDScriptParser::DictionaryNode *p_dicti } reduce_expression(element.value); all_is_constant = all_is_constant && element.key->is_constant && element.value->is_constant; + + if (element.key->is_constant) { + if (elements.has(element.key->reduced_value)) { + push_error(vformat(R"(Key "%s" was already used in this dictionary (at line %d).)", element.key->reduced_value, elements[element.key->reduced_value]->start_line), element.key); + } else { + elements[element.key->reduced_value] = element.value; + } + } } if (all_is_constant) { @@ -2104,6 +2200,33 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_identifier, bool can_be_builtin) { // TODO: This is opportunity to further infer types. + + // Check if we are inside and enum. This allows enum values to access other elements of the same enum. + if (current_enum) { + for (int i = 0; i < current_enum->values.size(); i++) { + const GDScriptParser::EnumNode::Value &element = current_enum->values[i]; + if (element.identifier->name == p_identifier->name) { + GDScriptParser::DataType type; + type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + type.kind = element.parent_enum->identifier ? GDScriptParser::DataType::ENUM_VALUE : GDScriptParser::DataType::BUILTIN; + type.builtin_type = Variant::INT; + type.is_constant = true; + if (element.parent_enum->identifier) { + type.enum_type = element.parent_enum->identifier->name; + } + p_identifier->set_datatype(type); + + if (element.resolved) { + p_identifier->is_constant = true; + p_identifier->reduced_value = element.value; + } else { + push_error(R"(Cannot use another enum element before it was declared.)", p_identifier); + } + return; // Found anyway. + } + } + } + // Check if identifier is local. // If that's the case, the declaration already was solved before. switch (p_identifier->source) { @@ -2203,6 +2326,37 @@ void GDScriptAnalyzer::reduce_literal(GDScriptParser::LiteralNode *p_literal) { } void GDScriptAnalyzer::reduce_preload(GDScriptParser::PreloadNode *p_preload) { + if (!p_preload->path) { + return; + } + + reduce_expression(p_preload->path); + + if (!p_preload->path->is_constant) { + push_error("Preloaded path must be a constant string.", p_preload->path); + return; + } + + if (p_preload->path->reduced_value.get_type() != Variant::STRING) { + push_error("Preloaded path must be a constant string.", p_preload->path); + } else { + p_preload->resolved_path = p_preload->path->reduced_value; + // TODO: Save this as script dependency. + if (p_preload->resolved_path.is_rel_path()) { + p_preload->resolved_path = parser->script_path.get_base_dir().plus_file(p_preload->resolved_path); + } + p_preload->resolved_path = p_preload->resolved_path.simplify_path(); + if (!FileAccess::exists(p_preload->resolved_path)) { + push_error(vformat(R"(Preload file "%s" does not exist.)", p_preload->resolved_path), p_preload->path); + } else { + // TODO: Don't load if validating: use completion cache. + p_preload->resource = ResourceLoader::load(p_preload->resolved_path); + if (p_preload->resource.is_null()) { + push_error(vformat(R"(Could not p_preload resource file "%s".)", p_preload->resolved_path), p_preload->path); + } + } + } + p_preload->is_constant = true; p_preload->reduced_value = p_preload->resource; p_preload->set_datatype(type_from_variant(p_preload->reduced_value)); @@ -2378,7 +2532,7 @@ void GDScriptAnalyzer::reduce_subscript(GDScriptParser::SubscriptNode *p_subscri // Check resulting type if possible. result_type.builtin_type = Variant::NIL; result_type.kind = GDScriptParser::DataType::BUILTIN; - result_type.type_source = GDScriptParser::DataType::INFERRED; + result_type.type_source = base_type.is_hard_type() ? GDScriptParser::DataType::ANNOTATED_INFERRED : GDScriptParser::DataType::INFERRED; switch (base_type.builtin_type) { // Can't index at all. @@ -2509,6 +2663,12 @@ void GDScriptAnalyzer::reduce_unary_op(GDScriptParser::UnaryOpNode *p_unary_op) GDScriptParser::DataType result; + if (p_unary_op->operand == nullptr) { + result.kind = GDScriptParser::DataType::VARIANT; + p_unary_op->set_datatype(result); + return; + } + if (p_unary_op->operand->is_constant) { p_unary_op->is_constant = true; p_unary_op->reduced_value = Variant::evaluate(p_unary_op->variant_op, p_unary_op->operand->reduced_value, Variant()); @@ -2555,9 +2715,27 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_variant(const Variant &p_va Ref<GDScript> gds = scr; if (gds.is_valid()) { result.kind = GDScriptParser::DataType::CLASS; - Ref<GDScriptParserRef> ref = get_parser_for(gds->get_path()); + // This might be an inner class, so we want to get the parser for the root. + // But still get the inner class from that tree. + GDScript *current = gds.ptr(); + List<StringName> class_chain; + while (current->_owner) { + // Push to front so it's in reverse. + class_chain.push_front(current->name); + current = current->_owner; + } + + Ref<GDScriptParserRef> ref = get_parser_for(current->path); ref->raise_status(GDScriptParserRef::INTERFACE_SOLVED); - result.class_type = ref->get_parser()->head; + + GDScriptParser::ClassNode *found = ref->get_parser()->head; + + // It should be okay to assume this exists, since we have a complete script already. + for (const List<StringName>::Element *E = class_chain.front(); E; E = E->next()) { + found = found->get_member(E->get()).m_class; + } + + result.class_type = found; result.script_path = ref->get_parser()->script_path; } else { result.kind = GDScriptParser::DataType::SCRIPT; @@ -2883,7 +3061,7 @@ GDScriptParser::DataType GDScriptAnalyzer::get_operation_type(Variant::Operator } // Avoid error in formatting operator (%) where it doesn't find a placeholder. - if (a_type == Variant::STRING) { + if (a_type == Variant::STRING && b_type != Variant::ARRAY) { a = String("%s"); } @@ -3089,6 +3267,9 @@ Error GDScriptAnalyzer::resolve_program() { List<String> parser_keys; depended_parsers.get_key_list(&parser_keys); for (const List<String>::Element *E = parser_keys.front(); E != nullptr; E = E->next()) { + if (depended_parsers[E->get()].is_null()) { + return ERR_PARSE_ERROR; + } depended_parsers[E->get()]->raise_status(GDScriptParserRef::FULLY_SOLVED); } depended_parsers.clear(); diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h index 06d3530cb6..da767522ad 100644 --- a/modules/gdscript/gdscript_analyzer.h +++ b/modules/gdscript/gdscript_analyzer.h @@ -41,6 +41,8 @@ class GDScriptAnalyzer { GDScriptParser *parser = nullptr; HashMap<String, Ref<GDScriptParserRef>> depended_parsers; + const GDScriptParser::EnumNode *current_enum = nullptr; + Error resolve_inheritance(GDScriptParser::ClassNode *p_class, bool p_recursive = true); GDScriptParser::DataType resolve_datatype(GDScriptParser::TypeNode *p_type); diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index 583283ff46..992f8f4b58 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -85,6 +85,17 @@ Error GDScriptParserRef::raise_status(Status p_new_status) { return result; } } + if (result != OK) { + if (parser != nullptr) { + memdelete(parser); + parser = nullptr; + } + if (analyzer != nullptr) { + memdelete(analyzer); + analyzer = nullptr; + } + return result; + } } return result; @@ -116,14 +127,17 @@ Ref<GDScriptParserRef> GDScriptCache::get_parser(const String &p_path, GDScriptP singleton->dependencies[p_owner].insert(p_path); } if (singleton->parser_map.has(p_path)) { - ref = singleton->parser_map[p_path]; + ref = Ref<GDScriptParserRef>(singleton->parser_map[p_path]); } else { + if (!FileAccess::exists(p_path)) { + r_error = ERR_FILE_NOT_FOUND; + return ref; + } GDScriptParser *parser = memnew(GDScriptParser); ref.instance(); ref->parser = parser; ref->path = p_path; - singleton->parser_map[p_path] = ref; - ref->unreference(); + singleton->parser_map[p_path] = ref.ptr(); } r_error = ref->raise_status(p_status); @@ -171,10 +185,7 @@ Ref<GDScript> GDScriptCache::get_shallow_script(const String &p_path, const Stri script->set_script_path(p_path); script->load_source_code(p_path); - singleton->shallow_gdscript_cache[p_path] = script; - // The one in cache is not a hard reference: if the script dies somewhere else it's fine. - // Scripts remove themselves from cache when they die. - script->unreference(); + singleton->shallow_gdscript_cache[p_path] = script.ptr(); return script; } @@ -202,7 +213,7 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro return script; } - singleton->full_gdscript_cache[p_path] = script; + singleton->full_gdscript_cache[p_path] = script.ptr(); singleton->shallow_gdscript_cache.erase(p_path); return script; @@ -211,7 +222,7 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro Error GDScriptCache::finish_compiling(const String &p_owner) { // Mark this as compiled. Ref<GDScript> script = get_shallow_script(p_owner); - singleton->full_gdscript_cache[p_owner] = script; + singleton->full_gdscript_cache[p_owner] = script.ptr(); singleton->shallow_gdscript_cache.erase(p_owner); Set<String> depends = singleton->dependencies[p_owner]; diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h index 770704d6eb..865df34051 100644 --- a/modules/gdscript/gdscript_cache.h +++ b/modules/gdscript/gdscript_cache.h @@ -70,9 +70,9 @@ public: class GDScriptCache { // String key is full path. - HashMap<String, Ref<GDScriptParserRef>> parser_map; - HashMap<String, Ref<GDScript>> shallow_gdscript_cache; - HashMap<String, Ref<GDScript>> full_gdscript_cache; + HashMap<String, GDScriptParserRef *> parser_map; + HashMap<String, GDScript *> shallow_gdscript_cache; + HashMap<String, GDScript *> full_gdscript_cache; HashMap<String, Set<String>> dependencies; friend class GDScript; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index e34d87f5cc..67a894912f 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -2609,16 +2609,27 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar p_script->_base = base.ptr(); if (p_class->base_type.kind == GDScriptParser::DataType::CLASS && p_class->base_type.class_type != nullptr) { - if (!parsed_classes.has(p_script->_base)) { - if (parsing_classes.has(p_script->_base)) { - String class_name = p_class->identifier ? p_class->identifier->name : "<main>"; - _set_error("Cyclic class reference for '" + class_name + "'.", p_class); - return ERR_PARSE_ERROR; + if (p_class->base_type.script_path == main_script->path) { + if (!parsed_classes.has(p_script->_base)) { + if (parsing_classes.has(p_script->_base)) { + String class_name = p_class->identifier ? p_class->identifier->name : "<main>"; + _set_error("Cyclic class reference for '" + class_name + "'.", p_class); + return ERR_PARSE_ERROR; + } + Error err = _parse_class_level(p_script->_base, p_class->base_type.class_type, p_keep_state); + if (err) { + return err; + } } - Error err = _parse_class_level(p_script->_base, p_class->base_type.class_type, p_keep_state); + } else { + Error err = OK; + base = GDScriptCache::get_full_script(p_class->base_type.script_path, err, main_script->path); if (err) { return err; } + if (base.is_null() && !base->is_valid()) { + return ERR_COMPILATION_FAILED; + } } } @@ -2696,11 +2707,7 @@ Error GDScriptCompiler::_parse_class_level(GDScript *p_script, const GDScriptPar const GDScriptParser::ConstantNode *constant = member.constant; StringName name = constant->identifier->name; - ERR_CONTINUE(constant->initializer->type != GDScriptParser::Node::LITERAL); - - const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(constant->initializer); - - p_script->constants.insert(name, literal->value); + p_script->constants.insert(name, constant->initializer->reduced_value); #ifdef TOOLS_ENABLED p_script->member_lines[name] = constant->start_line; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 88bf8cde0f..2e372575da 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -483,6 +483,8 @@ String GDScriptLanguage::make_function(const String &p_class, const String &p_na #ifdef TOOLS_ENABLED +#define COMPLETION_RECURSION_LIMIT 200 + struct GDScriptCompletionIdentifier { GDScriptParser::DataType type; String enumeration; @@ -766,9 +768,11 @@ static void _find_identifiers_in_suite(const GDScriptParser::SuiteNode *p_suite, } } -static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result); +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result, int p_recursion_depth); + +static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, bool p_only_functions, bool p_static, bool p_parent_only, Map<String, ScriptCodeCompletionOption> &r_result, int p_recursion_depth) { + ERR_FAIL_COND(p_recursion_depth > COMPLETION_RECURSION_LIMIT); -static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, bool p_only_functions, bool p_static, bool p_parent_only, Map<String, ScriptCodeCompletionOption> &r_result) { if (!p_parent_only) { bool outer = false; const GDScriptParser::ClassNode *clss = p_class; @@ -820,7 +824,6 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, break; case GDScriptParser::ClassNode::Member::SIGNAL: if (p_only_functions || outer) { - clss = clss->outer; continue; } option = ScriptCodeCompletionOption(member.signal->identifier->name, ScriptCodeCompletionOption::KIND_SIGNAL); @@ -840,10 +843,12 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class, base_type.type = p_class->base_type; base_type.type.is_meta_type = p_static; - _find_identifiers_in_base(base_type, p_only_functions, r_result); + _find_identifiers_in_base(base_type, p_only_functions, r_result, p_recursion_depth + 1); } -static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { +static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result, int p_recursion_depth) { + ERR_FAIL_COND(p_recursion_depth > COMPLETION_RECURSION_LIMIT); + GDScriptParser::DataType base_type = p_base.type; bool _static = base_type.is_meta_type; @@ -856,7 +861,7 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base while (!base_type.has_no_type()) { switch (base_type.kind) { case GDScriptParser::DataType::CLASS: { - _find_identifiers_in_class(base_type.class_type, p_only_functions, _static, false, r_result); + _find_identifiers_in_class(base_type.class_type, p_only_functions, _static, false, r_result, p_recursion_depth + 1); // This already finds all parent identifiers, so we are done. base_type = GDScriptParser::DataType(); } break; @@ -1007,14 +1012,14 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base } } -static void _find_identifiers(GDScriptParser::CompletionContext &p_context, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result) { +static void _find_identifiers(GDScriptParser::CompletionContext &p_context, bool p_only_functions, Map<String, ScriptCodeCompletionOption> &r_result, int p_recursion_depth) { if (!p_only_functions && p_context.current_suite) { // This includes function parameters, since they are also locals. _find_identifiers_in_suite(p_context.current_suite, r_result); } if (p_context.current_class) { - _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result); + _find_identifiers_in_class(p_context.current_class, p_only_functions, (!p_context.current_function || p_context.current_function->is_static), false, r_result, p_recursion_depth + 1); } for (int i = 0; i < GDScriptFunctions::FUNC_MAX; i++) { @@ -2453,7 +2458,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path break; } if (!_guess_expression_type(completion_context, static_cast<const GDScriptParser::AssignmentNode *>(completion_context.node)->assignee, type)) { - _find_identifiers(completion_context, false, options); + _find_identifiers(completion_context, false, options, 0); r_forced = true; break; } @@ -2462,7 +2467,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path _find_enumeration_candidates(completion_context, type.enumeration, options); r_forced = options.size() > 0; } else { - _find_identifiers(completion_context, false, options); + _find_identifiers(completion_context, false, options, 0); r_forced = true; } } break; @@ -2470,7 +2475,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path is_function = true; [[fallthrough]]; case GDScriptParser::COMPLETION_IDENTIFIER: { - _find_identifiers(completion_context, is_function, options); + _find_identifiers(completion_context, is_function, options, 0); } break; case GDScriptParser::COMPLETION_ATTRIBUTE_METHOD: is_function = true; @@ -2484,7 +2489,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path break; } - _find_identifiers_in_base(base, is_function, options); + _find_identifiers_in_base(base, is_function, options, 0); } } break; case GDScriptParser::COMPLETION_SUBSCRIPT: { @@ -2504,7 +2509,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path c.current_class = nullptr; } - _find_identifiers_in_base(base, false, options); + _find_identifiers_in_base(base, false, options, 0); } break; case GDScriptParser::COMPLETION_TYPE_ATTRIBUTE: { if (!completion_context.current_class) { @@ -2530,7 +2535,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path // TODO: Improve this to only list types. if (found) { - _find_identifiers_in_base(base, false, options); + _find_identifiers_in_base(base, false, options, 0); } r_forced = true; } break; @@ -2651,7 +2656,7 @@ Error GDScriptLanguage::complete_code(const String &p_code, const String &p_path if (!completion_context.current_class) { break; } - _find_identifiers_in_class(completion_context.current_class, true, false, true, options); + _find_identifiers_in_class(completion_context.current_class, true, false, true, options, 0); } break; } diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 0967f74285..03da5e926b 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -466,44 +466,40 @@ void GDScriptParser::pop_multiline() { } bool GDScriptParser::is_statement_end() { - return check(GDScriptTokenizer::Token::NEWLINE) || check(GDScriptTokenizer::Token::SEMICOLON); + return check(GDScriptTokenizer::Token::NEWLINE) || check(GDScriptTokenizer::Token::SEMICOLON) || check(GDScriptTokenizer::Token::TK_EOF); } void GDScriptParser::end_statement(const String &p_context) { bool found = false; - while (is_statement_end()) { + while (is_statement_end() && !is_at_end()) { // Remove sequential newlines/semicolons. found = true; advance(); } - if (!found) { + if (!found && !is_at_end()) { push_error(vformat(R"(Expected end of statement after %s, found "%s" instead.)", p_context, current.get_name())); } } void GDScriptParser::parse_program() { - if (current.type == GDScriptTokenizer::Token::TK_EOF) { - // Empty file. - push_error("Source file is empty."); - return; - } - head = alloc_node<ClassNode>(); current_class = head; if (match(GDScriptTokenizer::Token::ANNOTATION)) { // Check for @tool annotation. AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL); - if (annotation->name == "@tool") { - // TODO: don't allow @tool anywhere else. (Should all script annotations be the first thing?). - _is_tool = true; - if (previous.type != GDScriptTokenizer::Token::NEWLINE) { - push_error(R"(Expected newline after "@tool" annotation.)"); - } - // @tool annotation has no specific target. - annotation->apply(this, nullptr); - } else { - annotation_stack.push_back(annotation); + if (annotation != nullptr) { + if (annotation->name == "@tool") { + // TODO: don't allow @tool anywhere else. (Should all script annotations be the first thing?). + _is_tool = true; + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after "@tool" annotation.)"); + } + // @tool annotation has no specific target. + annotation->apply(this, nullptr); + } else { + annotation_stack.push_back(annotation); + } } } @@ -635,7 +631,6 @@ void GDScriptParser::parse_extends() { current_class->extends_path = previous.literal; if (!match(GDScriptTokenizer::Token::PERIOD)) { - end_statement("superclass path"); return; } } @@ -990,9 +985,15 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal() { signal->identifier = parse_identifier(); if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { - while (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { + do { + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; + } + ParameterNode *parameter = parse_parameter(); if (parameter == nullptr) { + push_error("Expected signal parameter name."); break; } if (parameter->default_value != nullptr) { @@ -1004,7 +1005,8 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal() { signal->parameters_indices[parameter->identifier->name] = signal->parameters.size(); signal->parameters.push_back(parameter); } - } + } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after signal parameters.)*"); } @@ -1026,7 +1028,7 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum() { push_multiline(true); consume(GDScriptTokenizer::Token::BRACE_OPEN, vformat(R"(Expected "{" after %s.)", named ? "enum name" : R"("enum")")); - int current_value = 0; + HashMap<StringName, int> elements; do { if (check(GDScriptTokenizer::Token::BRACE_CLOSE)) { @@ -1035,8 +1037,13 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum() { if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifer for enum key.)")) { EnumNode::Value item; item.identifier = parse_identifier(); + item.parent_enum = enum_node; + item.line = previous.start_line; + item.leftmost_column = previous.leftmost_column; - if (!named) { + if (elements.has(item.identifier->name)) { + push_error(vformat(R"(Name "%s" was already in this enum (at line %d).)", item.identifier->name, elements[item.identifier->name]), item.identifier); + } else if (!named) { // TODO: Abstract this recursive member check. ClassNode *parent = current_class; while (parent != nullptr) { @@ -1048,19 +1055,18 @@ GDScriptParser::EnumNode *GDScriptParser::parse_enum() { } } - if (match(GDScriptTokenizer::Token::EQUAL)) { - if (consume(GDScriptTokenizer::Token::LITERAL, R"(Expected integer value after "=".)")) { - item.custom_value = parse_literal(); + elements[item.identifier->name] = item.line; - if (item.custom_value->value.get_type() != Variant::INT) { - push_error(R"(Expected integer value after "=".)"); - item.custom_value = nullptr; - } else { - current_value = item.custom_value->value; - } + if (match(GDScriptTokenizer::Token::EQUAL)) { + ExpressionNode *value = parse_expression(false); + if (value == nullptr) { + push_error(R"(Expected expression value after "=".)"); } + item.custom_value = value; } - item.value = current_value++; + item.rightmost_column = previous.rightmost_column; + + item.index = enum_node->values.size(); enum_node->values.push_back(item); if (!named) { // Add as member of current class. @@ -1142,6 +1148,9 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function() { if (match(GDScriptTokenizer::Token::FORWARD_ARROW)) { make_completion_context(COMPLETION_TYPE_NAME_OR_VOID, function); function->return_type = parse_type(true); + if (function->return_type == nullptr) { + push_error(R"(Expected return type or "void" after "->".)"); + } } // TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens. @@ -1346,7 +1355,7 @@ GDScriptParser::Node *GDScriptParser::parse_statement() { advance(); ReturnNode *n_return = alloc_node<ReturnNode>(); if (!is_statement_end()) { - if (current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) { + if (current_function && current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) { push_error(R"(Constructor cannot return a value.)"); } n_return->return_value = parse_expression(false); @@ -2495,15 +2504,18 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_get_node(ExpressionNode *p make_completion_context(COMPLETION_GET_NODE, get_node); get_node->string = parse_literal(); return get_node; - } else if (check(GDScriptTokenizer::Token::IDENTIFIER)) { + } else if (current.is_node_name()) { GetNodeNode *get_node = alloc_node<GetNodeNode>(); int chain_position = 0; do { make_completion_context(COMPLETION_GET_NODE, get_node, chain_position++); - if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expect node identifer after "/".)")) { + if (!current.is_node_name()) { + push_error(R"(Expect node path after "/".)"); return nullptr; } - IdentifierNode *identifier = parse_identifier(); + advance(); + IdentifierNode *identifier = alloc_node<IdentifierNode>(); + identifier->name = previous.get_identifier(); get_node->chain.push_back(identifier); } while (match(GDScriptTokenizer::Token::SLASH)); return get_node; @@ -2527,29 +2539,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_preload(ExpressionNode *p_ if (preload->path == nullptr) { push_error(R"(Expected resource path after "(".)"); - } else if (preload->path->type != Node::LITERAL) { - push_error("Preloaded path must be a constant string."); - } else { - LiteralNode *path = static_cast<LiteralNode *>(preload->path); - if (path->value.get_type() != Variant::STRING) { - push_error("Preloaded path must be a constant string."); - } else { - preload->resolved_path = path->value; - // TODO: Save this as script dependency. - if (preload->resolved_path.is_rel_path()) { - preload->resolved_path = script_path.get_base_dir().plus_file(preload->resolved_path); - } - preload->resolved_path = preload->resolved_path.simplify_path(); - if (!FileAccess::exists(preload->resolved_path)) { - push_error(vformat(R"(Preload file "%s" does not exist.)", preload->resolved_path)); - } else { - // TODO: Don't load if validating: use completion cache. - preload->resource = ResourceLoader::load(preload->resolved_path); - if (preload->resource.is_null()) { - push_error(vformat(R"(Could not preload resource file "%s".)", preload->resolved_path)); - } - } - } } pop_completion_call(); diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index edfe330c0c..7d8ae7fc55 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -405,7 +405,10 @@ public: struct EnumNode : public Node { struct Value { IdentifierNode *identifier = nullptr; - LiteralNode *custom_value = nullptr; + ExpressionNode *custom_value = nullptr; + EnumNode *parent_enum = nullptr; + int index = -1; + bool resolved = false; int value = 0; int line = 0; int leftmost_column = 0; diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp index 7a4bdd88ba..737e920693 100644 --- a/modules/gdscript/gdscript_tokenizer.cpp +++ b/modules/gdscript/gdscript_tokenizer.cpp @@ -156,6 +156,64 @@ const char *GDScriptTokenizer::Token::get_name() const { return token_names[type]; } +bool GDScriptTokenizer::Token::is_identifier() const { + // Note: Most keywords should not be recognized as identifiers. + // These are only exceptions for stuff that already is on the engine's API. + switch (type) { + case IDENTIFIER: + case MATCH: // Used in String.match(). + return true; + default: + return false; + } +} + +bool GDScriptTokenizer::Token::is_node_name() const { + // This is meant to allow keywords with the $ notation, but not as general identifiers. + switch (type) { + case IDENTIFIER: + case AND: + case AS: + case ASSERT: + case AWAIT: + case BREAK: + case BREAKPOINT: + case CLASS_NAME: + case CLASS: + case CONST: + case CONTINUE: + case ELIF: + case ELSE: + case ENUM: + case EXTENDS: + case FOR: + case FUNC: + case IF: + case IN: + case IS: + case MATCH: + case NAMESPACE: + case NOT: + case OR: + case PASS: + case PRELOAD: + case RETURN: + case SELF: + case SIGNAL: + case STATIC: + case SUPER: + case TRAIT: + case UNDERSCORE: + case VAR: + case VOID: + case WHILE: + case YIELD: + return true; + default: + return false; + } +} + String GDScriptTokenizer::get_token_name(Token::Type p_token_type) { ERR_FAIL_INDEX_V_MSG(p_token_type, Token::TK_MAX, "<error>", "Using token type out of the enum."); return token_names[p_token_type]; diff --git a/modules/gdscript/gdscript_tokenizer.h b/modules/gdscript/gdscript_tokenizer.h index 059a226924..100ed3f132 100644 --- a/modules/gdscript/gdscript_tokenizer.h +++ b/modules/gdscript/gdscript_tokenizer.h @@ -168,9 +168,9 @@ public: String source; const char *get_name() const; - // TODO: Allow some keywords as identifiers? - bool is_identifier() const { return type == IDENTIFIER; } - StringName get_identifier() const { return literal; } + bool is_identifier() const; + bool is_node_name() const; + StringName get_identifier() const { return source; } Token(Type p_type) { type = p_type; diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index ae7898fdf2..4d79d9d395 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -237,7 +237,7 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p case ClassNode::Member::ENUM_VALUE: { lsp::DocumentSymbol symbol; - symbol.name = m.constant->identifier->name; + symbol.name = m.enum_value.identifier->name; symbol.kind = lsp::SymbolKind::EnumMember; symbol.deprecated = false; symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index c2549b4ad5..5edf72d63e 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -70,13 +70,14 @@ namespace GodotTools.BuildLogger { string line = $"{e.File}({e.LineNumber},{e.ColumnNumber}): error {e.Code}: {e.Message}"; - if (e.ProjectFile.Length > 0) + if (!string.IsNullOrEmpty(e.ProjectFile)) line += $" [{e.ProjectFile}]"; WriteLine(line); string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + - $@"{e.Code.CsvEscape()},{e.Message.CsvEscape()},{e.ProjectFile.CsvEscape()}"; + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(errorLine); } @@ -89,8 +90,9 @@ namespace GodotTools.BuildLogger WriteLine(line); - string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber},{e.Code.CsvEscape()}," + - $@"{e.Message.CsvEscape()},{(e.ProjectFile != null ? e.ProjectFile.CsvEscape() : string.Empty)}"; + string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; issuesStreamWriter.WriteLine(warningLine); } diff --git a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs index 012b69032e..e6b0e8f1df 100644 --- a/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs +++ b/modules/mono/editor/GodotTools/GodotTools.Core/StringExtensions.cs @@ -43,7 +43,7 @@ namespace GodotTools.Core path.StartsWith(DriveRoot, StringComparison.Ordinal); } - public static string ToSafeDirName(this string dirName, bool allowDirSeparator) + public static string ToSafeDirName(this string dirName, bool allowDirSeparator = false) { var invalidChars = new List<string> { ":", "*", "?", "\"", "<", ">", "|" }; diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 9cb50014b0..41c94f19c8 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -11,13 +11,21 @@ <ItemGroup> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> </ItemGroup> + <!-- + The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described + here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 + We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when + searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. + --> <ItemGroup> - <!-- - The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described - here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 - We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when - searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. - --> <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> </ItemGroup> + <Target Name="CopyMSBuildStubWindows" AfterTargets="Build" Condition="$([MSBuild]::IsOsPlatform(Windows))"> + <PropertyGroup> + <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> + <GodotOutputDataDir>$(GodotSourceRootPath)/bin/GodotSharp</GodotOutputDataDir> + </PropertyGroup> + <!-- Need to copy it here as well on Windows --> + <Copy SourceFiles="MSBuild.exe" DestinationFiles="$(GodotOutputDataDir)\Mono\lib\mono\v4.0\MSBuild.exe" /> + </Target> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index 5541876f9e..01d7c99662 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; @@ -41,7 +42,8 @@ namespace GodotTools.ProjectEditor var root = GenGameProject(name); - root.Save(path); + // Save (without BOM) + root.Save(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); return Guid.NewGuid().ToString().ToUpper(); } diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index 6399991b84..ff7ce97c47 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -205,23 +205,8 @@ namespace GodotTools if (File.Exists(editorScriptsMetadataPath)) File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath); - var currentPlayRequest = GodotSharpEditor.Instance.CurrentPlaySettings; - - if (currentPlayRequest != null) - { - if (currentPlayRequest.Value.HasDebugger) - { - // Set the environment variable that will tell the player to connect to the IDE debugger - // TODO: We should probably add a better way to do this - Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", - "--debugger-agent=transport=dt_socket" + - $",address={currentPlayRequest.Value.DebuggerHost}:{currentPlayRequest.Value.DebuggerPort}" + - ",server=n"); - } - - if (!currentPlayRequest.Value.BuildBeforePlaying) - return true; // Requested play from an external editor/IDE which already built the project - } + if (GodotSharpEditor.Instance.SkipBuildBeforePlaying) + return true; // Requested play from an external editor/IDE which already built the project return BuildProjectBlocking("Debug"); } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index 554763eecb..e819848212 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -430,7 +430,7 @@ namespace GodotTools.Export private static string DetermineDataDirNameForProject() { var appName = (string)ProjectSettings.GetSetting("application/config/name"); - string appNameSafe = appName.ToSafeDirName(allowDirSeparator: false); + string appNameSafe = appName.ToSafeDirName(); return $"data_{appNameSafe}"; } diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index a363ecc920..3148458d7e 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -38,13 +38,14 @@ namespace GodotTools public BottomPanel BottomPanel { get; private set; } - public PlaySettings? CurrentPlaySettings { get; set; } + public bool SkipBuildBeforePlaying { get; set; } = false; public static string ProjectAssemblyName { get { var projectAssemblyName = (string)ProjectSettings.GetSetting("application/config/name"); + projectAssemblyName = projectAssemblyName.ToSafeDirName(); if (string.IsNullOrEmpty(projectAssemblyName)) projectAssemblyName = "UnnamedProject"; return projectAssemblyName; diff --git a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs index 17f3339560..eb34a2d0f7 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Ides/MessagingServer.cs @@ -330,9 +330,10 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = new PlaySettings(); + // TODO: Add BuildBeforePlaying flag to PlayRequest + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; }); return Task.FromResult<Response>(new PlayResponse()); } @@ -341,10 +342,22 @@ namespace GodotTools.Ides { DispatchToMainThread(() => { - GodotSharpEditor.Instance.CurrentPlaySettings = - new PlaySettings(request.DebuggerHost, request.DebuggerPort, request.BuildBeforePlaying ?? true); + // Tell the build callback whether the editor already built the solution or not + GodotSharpEditor.Instance.SkipBuildBeforePlaying = !(request.BuildBeforePlaying ?? true); + + // Pass the debugger agent settings to the player via an environment variables + // TODO: It would be better if this was an argument in EditorRunPlay instead + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", + "--debugger-agent=transport=dt_socket" + + $",address={request.DebuggerHost}:{request.DebuggerPort}" + + ",server=n"); + + // Run the game Internal.EditorRunPlay(); - GodotSharpEditor.Instance.CurrentPlaySettings = null; + + // Restore normal settings + Environment.SetEnvironmentVariable("GODOT_MONO_DEBUGGER_AGENT", ""); + GodotSharpEditor.Instance.SkipBuildBeforePlaying = false; }); return Task.FromResult<Response>(new DebugPlayResponse()); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 35852f31ef..1b55090451 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -56,6 +56,8 @@ import android.content.SharedPreferences.Editor; import android.content.pm.ConfigurationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Point; +import android.graphics.Rect; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -75,6 +77,7 @@ import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.Button; @@ -160,8 +163,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public GodotRenderView mRenderView; private boolean godot_initialized = false; - private GodotEditText mEditText; - private SensorManager mSensorManager; private Sensor mAccelerometer; private Sensor mGravity; @@ -218,6 +219,12 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC containerLayout = new FrameLayout(activity); containerLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + // GodotEditText layout + GodotEditText editText = new GodotEditText(activity); + editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + // ...add to FrameLayout + containerLayout.addView(editText); + GodotLib.setup(command_line); final String videoDriver = GodotLib.getGlobal("rendering/quality/driver/driver_name"); @@ -230,9 +237,21 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC View view = mRenderView.getView(); containerLayout.addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + editText.setView(mRenderView); + io.setEdit(editText); - mEditText = new GodotEditText(activity, mRenderView); - io.setEdit(mEditText); + view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Point fullSize = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(fullSize); + Rect gameSize = new Rect(); + mRenderView.getView().getWindowVisibleDisplayFrame(gameSize); + + final int keyboardHeight = fullSize.y - gameSize.bottom; + GodotLib.setVirtualKeyboardHeight(keyboardHeight); + } + }); mRenderView.queueOnRenderThread(new Runnable() { @Override @@ -448,7 +467,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); @@ -585,21 +603,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } @Override - public void onStart() { - super.onStart(); - - mRenderView.getView().post(new Runnable() { - @Override - public void run() { - mEditText.onInitView(); - } - }); - } - - @Override public void onDestroy() { - mEditText.onDestroyView(); - for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onMainDestroy(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index 6855f91f1c..c95339c583 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -32,27 +32,16 @@ package org.godotengine.godot.input; import org.godotengine.godot.*; -import android.app.Activity; import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; import android.os.Handler; import android.os.Message; import android.text.InputFilter; import android.text.InputType; import android.util.AttributeSet; -import android.view.Gravity; import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewTreeObserver; -import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.PopupWindow; import java.lang.ref.WeakReference; @@ -67,8 +56,6 @@ public class GodotEditText extends EditText { // Fields // =========================================================== private GodotRenderView mRenderView; - private View mKeyboardView; - private PopupWindow mKeyboardWindow; private GodotTextInputWrapper mInputWrapper; private EditHandler sHandler = new EditHandler(this); private String mOriginText; @@ -93,52 +80,24 @@ public class GodotEditText extends EditText { // =========================================================== // Constructors // =========================================================== - public GodotEditText(final Context context, final GodotRenderView view) { + public GodotEditText(final Context context) { super(context); + initView(); + } - setPadding(0, 0, 0, 0); - setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE); - setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - - mRenderView = view; - mInputWrapper = new GodotTextInputWrapper(mRenderView, this); - setOnEditorActionListener(mInputWrapper); - view.getView().requestFocus(); - - // Create a popup window with an invisible layout for the virtual keyboard, - // so the view can be resized to get the vk height without resizing the main godot view. - final FrameLayout keyboardLayout = new FrameLayout(context); - keyboardLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - keyboardLayout.setVisibility(View.INVISIBLE); - keyboardLayout.addView(this); - mKeyboardView = keyboardLayout; - - mKeyboardWindow = new PopupWindow(keyboardLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - mKeyboardWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - mKeyboardWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - mKeyboardWindow.setFocusable(true); // for the text edit to work - mKeyboardWindow.setTouchable(false); // inputs need to go through - - keyboardLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Point fullSize = new Point(); - ((Activity)mRenderView.getView().getContext()).getWindowManager().getDefaultDisplay().getSize(fullSize); - Rect gameSize = new Rect(); - mKeyboardWindow.getContentView().getWindowVisibleDisplayFrame(gameSize); - - final int keyboardHeight = fullSize.y - gameSize.bottom; - GodotLib.setVirtualKeyboardHeight(keyboardHeight); - } - }); + public GodotEditText(final Context context, final AttributeSet attrs) { + super(context, attrs); + initView(); } - public void onInitView() { - mKeyboardWindow.showAtLocation(mRenderView.getView(), Gravity.NO_GRAVITY, 0, 0); + public GodotEditText(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + initView(); } - public void onDestroyView() { - mKeyboardWindow.dismiss(); + protected void initView() { + setPadding(0, 0, 0, 0); + setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE); } public boolean isMultiline() { @@ -170,7 +129,7 @@ public class GodotEditText extends EditText { edit.mInputWrapper.setOriginText(text); edit.addTextChangedListener(edit.mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mKeyboardView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(edit, 0); } } break; @@ -179,7 +138,7 @@ public class GodotEditText extends EditText { GodotEditText edit = (GodotEditText)msg.obj; edit.removeTextChangedListener(mInputWrapper); - final InputMethodManager imm = (InputMethodManager)mKeyboardView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); edit.mRenderView.getView().requestFocus(); } break; @@ -193,6 +152,17 @@ public class GodotEditText extends EditText { } // =========================================================== + // Getter & Setter + // =========================================================== + public void setView(final GodotRenderView view) { + mRenderView = view; + if (mInputWrapper == null) + mInputWrapper = new GodotTextInputWrapper(mRenderView, this); + setOnEditorActionListener(mInputWrapper); + view.getView().requestFocus(); + } + + // =========================================================== // Methods for/from SuperClass/Interfaces // =========================================================== @Override diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index f4ef40a0ba..66579c1ad7 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -235,7 +235,10 @@ def configure(env): env.Append(CPPDEFINES=["ICLOUD_ENABLED"]) env.Prepend( - CPPPATH=["$IPHONESDK/usr/include", "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers",] + CPPPATH=[ + "$IPHONESDK/usr/include", + "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers", + ] ) env["ENV"]["CODESIGN_ALLOCATE"] = "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate" diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp index 4a751488cb..a889717f20 100644 --- a/platform/iphone/export/export.cpp +++ b/platform/iphone/export/export.cpp @@ -86,6 +86,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { struct IOSExportAsset { String exported_path; bool is_framework; // framework is anything linked to the binary, otherwise it's a resource + bool should_embed; }; String _get_additional_plist_content(); @@ -100,7 +101,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<String> _get_preset_architectures(const Ref<EditorExportPreset> &p_preset); void _add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets); - Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets); + Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets); Error _export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets); bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { @@ -912,15 +913,6 @@ struct ExportLibsData { }; void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets) { - Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); - Vector<String> frameworks; - for (int i = 0; i < export_plugins.size(); ++i) { - Vector<String> plugin_frameworks = export_plugins[i]->get_ios_frameworks(); - for (int j = 0; j < plugin_frameworks.size(); ++j) { - frameworks.push_back(plugin_frameworks[j]); - } - } - // that is just a random number, we just need Godot IDs not to clash with // existing IDs in the project. PbxId current_id = { 0x58938401, 0, 0 }; @@ -945,15 +937,19 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese String type; if (asset.exported_path.ends_with(".framework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.framework"; } else if (asset.exported_path.ends_with(".xcframework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.xcframework"; } else if (asset.exported_path.ends_with(".dylib")) { @@ -1026,7 +1022,7 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } } -Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets) { +Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); String binary_name = p_out_dir.get_file().get_basename(); @@ -1035,7 +1031,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir String asset = p_assets[f_idx]; if (!asset.begins_with("res://")) { // either SDK-builtin or already a part of the export template - IOSExportAsset exported_asset = { asset, p_is_framework }; + IOSExportAsset exported_asset = { asset, p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); } else { DirAccess *da = DirAccess::create_for_path(asset); @@ -1101,7 +1097,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir memdelete(filesystem_da); return err; } - IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework }; + IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); if (create_framework) { @@ -1165,19 +1161,23 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets) { Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { - Vector<String> frameworks = export_plugins[i]->get_ios_frameworks(); - Error err = _export_additional_assets(p_out_dir, frameworks, true, r_exported_assets); + Vector<String> linked_frameworks = export_plugins[i]->get_ios_frameworks(); + Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets); + ERR_FAIL_COND_V(err, err); + + Vector<String> embedded_frameworks = export_plugins[i]->get_ios_embedded_frameworks(); + err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs(); for (int j = 0; j < project_static_libs.size(); j++) { project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project } - err = _export_additional_assets(p_out_dir, project_static_libs, true, r_exported_assets); + err = _export_additional_assets(p_out_dir, project_static_libs, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files(); - err = _export_additional_assets(p_out_dir, ios_bundle_files, false, r_exported_assets); + err = _export_additional_assets(p_out_dir, ios_bundle_files, false, false, r_exported_assets); ERR_FAIL_COND_V(err, err); } @@ -1185,7 +1185,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir for (int i = 0; i < p_libraries.size(); ++i) { library_paths.push_back(p_libraries[i].path); } - Error err = _export_additional_assets(p_out_dir, library_paths, true, r_exported_assets); + Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); return OK; diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index e05d540c50..a0aced26ab 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -681,14 +681,6 @@ DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, u return id; } -void DisplayServerX11::show_window(WindowID p_id) { - _THREAD_SAFE_METHOD_ - - WindowData &wd = windows[p_id]; - - XMapWindow(x11_display, wd.x11_window); -} - void DisplayServerX11::delete_sub_window(WindowID p_id) { _THREAD_SAFE_METHOD_ @@ -3153,6 +3145,8 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u WindowData wd; wd.x11_window = XCreateWindow(x11_display, RootWindow(x11_display, visualInfo->screen), p_rect.position.x, p_rect.position.y, p_rect.size.width > 0 ? p_rect.size.width : 1, p_rect.size.height > 0 ? p_rect.size.height : 1, 0, visualInfo->depth, InputOutput, visualInfo->visual, valuemask, &windowAttributes); + XMapWindow(x11_display, wd.x11_window); + //associate PID // make PID known to X11 { @@ -3322,7 +3316,6 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u if (cursors[current_cursor] != None) { XDefineCursor(x11_display, wd.x11_window, cursors[current_cursor]); } - return id; } @@ -3562,7 +3555,6 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode window_set_flag(WindowFlags(i), true, main_window); } } - show_window(main_window); //create RenderingDevice if used #if defined(VULKAN_ENABLED) diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index 8e1f941bbf..fb50b484a0 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -277,7 +277,6 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()); - virtual void show_window(WindowID p_id); virtual void delete_sub_window(WindowID p_id); virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index d8f3f81ff6..68e8454fd0 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -230,7 +230,6 @@ public: virtual Vector<int> get_window_list() const override; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override; - virtual void show_window(WindowID p_id) override; virtual void delete_sub_window(WindowID p_id) override; virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index c4a5849d43..1676e0d425 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -311,8 +311,6 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); } - DS_OSX->windows.erase(window_id); - if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { DisplayServerOSX::WindowData &pwd = DS_OSX->windows[wd.transient_parent]; [pwd.window_object makeKeyAndOrderFront:nil]; // Move focus back to parent. @@ -332,6 +330,8 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->context_vulkan->window_destroy(window_id); } #endif + + DS_OSX->windows.erase(window_id); } - (void)windowDidEnterFullScreen:(NSNotification *)notification { @@ -2314,23 +2314,18 @@ DisplayServer::WindowID DisplayServerOSX::create_sub_window(WindowMode p_mode, u _THREAD_SAFE_METHOD_ WindowID id = _create_window(p_mode, p_rect); + WindowData &wd = windows[id]; for (int i = 0; i < WINDOW_FLAG_MAX; i++) { if (p_flags & (1 << i)) { window_set_flag(WindowFlags(i), true, id); } } - - return id; -} - -void DisplayServerOSX::show_window(WindowID p_id) { - WindowData &wd = windows[p_id]; - if (wd.no_focus) { [wd.window_object orderFront:nil]; } else { [wd.window_object makeKeyAndOrderFront:nil]; } + return id; } void DisplayServerOSX::_send_window_event(const WindowData &wd, WindowEvent p_event) { @@ -3779,7 +3774,7 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode window_set_flag(WindowFlags(i), true, main_window); } } - show_window(MAIN_WINDOW_ID); + [windows[main_window].window_object makeKeyAndOrderFront:nil]; #if defined(OPENGL_ENABLED) if (rendering_driver == "opengl_es") { @@ -3808,9 +3803,11 @@ DisplayServerOSX::~DisplayServerOSX() { } //destroy all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - [E->get().window_object setContentView:nil]; - [E->get().window_object close]; + for (Map<WindowID, WindowData>::Element *E = windows.front(); E;) { + Map<WindowID, WindowData>::Element *F = E; + E = E->next(); + [F->get().window_object setContentView:nil]; + [F->get().window_object close]; } //destroy drivers diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index cf7bebfbdf..9469e35536 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -495,17 +495,13 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod _update_window_style(window_id); - return window_id; -} - -void DisplayServerWindows::show_window(WindowID p_id) { - WindowData &wd = windows[p_id]; - - ShowWindow(wd.hWnd, wd.no_focus ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window - if (!wd.no_focus) { + ShowWindow(wd.hWnd, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window + if (!(p_flags & WINDOW_FLAG_NO_FOCUS_BIT)) { SetForegroundWindow(wd.hWnd); // Slightly Higher Priority SetFocus(wd.hWnd); // Sets Keyboard Focus To } + + return window_id; } void DisplayServerWindows::delete_sub_window(WindowID p_window) { @@ -3143,7 +3139,9 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } - show_window(MAIN_WINDOW_ID); + ShowWindow(windows[MAIN_WINDOW_ID].hWnd, SW_SHOW); // Show The Window + SetForegroundWindow(windows[MAIN_WINDOW_ID].hWnd); // Slightly Higher Priority + SetFocus(windows[MAIN_WINDOW_ID].hWnd); // Sets Keyboard Focus To #if defined(VULKAN_ENABLED) diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 52f5b0f4a9..0ad8cd3d07 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -461,7 +461,6 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()); - virtual void show_window(WindowID p_window); virtual void delete_sub_window(WindowID p_window); virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index 68e99445d8..fd4d5981ff 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -56,7 +56,7 @@ void Camera2D::_update_scroll() { viewport->set_canvas_transform(xform); - Size2 screen_size = viewport->get_visible_rect().size; + Size2 screen_size = _get_camera_screen_size(); Point2 screen_offset = (anchor_mode == ANCHOR_MODE_DRAG_CENTER ? (screen_size * 0.5) : Point2()); get_tree()->call_group_flags(SceneTree::GROUP_CALL_REALTIME, group_name, "_camera_moved", xform, screen_offset); @@ -94,7 +94,7 @@ Transform2D Camera2D::get_camera_transform() { ERR_FAIL_COND_V(custom_viewport && !ObjectDB::get_instance(custom_viewport_id), Transform2D()); - Size2 screen_size = viewport->get_visible_rect().size; + Size2 screen_size = _get_camera_screen_size(); Point2 new_camera_pos = get_global_transform().get_origin(); Point2 ret_camera_pos; @@ -274,7 +274,7 @@ void Camera2D::_notification(int p_what) { } Transform2D inv_camera_transform = get_camera_transform().affine_inverse(); - Size2 screen_size = get_viewport_rect().size; + Size2 screen_size = _get_camera_screen_size(); Vector2 screen_endpoints[4] = { inv_camera_transform.xform(Vector2(0, 0)), @@ -321,7 +321,7 @@ void Camera2D::_notification(int p_what) { } Transform2D inv_camera_transform = get_camera_transform().affine_inverse(); - Size2 screen_size = get_viewport_rect().size; + Size2 screen_size = _get_camera_screen_size(); Vector2 margin_endpoints[4] = { inv_camera_transform.xform(Vector2((screen_size.width / 2) - ((screen_size.width / 2) * drag_margin[MARGIN_LEFT]), (screen_size.height / 2) - ((screen_size.height / 2) * drag_margin[MARGIN_TOP]))), @@ -469,7 +469,7 @@ void Camera2D::reset_smoothing() { void Camera2D::align() { ERR_FAIL_COND(custom_viewport && !ObjectDB::get_instance(custom_viewport_id)); - Size2 screen_size = viewport->get_visible_rect().size; + Size2 screen_size = _get_camera_screen_size(); Point2 current_camera_pos = get_global_transform().get_origin(); if (anchor_mode == ANCHOR_MODE_DRAG_CENTER) { @@ -507,6 +507,14 @@ Point2 Camera2D::get_camera_screen_center() const { return camera_screen_center; } +Size2 Camera2D::_get_camera_screen_size() const { + // special case if the camera2D is in the root viewport + if (Engine::get_singleton()->is_editor_hint() && get_viewport()->get_parent_viewport() == get_tree()->get_root()) { + return Size2(ProjectSettings::get_singleton()->get("display/window/size/width"), ProjectSettings::get_singleton()->get("display/window/size/height")); + } + return get_viewport_rect().size; +} + void Camera2D::set_h_drag_enabled(bool p_enabled) { h_drag_enabled = p_enabled; } diff --git a/scene/2d/camera_2d.h b/scene/2d/camera_2d.h index 0a4e269c40..867a5562b2 100644 --- a/scene/2d/camera_2d.h +++ b/scene/2d/camera_2d.h @@ -94,6 +94,8 @@ protected: Camera2DProcessMode process_mode; + Size2 _get_camera_screen_size() const; + protected: virtual Transform2D get_camera_transform(); void _notification(int p_what); diff --git a/scene/3d/area_3d.cpp b/scene/3d/area_3d.cpp index a024757927..dc35f069c0 100644 --- a/scene/3d/area_3d.cpp +++ b/scene/3d/area_3d.cpp @@ -222,6 +222,9 @@ void Area3D::_clear_monitoring() { } //ERR_CONTINUE(!node); + node->disconnect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area3D::_body_enter_tree)); + node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area3D::_body_exit_tree)); + if (!E->get().in_tree) { continue; } @@ -231,9 +234,6 @@ void Area3D::_clear_monitoring() { } emit_signal(SceneStringNames::get_singleton()->body_exited, node); - - node->disconnect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area3D::_body_enter_tree)); - node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area3D::_body_exit_tree)); } } @@ -251,6 +251,9 @@ void Area3D::_clear_monitoring() { } //ERR_CONTINUE(!node); + node->disconnect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area3D::_area_enter_tree)); + node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area3D::_area_exit_tree)); + if (!E->get().in_tree) { continue; } @@ -260,9 +263,6 @@ void Area3D::_clear_monitoring() { } emit_signal(SceneStringNames::get_singleton()->area_exited, obj); - - node->disconnect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area3D::_area_enter_tree)); - node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area3D::_area_exit_tree)); } } } diff --git a/scene/main/window.cpp b/scene/main/window.cpp index a5c5be8a44..68ffdfe2e8 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -247,7 +247,6 @@ void Window::_make_window() { } RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_VISIBLE); - DisplayServer::get_singleton()->show_window(window_id); } void Window::_update_from_window() { diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 996d249bbf..0f6475cf0d 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -880,6 +880,7 @@ void register_scene_types() { ClassDB::add_compatibility_class("VehicleBody", "VehicleBody3D"); ClassDB::add_compatibility_class("VehicleWheel", "VehicleWheel3D"); ClassDB::add_compatibility_class("ViewportContainer", "SubViewportContainer"); + ClassDB::add_compatibility_class("Viewport", "SubViewport"); ClassDB::add_compatibility_class("VisibilityEnabler", "VisibilityEnabler3D"); ClassDB::add_compatibility_class("VisibilityNotifier", "VisibilityNotifier3D"); ClassDB::add_compatibility_class("VisualServer", "RenderingServer"); diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index a174c3eebe..ee8e63266d 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -134,6 +134,7 @@ Color Environment::get_ambient_light_color() const { void Environment::set_ambient_source(AmbientSource p_source) { ambient_source = p_source; _update_ambient_light(); + _change_notify(); } Environment::AmbientSource Environment::get_ambient_source() const { @@ -161,6 +162,7 @@ float Environment::get_ambient_light_sky_contribution() const { void Environment::set_reflection_source(ReflectionSource p_source) { reflection_source = p_source; _update_ambient_light(); + _change_notify(); } Environment::ReflectionSource Environment::get_reflection_source() const { @@ -758,6 +760,7 @@ void Environment::_update_volumetric_fog() { void Environment::set_volumetric_fog_enabled(bool p_enable) { volumetric_fog_enabled = p_enable; _update_volumetric_fog(); + _change_notify(); } bool Environment::is_volumetric_fog_enabled() const { diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 8f6d6d3b99..32d4e9e569 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -186,10 +186,6 @@ DisplayServer::WindowID DisplayServer::create_sub_window(WindowMode p_mode, uint ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Sub-windows not supported by this display server."); } -void DisplayServer::show_window(WindowID p_id) { - ERR_FAIL_MSG("Sub-windows not supported by this display server."); -} - void DisplayServer::delete_sub_window(WindowID p_id) { ERR_FAIL_MSG("Sub-windows not supported by this display server."); } diff --git a/servers/display_server.h b/servers/display_server.h index b652418244..fc6520fa5e 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -220,7 +220,6 @@ public: }; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()); - virtual void show_window(WindowID p_id); virtual void delete_sub_window(WindowID p_id); virtual WindowID get_window_at_screen_position(const Point2i &p_position) const = 0; diff --git a/servers/physics_2d/body_pair_2d_sw.cpp b/servers/physics_2d/body_pair_2d_sw.cpp index 5997959432..2021aab17c 100644 --- a/servers/physics_2d/body_pair_2d_sw.cpp +++ b/servers/physics_2d/body_pair_2d_sw.cpp @@ -478,8 +478,8 @@ void BodyPair2DSW::solve(real_t p_step) { Vector2 jb = c.normal * (c.acc_bias_impulse - jbnOld); - A->apply_bias_impulse(c.rA, -jb); - B->apply_bias_impulse(c.rB, jb); + A->apply_bias_impulse(-jb, c.rA); + B->apply_bias_impulse(jb, c.rB); real_t jn = -(c.bounce + vn) * c.mass_normal; real_t jnOld = c.acc_normal_impulse; diff --git a/servers/rendering/rasterizer_rd/rasterizer_effects_rd.cpp b/servers/rendering/rasterizer_rd/rasterizer_effects_rd.cpp index 390272b4b8..b3279f041f 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_effects_rd.cpp +++ b/servers/rendering/rasterizer_rd/rasterizer_effects_rd.cpp @@ -1171,7 +1171,7 @@ void RasterizerEffectsRD::cubemap_filter(RID p_source_cubemap, Vector<RID> p_des RD::get_singleton()->compute_list_end(); } -void RasterizerEffectsRD::render_sky(RD::DrawListID p_list, float p_time, RID p_fb, RID p_samplers, RID p_lights, RenderPipelineVertexFormatCacheRD *p_pipeline, RID p_uniform_set, RID p_texture_set, const CameraMatrix &p_camera, const Basis &p_orientation, float p_multiplier, const Vector3 &p_position) { +void RasterizerEffectsRD::render_sky(RD::DrawListID p_list, float p_time, RID p_fb, RID p_samplers, RID p_fog, RenderPipelineVertexFormatCacheRD *p_pipeline, RID p_uniform_set, RID p_texture_set, const CameraMatrix &p_camera, const Basis &p_orientation, float p_multiplier, const Vector3 &p_position) { SkyPushConstant sky_push_constant; zeromem(&sky_push_constant, sizeof(SkyPushConstant)); @@ -1198,7 +1198,7 @@ void RasterizerEffectsRD::render_sky(RD::DrawListID p_list, float p_time, RID p_ RD::get_singleton()->draw_list_bind_uniform_set(draw_list, p_uniform_set, 1); } RD::get_singleton()->draw_list_bind_uniform_set(draw_list, p_texture_set, 2); - RD::get_singleton()->draw_list_bind_uniform_set(draw_list, p_lights, 3); + RD::get_singleton()->draw_list_bind_uniform_set(draw_list, p_fog, 3); RD::get_singleton()->draw_list_bind_index_array(draw_list, index_array); diff --git a/servers/rendering/rasterizer_rd/rasterizer_effects_rd.h b/servers/rendering/rasterizer_rd/rasterizer_effects_rd.h index 2ba9610498..b0964f23e7 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_effects_rd.h +++ b/servers/rendering/rasterizer_rd/rasterizer_effects_rd.h @@ -638,7 +638,7 @@ public: void roughness_limit(RID p_source_normal, RID p_roughness, const Size2i &p_size, float p_curve); void cubemap_downsample(RID p_source_cubemap, RID p_dest_cubemap, const Size2i &p_size); void cubemap_filter(RID p_source_cubemap, Vector<RID> p_dest_cubemap, bool p_use_array); - void render_sky(RD::DrawListID p_list, float p_time, RID p_fb, RID p_samplers, RID p_lights, RenderPipelineVertexFormatCacheRD *p_pipeline, RID p_uniform_set, RID p_texture_set, const CameraMatrix &p_camera, const Basis &p_orientation, float p_multiplier, const Vector3 &p_position); + void render_sky(RD::DrawListID p_list, float p_time, RID p_fb, RID p_samplers, RID p_fog, RenderPipelineVertexFormatCacheRD *p_pipeline, RID p_uniform_set, RID p_texture_set, const CameraMatrix &p_camera, const Basis &p_orientation, float p_multiplier, const Vector3 &p_position); void screen_space_reflection(RID p_diffuse, RID p_normal_roughness, RS::EnvironmentSSRRoughnessQuality p_roughness_quality, RID p_blur_radius, RID p_blur_radius2, RID p_metallic, const Color &p_metallic_mask, RID p_depth, RID p_scale_depth, RID p_scale_normal, RID p_output, RID p_output_blur, const Size2i &p_screen_size, int p_max_steps, float p_fade_in, float p_fade_out, float p_tolerance, const CameraMatrix &p_camera); void merge_specular(RID p_dest_framebuffer, RID p_specular, RID p_base, RID p_reflection); diff --git a/servers/rendering/rasterizer_rd/rasterizer_rd.cpp b/servers/rendering/rasterizer_rd/rasterizer_rd.cpp index 18cf4fa340..509bd3ee73 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_rd.cpp +++ b/servers/rendering/rasterizer_rd/rasterizer_rd.cpp @@ -90,7 +90,7 @@ void RasterizerRD::begin_frame(double frame_step) { void RasterizerRD::end_frame(bool p_swap_buffers) { #ifndef _MSC_VER -#warning TODO: likely passa bool to swap buffers to avoid display? +#warning TODO: likely pass a bool to swap buffers to avoid display? #endif RD::get_singleton()->swap_buffers(); //probably should pass some bool to avoid display? } diff --git a/servers/rendering/rasterizer_rd/rasterizer_scene_high_end_rd.cpp b/servers/rendering/rasterizer_rd/rasterizer_scene_high_end_rd.cpp index f65c5d5de3..11abb8f4a8 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_scene_high_end_rd.cpp +++ b/servers/rendering/rasterizer_rd/rasterizer_scene_high_end_rd.cpp @@ -1156,8 +1156,19 @@ void RasterizerSceneHighEndRD::_setup_environment(RID p_environment, RID p_rende if (render_buffers_has_volumetric_fog(p_render_buffers)) { scene_state.ubo.volumetric_fog_enabled = true; - scene_state.ubo.volumetric_fog_inv_length = 1.0 / render_buffers_get_volumetric_fog_end(p_render_buffers); - scene_state.ubo.volumetric_fog_detail_spread = 1.0 / render_buffers_get_volumetric_fog_detail_spread(p_render_buffers); //reverse lookup + float fog_end = render_buffers_get_volumetric_fog_end(p_render_buffers); + if (fog_end > 0.0) { + scene_state.ubo.volumetric_fog_inv_length = 1.0 / fog_end; + } else { + scene_state.ubo.volumetric_fog_inv_length = 1.0; + } + + float fog_detail_spread = render_buffers_get_volumetric_fog_detail_spread(p_render_buffers); //reverse lookup + if (fog_detail_spread > 0.0) { + scene_state.ubo.volumetric_fog_detail_spread = 1.0 / fog_detail_spread; + } else { + scene_state.ubo.volumetric_fog_detail_spread = 1.0; + } } } #if 0 @@ -1696,6 +1707,7 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor } RID radiance_uniform_set; bool draw_sky = false; + bool draw_sky_fog_only = false; Color clear_color; bool keep_color = false; @@ -1711,12 +1723,20 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor clear_color.r *= bg_energy; clear_color.g *= bg_energy; clear_color.b *= bg_energy; + if (render_buffers_has_volumetric_fog(p_render_buffer) || environment_is_fog_enabled(p_environment)) { + draw_sky_fog_only = true; + storage->material_set_param(sky_scene_state.fog_material, "clear_color", Variant(clear_color.to_linear())); + } } break; case RS::ENV_BG_COLOR: { clear_color = environment_get_bg_color(p_environment); clear_color.r *= bg_energy; clear_color.g *= bg_energy; clear_color.b *= bg_energy; + if (render_buffers_has_volumetric_fog(p_render_buffer) || environment_is_fog_enabled(p_environment)) { + draw_sky_fog_only = true; + storage->material_set_param(sky_scene_state.fog_material, "clear_color", Variant(clear_color.to_linear())); + } } break; case RS::ENV_BG_SKY: { draw_sky = true; @@ -1733,18 +1753,19 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor } } // setup sky if used for ambient, reflections, or background - if (draw_sky || environment_get_reflection_source(p_environment) == RS::ENV_REFLECTION_SOURCE_SKY || environment_get_ambient_source(p_environment) == RS::ENV_AMBIENT_SOURCE_SKY) { + if (draw_sky || draw_sky_fog_only || environment_get_reflection_source(p_environment) == RS::ENV_REFLECTION_SOURCE_SKY || environment_get_ambient_source(p_environment) == RS::ENV_AMBIENT_SOURCE_SKY) { + RENDER_TIMESTAMP("Setup Sky"); + CameraMatrix projection = p_cam_projection; + if (p_reflection_probe.is_valid()) { + CameraMatrix correction; + correction.set_depth_correction(true); + projection = correction * p_cam_projection; + } + + _setup_sky(p_environment, p_render_buffer, projection, p_cam_transform, screen_size); + RID sky = environment_get_sky(p_environment); if (sky.is_valid()) { - RENDER_TIMESTAMP("Setup Sky"); - CameraMatrix projection = p_cam_projection; - if (p_reflection_probe.is_valid()) { - CameraMatrix correction; - correction.set_depth_correction(true); - projection = correction * p_cam_projection; - } - - _setup_sky(p_environment, p_cam_transform.origin, screen_size); _update_sky(p_environment, projection, p_cam_transform); radiance_uniform_set = sky_get_radiance_uniform_set_rd(sky, default_shader_rd, RADIANCE_UNIFORM_SET); } else { @@ -1813,8 +1834,8 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor bool can_continue_depth = !scene_state.used_depth_texture && !using_ssr && !using_sss; { - bool will_continue_color = (can_continue_color || draw_sky || debug_giprobes || debug_sdfgi_probes); - bool will_continue_depth = (can_continue_depth || draw_sky || debug_giprobes || debug_sdfgi_probes); + bool will_continue_color = (can_continue_color || draw_sky || draw_sky_fog_only || debug_giprobes || debug_sdfgi_probes); + bool will_continue_depth = (can_continue_depth || draw_sky || draw_sky_fog_only || debug_giprobes || debug_sdfgi_probes); //regular forward for now Vector<Color> c; @@ -1841,8 +1862,8 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor if (debug_giprobes) { //debug giprobes - bool will_continue_color = (can_continue_color || draw_sky); - bool will_continue_depth = (can_continue_depth || draw_sky); + bool will_continue_color = (can_continue_color || draw_sky || draw_sky_fog_only); + bool will_continue_depth = (can_continue_depth || draw_sky || draw_sky_fog_only); CameraMatrix dc; dc.set_depth_correction(true); @@ -1856,8 +1877,8 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor if (debug_sdfgi_probes) { //debug giprobes - bool will_continue_color = (can_continue_color || draw_sky); - bool will_continue_depth = (can_continue_depth || draw_sky); + bool will_continue_color = (can_continue_color || draw_sky || draw_sky_fog_only); + bool will_continue_depth = (can_continue_depth || draw_sky || draw_sky_fog_only); CameraMatrix dc; dc.set_depth_correction(true); @@ -1867,7 +1888,7 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor RD::get_singleton()->draw_list_end(); } - if (draw_sky) { + if (draw_sky || draw_sky_fog_only) { RENDER_TIMESTAMP("Render Sky"); CameraMatrix projection = p_cam_projection; @@ -2532,8 +2553,13 @@ void RasterizerSceneHighEndRD::_update_render_buffers_uniform_set(RID p_render_b RD::Uniform u; u.binding = 10; u.type = RD::UNIFORM_TYPE_TEXTURE; - RID vfog = render_buffers_get_volumetric_fog_texture(p_render_buffers); - if (vfog.is_null()) { + RID vfog = RID(); + if (p_render_buffers.is_valid() && render_buffers_has_volumetric_fog(p_render_buffers)) { + vfog = render_buffers_get_volumetric_fog_texture(p_render_buffers); + if (vfog.is_null()) { + vfog = storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_3D_WHITE); + } + } else { vfog = storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_3D_WHITE); } u.ids.push_back(vfog); diff --git a/servers/rendering/rasterizer_rd/rasterizer_scene_rd.cpp b/servers/rendering/rasterizer_rd/rasterizer_scene_rd.cpp index 5e39455c29..34818d2683 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_scene_rd.cpp +++ b/servers/rendering/rasterizer_rd/rasterizer_scene_rd.cpp @@ -2066,22 +2066,33 @@ RID RasterizerSceneRD::sky_get_material(RID p_sky) const { void RasterizerSceneRD::_draw_sky(bool p_can_continue_color, bool p_can_continue_depth, RID p_fb, RID p_environment, const CameraMatrix &p_projection, const Transform &p_transform) { ERR_FAIL_COND(!is_environment(p_environment)); + SkyMaterialData *material = nullptr; + Sky *sky = sky_owner.getornull(environment_get_sky(p_environment)); - ERR_FAIL_COND(!sky); - RID sky_material = sky_get_material(environment_get_sky(p_environment)); + RID sky_material; - SkyMaterialData *material = nullptr; + RS::EnvironmentBG background = environment_get_background(p_environment); - if (sky_material.is_valid()) { - material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); - if (!material || !material->shader_data->valid) { - material = nullptr; + if (!(background == RS::ENV_BG_CLEAR_COLOR || background == RS::ENV_BG_COLOR) || sky) { + ERR_FAIL_COND(!sky); + sky_material = sky_get_material(environment_get_sky(p_environment)); + + if (sky_material.is_valid()) { + material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); + if (!material || !material->shader_data->valid) { + material = nullptr; + } + } + + if (!material) { + sky_material = sky_shader.default_material; + material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); } } - if (!material) { - sky_material = sky_shader.default_material; + if (background == RS::ENV_BG_CLEAR_COLOR || background == RS::ENV_BG_COLOR) { + sky_material = sky_scene_state.fog_material; material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); } @@ -2121,7 +2132,7 @@ void RasterizerSceneRD::_draw_sky(bool p_can_continue_color, bool p_can_continue clear_colors.push_back(Color(0.0, 0.0, 0.0)); RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(sky->quarter_res_framebuffer, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); - storage->get_effects()->render_sky(draw_list, time, sky->quarter_res_framebuffer, sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); + storage->get_effects()->render_sky(draw_list, time, sky->quarter_res_framebuffer, sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } @@ -2134,149 +2145,191 @@ void RasterizerSceneRD::_draw_sky(bool p_can_continue_color, bool p_can_continue clear_colors.push_back(Color(0.0, 0.0, 0.0)); RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(sky->half_res_framebuffer, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors); - storage->get_effects()->render_sky(draw_list, time, sky->half_res_framebuffer, sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); + storage->get_effects()->render_sky(draw_list, time, sky->half_res_framebuffer, sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } RenderPipelineVertexFormatCacheRD *pipeline = &shader_data->pipelines[SKY_VERSION_BACKGROUND]; - RID texture_uniform_set = _get_sky_textures(sky, SKY_TEXTURE_SET_BACKGROUND); + RID texture_uniform_set; + if (sky) { + texture_uniform_set = _get_sky_textures(sky, SKY_TEXTURE_SET_BACKGROUND); + } else { + texture_uniform_set = sky_scene_state.fog_only_texture_uniform_set; + } RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin(p_fb, RD::INITIAL_ACTION_CONTINUE, p_can_continue_color ? RD::FINAL_ACTION_CONTINUE : RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_CONTINUE, p_can_continue_depth ? RD::FINAL_ACTION_CONTINUE : RD::FINAL_ACTION_READ); - storage->get_effects()->render_sky(draw_list, time, p_fb, sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); + storage->get_effects()->render_sky(draw_list, time, p_fb, sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, camera, sky_transform, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } -void RasterizerSceneRD::_setup_sky(RID p_environment, const Vector3 &p_position, const Size2i p_screen_size) { +void RasterizerSceneRD::_setup_sky(RID p_environment, RID p_render_buffers, const CameraMatrix &p_projection, const Transform &p_transform, const Size2i p_screen_size) { ERR_FAIL_COND(!is_environment(p_environment)); + SkyMaterialData *material = nullptr; + Sky *sky = sky_owner.getornull(environment_get_sky(p_environment)); - ERR_FAIL_COND(!sky); - RID sky_material = sky_get_material(environment_get_sky(p_environment)); + RID sky_material; - SkyMaterialData *material = nullptr; + SkyShaderData *shader_data = nullptr; - if (sky_material.is_valid()) { - material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); - if (!material || !material->shader_data->valid) { - material = nullptr; + RS::EnvironmentBG background = environment_get_background(p_environment); + + if (!(background == RS::ENV_BG_CLEAR_COLOR || background == RS::ENV_BG_COLOR) || sky) { + ERR_FAIL_COND(!sky); + sky_material = sky_get_material(environment_get_sky(p_environment)); + + if (sky_material.is_valid()) { + material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); + if (!material || !material->shader_data->valid) { + material = nullptr; + } } - } - if (!material) { - sky_material = sky_shader.default_material; - material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); - } + if (!material) { + sky_material = sky_shader.default_material; + material = (SkyMaterialData *)storage->material_get_data(sky_material, RasterizerStorageRD::SHADER_TYPE_SKY); + } - ERR_FAIL_COND(!material); + ERR_FAIL_COND(!material); - SkyShaderData *shader_data = material->shader_data; + shader_data = material->shader_data; - ERR_FAIL_COND(!shader_data); + ERR_FAIL_COND(!shader_data); + } - // Invalidate supbass buffers if screen size changes - if (sky->screen_size != p_screen_size) { - sky->screen_size = p_screen_size; - sky->screen_size.x = sky->screen_size.x < 4 ? 4 : sky->screen_size.x; - sky->screen_size.y = sky->screen_size.y < 4 ? 4 : sky->screen_size.y; - if (shader_data->uses_half_res) { - if (sky->half_res_pass.is_valid()) { - RD::get_singleton()->free(sky->half_res_pass); - sky->half_res_pass = RID(); + if (sky) { + // Invalidate supbass buffers if screen size changes + if (sky->screen_size != p_screen_size) { + sky->screen_size = p_screen_size; + sky->screen_size.x = sky->screen_size.x < 4 ? 4 : sky->screen_size.x; + sky->screen_size.y = sky->screen_size.y < 4 ? 4 : sky->screen_size.y; + if (shader_data->uses_half_res) { + if (sky->half_res_pass.is_valid()) { + RD::get_singleton()->free(sky->half_res_pass); + sky->half_res_pass = RID(); + } + _sky_invalidate(sky); } - _sky_invalidate(sky); - } - if (shader_data->uses_quarter_res) { - if (sky->quarter_res_pass.is_valid()) { - RD::get_singleton()->free(sky->quarter_res_pass); - sky->quarter_res_pass = RID(); + if (shader_data->uses_quarter_res) { + if (sky->quarter_res_pass.is_valid()) { + RD::get_singleton()->free(sky->quarter_res_pass); + sky->quarter_res_pass = RID(); + } + _sky_invalidate(sky); } + } + + // Create new subpass buffers if necessary + if ((shader_data->uses_half_res && sky->half_res_pass.is_null()) || + (shader_data->uses_quarter_res && sky->quarter_res_pass.is_null()) || + sky->radiance.is_null()) { _sky_invalidate(sky); + _update_dirty_skys(); } - } - // Create new subpass buffers if necessary - if ((shader_data->uses_half_res && sky->half_res_pass.is_null()) || - (shader_data->uses_quarter_res && sky->quarter_res_pass.is_null()) || - sky->radiance.is_null()) { - _sky_invalidate(sky); - _update_dirty_skys(); - } + if (shader_data->uses_time && time - sky->prev_time > 0.00001) { + sky->prev_time = time; + sky->reflection.dirty = true; + RenderingServerRaster::redraw_request(); + } - if (shader_data->uses_time && time - sky->prev_time > 0.00001) { - sky->prev_time = time; - sky->reflection.dirty = true; - RenderingServerRaster::redraw_request(); - } + if (material != sky->prev_material) { + sky->prev_material = material; + sky->reflection.dirty = true; + } - if (material != sky->prev_material) { - sky->prev_material = material; - sky->reflection.dirty = true; - } + if (material->uniform_set_updated) { + material->uniform_set_updated = false; + sky->reflection.dirty = true; + } - if (material->uniform_set_updated) { - material->uniform_set_updated = false; - sky->reflection.dirty = true; - } + if (!p_transform.origin.is_equal_approx(sky->prev_position) && shader_data->uses_position) { + sky->prev_position = p_transform.origin; + sky->reflection.dirty = true; + } - if (!p_position.is_equal_approx(sky->prev_position) && shader_data->uses_position) { - sky->prev_position = p_position; - sky->reflection.dirty = true; - } + if (shader_data->uses_light) { + // Check whether the directional_light_buffer changes + bool light_data_dirty = false; - if (shader_data->uses_light || sky_scene_state.light_uniform_set.is_null()) { - // Check whether the directional_light_buffer changes - bool light_data_dirty = false; - - if (sky_scene_state.directional_light_count != sky_scene_state.last_frame_directional_light_count) { - light_data_dirty = true; - for (uint32_t i = sky_scene_state.directional_light_count; i < sky_scene_state.max_directional_lights; i++) { - sky_scene_state.directional_lights[i].enabled = false; - } - } - if (!light_data_dirty) { - for (uint32_t i = 0; i < sky_scene_state.directional_light_count; i++) { - if (sky_scene_state.directional_lights[i].direction[0] != sky_scene_state.last_frame_directional_lights[i].direction[0] || - sky_scene_state.directional_lights[i].direction[1] != sky_scene_state.last_frame_directional_lights[i].direction[1] || - sky_scene_state.directional_lights[i].direction[2] != sky_scene_state.last_frame_directional_lights[i].direction[2] || - sky_scene_state.directional_lights[i].energy != sky_scene_state.last_frame_directional_lights[i].energy || - sky_scene_state.directional_lights[i].color[0] != sky_scene_state.last_frame_directional_lights[i].color[0] || - sky_scene_state.directional_lights[i].color[1] != sky_scene_state.last_frame_directional_lights[i].color[1] || - sky_scene_state.directional_lights[i].color[2] != sky_scene_state.last_frame_directional_lights[i].color[2] || - sky_scene_state.directional_lights[i].enabled != sky_scene_state.last_frame_directional_lights[i].enabled || - sky_scene_state.directional_lights[i].size != sky_scene_state.last_frame_directional_lights[i].size) { - light_data_dirty = true; - break; + if (sky_scene_state.ubo.directional_light_count != sky_scene_state.last_frame_directional_light_count) { + light_data_dirty = true; + for (uint32_t i = sky_scene_state.ubo.directional_light_count; i < sky_scene_state.max_directional_lights; i++) { + sky_scene_state.directional_lights[i].enabled = false; + } + } + if (!light_data_dirty) { + for (uint32_t i = 0; i < sky_scene_state.ubo.directional_light_count; i++) { + if (sky_scene_state.directional_lights[i].direction[0] != sky_scene_state.last_frame_directional_lights[i].direction[0] || + sky_scene_state.directional_lights[i].direction[1] != sky_scene_state.last_frame_directional_lights[i].direction[1] || + sky_scene_state.directional_lights[i].direction[2] != sky_scene_state.last_frame_directional_lights[i].direction[2] || + sky_scene_state.directional_lights[i].energy != sky_scene_state.last_frame_directional_lights[i].energy || + sky_scene_state.directional_lights[i].color[0] != sky_scene_state.last_frame_directional_lights[i].color[0] || + sky_scene_state.directional_lights[i].color[1] != sky_scene_state.last_frame_directional_lights[i].color[1] || + sky_scene_state.directional_lights[i].color[2] != sky_scene_state.last_frame_directional_lights[i].color[2] || + sky_scene_state.directional_lights[i].enabled != sky_scene_state.last_frame_directional_lights[i].enabled || + sky_scene_state.directional_lights[i].size != sky_scene_state.last_frame_directional_lights[i].size) { + light_data_dirty = true; + break; + } } } + + if (light_data_dirty) { + RD::get_singleton()->buffer_update(sky_scene_state.directional_light_buffer, 0, sizeof(SkyDirectionalLightData) * sky_scene_state.max_directional_lights, sky_scene_state.directional_lights, true); + + RasterizerSceneRD::SkyDirectionalLightData *temp = sky_scene_state.last_frame_directional_lights; + sky_scene_state.last_frame_directional_lights = sky_scene_state.directional_lights; + sky_scene_state.directional_lights = temp; + sky_scene_state.last_frame_directional_light_count = sky_scene_state.ubo.directional_light_count; + sky->reflection.dirty = true; + } } + } - if (light_data_dirty || sky_scene_state.light_uniform_set.is_null()) { - RD::get_singleton()->buffer_update(sky_scene_state.directional_light_buffer, 0, sizeof(SkyDirectionalLightData) * sky_scene_state.max_directional_lights, sky_scene_state.directional_lights, true); + //setup fog variables + sky_scene_state.ubo.volumetric_fog_enabled = false; + if (p_render_buffers.is_valid()) { + if (render_buffers_has_volumetric_fog(p_render_buffers)) { + sky_scene_state.ubo.volumetric_fog_enabled = true; - if (sky_scene_state.light_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(sky_scene_state.light_uniform_set)) { - RD::get_singleton()->free(sky_scene_state.light_uniform_set); + float fog_end = render_buffers_get_volumetric_fog_end(p_render_buffers); + if (fog_end > 0.0) { + sky_scene_state.ubo.volumetric_fog_inv_length = 1.0 / fog_end; + } else { + sky_scene_state.ubo.volumetric_fog_inv_length = 1.0; } - Vector<RD::Uniform> uniforms; - { - RD::Uniform u; - u.binding = 0; - u.type = RD::UNIFORM_TYPE_UNIFORM_BUFFER; - u.ids.push_back(sky_scene_state.directional_light_buffer); - uniforms.push_back(u); + float fog_detail_spread = render_buffers_get_volumetric_fog_detail_spread(p_render_buffers); //reverse lookup + if (fog_detail_spread > 0.0) { + sky_scene_state.ubo.volumetric_fog_detail_spread = 1.0 / fog_detail_spread; + } else { + sky_scene_state.ubo.volumetric_fog_detail_spread = 1.0; } + } - sky_scene_state.light_uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_LIGHTS); + RID fog_uniform_set = render_buffers_get_volumetric_fog_sky_uniform_set(p_render_buffers); - RasterizerSceneRD::SkyDirectionalLightData *temp = sky_scene_state.last_frame_directional_lights; - sky_scene_state.last_frame_directional_lights = sky_scene_state.directional_lights; - sky_scene_state.directional_lights = temp; - sky_scene_state.last_frame_directional_light_count = sky_scene_state.directional_light_count; - sky->reflection.dirty = true; + if (fog_uniform_set != RID()) { + sky_scene_state.fog_uniform_set = fog_uniform_set; + } else { + sky_scene_state.fog_uniform_set = sky_scene_state.default_fog_uniform_set; } } + + sky_scene_state.ubo.z_far = p_projection.get_z_far(); + sky_scene_state.ubo.fog_enabled = environment_is_fog_enabled(p_environment); + sky_scene_state.ubo.fog_density = environment_get_fog_density(p_environment); + Color fog_color = environment_get_fog_light_color(p_environment).to_linear(); + float fog_energy = environment_get_fog_light_energy(p_environment); + sky_scene_state.ubo.fog_light_color[0] = fog_color.r * fog_energy; + sky_scene_state.ubo.fog_light_color[1] = fog_color.g * fog_energy; + sky_scene_state.ubo.fog_light_color[2] = fog_color.b * fog_energy; + sky_scene_state.ubo.fog_sun_scatter = environment_get_fog_sun_scatter(p_environment); + + RD::get_singleton()->buffer_update(sky_scene_state.uniform_buffer, 0, sizeof(SkySceneState::UBO), &sky_scene_state.ubo, true); } void RasterizerSceneRD::_update_sky(RID p_environment, const CameraMatrix &p_projection, const Transform &p_transform) { @@ -2371,7 +2424,7 @@ void RasterizerSceneRD::_update_sky(RID p_environment, const CameraMatrix &p_pro RID texture_uniform_set = _get_sky_textures(sky, SKY_TEXTURE_SET_CUBEMAP_QUARTER_RES); cubemap_draw_list = RD::get_singleton()->draw_list_begin(sky->reflection.layers[0].mipmaps[2].framebuffers[i], RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_DISCARD); - storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[2].framebuffers[i], sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); + storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[2].framebuffers[i], sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } } @@ -2389,7 +2442,7 @@ void RasterizerSceneRD::_update_sky(RID p_environment, const CameraMatrix &p_pro RID texture_uniform_set = _get_sky_textures(sky, SKY_TEXTURE_SET_CUBEMAP_HALF_RES); cubemap_draw_list = RD::get_singleton()->draw_list_begin(sky->reflection.layers[0].mipmaps[1].framebuffers[i], RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_DISCARD); - storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[1].framebuffers[i], sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); + storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[1].framebuffers[i], sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } } @@ -2403,7 +2456,7 @@ void RasterizerSceneRD::_update_sky(RID p_environment, const CameraMatrix &p_pro RID texture_uniform_set = _get_sky_textures(sky, SKY_TEXTURE_SET_CUBEMAP); cubemap_draw_list = RD::get_singleton()->draw_list_begin(sky->reflection.layers[0].mipmaps[0].framebuffers[i], RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_READ, RD::INITIAL_ACTION_KEEP, RD::FINAL_ACTION_DISCARD); - storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[0].framebuffers[i], sky_scene_state.sampler_uniform_set, sky_scene_state.light_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); + storage->get_effects()->render_sky(cubemap_draw_list, time, sky->reflection.layers[0].mipmaps[0].framebuffers[i], sky_scene_state.uniform_set, sky_scene_state.fog_uniform_set, pipeline, material->uniform_set, texture_uniform_set, cm, local_view.basis, multiplier, p_transform.origin); RD::get_singleton()->draw_list_end(); } @@ -2965,7 +3018,7 @@ float RasterizerSceneRD::environment_get_fog_height_density(RID p_env) const { return env->fog_height_density; } -void RasterizerSceneRD::environment_set_volumetric_fog(RID p_env, bool p_enable, float p_density, const Color &p_light, float p_light_energy, float p_lenght, float p_detail_spread, float p_gi_inject, RenderingServer::EnvVolumetricFogShadowFilter p_shadow_filter) { +void RasterizerSceneRD::environment_set_volumetric_fog(RID p_env, bool p_enable, float p_density, const Color &p_light, float p_light_energy, float p_length, float p_detail_spread, float p_gi_inject, RenderingServer::EnvVolumetricFogShadowFilter p_shadow_filter) { Environment *env = environment_owner.getornull(p_env); ERR_FAIL_COND(!env); @@ -2973,7 +3026,7 @@ void RasterizerSceneRD::environment_set_volumetric_fog(RID p_env, bool p_enable, env->volumetric_fog_density = p_density; env->volumetric_fog_light = p_light; env->volumetric_fog_light_energy = p_light_energy; - env->volumetric_fog_length = p_lenght; + env->volumetric_fog_length = p_length; env->volumetric_fog_detail_spread = p_detail_spread; env->volumetric_fog_shadow_filter = p_shadow_filter; env->volumetric_fog_gi_inject = p_gi_inject; @@ -5607,6 +5660,17 @@ RID RasterizerSceneRD::render_buffers_get_volumetric_fog_texture(RID p_render_bu return rb->volumetric_fog->fog_map; } +RID RasterizerSceneRD::render_buffers_get_volumetric_fog_sky_uniform_set(RID p_render_buffers) { + const RenderBuffers *rb = render_buffers_owner.getornull(p_render_buffers); + ERR_FAIL_COND_V(!rb, RID()); + + if (!rb->volumetric_fog) { + return RID(); + } + + return rb->volumetric_fog->sky_uniform_set; +} + float RasterizerSceneRD::render_buffers_get_volumetric_fog_end(RID p_render_buffers) { const RenderBuffers *rb = render_buffers_owner.getornull(p_render_buffers); ERR_FAIL_COND_V(!rb || !rb->volumetric_fog, 0); @@ -5838,7 +5902,7 @@ void RasterizerSceneRD::_setup_lights(RID *p_light_cull_result, int p_light_cull uint32_t light_count = 0; r_directional_light_count = 0; r_positional_light_count = 0; - sky_scene_state.directional_light_count = 0; + sky_scene_state.ubo.directional_light_count = 0; for (int i = 0; i < p_light_cull_count; i++) { RID li = p_light_cull_result[i]; @@ -6010,7 +6074,7 @@ void RasterizerSceneRD::_setup_lights(RID *p_light_cull_result, int p_light_cull sky_light_data.enabled = true; sky_light_data.size = angular_diameter; - sky_scene_state.directional_light_count++; + sky_scene_state.ubo.directional_light_count++; } r_directional_light_count++; @@ -6317,7 +6381,22 @@ void RasterizerSceneRD::_volumetric_fog_erase(RenderBuffers *rb) { RD::get_singleton()->free(rb->volumetric_fog->light_density_map); RD::get_singleton()->free(rb->volumetric_fog->fog_map); + + if (rb->volumetric_fog->uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(rb->volumetric_fog->uniform_set)) { + RD::get_singleton()->free(rb->volumetric_fog->uniform_set); + } + if (rb->volumetric_fog->uniform_set2.is_valid() && RD::get_singleton()->uniform_set_is_valid(rb->volumetric_fog->uniform_set2)) { + RD::get_singleton()->free(rb->volumetric_fog->uniform_set2); + } + if (rb->volumetric_fog->sdfgi_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(rb->volumetric_fog->sdfgi_uniform_set)) { + RD::get_singleton()->free(rb->volumetric_fog->sdfgi_uniform_set); + } + if (rb->volumetric_fog->sky_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(rb->volumetric_fog->sky_uniform_set)) { + RD::get_singleton()->free(rb->volumetric_fog->sky_uniform_set); + } + memdelete(rb->volumetric_fog); + rb->volumetric_fog = nullptr; } @@ -6407,6 +6486,17 @@ void RasterizerSceneRD::_update_volumetric_fog(RID p_render_buffers, RID p_envir rb->volumetric_fog->fog_map = RD::get_singleton()->texture_create(tf, RD::TextureView()); _render_buffers_uniform_set_changed(p_render_buffers); + + Vector<RD::Uniform> uniforms; + { + RD::Uniform u; + u.binding = 0; + u.type = RD::UNIFORM_TYPE_TEXTURE; + u.ids.push_back(rb->volumetric_fog->fog_map); + uniforms.push_back(u); + } + + rb->volumetric_fog->sky_uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_FOG); } //update directional shadow @@ -7907,6 +7997,7 @@ RasterizerSceneRD::RasterizerSceneRD(RasterizerStorageRD *p_storage) { actions.custom_samplers["RADIANCE"] = "material_samplers[3]"; actions.usage_defines["HALF_RES_COLOR"] = "\n#define USES_HALF_RES_COLOR\n"; actions.usage_defines["QUARTER_RES_COLOR"] = "\n#define USES_QUARTER_RES_COLOR\n"; + actions.render_mode_defines["disable_fog"] = "#define DISABLE_FOG\n"; actions.sampler_array_name = "material_samplers"; actions.base_texture_binding_index = 1; @@ -7931,6 +8022,8 @@ RasterizerSceneRD::RasterizerSceneRD(RasterizerStorageRD *p_storage) { SkyMaterialData *md = (SkyMaterialData *)storage->material_get_data(sky_shader.default_material, RasterizerStorageRD::SHADER_TYPE_SKY); sky_shader.default_shader_rd = sky_shader.shader.version_get_shader(md->shader_data->version, SKY_VERSION_BACKGROUND); + sky_scene_state.uniform_buffer = RD::get_singleton()->uniform_buffer_create(sizeof(SkySceneState::UBO)); + Vector<RD::Uniform> uniforms; { @@ -7962,7 +8055,70 @@ RasterizerSceneRD::RasterizerSceneRD(RasterizerStorageRD *p_storage) { uniforms.push_back(u); } - sky_scene_state.sampler_uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_SAMPLERS); + { + RD::Uniform u; + u.binding = 2; + u.type = RD::UNIFORM_TYPE_UNIFORM_BUFFER; + u.ids.push_back(sky_scene_state.uniform_buffer); + uniforms.push_back(u); + } + + { + RD::Uniform u; + u.binding = 3; + u.type = RD::UNIFORM_TYPE_UNIFORM_BUFFER; + u.ids.push_back(sky_scene_state.directional_light_buffer); + uniforms.push_back(u); + } + + sky_scene_state.uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_UNIFORMS); + } + + { + Vector<RD::Uniform> uniforms; + { + RD::Uniform u; + u.binding = 0; + u.type = RD::UNIFORM_TYPE_TEXTURE; + RID vfog = storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_3D_WHITE); + u.ids.push_back(vfog); + uniforms.push_back(u); + } + + sky_scene_state.default_fog_uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_FOG); + } + + { + // Need defaults for using fog with clear color + sky_scene_state.fog_shader = storage->shader_create(); + storage->shader_set_code(sky_scene_state.fog_shader, "shader_type sky; uniform vec4 clear_color; void fragment() { COLOR = clear_color.rgb; } \n"); + sky_scene_state.fog_material = storage->material_create(); + storage->material_set_shader(sky_scene_state.fog_material, sky_scene_state.fog_shader); + + Vector<RD::Uniform> uniforms; + { + RD::Uniform u; + u.type = RD::UNIFORM_TYPE_TEXTURE; + u.binding = 0; + u.ids.push_back(storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_CUBEMAP_BLACK)); + uniforms.push_back(u); + } + { + RD::Uniform u; + u.type = RD::UNIFORM_TYPE_TEXTURE; + u.binding = 1; + u.ids.push_back(storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_WHITE)); + uniforms.push_back(u); + } + { + RD::Uniform u; + u.type = RD::UNIFORM_TYPE_TEXTURE; + u.binding = 2; + u.ids.push_back(storage->texture_rd_get_default(RasterizerStorageRD::DEFAULT_RD_TEXTURE_WHITE)); + uniforms.push_back(u); + } + + sky_scene_state.fog_only_texture_uniform_set = RD::get_singleton()->uniform_set_create(uniforms, sky_shader.default_shader_rd, SKY_SET_TEXTURES); } { @@ -8179,11 +8335,8 @@ RasterizerSceneRD::~RasterizerSceneRD() { RD::get_singleton()->free(E->get().cubemap); } - if (sky_scene_state.sampler_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(sky_scene_state.sampler_uniform_set)) { - RD::get_singleton()->free(sky_scene_state.sampler_uniform_set); - } - if (sky_scene_state.light_uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(sky_scene_state.light_uniform_set)) { - RD::get_singleton()->free(sky_scene_state.light_uniform_set); + if (sky_scene_state.uniform_set.is_valid() && RD::get_singleton()->uniform_set_is_valid(sky_scene_state.uniform_set)) { + RD::get_singleton()->free(sky_scene_state.uniform_set); } RD::get_singleton()->free(default_giprobe_buffer); @@ -8199,14 +8352,19 @@ RasterizerSceneRD::~RasterizerSceneRD() { sdfgi_shader.integrate.version_free(sdfgi_shader.integrate_shader); sdfgi_shader.preprocess.version_free(sdfgi_shader.preprocess_shader); + volumetric_fog.shader.version_free(volumetric_fog.shader_version); + memdelete_arr(gi_probe_lights); SkyMaterialData *md = (SkyMaterialData *)storage->material_get_data(sky_shader.default_material, RasterizerStorageRD::SHADER_TYPE_SKY); sky_shader.shader.version_free(md->shader_data->version); RD::get_singleton()->free(sky_scene_state.directional_light_buffer); + RD::get_singleton()->free(sky_scene_state.uniform_buffer); memdelete_arr(sky_scene_state.directional_lights); memdelete_arr(sky_scene_state.last_frame_directional_lights); storage->free(sky_shader.default_shader); storage->free(sky_shader.default_material); + storage->free(sky_scene_state.fog_shader); + storage->free(sky_scene_state.fog_material); memdelete_arr(directional_penumbra_shadow_kernel); memdelete_arr(directional_soft_shadow_kernel); memdelete_arr(penumbra_shadow_kernel); diff --git a/servers/rendering/rasterizer_rd/rasterizer_scene_rd.h b/servers/rendering/rasterizer_rd/rasterizer_scene_rd.h index 4c48960587..f504240f50 100644 --- a/servers/rendering/rasterizer_rd/rasterizer_scene_rd.h +++ b/servers/rendering/rasterizer_rd/rasterizer_scene_rd.h @@ -63,14 +63,37 @@ protected: }; struct SkySceneState { + struct UBO { + uint32_t volumetric_fog_enabled; + float volumetric_fog_inv_length; + float volumetric_fog_detail_spread; + uint32_t volumetric_fog_pad; + + float fog_light_color[3]; + float fog_sun_scatter; + + uint32_t fog_enabled; + float fog_density; + + float z_far; + uint32_t directional_light_count; + }; + + UBO ubo; + SkyDirectionalLightData *directional_lights; SkyDirectionalLightData *last_frame_directional_lights; uint32_t max_directional_lights; - uint32_t directional_light_count; uint32_t last_frame_directional_light_count; RID directional_light_buffer; - RID sampler_uniform_set; - RID light_uniform_set; + RID uniform_set; + RID uniform_buffer; + RID fog_uniform_set; + RID default_fog_uniform_set; + + RID fog_shader; + RID fog_material; + RID fog_only_texture_uniform_set; } sky_scene_state; struct RenderBufferData { @@ -105,7 +128,7 @@ protected: void _process_ssr(RID p_render_buffers, RID p_dest_framebuffer, RID p_normal_buffer, RID p_specular_buffer, RID p_metallic, const Color &p_metallic_mask, RID p_environment, const CameraMatrix &p_projection, bool p_use_additive); void _process_sss(RID p_render_buffers, const CameraMatrix &p_camera); - void _setup_sky(RID p_environment, const Vector3 &p_position, const Size2i p_screen_size); + void _setup_sky(RID p_environment, RID p_render_buffers, const CameraMatrix &p_projection, const Transform &p_transform, const Size2i p_screen_size); void _update_sky(RID p_environment, const CameraMatrix &p_projection, const Transform &p_transform); void _draw_sky(bool p_can_continue_color, bool p_can_continue_depth, RID p_fb, RID p_environment, const CameraMatrix &p_projection, const Transform &p_transform); void _process_gi(RID p_render_buffers, RID p_normal_roughness_buffer, RID p_ambient_buffer, RID p_reflection_buffer, RID p_gi_probe_buffer, RID p_environment, const CameraMatrix &p_projection, const Transform &p_transform, RID *p_gi_probe_cull_result, int p_gi_probe_cull_count); @@ -244,10 +267,10 @@ private: }; enum SkySet { - SKY_SET_SAMPLERS, + SKY_SET_UNIFORMS, SKY_SET_MATERIAL, SKY_SET_TEXTURES, - SKY_SET_LIGHTS, + SKY_SET_FOG, SKY_SET_MAX }; @@ -1355,6 +1378,7 @@ private: RID uniform_set; RID uniform_set2; RID sdfgi_uniform_set; + RID sky_uniform_set; int last_shadow_filter = -1; }; @@ -1837,6 +1861,7 @@ public: bool render_buffers_has_volumetric_fog(RID p_render_buffers) const; RID render_buffers_get_volumetric_fog_texture(RID p_render_buffers); + RID render_buffers_get_volumetric_fog_sky_uniform_set(RID p_render_buffers); float render_buffers_get_volumetric_fog_end(RID p_render_buffers); float render_buffers_get_volumetric_fog_detail_spread(RID p_render_buffers); diff --git a/servers/rendering/rasterizer_rd/shaders/sky.glsl b/servers/rendering/rasterizer_rd/shaders/sky.glsl index 9c59be6841..7b6de6a555 100644 --- a/servers/rendering/rasterizer_rd/shaders/sky.glsl +++ b/servers/rendering/rasterizer_rd/shaders/sky.glsl @@ -58,6 +58,35 @@ layout(set = 0, binding = 1, std430) restrict readonly buffer GlobalVariableData } global_variables; +layout(set = 0, binding = 2, std140) uniform SceneData { + bool volumetric_fog_enabled; + float volumetric_fog_inv_length; + float volumetric_fog_detail_spread; + uint volumetric_fog_pad; + + vec3 fog_light_color; + float fog_sun_scatter; + + bool fog_enabled; + float fog_density; + + float z_far; + uint directional_light_count; +} +scene_data; + +struct DirectionalLightData { + vec4 direction_energy; + vec4 color_size; + bool enabled; +}; + +layout(set = 0, binding = 3, std140) uniform DirectionalLights { + DirectionalLightData data[MAX_DIRECTIONAL_LIGHT_DATA_STRUCTS]; +} + +directional_lights; + #ifdef USE_MATERIAL_UNIFORMS layout(set = 1, binding = 0, std140) uniform MaterialUniforms{ /* clang-format off */ @@ -77,6 +106,8 @@ layout(set = 2, binding = 1) uniform texture2D half_res; layout(set = 2, binding = 2) uniform texture2D quarter_res; #endif +layout(set = 3, binding = 0) uniform texture3D volumetric_fog_texture; + #ifdef USE_CUBEMAP_PASS #define AT_CUBEMAP_PASS true #else @@ -95,18 +126,6 @@ layout(set = 2, binding = 2) uniform texture2D quarter_res; #define AT_QUARTER_RES_PASS false #endif -struct DirectionalLightData { - vec4 direction_energy; - vec4 color_size; - bool enabled; -}; - -layout(set = 3, binding = 0, std140) uniform DirectionalLights { - DirectionalLightData data[MAX_DIRECTIONAL_LIGHT_DATA_STRUCTS]; -} - -directional_lights; - /* clang-format off */ FRAGMENT_SHADER_GLOBALS @@ -115,6 +134,30 @@ FRAGMENT_SHADER_GLOBALS layout(location = 0) out vec4 frag_color; +vec4 volumetric_fog_process(vec2 screen_uv) { + vec3 fog_pos = vec3(screen_uv, 1.0); + + return texture(sampler3D(volumetric_fog_texture, material_samplers[SAMPLER_LINEAR_CLAMP]), fog_pos); +} + +vec4 fog_process(vec3 view) { + vec3 fog_color = scene_data.fog_light_color; + + if (scene_data.fog_sun_scatter > 0.001) { + vec4 sun_scatter = vec4(0.0); + float sun_total = 0.0; + for (uint i = 0; i < scene_data.directional_light_count; i++) { + vec3 light_color = directional_lights.data[i].color_size.xyz * directional_lights.data[i].direction_energy.w; + float light_amount = pow(max(dot(view, directional_lights.data[i].direction_energy.xyz), 0.0), 8.0); + fog_color += light_color * light_amount * scene_data.fog_sun_scatter; + } + } + + float fog_amount = clamp(1.0 - exp(-scene_data.z_far * scene_data.fog_density), 0.0, 1.0); + + return vec4(fog_color, fog_amount); +} + void main() { vec3 cube_normal; cube_normal.z = -1.0; @@ -178,6 +221,20 @@ FRAGMENT_SHADER_CODE frag_color.rgb = color * params.position_multiplier.w; frag_color.a = alpha; +#if !defined(DISABLE_FOG) && !defined(USE_CUBEMAP_PASS) + + if (scene_data.volumetric_fog_enabled) { + vec4 fog = volumetric_fog_process(uv); + frag_color.rgb = mix(frag_color.rgb, fog.rgb, fog.a); + } + + if (scene_data.fog_enabled) { + vec4 fog = fog_process(cube_normal); + frag_color.rgb = mix(frag_color.rgb, fog.rgb, fog.a); + } + +#endif // DISABLE_FOG + // Blending is disabled for Sky, so alpha doesn't blend // alpha is used for subsurface scattering so make sure it doesn't get applied to Sky if (!AT_CUBEMAP_PASS && !AT_HALF_RES_PASS && !AT_QUARTER_RES_PASS) { diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index 06cb6171a5..76aeb5989c 100644 --- a/servers/rendering/shader_types.cpp +++ b/servers/rendering/shader_types.cpp @@ -328,6 +328,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SKY].modes.push_back("use_half_res_pass"); shader_modes[RS::SHADER_SKY].modes.push_back("use_quarter_res_pass"); + shader_modes[RS::SHADER_SKY].modes.push_back("disable_fog"); shader_types.insert("spatial"); shader_types.insert("canvas_item"); diff --git a/tests/test_gradient.h b/tests/test_gradient.h new file mode 100644 index 0000000000..88fe06b3ec --- /dev/null +++ b/tests/test_gradient.h @@ -0,0 +1,152 @@ +/*************************************************************************/ +/* test_gradient.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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_GRADIENT_H +#define TEST_GRADIENT_H + +#include "core/class_db.h" +#include "core/color.h" +#include "scene/resources/gradient.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestGradient { + +TEST_CASE("[Gradient] Default gradient") { + // Black-white gradient. + Ref<Gradient> gradient = memnew(Gradient); + + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Default gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.4).is_equal_approx(Color(0.4, 0.4, 0.4)), + "Default gradient should return the expected interpolated value at offset 0.4."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.8).is_equal_approx(Color(0.8, 0.8, 0.8)), + "Default gradient should return the expected interpolated value at offset 0.8."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1.0."); + + // Out of bounds checks. + CHECK_MESSAGE( + gradient->get_color_at_offset(-1.0).is_equal_approx(Color(0, 0, 0)), + "Default gradient should return the expected interpolated value at offset -1.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1234.0).is_equal_approx(Color(1, 1, 1)), + "Default gradient should return the expected interpolated value at offset 1234.0."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified in order)") { + // Red-yellow-green gradient (with overbright green). + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.0, Color(1, 0, 0) }); + points.push_back({ 0.5, Color(1, 1, 0) }); + points.push_back({ 1.0, Color(0, 2, 0) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 3, + "Custom gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 0, 0)), + "Custom gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.25).is_equal_approx(Color(1, 0.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.25."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(1, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.75).is_equal_approx(Color(0.5, 1.5, 0)), + "Custom gradient should return the expected interpolated value at offset 0.75."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 2, 0)), + "Custom gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(1); + CHECK_MESSAGE( + gradient->get_points_count() == 2, + "Custom gradient should contain the expected number of points after removing one point."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.5).is_equal_approx(Color(0.5, 1, 0)), + "Custom gradient should return the expected interpolated value at offset 0.5 after removing point at index 1."); +} + +TEST_CASE("[Gradient] Custom gradient (points specified out-of-order)") { + // HSL rainbow with points specified out of order. + // These should be sorted automatically when adding points. + Ref<Gradient> gradient = memnew(Gradient); + Vector<Gradient::Point> points; + points.push_back({ 0.2, Color(1, 0, 0) }); + points.push_back({ 0.0, Color(1, 1, 0) }); + points.push_back({ 0.8, Color(0, 1, 0) }); + points.push_back({ 0.4, Color(0, 1, 1) }); + points.push_back({ 1.0, Color(0, 0, 1) }); + points.push_back({ 0.6, Color(1, 0, 1) }); + gradient->set_points(points); + + CHECK_MESSAGE( + gradient->get_points_count() == 6, + "Custom out-of-order gradient should contain the expected number of points."); + + CHECK_MESSAGE( + gradient->get_color_at_offset(0.0).is_equal_approx(Color(1, 1, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.0."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.3).is_equal_approx(Color(0.5, 0.5, 0.5)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.3."); + CHECK_MESSAGE( + gradient->get_color_at_offset(0.6).is_equal_approx(Color(1, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.6."); + CHECK_MESSAGE( + gradient->get_color_at_offset(1.0).is_equal_approx(Color(0, 0, 1)), + "Custom out-of-order gradient should return the expected interpolated value at offset 1.0."); + + gradient->remove_point(0); + CHECK_MESSAGE( + gradient->get_points_count() == 5, + "Custom out-of-order gradient should contain the expected number of points after removing one point."); + // The color will be clamped to the nearest point (which is at offset 0.2). + CHECK_MESSAGE( + gradient->get_color_at_offset(0.1).is_equal_approx(Color(1, 0, 0)), + "Custom out-of-order gradient should return the expected interpolated value at offset 0.1 after removing point at index 0."); +} + +} // namespace TestGradient + +#endif // TEST_GRADIENT_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 137882841c..892cc26a51 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -37,6 +37,7 @@ #include "test_class_db.h" #include "test_color.h" #include "test_gdscript.h" +#include "test_gradient.h" #include "test_gui.h" #include "test_math.h" #include "test_oa_hash_map.h" |