summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/actions/upload-artifact/action.yml3
-rw-r--r--core/io/dir_access.cpp2
-rw-r--r--core/io/resource_importer.cpp14
-rw-r--r--doc/classes/EditorScript.xml5
-rw-r--r--doc/classes/TextServer.xml5
-rw-r--r--editor/debugger/script_editor_debugger.cpp2
-rw-r--r--editor/editor_file_system.cpp13
-rw-r--r--editor/editor_interface.cpp2
-rw-r--r--editor/editor_script.cpp20
-rw-r--r--editor/editor_settings.cpp2
-rw-r--r--editor/export/codesign.cpp (renamed from platform/macos/export/codesign.cpp)0
-rw-r--r--editor/export/codesign.h (renamed from platform/macos/export/codesign.h)6
-rw-r--r--editor/export/lipo.cpp (renamed from platform/macos/export/lipo.cpp)108
-rw-r--r--editor/export/lipo.h (renamed from platform/macos/export/lipo.h)11
-rw-r--r--editor/export/macho.cpp (renamed from platform/macos/export/macho.cpp)0
-rw-r--r--editor/export/macho.h (renamed from platform/macos/export/macho.h)23
-rw-r--r--editor/project_manager.cpp2
-rw-r--r--editor/project_manager/quick_settings_dialog.cpp34
-rw-r--r--editor/project_manager/quick_settings_dialog.h3
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml2
-rw-r--r--modules/gdscript/gdscript_parser.cpp110
-rw-r--r--modules/gdscript/tests/scripts/parser/features/export_variable.gd6
-rw-r--r--modules/gdscript/tests/scripts/parser/features/export_variable.notest.gd2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/export_variable.out6
-rw-r--r--modules/gdscript/tests/scripts/parser/features/export_variable_global.notest.gd2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/export_variable_unnamed.notest.gd1
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.cpp2
-rw-r--r--modules/text_server_adv/text_server_adv.cpp154
-rw-r--r--modules/text_server_fb/text_server_fb.cpp132
-rw-r--r--platform/ios/doc_classes/EditorExportPlatformIOS.xml3
-rw-r--r--platform/ios/export/export_plugin.cpp242
-rw-r--r--platform/ios/export/export_plugin.h2
-rw-r--r--platform/linuxbsd/tts_linux.cpp8
-rw-r--r--platform/macos/export/export_plugin.cpp6
-rw-r--r--scene/audio/audio_stream_player_internal.cpp3
-rw-r--r--scene/resources/material.cpp18
36 files changed, 745 insertions, 209 deletions
diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml
index 8033839a7c..80c680103d 100644
--- a/.github/actions/upload-artifact/action.yml
+++ b/.github/actions/upload-artifact/action.yml
@@ -16,4 +16,5 @@ runs:
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
- retention-days: 14
+ # Default is 90 days.
+ retention-days: 60
diff --git a/core/io/dir_access.cpp b/core/io/dir_access.cpp
index 5df67b1103..2f3fe4deeb 100644
--- a/core/io/dir_access.cpp
+++ b/core/io/dir_access.cpp
@@ -339,6 +339,8 @@ String DirAccess::get_full_path(const String &p_path, AccessType p_access) {
}
Error DirAccess::copy(const String &p_from, const String &p_to, int p_chmod_flags) {
+ ERR_FAIL_COND_V_MSG(p_from == p_to, ERR_INVALID_PARAMETER, "Source and destination path are equal.");
+
//printf("copy %s -> %s\n",p_from.ascii().get_data(),p_to.ascii().get_data());
Error err;
{
diff --git a/core/io/resource_importer.cpp b/core/io/resource_importer.cpp
index 4c16650439..9e6f3ba314 100644
--- a/core/io/resource_importer.cpp
+++ b/core/io/resource_importer.cpp
@@ -118,9 +118,21 @@ Error ResourceFormatImporter::_get_path_and_type(const String &p_path, PathAndTy
}
#endif
- if (r_path_and_type.path.is_empty() || r_path_and_type.type.is_empty()) {
+ if (r_path_and_type.type.is_empty()) {
return ERR_FILE_CORRUPT;
}
+ if (r_path_and_type.path.is_empty()) {
+ // Some importers may not write files to the .godot folder, so the path can be empty.
+ if (r_path_and_type.importer.is_empty()) {
+ return ERR_FILE_CORRUPT;
+ }
+
+ // It's only invalid if the extension for the importer is not empty.
+ Ref<ResourceImporter> importer = get_importer_by_name(r_path_and_type.importer);
+ if (importer.is_null() || !importer->get_save_extension().is_empty()) {
+ return ERR_FILE_CORRUPT;
+ }
+ }
return OK;
}
diff --git a/doc/classes/EditorScript.xml b/doc/classes/EditorScript.xml
index 24480437fd..bd18852dbc 100644
--- a/doc/classes/EditorScript.xml
+++ b/doc/classes/EditorScript.xml
@@ -44,8 +44,7 @@
<return type="void" />
<param index="0" name="node" type="Node" />
<description>
- Adds [param node] as a child of the root node in the editor context.
- [b]Warning:[/b] The implementation of this method is currently disabled.
+ Makes [param node] root of the currently opened scene. Only works if the scene is empty. If the [param node] is a scene instance, an inheriting scene will be created.
</description>
</method>
<method name="get_editor_interface" qualifiers="const" deprecated="[EditorInterface] is a global singleton and can be accessed directly by its name.">
@@ -57,7 +56,7 @@
<method name="get_scene" qualifiers="const">
<return type="Node" />
<description>
- Returns the Editor's currently active scene.
+ Returns the edited (current) scene's root [Node]. Equivalent of [method EditorInterface.get_edited_scene_root].
</description>
</method>
</methods>
diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml
index 34137a18ef..4fa9700f9c 100644
--- a/doc/classes/TextServer.xml
+++ b/doc/classes/TextServer.xml
@@ -1739,8 +1739,9 @@
When [param chars_per_line] is greater than zero, line break boundaries are returned instead.
[codeblock]
var ts = TextServerManager.get_primary_interface()
- print(ts.string_get_word_breaks("Godot Engine")) # Prints [0, 5, 6, 12]
- print(ts.string_get_word_breaks("Godot Engine", "en", 5)) # Prints [0, 5, 6, 11, 11, 12]
+ print(ts.string_get_word_breaks("The Godot Engine, 4")) # Prints [0, 3, 4, 9, 10, 16, 18, 19], which corresponds to the following substrings: "The", "Godot", "Engine", "4"
+ print(ts.string_get_word_breaks("The Godot Engine, 4", "en", 5)) # Prints [0, 3, 4, 9, 10, 15, 15, 19], which corresponds to the following substrings: "The", "Godot", "Engin", "e, 4"
+ print(ts.string_get_word_breaks("The Godot Engine, 4", "en", 10)) # Prints [0, 9, 10, 19], which corresponds to the following substrings: "The Godot", "Engine, 4"
[/codeblock]
</description>
</method>
diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp
index b062b20000..5e96daf69c 100644
--- a/editor/debugger/script_editor_debugger.cpp
+++ b/editor/debugger/script_editor_debugger.cpp
@@ -840,7 +840,7 @@ void ScriptEditorDebugger::_set_reason_text(const String &p_reason, MessageType
for (int i = 0; i < boundaries.size(); i += 2) {
const int start = boundaries[i];
const int end = boundaries[i + 1];
- lines.append(p_reason.substr(start, end - start + 1));
+ lines.append(p_reason.substr(start, end - start));
}
reason->set_tooltip_text(String("\n").join(lines));
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index 4bca42bf8b..d84ccb0c03 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -2468,18 +2468,23 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
int from = 0;
for (int i = 0; i < reimport_files.size(); i++) {
if (groups_to_reimport.has(reimport_files[i].path)) {
+ from = i + 1;
continue;
}
if (use_multiple_threads && reimport_files[i].threaded) {
- if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer) {
+ if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer || groups_to_reimport.has(reimport_files[i + 1].path)) {
if (from - i == 0) {
// Single file, do not use threads.
pr.step(reimport_files[i].path.get_file(), i);
_reimport_file(reimport_files[i].path);
} else {
Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(reimport_files[from].importer);
- ERR_CONTINUE(!importer.is_valid());
+ if (importer.is_null()) {
+ ERR_PRINT(vformat("Invalid importer for \"%s\".", reimport_files[from].importer));
+ from = i + 1;
+ continue;
+ }
importer->import_threaded_begin();
@@ -2509,6 +2514,10 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
} else {
pr.step(reimport_files[i].path.get_file(), i);
_reimport_file(reimport_files[i].path);
+
+ // We need to increment the counter, maybe the next file is multithreaded
+ // and doesn't have the same importer.
+ from = i + 1;
}
}
diff --git a/editor/editor_interface.cpp b/editor/editor_interface.cpp
index 35e846b344..46113ab2cb 100644
--- a/editor/editor_interface.cpp
+++ b/editor/editor_interface.cpp
@@ -39,6 +39,7 @@
#include "editor/editor_undo_redo_manager.h"
#include "editor/filesystem_dock.h"
#include "editor/gui/editor_run_bar.h"
+#include "editor/gui/editor_scene_tabs.h"
#include "editor/gui/scene_tree_editor.h"
#include "editor/inspector_dock.h"
#include "editor/plugins/node_3d_editor_plugin.h"
@@ -452,6 +453,7 @@ void EditorInterface::save_scene_as(const String &p_scene, bool p_with_preview)
void EditorInterface::mark_scene_as_unsaved() {
EditorUndoRedoManager::get_singleton()->set_history_as_unsaved(EditorNode::get_editor_data().get_current_edited_scene_history_id());
+ EditorSceneTabs::get_singleton()->update_scene_tabs();
}
void EditorInterface::save_all_scenes() {
diff --git a/editor/editor_script.cpp b/editor/editor_script.cpp
index a2af5a760e..30a4b6811c 100644
--- a/editor/editor_script.cpp
+++ b/editor/editor_script.cpp
@@ -32,7 +32,10 @@
#include "editor/editor_interface.h"
#include "editor/editor_node.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/gui/editor_scene_tabs.h"
#include "scene/main/node.h"
+#include "scene/resources/packed_scene.h"
void EditorScript::add_root_node(Node *p_node) {
if (!EditorNode::get_singleton()) {
@@ -41,11 +44,24 @@ void EditorScript::add_root_node(Node *p_node) {
}
if (EditorNode::get_singleton()->get_edited_scene()) {
- EditorNode::add_io_error("EditorScript::add_root_node: " + TTR("There is an edited scene already."));
+ EditorNode::add_io_error("EditorScript::add_root_node: " + TTR("The current scene already has a root node."));
return;
}
- //editor->set_edited_scene(p_node);
+ const String &scene_path = p_node->get_scene_file_path();
+ if (!scene_path.is_empty()) {
+ Ref<PackedScene> scene = ResourceLoader::load(scene_path);
+ if (scene.is_valid()) {
+ memfree(scene->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE)); // Ensure node cache.
+
+ p_node->set_scene_inherited_state(scene->get_state());
+ p_node->set_scene_file_path(String());
+ }
+ }
+
+ EditorNode::get_singleton()->set_edited_scene(p_node);
+ EditorUndoRedoManager::get_singleton()->set_history_as_unsaved(EditorNode::get_editor_data().get_current_edited_scene_history_id());
+ EditorSceneTabs::get_singleton()->update_scene_tabs();
}
Node *EditorScript::get_scene() const {
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 024aec22b6..702d15bf7b 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -662,7 +662,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
_initial_set("text_editor/script_list/sort_members_outline_alphabetically", false);
// Completion
- EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "text_editor/completion/idle_parse_delay", 2.0, "0.1,10,0.01")
+ EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "text_editor/completion/idle_parse_delay", 1.5, "0.1,10,0.01")
_initial_set("text_editor/completion/auto_brace_complete", true);
_initial_set("text_editor/completion/code_complete_enabled", true);
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "text_editor/completion/code_complete_delay", 0.3, "0.01,5,0.01,or_greater")
diff --git a/platform/macos/export/codesign.cpp b/editor/export/codesign.cpp
index 72d496b04d..72d496b04d 100644
--- a/platform/macos/export/codesign.cpp
+++ b/editor/export/codesign.cpp
diff --git a/platform/macos/export/codesign.h b/editor/export/codesign.h
index 49d53b376e..9a858c49ac 100644
--- a/platform/macos/export/codesign.h
+++ b/editor/export/codesign.h
@@ -28,8 +28,8 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-#ifndef MACOS_CODESIGN_H
-#define MACOS_CODESIGN_H
+#ifndef CODESIGN_H
+#define CODESIGN_H
// macOS code signature creation utility.
//
@@ -364,4 +364,4 @@ public:
#endif // MODULE_REGEX_ENABLED
-#endif // MACOS_CODESIGN_H
+#endif // CODESIGN_H
diff --git a/platform/macos/export/lipo.cpp b/editor/export/lipo.cpp
index 2d77e5960d..9038994b06 100644
--- a/platform/macos/export/lipo.cpp
+++ b/editor/export/lipo.cpp
@@ -37,7 +37,7 @@ bool LipO::is_lipo(const String &p_path) {
return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf);
}
-bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_files) {
+bool LipO::create_file(const String &p_output_path, const Vector<String> &p_files) {
close();
fa = FileAccess::open(p_output_path, FileAccess::WRITE);
@@ -125,6 +125,100 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f
return true;
}
+bool LipO::create_file(const String &p_output_path, const Vector<String> &p_files, const Vector<Vector2i> &p_cputypes) {
+ close();
+
+ fa = FileAccess::open(p_output_path, FileAccess::WRITE);
+ ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_output_path));
+ ERR_FAIL_COND_V(p_files.size() != p_cputypes.size(), false);
+
+ uint64_t max_size = 0;
+ for (int i = 0; i < p_files.size(); i++) {
+ Ref<FileAccess> fb = FileAccess::open(p_files[i], FileAccess::READ);
+ if (fb.is_null()) {
+ close();
+ ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s\".", p_files[i]));
+ }
+
+ {
+ FatArch arch;
+ MachO mh;
+ if (MachO::is_macho(p_files[i]) && mh.open_file(p_files[i])) {
+ arch.cputype = mh.get_cputype();
+ arch.cpusubtype = mh.get_cpusubtype();
+ arch.offset = 0;
+ arch.size = mh.get_size();
+ arch.align = mh.get_align();
+ ERR_FAIL_V_MSG(arch.cputype != (uint32_t)p_cputypes[i].x || arch.cpusubtype != (uint32_t)p_cputypes[i].y, vformat("Mismatching MachO architecture: \"%s\".", p_files[i]));
+ } else {
+ arch.cputype = (uint32_t)p_cputypes[i].x;
+ arch.cpusubtype = (uint32_t)p_cputypes[i].y;
+ arch.offset = 0;
+ arch.size = fb->get_length();
+ arch.align = 0x03;
+ }
+ max_size += arch.size;
+
+ archs.push_back(arch);
+ }
+ }
+
+ // Write header.
+ bool is_64 = (max_size >= std::numeric_limits<uint32_t>::max());
+ if (is_64) {
+ fa->store_32(0xbfbafeca);
+ } else {
+ fa->store_32(0xbebafeca);
+ }
+ fa->store_32(BSWAP32(archs.size()));
+ uint64_t offset = archs.size() * (is_64 ? 32 : 20) + 8;
+ for (int i = 0; i < archs.size(); i++) {
+ archs.write[i].offset = offset + PAD(offset, uint64_t(1) << archs[i].align);
+ if (is_64) {
+ fa->store_32(BSWAP32(archs[i].cputype));
+ fa->store_32(BSWAP32(archs[i].cpusubtype));
+ fa->store_64(BSWAP64(archs[i].offset));
+ fa->store_64(BSWAP64(archs[i].size));
+ fa->store_32(BSWAP32(archs[i].align));
+ fa->store_32(0);
+ } else {
+ fa->store_32(BSWAP32(archs[i].cputype));
+ fa->store_32(BSWAP32(archs[i].cpusubtype));
+ fa->store_32(BSWAP32(archs[i].offset));
+ fa->store_32(BSWAP32(archs[i].size));
+ fa->store_32(BSWAP32(archs[i].align));
+ }
+ offset = archs[i].offset + archs[i].size;
+ }
+
+ // Write files and padding.
+ for (int i = 0; i < archs.size(); i++) {
+ Ref<FileAccess> fb = FileAccess::open(p_files[i], FileAccess::READ);
+ if (fb.is_null()) {
+ close();
+ ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s\".", p_files[i]));
+ }
+ uint64_t cur = fa->get_position();
+ for (uint64_t j = cur; j < archs[i].offset; j++) {
+ fa->store_8(0);
+ }
+ int pages = archs[i].size / 4096;
+ int remain = archs[i].size % 4096;
+ unsigned char step[4096];
+ for (int j = 0; j < pages; j++) {
+ uint64_t br = fb->get_buffer(step, 4096);
+ if (br > 0) {
+ fa->store_buffer(step, br);
+ }
+ }
+ uint64_t br = fb->get_buffer(step, remain);
+ if (br > 0) {
+ fa->store_buffer(step, br);
+ }
+ }
+ return true;
+}
+
bool LipO::open_file(const String &p_path) {
close();
@@ -198,6 +292,18 @@ int LipO::get_arch_count() const {
return archs.size();
}
+uint32_t LipO::get_arch_cputype(int p_index) const {
+ ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened.");
+ ERR_FAIL_INDEX_V(p_index, archs.size(), 0);
+ return archs[p_index].cputype;
+}
+
+uint32_t LipO::get_arch_cpusubtype(int p_index) const {
+ ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened.");
+ ERR_FAIL_INDEX_V(p_index, archs.size(), 0);
+ return archs[p_index].cpusubtype;
+}
+
bool LipO::extract_arch(int p_index, const String &p_path) {
ERR_FAIL_COND_V_MSG(fa.is_null(), false, "LipO: File not opened.");
ERR_FAIL_INDEX_V(p_index, archs.size(), false);
diff --git a/platform/macos/export/lipo.h b/editor/export/lipo.h
index e375fc5a66..0b65959bc5 100644
--- a/platform/macos/export/lipo.h
+++ b/editor/export/lipo.h
@@ -28,8 +28,8 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-#ifndef MACOS_LIPO_H
-#define MACOS_LIPO_H
+#ifndef LIPO_H
+#define LIPO_H
// Universal / Universal 2 fat binary file creator and extractor.
@@ -57,10 +57,13 @@ class LipO : public RefCounted {
public:
static bool is_lipo(const String &p_path);
- bool create_file(const String &p_output_path, const PackedStringArray &p_files);
+ bool create_file(const String &p_output_path, const Vector<String> &p_files);
+ bool create_file(const String &p_output_path, const Vector<String> &p_files, const Vector<Vector2i> &p_cputypes);
bool open_file(const String &p_path);
int get_arch_count() const;
+ uint32_t get_arch_cputype(int p_index) const;
+ uint32_t get_arch_cpusubtype(int p_index) const;
bool extract_arch(int p_index, const String &p_path);
void close();
@@ -68,4 +71,4 @@ public:
~LipO();
};
-#endif // MACOS_LIPO_H
+#endif // LIPO_H
diff --git a/platform/macos/export/macho.cpp b/editor/export/macho.cpp
index a829774a88..a829774a88 100644
--- a/platform/macos/export/macho.cpp
+++ b/editor/export/macho.cpp
diff --git a/platform/macos/export/macho.h b/editor/export/macho.h
index a84de7de60..e7393a2fee 100644
--- a/platform/macos/export/macho.h
+++ b/editor/export/macho.h
@@ -28,8 +28,8 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-#ifndef MACOS_MACHO_H
-#define MACOS_MACHO_H
+#ifndef MACHO_H
+#define MACHO_H
// Mach-O binary object file format parser and editor.
@@ -39,6 +39,7 @@
#include "core/object/ref_counted.h"
class MachO : public RefCounted {
+public:
struct MachHeader {
uint32_t cputype;
uint32_t cpusubtype;
@@ -98,6 +99,21 @@ class MachO : public RefCounted {
LC_LINKER_OPTIMIZATION_HINT = 0x0000002e,
LC_VERSION_MIN_TVOS = 0x0000002f,
LC_VERSION_MIN_WATCHOS = 0x00000030,
+ LC_BUILD_VERSION = 0x00000032,
+ };
+
+ enum PlatformID {
+ PLATFORM_UNKNOWN = 0,
+ PLATFORM_MACOS = 1,
+ PLATFORM_IOS = 2,
+ PLATFORM_TVOS = 3,
+ PLATFORM_WATCHOS = 4,
+ PLATFORM_BRIDGEOS = 5,
+ PLATFORM_MACCATALYST = 6,
+ PLATFORM_IOSSIMULATOR = 7,
+ PLATFORM_TVOSSIMULATOR = 8,
+ PLATFORM_WATCHOSSIMULATOR = 9,
+ PLATFORM_DRIVERKIT = 10,
};
struct LoadCommandHeader {
@@ -158,6 +174,7 @@ class MachO : public RefCounted {
uint32_t reserved3;
};
+private:
Ref<FileAccess> fa;
bool swap = false;
@@ -208,4 +225,4 @@ public:
bool set_signature_size(uint64_t p_size);
};
-#endif // MACOS_MACHO_H
+#endif // MACHO_H
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index b7dce1a224..52d7b14b65 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -385,7 +385,7 @@ void ProjectManager::_dim_window() {
// Quick settings.
void ProjectManager::_show_quick_settings() {
- quick_settings_dialog->popup_centered(Size2(600, 200) * EDSCALE);
+ quick_settings_dialog->popup_centered(Size2(640, 200) * EDSCALE);
}
void ProjectManager::_restart_confirmed() {
diff --git a/editor/project_manager/quick_settings_dialog.cpp b/editor/project_manager/quick_settings_dialog.cpp
index 05e8523518..ee53bd7927 100644
--- a/editor/project_manager/quick_settings_dialog.cpp
+++ b/editor/project_manager/quick_settings_dialog.cpp
@@ -47,6 +47,7 @@ void QuickSettingsDialog::_fetch_setting_values() {
editor_themes.clear();
editor_scales.clear();
editor_network_modes.clear();
+ editor_directory_naming_conventions.clear();
{
List<PropertyInfo> editor_settings_properties;
@@ -61,6 +62,8 @@ void QuickSettingsDialog::_fetch_setting_values() {
editor_scales = pi.hint_string.split(",");
} else if (pi.name == "network/connection/network_mode") {
editor_network_modes = pi.hint_string.split(",");
+ } else if (pi.name == "project_manager/directory_naming_convention") {
+ editor_directory_naming_conventions = pi.hint_string.split(",");
}
}
}
@@ -120,6 +123,19 @@ void QuickSettingsDialog::_update_current_values() {
}
}
}
+
+ // Project directory naming options.
+ {
+ const int current_directory_naming = EDITOR_GET("project_manager/directory_naming_convention");
+
+ for (int i = 0; i < editor_directory_naming_conventions.size(); i++) {
+ const String &directory_naming_value = editor_directory_naming_conventions[i];
+ if (current_directory_naming == i) {
+ directory_naming_convention_button->set_text(directory_naming_value);
+ directory_naming_convention_button->select(i);
+ }
+ }
+ }
}
void QuickSettingsDialog::_add_setting_control(const String &p_text, Control *p_control) {
@@ -155,6 +171,10 @@ void QuickSettingsDialog::_network_mode_selected(int p_id) {
_set_setting_value("network/connection/network_mode", p_id);
}
+void QuickSettingsDialog::_directory_naming_convention_selected(int p_id) {
+ _set_setting_value("project_manager/directory_naming_convention", p_id);
+}
+
void QuickSettingsDialog::_set_setting_value(const String &p_setting, const Variant &p_value, bool p_restart_required) {
EditorSettings::get_singleton()->set(p_setting, p_value);
EditorSettings::get_singleton()->notify_changes();
@@ -284,6 +304,20 @@ QuickSettingsDialog::QuickSettingsDialog() {
_add_setting_control(TTR("Network Mode"), network_mode_option_button);
}
+ // Project directory naming options.
+ {
+ directory_naming_convention_button = memnew(OptionButton);
+ directory_naming_convention_button->set_fit_to_longest_item(false);
+ directory_naming_convention_button->connect(SceneStringName(item_selected), callable_mp(this, &QuickSettingsDialog::_directory_naming_convention_selected));
+
+ for (int i = 0; i < editor_directory_naming_conventions.size(); i++) {
+ const String &directory_naming_convention = editor_directory_naming_conventions[i];
+ directory_naming_convention_button->add_item(directory_naming_convention, i);
+ }
+
+ _add_setting_control(TTR("Directory Naming Convention"), directory_naming_convention_button);
+ }
+
_update_current_values();
#ifdef ANDROID_ENABLED
diff --git a/editor/project_manager/quick_settings_dialog.h b/editor/project_manager/quick_settings_dialog.h
index c4c4e601c9..7a03996934 100644
--- a/editor/project_manager/quick_settings_dialog.h
+++ b/editor/project_manager/quick_settings_dialog.h
@@ -47,6 +47,7 @@ class QuickSettingsDialog : public AcceptDialog {
Vector<String> editor_themes;
Vector<String> editor_scales;
Vector<String> editor_network_modes;
+ Vector<String> editor_directory_naming_conventions;
void _fetch_setting_values();
void _update_current_values();
@@ -60,6 +61,7 @@ class QuickSettingsDialog : public AcceptDialog {
OptionButton *theme_option_button = nullptr;
OptionButton *scale_option_button = nullptr;
OptionButton *network_mode_option_button = nullptr;
+ OptionButton *directory_naming_convention_button = nullptr;
Label *custom_theme_label = nullptr;
@@ -67,6 +69,7 @@ class QuickSettingsDialog : public AcceptDialog {
void _theme_selected(int p_id);
void _scale_selected(int p_id);
void _network_mode_selected(int p_id);
+ void _directory_naming_convention_selected(int p_id);
void _set_setting_value(const String &p_setting, const Variant &p_value, bool p_restart_required = false);
Label *restart_required_label = nullptr;
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 1909ca5ab5..6e7ac0dec9 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -314,7 +314,7 @@
@export var image_array: Array[Image]
@export var node_array: Array[Node]
[/codeblock]
- [b]Note:[/b] Custom resources and nodes must be registered as global classes using [code]class_name[/code].
+ [b]Note:[/b] Custom resources and nodes should be registered as global classes using [code]class_name[/code], since the Inspector currently only supports global classes. Otherwise, a less specific type will be exported instead.
[b]Note:[/b] Node export is only supported in [Node]-derived classes and has a number of other limitations.
</description>
</annotation>
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index f1a35c84b7..d0948e4831 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -4160,6 +4160,64 @@ static String _get_annotation_error_string(const StringName &p_annotation_name,
return vformat(R"("%s" annotation requires a variable of type %s, but type "%s" was given instead.)", p_annotation_name, string, p_provided_type.to_string());
}
+static StringName _find_narrowest_native_or_global_class(const GDScriptParser::DataType &p_type) {
+ switch (p_type.kind) {
+ case GDScriptParser::DataType::NATIVE: {
+ if (p_type.is_meta_type) {
+ return Object::get_class_static(); // `GDScriptNativeClass` is not an exposed class.
+ }
+ return p_type.native_type;
+ } break;
+ case GDScriptParser::DataType::SCRIPT: {
+ Ref<Script> script;
+ if (p_type.script_type.is_valid()) {
+ script = p_type.script_type;
+ } else {
+ script = ResourceLoader::load(p_type.script_path, SNAME("Script"));
+ }
+
+ if (p_type.is_meta_type) {
+ return script.is_valid() ? script->get_class() : Script::get_class_static();
+ }
+ if (script.is_null()) {
+ return p_type.native_type;
+ }
+ if (script->get_global_name() != StringName()) {
+ return script->get_global_name();
+ }
+
+ Ref<Script> base_script = script->get_base_script();
+ if (base_script.is_null()) {
+ return script->get_instance_base_type();
+ }
+
+ GDScriptParser::DataType base_type;
+ base_type.kind = GDScriptParser::DataType::SCRIPT;
+ base_type.builtin_type = Variant::OBJECT;
+ base_type.native_type = base_script->get_instance_base_type();
+ base_type.script_type = base_script;
+ base_type.script_path = base_script->get_path();
+
+ return _find_narrowest_native_or_global_class(base_type);
+ } break;
+ case GDScriptParser::DataType::CLASS: {
+ if (p_type.is_meta_type) {
+ return GDScript::get_class_static();
+ }
+ if (p_type.class_type == nullptr) {
+ return p_type.native_type;
+ }
+ if (p_type.class_type->get_global_name() != StringName()) {
+ return p_type.class_type->get_global_name();
+ }
+ return _find_narrowest_native_or_global_class(p_type.class_type->base_type);
+ } break;
+ default: {
+ ERR_FAIL_V(StringName());
+ } break;
+ }
+}
+
template <PropertyHint t_hint, Variant::Type t_type>
bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
@@ -4302,57 +4360,9 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node
variable->export_info.hint_string = String();
break;
case GDScriptParser::DataType::NATIVE:
- if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) {
- variable->export_info.type = Variant::OBJECT;
- variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE;
- variable->export_info.hint_string = export_type.native_type;
- } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) {
- variable->export_info.type = Variant::OBJECT;
- variable->export_info.hint = PROPERTY_HINT_NODE_TYPE;
- variable->export_info.hint_string = export_type.native_type;
- } else {
- push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation);
- return false;
- }
- break;
+ case GDScriptParser::DataType::SCRIPT:
case GDScriptParser::DataType::CLASS: {
- StringName class_name;
- if (export_type.class_type) {
- class_name = export_type.class_type->get_global_name();
- }
- if (class_name == StringName()) {
- push_error(R"(Script export type must be a global class.)", p_annotation);
- return false;
- }
- if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) {
- variable->export_info.type = Variant::OBJECT;
- variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE;
- variable->export_info.hint_string = class_name;
- } else if (ClassDB::is_parent_class(export_type.native_type, SNAME("Node"))) {
- variable->export_info.type = Variant::OBJECT;
- variable->export_info.hint = PROPERTY_HINT_NODE_TYPE;
- variable->export_info.hint_string = class_name;
- } else {
- push_error(R"(Export type can only be built-in, a resource, a node, or an enum.)", p_annotation);
- return false;
- }
- } break;
-
- case GDScriptParser::DataType::SCRIPT: {
- StringName class_name;
- if (export_type.script_type.is_valid()) {
- class_name = export_type.script_type->get_global_name();
- }
- if (class_name == StringName()) {
- Ref<Script> script = ResourceLoader::load(export_type.script_path, SNAME("Script"));
- if (script.is_valid()) {
- class_name = script->get_global_name();
- }
- }
- if (class_name == StringName()) {
- push_error(R"(Script export type must be a global class.)", p_annotation);
- return false;
- }
+ const StringName class_name = _find_narrowest_native_or_global_class(export_type);
if (ClassDB::is_parent_class(export_type.native_type, SNAME("Resource"))) {
variable->export_info.type = Variant::OBJECT;
variable->export_info.hint = PROPERTY_HINT_RESOURCE_TYPE;
diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.gd b/modules/gdscript/tests/scripts/parser/features/export_variable.gd
index 483e6cab0d..8bcb2bcb9a 100644
--- a/modules/gdscript/tests/scripts/parser/features/export_variable.gd
+++ b/modules/gdscript/tests/scripts/parser/features/export_variable.gd
@@ -2,7 +2,8 @@ class_name ExportVariableTest
extends Node
const Utils = preload("../../utils.notest.gd")
-const PreloadedScript = preload("./export_variable.notest.gd")
+const PreloadedGlobalClass = preload("./export_variable_global.notest.gd")
+const PreloadedUnnamedClass = preload("./export_variable_unnamed.notest.gd")
# Built-in types.
@export var test_weak_int = 1
@@ -24,7 +25,8 @@ const PreloadedScript = preload("./export_variable.notest.gd")
# Global custom classes.
@export var test_global_class: ExportVariableTest
-@export var test_preloaded_script: PreloadedScript
+@export var test_preloaded_global_class: PreloadedGlobalClass
+@export var test_preloaded_unnamed_class: PreloadedUnnamedClass # GH-93168
# Arrays.
@export var test_array: Array
diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.notest.gd b/modules/gdscript/tests/scripts/parser/features/export_variable.notest.gd
deleted file mode 100644
index 6d064351c1..0000000000
--- a/modules/gdscript/tests/scripts/parser/features/export_variable.notest.gd
+++ /dev/null
@@ -1,2 +0,0 @@
-class_name ExportPreloadedClassTest
-extends Node
diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.out b/modules/gdscript/tests/scripts/parser/features/export_variable.out
index bb094e14b4..d10462bb8d 100644
--- a/modules/gdscript/tests/scripts/parser/features/export_variable.out
+++ b/modules/gdscript/tests/scripts/parser/features/export_variable.out
@@ -25,8 +25,10 @@ var test_timer: Timer = null
hint=NODE_TYPE hint_string="Timer" usage=DEFAULT|SCRIPT_VARIABLE class_name=&"Timer"
var test_global_class: ExportVariableTest = null
hint=NODE_TYPE hint_string="ExportVariableTest" usage=DEFAULT|SCRIPT_VARIABLE class_name=&"ExportVariableTest"
-var test_preloaded_script: ExportPreloadedClassTest = null
- hint=NODE_TYPE hint_string="ExportPreloadedClassTest" usage=DEFAULT|SCRIPT_VARIABLE class_name=&"ExportPreloadedClassTest"
+var test_preloaded_global_class: ExportVariableTestGlobalClass = null
+ hint=NODE_TYPE hint_string="ExportVariableTestGlobalClass" usage=DEFAULT|SCRIPT_VARIABLE class_name=&"ExportVariableTestGlobalClass"
+var test_preloaded_unnamed_class: Node2D = null
+ hint=NODE_TYPE hint_string="Node2D" usage=DEFAULT|SCRIPT_VARIABLE class_name=&"Node2D"
var test_array: Array = []
hint=NONE hint_string="" usage=DEFAULT|SCRIPT_VARIABLE class_name=&""
var test_array_bool: Array = Array[bool]([])
diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable_global.notest.gd b/modules/gdscript/tests/scripts/parser/features/export_variable_global.notest.gd
new file mode 100644
index 0000000000..caa2ead214
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/export_variable_global.notest.gd
@@ -0,0 +1,2 @@
+class_name ExportVariableTestGlobalClass
+extends Node2D
diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable_unnamed.notest.gd b/modules/gdscript/tests/scripts/parser/features/export_variable_unnamed.notest.gd
new file mode 100644
index 0000000000..e251cf8aee
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/export_variable_unnamed.notest.gd
@@ -0,0 +1 @@
+extends Node2D
diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp
index ca3fa43207..852975b8eb 100644
--- a/modules/multiplayer/multiplayer_synchronizer.cpp
+++ b/modules/multiplayer/multiplayer_synchronizer.cpp
@@ -270,7 +270,7 @@ void MultiplayerSynchronizer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_replication_interval", "get_replication_interval");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "delta_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_delta_interval", "get_delta_interval");
- ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replication_config", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig", PROPERTY_USAGE_NO_EDITOR), "set_replication_config", "get_replication_config");
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replication_config", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT), "set_replication_config", "get_replication_config");
ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_update_mode", PROPERTY_HINT_ENUM, "Idle,Physics,None"), "set_visibility_update_mode", "get_visibility_update_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "public_visibility"), "set_visibility_public", "is_visibility_public");
diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp
index 33ba2da761..0c87199635 100644
--- a/modules/text_server_adv/text_server_adv.cpp
+++ b/modules/text_server_adv/text_server_adv.cpp
@@ -7048,10 +7048,10 @@ PackedInt32Array TextServerAdvanced::_string_get_word_breaks(const String &p_str
HashSet<int> breaks;
UErrorCode err = U_ZERO_ERROR;
- UBreakIterator *bi = ubrk_open(UBRK_LINE, lang.ascii().get_data(), (const UChar *)utf16.get_data(), utf16.length(), &err);
+ UBreakIterator *bi = ubrk_open(UBRK_WORD, lang.ascii().get_data(), (const UChar *)utf16.get_data(), utf16.length(), &err);
if (U_SUCCESS(err)) {
while (ubrk_next(bi) != UBRK_DONE) {
- int pos = _convert_pos(p_string, utf16, ubrk_current(bi)) - 1;
+ int pos = _convert_pos(p_string, utf16, ubrk_current(bi));
if (pos != p_string.length() - 1) {
breaks.insert(pos);
}
@@ -7061,79 +7061,111 @@ PackedInt32Array TextServerAdvanced::_string_get_word_breaks(const String &p_str
PackedInt32Array ret;
- int line_start = 0;
- int line_end = 0; // End of last word on current line.
- int word_start = 0; // -1 if no word encountered. Leading spaces are part of a word.
- int word_length = 0;
+ if (p_chars_per_line > 0) {
+ int line_start = 0;
+ int last_break = -1;
+ int line_length = 0;
- for (int i = 0; i < p_string.length(); i++) {
- const char32_t c = p_string[i];
+ for (int i = 0; i < p_string.length(); i++) {
+ const char32_t c = p_string[i];
- if (is_linebreak(c)) {
- // Force newline.
- ret.push_back(line_start);
- ret.push_back(i);
- line_start = i + 1;
- line_end = line_start;
- word_start = line_start;
- word_length = 0;
- } else if (c == 0xfffc) {
- continue;
- } else if ((u_ispunct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || is_whitespace(c)) {
- // A whitespace ends current word.
- if (word_length > 0) {
- line_end = i - 1;
- word_start = -1;
- word_length = 0;
- }
- } else if (breaks.has(i)) {
- // End current word, no space.
- if (word_length > 0) {
- line_end = i;
- word_start = i + 1;
- word_length = 0;
- }
- if (p_chars_per_line <= 0) {
- ret.push_back(line_start);
- ret.push_back(line_end + 1);
- line_start = word_start;
- line_end = line_start;
- }
- } else {
- if (word_start == -1) {
- word_start = i;
- if (p_chars_per_line <= 0) {
+ bool is_lb = is_linebreak(c);
+ bool is_ws = is_whitespace(c);
+ bool is_p = (u_ispunct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || c == 0xfffc;
+
+ if (is_lb) {
+ if (line_length > 0) {
ret.push_back(line_start);
- ret.push_back(line_end + 1);
- line_start = word_start;
- line_end = line_start;
+ ret.push_back(i);
}
+ line_start = i;
+ line_length = 0;
+ last_break = -1;
+ continue;
+ } else if (breaks.has(i) || is_ws || is_p) {
+ last_break = i;
}
- word_length += 1;
- if (p_chars_per_line > 0) {
- if (word_length > p_chars_per_line) {
- // Word too long: wrap before current character.
+ if (line_length == p_chars_per_line) {
+ if (last_break != -1) {
+ int last_break_w_spaces = last_break;
+ while (last_break > line_start && is_whitespace(p_string[last_break - 1])) {
+ last_break--;
+ }
+ if (line_start != last_break) {
+ ret.push_back(line_start);
+ ret.push_back(last_break);
+ }
+ while (last_break_w_spaces < p_string.length() && is_whitespace(p_string[last_break_w_spaces])) {
+ last_break_w_spaces++;
+ }
+ line_start = last_break_w_spaces;
+ if (last_break_w_spaces < i) {
+ line_length = i - last_break_w_spaces;
+ } else {
+ i = last_break_w_spaces;
+ line_length = 0;
+ }
+ } else {
ret.push_back(line_start);
ret.push_back(i);
line_start = i;
- line_end = i;
+ line_length = 0;
+ }
+ last_break = -1;
+ }
+ line_length++;
+ }
+ if (line_length > 0) {
+ ret.push_back(line_start);
+ ret.push_back(p_string.length());
+ }
+ } else {
+ int word_start = 0; // -1 if no word encountered. Leading spaces are part of a word.
+ int word_length = 0;
+
+ for (int i = 0; i < p_string.length(); i++) {
+ const char32_t c = p_string[i];
+
+ bool is_lb = is_linebreak(c);
+ bool is_ws = is_whitespace(c);
+ bool is_p = (u_ispunct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || c == 0xfffc;
+
+ if (word_start == -1) {
+ if (!is_lb && !is_ws && !is_p) {
word_start = i;
- word_length = 1;
- } else if (i - line_start + 1 > p_chars_per_line) {
- // Line too long: wrap after the last word.
- ret.push_back(line_start);
- ret.push_back(line_end + 1);
- line_start = word_start;
- line_end = line_start;
}
+ continue;
+ }
+
+ if (is_lb) {
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(i);
+ }
+ word_start = -1;
+ word_length = 0;
+ } else if (breaks.has(i) || is_ws || is_p) {
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(i);
+ }
+ if (is_ws || is_p) {
+ word_start = -1;
+ } else {
+ word_start = i;
+ }
+ word_length = 0;
}
+
+ word_length++;
+ }
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(p_string.length());
}
}
- if (line_start < p_string.length()) {
- ret.push_back(line_start);
- ret.push_back(p_string.length());
- }
+
return ret;
}
diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp
index 697c3366c5..6cf6b236ed 100644
--- a/modules/text_server_fb/text_server_fb.cpp
+++ b/modules/text_server_fb/text_server_fb.cpp
@@ -4492,65 +4492,105 @@ String TextServerFallback::_string_to_title(const String &p_string, const String
PackedInt32Array TextServerFallback::_string_get_word_breaks(const String &p_string, const String &p_language, int64_t p_chars_per_line) const {
PackedInt32Array ret;
- int line_start = 0;
- int line_end = 0; // End of last word on current line.
- int word_start = 0; // -1 if no word encountered. Leading spaces are part of a word.
- int word_length = 0;
+ if (p_chars_per_line > 0) {
+ int line_start = 0;
+ int last_break = -1;
+ int line_length = 0;
- for (int i = 0; i < p_string.length(); i++) {
- const char32_t c = p_string[i];
+ for (int i = 0; i < p_string.length(); i++) {
+ const char32_t c = p_string[i];
- if (is_linebreak(c)) {
- // Force newline.
- ret.push_back(line_start);
- ret.push_back(i);
- line_start = i + 1;
- line_end = line_start;
- word_start = line_start;
- word_length = 0;
- } else if (c == 0xfffc) {
- continue;
- } else if ((is_punct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || is_whitespace(c)) {
- // A whitespace ends current word.
- if (word_length > 0) {
- line_end = i - 1;
- word_start = -1;
- word_length = 0;
- }
- } else {
- if (word_start == -1) {
- word_start = i;
- if (p_chars_per_line <= 0) {
+ bool is_lb = is_linebreak(c);
+ bool is_ws = is_whitespace(c);
+ bool is_p = (is_punct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || c == 0xfffc;
+
+ if (is_lb) {
+ if (line_length > 0) {
ret.push_back(line_start);
- ret.push_back(line_end + 1);
- line_start = word_start;
- line_end = line_start;
+ ret.push_back(i);
}
+ line_start = i;
+ line_length = 0;
+ last_break = -1;
+ continue;
+ } else if (is_ws || is_p) {
+ last_break = i;
}
- word_length += 1;
- if (p_chars_per_line > 0) {
- if (word_length > p_chars_per_line) {
- // Word too long: wrap before current character.
+ if (line_length == p_chars_per_line) {
+ if (last_break != -1) {
+ int last_break_w_spaces = last_break;
+ while (last_break > line_start && is_whitespace(p_string[last_break - 1])) {
+ last_break--;
+ }
+ if (line_start != last_break) {
+ ret.push_back(line_start);
+ ret.push_back(last_break);
+ }
+ while (last_break_w_spaces < p_string.length() && is_whitespace(p_string[last_break_w_spaces])) {
+ last_break_w_spaces++;
+ }
+ line_start = last_break_w_spaces;
+ if (last_break_w_spaces < i) {
+ line_length = i - last_break_w_spaces;
+ } else {
+ i = last_break_w_spaces;
+ line_length = 0;
+ }
+ } else {
ret.push_back(line_start);
ret.push_back(i);
line_start = i;
- line_end = i;
+ line_length = 0;
+ }
+ last_break = -1;
+ }
+ line_length++;
+ }
+ if (line_length > 0) {
+ ret.push_back(line_start);
+ ret.push_back(p_string.length());
+ }
+ } else {
+ int word_start = 0; // -1 if no word encountered. Leading spaces are part of a word.
+ int word_length = 0;
+
+ for (int i = 0; i < p_string.length(); i++) {
+ const char32_t c = p_string[i];
+
+ bool is_lb = is_linebreak(c);
+ bool is_ws = is_whitespace(c);
+ bool is_p = (is_punct(c) && c != 0x005F) || is_underscore(c) || c == '\t' || c == 0xfffc;
+
+ if (word_start == -1) {
+ if (!is_lb && !is_ws && !is_p) {
word_start = i;
- word_length = 1;
- } else if (i - line_start + 1 > p_chars_per_line) {
- // Line too long: wrap after the last word.
- ret.push_back(line_start);
- ret.push_back(line_end + 1);
- line_start = word_start;
- line_end = line_start;
}
+ continue;
}
+
+ if (is_lb) {
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(i);
+ }
+ word_start = -1;
+ word_length = 0;
+ } else if (is_ws || is_p) {
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(i);
+ }
+ word_start = -1;
+ word_length = 0;
+ }
+
+ word_length++;
+ }
+ if (word_start != -1 && word_length > 0) {
+ ret.push_back(word_start);
+ ret.push_back(p_string.length());
}
- }
- if (line_start < p_string.length()) {
- ret.push_back(line_start);
- ret.push_back(p_string.length());
}
return ret;
}
diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
index 20c1647843..87994ef22b 100644
--- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml
+++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml
@@ -41,6 +41,9 @@
<member name="application/export_project_only" type="bool" setter="" getter="">
If [code]true[/code], exports iOS project files without building an XCArchive or [code].ipa[/code] file. If [code]false[/code], exports iOS project files and builds an XCArchive and [code].ipa[/code] file at the same time. When combining Godot with Fastlane or other build pipelines, you may want to set this to [code]true[/code].
</member>
+ <member name="application/generate_simulator_library_if_missing" type="bool" setter="" getter="">
+ If [code]true[/code], and ARM64 simulator library is missing from the export template, it is automatically generated from ARM64 device library.
+ </member>
<member name="application/icon_interpolation" type="int" setter="" getter="">
Interpolation method used to resize application icon.
</member>
diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp
index 2194804b3b..490f73a36d 100644
--- a/platform/ios/export/export_plugin.cpp
+++ b/platform/ios/export/export_plugin.cpp
@@ -40,6 +40,8 @@
#include "editor/editor_paths.h"
#include "editor/editor_string_names.h"
#include "editor/export/editor_export.h"
+#include "editor/export/lipo.h"
+#include "editor/export/macho.h"
#include "editor/import/resource_importer_texture_settings.h"
#include "editor/plugins/script_editor_plugin.h"
#include "editor/themes/editor_scale.h"
@@ -248,7 +250,7 @@ bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPre
}
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
- if (p_option.begins_with("privacy")) {
+ if (p_option.begins_with("privacy") || p_option == "application/generate_simulator_library_if_missing") {
return advanced_options_enabled;
}
@@ -288,6 +290,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/delete_old_export_files_unconditionally"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/generate_simulator_library_if_missing"), true));
Vector<PluginConfigIOS> found_plugins = get_plugins();
for (int i = 0; i < found_plugins.size(); i++) {
@@ -1150,6 +1153,167 @@ struct ExportLibsData {
String dest_dir;
};
+bool EditorExportPlatformIOS::_archive_has_arm64(const String &p_path, uint32_t *r_cputype, uint32_t *r_cpusubtype) const {
+ bool has_arm64_image = false;
+ if (FileAccess::exists(p_path)) {
+ if (LipO::is_lipo(p_path)) {
+ // LipO.
+ Ref<LipO> lipo;
+ lipo.instantiate();
+ if (lipo->open_file(p_path)) {
+ for (int i = 0; i < lipo->get_arch_count(); i++) {
+ if (lipo->get_arch_cputype(i) == 0x100000c && lipo->get_arch_cpusubtype(i) == 0) {
+ has_arm64_image = true;
+ break;
+ }
+ }
+ }
+ lipo->close();
+ } else {
+ // Single architecture archive.
+ Ref<FileAccess> sim_f = FileAccess::open(p_path, FileAccess::READ);
+ if (sim_f.is_valid()) {
+ char magic[9] = {};
+ sim_f->get_buffer((uint8_t *)&magic[0], 8);
+ if (String(magic) == String("!<arch>\n")) {
+ while (!sim_f->eof_reached()) {
+ // Read file metadata.
+ char name_short[17] = {};
+ char size_short[11] = {};
+ sim_f->get_buffer((uint8_t *)&name_short[0], 16);
+ sim_f->seek(sim_f->get_position() + 12 + 6 + 6 + 8); // Skip modification time, owner ID, group ID, file mode.
+ sim_f->get_buffer((uint8_t *)&size_short[0], 10);
+ sim_f->seek(sim_f->get_position() + 2); // Skip end marker.
+
+ int64_t file_size = String(size_short).to_int();
+ int64_t next_off = sim_f->get_position() + file_size;
+
+ String name = String(name_short); // Skip extended name.
+ if (name.is_empty() || file_size == 0) {
+ break;
+ }
+ if (name.begins_with("#1/")) {
+ int64_t name_len = String(name_short).replace("#1/", "").to_int();
+ sim_f->seek(sim_f->get_position() + name_len);
+ }
+
+ // Read file content.
+ uint32_t obj_magic = sim_f->get_32();
+
+ bool swap = (obj_magic == 0xcffaedfe || obj_magic == 0xcefaedfe);
+ if (obj_magic == 0xcefaedfe || obj_magic == 0xfeedface || obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) {
+ uint32_t cputype = sim_f->get_32();
+ uint32_t cpusubtype = sim_f->get_32();
+ if (swap) {
+ cputype = BSWAP32(cputype);
+ cpusubtype = BSWAP32(cpusubtype);
+ }
+ if (r_cputype) {
+ *r_cputype = cputype;
+ }
+ if (r_cpusubtype) {
+ *r_cpusubtype = cpusubtype;
+ }
+ if (cputype == 0x100000c && cpusubtype == 0) {
+ has_arm64_image = true;
+ }
+ break;
+ }
+ sim_f->seek(next_off);
+ }
+ }
+ sim_f->close();
+ }
+ }
+ }
+ return has_arm64_image;
+}
+
+int EditorExportPlatformIOS::_archive_convert_to_simulator(const String &p_path) const {
+ int commands_patched = 0;
+ Ref<FileAccess> sim_f = FileAccess::open(p_path, FileAccess::READ_WRITE);
+ if (sim_f.is_valid()) {
+ char magic[9] = {};
+ sim_f->get_buffer((uint8_t *)&magic[0], 8);
+ if (String(magic) == String("!<arch>\n")) {
+ while (!sim_f->eof_reached()) {
+ // Read file metadata.
+ char name_short[17] = {};
+ char size_short[11] = {};
+ sim_f->get_buffer((uint8_t *)&name_short[0], 16);
+ sim_f->seek(sim_f->get_position() + 12 + 6 + 6 + 8); // Skip modification time, owner ID, group ID, file mode.
+ sim_f->get_buffer((uint8_t *)&size_short[0], 10);
+ sim_f->seek(sim_f->get_position() + 2); // Skip end marker.
+
+ int64_t file_size = String(size_short).to_int();
+ int64_t next_off = sim_f->get_position() + file_size;
+
+ String name = String(name_short); // Skip extended name.
+ if (name.is_empty() || file_size == 0) {
+ break;
+ }
+ if (name.begins_with("#1/")) {
+ int64_t name_len = String(name_short).replace("#1/", "").to_int();
+ sim_f->seek(sim_f->get_position() + name_len);
+ }
+
+ // Read file content.
+ uint32_t obj_magic = sim_f->get_32();
+
+ bool swap = (obj_magic == 0xcffaedfe || obj_magic == 0xcefaedfe);
+ if (obj_magic == 0xcefaedfe || obj_magic == 0xfeedface || obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) {
+ uint32_t cputype = sim_f->get_32();
+ uint32_t cpusubtype = sim_f->get_32();
+ uint32_t filetype = sim_f->get_32();
+ uint32_t ncmds = sim_f->get_32();
+ sim_f->get_32(); // Commands total size.
+ sim_f->get_32(); // Commands flags.
+ if (obj_magic == 0xcffaedfe || obj_magic == 0xfeedfacf) {
+ sim_f->get_32(); // Reserved, 64-bit only.
+ }
+ if (swap) {
+ ncmds = BSWAP32(ncmds);
+ cputype = BSWAP32(cputype);
+ cpusubtype = BSWAP32(cpusubtype);
+ filetype = BSWAP32(filetype);
+ }
+ if (cputype == 0x100000C && cpusubtype == 0 && filetype == 1) {
+ // ARM64, object file.
+ for (uint32_t i = 0; i < ncmds; i++) {
+ int64_t cmdofs = sim_f->get_position();
+ uint32_t cmdid = sim_f->get_32();
+ uint32_t cmdsize = sim_f->get_32();
+ if (swap) {
+ cmdid = BSWAP32(cmdid);
+ cmdsize = BSWAP32(cmdsize);
+ }
+ if (cmdid == MachO::LoadCommandID::LC_BUILD_VERSION) {
+ int64_t platform = sim_f->get_32();
+ if (swap) {
+ platform = BSWAP32(platform);
+ }
+ if (platform == MachO::PlatformID::PLATFORM_IOS) {
+ sim_f->seek(cmdofs + 4 + 4);
+ uint32_t new_id = MachO::PlatformID::PLATFORM_IOSSIMULATOR;
+ if (swap) {
+ new_id = BSWAP32(new_id);
+ }
+ sim_f->store_32(new_id);
+ commands_patched++;
+ }
+ }
+ sim_f->seek(cmdofs + cmdsize);
+ }
+ }
+ }
+ sim_f->seek(next_off);
+ }
+ }
+ sim_f->close();
+ }
+ return commands_patched;
+}
+
void EditorExportPlatformIOS::_check_xcframework_content(const String &p_path, int &r_total_libs, int &r_static_libs, int &r_dylibs, int &r_frameworks) const {
Ref<PList> plist;
plist.instantiate();
@@ -2146,7 +2310,7 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres
ret = unzGoToNextFile(src_pkg_zip);
}
- /* we're done with our source zip */
+ // We're done with our source zip.
unzClose(src_pkg_zip);
if (!found_library) {
@@ -2154,6 +2318,80 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres
return ERR_FILE_NOT_FOUND;
}
+ // Check and generate missing ARM64 simulator library.
+ if (p_preset->get("application/generate_simulator_library_if_missing").operator bool()) {
+ String sim_lib_path = dest_dir + String(binary_name + ".xcframework").path_join("ios-arm64_x86_64-simulator").path_join("libgodot.a");
+ String dev_lib_path = dest_dir + String(binary_name + ".xcframework").path_join("ios-arm64").path_join("libgodot.a");
+ String tmp_lib_path = EditorPaths::get_singleton()->get_cache_dir().path_join(binary_name + "_lipo_");
+ uint32_t cputype = 0;
+ uint32_t cpusubtype = 0;
+ if (!_archive_has_arm64(sim_lib_path, &cputype, &cpusubtype) && _archive_has_arm64(dev_lib_path) && FileAccess::exists(dev_lib_path)) {
+ add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("ARM64 simulator library, generating from device library."));
+
+ Vector<String> tmp_lib_files;
+ Vector<Vector2i> tmp_lib_cputypes;
+ // Extract/copy simulator lib.
+ if (FileAccess::exists(sim_lib_path)) {
+ if (LipO::is_lipo(sim_lib_path)) {
+ Ref<LipO> lipo;
+ lipo.instantiate();
+ if (lipo->open_file(sim_lib_path)) {
+ for (int i = 0; i < lipo->get_arch_count(); i++) {
+ const String &f_name = tmp_lib_path + itos(tmp_lib_files.size());
+ lipo->extract_arch(i, f_name);
+ tmp_lib_files.push_back(f_name);
+ tmp_lib_cputypes.push_back(Vector2i(lipo->get_arch_cputype(i), lipo->get_arch_cpusubtype(i)));
+ }
+ }
+ } else {
+ const String &f_name = tmp_lib_path + itos(tmp_lib_files.size());
+ tmp_app_path->copy(sim_lib_path, f_name);
+ tmp_lib_files.push_back(f_name);
+ tmp_lib_cputypes.push_back(Vector2i(cputype, cpusubtype));
+ }
+ }
+ // Copy device lib.
+ if (LipO::is_lipo(dev_lib_path)) {
+ Ref<LipO> lipo;
+ lipo.instantiate();
+ if (lipo->open_file(dev_lib_path)) {
+ for (int i = 0; i < lipo->get_arch_count(); i++) {
+ if (lipo->get_arch_cputype(i) == 0x100000c && lipo->get_arch_cpusubtype(i) == 0) {
+ const String &f_name = tmp_lib_path + itos(tmp_lib_files.size());
+ lipo->extract_arch(i, f_name);
+ tmp_lib_files.push_back(f_name);
+ tmp_lib_cputypes.push_back(Vector2i(0x100000c, 0)); // ARM64.
+ break;
+ }
+ }
+ }
+ } else {
+ const String &f_name = tmp_lib_path + itos(tmp_lib_files.size());
+ tmp_app_path->copy(dev_lib_path, f_name);
+ tmp_lib_files.push_back(f_name);
+ tmp_lib_cputypes.push_back(Vector2i(0x100000c, 0)); // ARM64.
+ }
+
+ // Patch device lib.
+ int patch_count = _archive_convert_to_simulator(tmp_lib_path + itos(tmp_lib_files.size() - 1));
+ if (patch_count == 0) {
+ add_message(EXPORT_MESSAGE_WARNING, TTR("Export"), TTR("Unable to generate ARM64 simulator library."));
+ } else {
+ // Repack.
+ Ref<LipO> lipo;
+ lipo.instantiate();
+ lipo->create_file(sim_lib_path, tmp_lib_files, tmp_lib_cputypes);
+ }
+
+ // Cleanup.
+ for (const String &E : tmp_lib_files) {
+ tmp_app_path->remove(E);
+ }
+ }
+ }
+
+ // Generate translations files.
+
Dictionary appnames = GLOBAL_GET("application/config/name_localized");
Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized");
Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized");
diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h
index 197f27da76..1964906c27 100644
--- a/platform/ios/export/export_plugin.h
+++ b/platform/ios/export/export_plugin.h
@@ -135,6 +135,8 @@ class EditorExportPlatformIOS : public EditorExportPlatform {
Vector<ExportArchitecture> _get_supported_architectures() const;
Vector<String> _get_preset_architectures(const Ref<EditorExportPreset> &p_preset) const;
+ bool _archive_has_arm64(const String &p_path, uint32_t *r_cputype = nullptr, uint32_t *r_cpusubtype = nullptr) const;
+ int _archive_convert_to_simulator(const String &p_path) const;
void _check_xcframework_content(const String &p_path, int &r_total_libs, int &r_static_libs, int &r_dylibs, int &r_frameworks) const;
Error _convert_to_framework(const String &p_source, const String &p_destination, const String &p_id) const;
diff --git a/platform/linuxbsd/tts_linux.cpp b/platform/linuxbsd/tts_linux.cpp
index 46291bb4da..6c1f49f046 100644
--- a/platform/linuxbsd/tts_linux.cpp
+++ b/platform/linuxbsd/tts_linux.cpp
@@ -149,12 +149,18 @@ void TTS_Linux::_speech_event(int p_msg_id, int p_type) {
}
PackedInt32Array breaks = TS->string_get_word_breaks(message.text, language);
+ int prev_end = -1;
for (int i = 0; i < breaks.size(); i += 2) {
const int start = breaks[i];
const int end = breaks[i + 1];
- text += message.text.substr(start, end - start + 1);
+ if (prev_end != -1 && prev_end != start) {
+ text += message.text.substr(prev_end, start - prev_end);
+ }
+ text += message.text.substr(start, end - start);
text += "<mark name=\"" + String::num_int64(end, 10) + "\"/>";
+ prev_end = end;
}
+
spd_set_synthesis_voice(synth, message.voice.utf8().get_data());
spd_set_volume(synth, message.volume * 2 - 100);
spd_set_voice_pitch(synth, (message.pitch - 1) * 100);
diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp
index 32278ba4a1..73e2f2d45b 100644
--- a/platform/macos/export/export_plugin.cpp
+++ b/platform/macos/export/export_plugin.cpp
@@ -30,10 +30,7 @@
#include "export_plugin.h"
-#include "codesign.h"
-#include "lipo.h"
#include "logo_svg.gen.h"
-#include "macho.h"
#include "run_icon_svg.gen.h"
#include "core/io/image_loader.h"
@@ -43,6 +40,9 @@
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/editor_string_names.h"
+#include "editor/export/codesign.h"
+#include "editor/export/lipo.h"
+#include "editor/export/macho.h"
#include "editor/import/resource_importer_texture_settings.h"
#include "editor/themes/editor_scale.h"
#include "scene/resources/image_texture.h"
diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp
index 853638e47f..b3a55ddee0 100644
--- a/scene/audio/audio_stream_player_internal.cpp
+++ b/scene/audio/audio_stream_player_internal.cpp
@@ -276,9 +276,6 @@ bool AudioStreamPlayerInternal::is_playing() const {
if (AudioServer::get_singleton()->is_playback_active(playback)) {
return true;
}
- if (AudioServer::get_singleton()->is_sample_playback_active(playback)) {
- return true;
- }
}
return false;
}
diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp
index 65b97acabf..b2567e431b 100644
--- a/scene/resources/material.cpp
+++ b/scene/resources/material.cpp
@@ -1337,7 +1337,7 @@ void fragment() {)";
}
// Heightmapping isn't supported at the same time as triplanar mapping.
- if (!RenderingServer::get_singleton()->is_low_end() && features[FEATURE_HEIGHT_MAPPING] && !flags[FLAG_UV1_USE_TRIPLANAR]) {
+ if (features[FEATURE_HEIGHT_MAPPING] && !flags[FLAG_UV1_USE_TRIPLANAR]) {
// Binormal is negative due to mikktspace. Flipping it "unflips" it.
code += R"(
{
@@ -1637,21 +1637,20 @@ void fragment() {)";
// Use the slightly more expensive circular fade (distance to the object) instead of linear
// (Z distance), so that the fade is always the same regardless of the camera angle.
if ((distance_fade == DISTANCE_FADE_OBJECT_DITHER || distance_fade == DISTANCE_FADE_PIXEL_DITHER)) {
- if (!RenderingServer::get_singleton()->is_low_end()) {
- code += "\n {";
+ code += "\n {";
- if (distance_fade == DISTANCE_FADE_OBJECT_DITHER) {
- code += R"(
+ if (distance_fade == DISTANCE_FADE_OBJECT_DITHER) {
+ code += R"(
// Distance Fade: Object Dither
float fade_distance = length((VIEW_MATRIX * MODEL_MATRIX[3]));
)";
- } else {
- code += R"(
+ } else {
+ code += R"(
// Distance Fade: Pixel Dither
float fade_distance = length(VERTEX);
)";
- }
- code += R"(
+ }
+ code += R"(
// Use interleaved gradient noise, which is fast but still looks good.
const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
float fade = clamp(smoothstep(distance_fade_min, distance_fade_max, fade_distance), 0.0, 1.0);
@@ -1661,7 +1660,6 @@ void fragment() {)";
}
}
)";
- }
} else {
code += R"(
// Distance Fade: Pixel Alpha