diff options
Diffstat (limited to 'platform')
193 files changed, 11121 insertions, 2522 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index d031d14499..ad226255bc 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -8,6 +8,7 @@ android_files = [ "file_access_android.cpp", "audio_driver_opensl.cpp", "dir_access_jandroid.cpp", + "tts_android.cpp", "thread_jandroid.cpp", "net_socket_android.cpp", "java_godot_lib_jni.cpp", @@ -53,10 +54,17 @@ else: if lib_arch_dir != "": if env["target"] == "release": lib_type_dir = "release" - else: # release_debug, debug + elif env["target"] == "release_debug": lib_type_dir = "debug" + else: # debug + lib_type_dir = "dev" - out_dir = "#platform/android/java/lib/libs/" + lib_type_dir + "/" + lib_arch_dir + if env["tools"]: + lib_tools_dir = "tools/" + else: + lib_tools_dir = "" + + out_dir = "#platform/android/java/lib/libs/" + lib_tools_dir + lib_type_dir + "/" + lib_arch_dir env_android.Command( out_dir + "/libgodot_android.so", "#bin/libgodot" + env["SHLIBSUFFIX"], Move("$TARGET", "$SOURCE") ) diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 2b237c4006..e9c0ec1475 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -88,4 +88,4 @@ public: void process_key_event(int p_keycode, int p_scancode, int p_unicode_char, bool p_pressed); }; -#endif +#endif // ANDROID_INPUT_HANDLER_H diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index f544f29b10..f80f1e3051 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -64,14 +64,14 @@ void JavaClassWrapper::_bind_methods() { #if !defined(ANDROID_ENABLED) -Variant JavaClass::call(const StringName &, const Variant **, int, Callable::CallError &) { +Variant JavaClass::callp(const StringName &, const Variant **, int, Callable::CallError &) { return Variant(); } JavaClass::JavaClass() { } -Variant JavaObject::call(const StringName &, const Variant **, int, Callable::CallError &) { +Variant JavaObject::callp(const StringName &, const Variant **, int, Callable::CallError &) { return Variant(); } diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index d4d1208757..ac8d6585d3 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -63,7 +63,7 @@ class JavaClass : public RefCounted { ARG_TYPE_MASK = (1 << 16) - 1 }; - Map<StringName, Variant> constant_map; + RBMap<StringName, Variant> constant_map; struct MethodInfo { bool _static = false; @@ -174,12 +174,12 @@ class JavaClass : public RefCounted { bool _call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret); friend class JavaClassWrapper; - Map<StringName, List<MethodInfo>> methods; + HashMap<StringName, List<MethodInfo>> methods; jclass _class; #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; JavaClass(); }; @@ -195,7 +195,7 @@ class JavaObject : public RefCounted { #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; #ifdef ANDROID_ENABLED JavaObject(const Ref<JavaClass> &p_base, jobject *p_instance); @@ -207,7 +207,7 @@ class JavaClassWrapper : public Object { GDCLASS(JavaClassWrapper, Object); #ifdef ANDROID_ENABLED - Map<String, Ref<JavaClass>> class_cache; + RBMap<String, Ref<JavaClass>> class_cache; friend class JavaClass; jclass activityClass; jmethodID findClass; diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 57d08ac83e..690fddae21 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -48,13 +48,13 @@ class JNISingleton : public Object { }; jobject instance; - Map<StringName, MethodData> method_map; + RBMap<StringName, MethodData> method_map; #endif public: - virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { #ifdef ANDROID_ENABLED - Map<StringName, MethodData>::Element *E = method_map.find(p_method); + RBMap<StringName, MethodData>::Element *E = method_map.find(p_method); // Check the method we're looking for is in the JNISingleton map and that // the arguments match. @@ -70,10 +70,10 @@ public: if (call_error) { // The method is not in this map, defaulting to the regular instance calls. - return Object::call(p_method, p_args, p_argcount, r_error); + return Object::callp(p_method, p_args, p_argcount, r_error); } - ERR_FAIL_COND_V(!instance, Variant()); + ERR_FAIL_NULL_V(instance, Variant()); r_error.error = Callable::CallError::CALL_OK; @@ -176,7 +176,7 @@ public: #else // ANDROID_ENABLED // Defaulting to the regular instance calls. - return Object::call(p_method, p_args, p_argcount, r_error); + return Object::callp(p_method, p_args, p_argcount, r_error); #endif } diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 8495d2cc18..6b22a0ffa1 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -75,13 +75,11 @@ void AudioDriverOpenSL::_buffer_callback( void AudioDriverOpenSL::_buffer_callbacks( SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); ad->_buffer_callback(queueItf); } -AudioDriverOpenSL *AudioDriverOpenSL::s_ad = nullptr; - const char *AudioDriverOpenSL::get_name() const { return "Android"; } @@ -133,8 +131,6 @@ void AudioDriverOpenSL::start() { ERR_FAIL_COND(res != SL_RESULT_SUCCESS); SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, BUFFER_COUNT }; - //bufferQueue.locatorType = SL_DATALOCATOR_BUFFERQUEUE; - //bufferQueue.numBuffers = BUFFER_COUNT; /* Four buffers in our buffer queue */ /* Setup the format of the content in the buffer queue */ pcm.formatType = SL_DATAFORMAT_PCM; pcm.numChannels = 2; @@ -155,13 +151,8 @@ void AudioDriverOpenSL::start() { locator_outputmix.outputMix = OutputMix; audioSink.pLocator = (void *)&locator_outputmix; audioSink.pFormat = nullptr; - /* Initialize the context for Buffer queue callbacks */ - //cntxt.pDataBase = (void*)&pcmData; - //cntxt.pData = cntxt.pDataBase; - //cntxt.size = sizeof(pcmData); /* Create the music player */ - { const SLInterfaceID ids[2] = { SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND }; const SLboolean req[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; @@ -208,7 +199,7 @@ void AudioDriverOpenSL::_record_buffer_callback(SLAndroidSimpleBufferQueueItf qu } void AudioDriverOpenSL::_record_buffer_callbacks(SLAndroidSimpleBufferQueueItf queueItf, void *pContext) { - AudioDriverOpenSL *ad = (AudioDriverOpenSL *)pContext; + AudioDriverOpenSL *ad = static_cast<AudioDriverOpenSL *>(pContext); ad->_record_buffer_callback(queueItf); } @@ -341,5 +332,4 @@ void AudioDriverOpenSL::set_pause(bool p_pause) { } AudioDriverOpenSL::AudioDriverOpenSL() { - s_ad = this; } diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h index eea1fc227f..7b09438858 100644 --- a/platform/android/audio_driver_opensl.h +++ b/platform/android/audio_driver_opensl.h @@ -105,4 +105,4 @@ public: AudioDriverOpenSL(); }; -#endif // AUDIO_DRIVER_ANDROID_H +#endif // AUDIO_DRIVER_OPENSL_H diff --git a/platform/android/detect.py b/platform/android/detect.py index 3319d5890c..0099ac7e0d 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -188,8 +188,11 @@ def configure(env): if env["target"].startswith("release"): if env["optimize"] == "speed": # optimize for speed (default) - env.Append(LINKFLAGS=["-O2"]) - env.Append(CCFLAGS=["-O2", "-fomit-frame-pointer"]) + # `-O2` is more friendly to debuggers than `-O3`, leading to better crash backtraces + # when using `target=release_debug`. + opt = "-O3" if env["target"] == "release" else "-O2" + env.Append(LINKFLAGS=[opt]) + env.Append(CCFLAGS=[opt, "-fomit-frame-pointer"]) elif env["optimize"] == "size": # optimize for size env.Append(CCFLAGS=["-Os"]) env.Append(LINKFLAGS=["-Os"]) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 7fb4f54fca..5b9eee8117 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -42,7 +42,7 @@ jmethodID DirAccessJAndroid::_dir_next = nullptr; jmethodID DirAccessJAndroid::_dir_close = nullptr; jmethodID DirAccessJAndroid::_dir_is_dir = nullptr; -DirAccess *DirAccessJAndroid::create_fs() { +Ref<DirAccess> DirAccessJAndroid::create_fs() { return memnew(DirAccessJAndroid); } @@ -55,7 +55,6 @@ Error DirAccessJAndroid::list_dir_begin() { if (res <= 0) { return ERR_CANT_OPEN; } - id = res; return OK; @@ -69,7 +68,6 @@ String DirAccessJAndroid::get_next() { if (!str) { return ""; } - String ret = jstring_to_string((jstring)str, env); env->DeleteLocalRef((jobject)str); return ret; @@ -89,7 +87,6 @@ void DirAccessJAndroid::list_dir_end() { if (id == 0) { return; } - JNIEnv *env = get_jni_env(); env->CallVoidMethod(io, _dir_close, id); id = 0; @@ -143,7 +140,7 @@ Error DirAccessJAndroid::change_dir(String p_dir) { return OK; } -String DirAccessJAndroid::get_current_dir(bool p_include_drive) { +String DirAccessJAndroid::get_current_dir(bool p_include_drive) const { return "res://" + current_dir; } @@ -155,9 +152,9 @@ bool DirAccessJAndroid::file_exists(String p_file) { sd = current_dir.plus_file(p_file); } - FileAccessAndroid *f = memnew(FileAccessAndroid); + Ref<FileAccessAndroid> f; + f.instantiate(); bool exists = f->file_exists(sd); - memdelete(f); return exists; } @@ -228,12 +225,9 @@ void DirAccessJAndroid::setup(jobject p_io) { _dir_next = env->GetMethodID(cls, "dir_next", "(I)Ljava/lang/String;"); _dir_close = env->GetMethodID(cls, "dir_close", "(I)V"); _dir_is_dir = env->GetMethodID(cls, "dir_is_dir", "(I)Z"); - - //(*env)->CallVoidMethod(env,obj,aMethodID, myvar); } DirAccessJAndroid::DirAccessJAndroid() { - id = 0; } DirAccessJAndroid::~DirAccessJAndroid() { diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 4f4a984b12..0e1b12cb58 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -36,8 +36,6 @@ #include <stdio.h> class DirAccessJAndroid : public DirAccess { - //AAssetDir* aad; - static jobject io; static jclass cls; @@ -46,12 +44,12 @@ class DirAccessJAndroid : public DirAccess { static jmethodID _dir_close; static jmethodID _dir_is_dir; - int id; + int id = 0; String current_dir; String current; - static DirAccess *create_fs(); + static Ref<DirAccess> create_fs(); public: virtual Error list_dir_begin(); ///< This starts dir listing @@ -64,7 +62,7 @@ public: virtual String get_drive(int p_drive); virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success - virtual String get_current_dir(bool p_include_drive = true); ///< return current dir location + virtual String get_current_dir(bool p_include_drive = true) const; ///< return current dir location virtual bool file_exists(String p_file); virtual bool dir_exists(String p_dir); diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index a7a8801bdc..3be220d110 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -34,6 +34,7 @@ #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" #include "os_android.h" +#include "tts_android.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" @@ -42,7 +43,7 @@ #endif DisplayServerAndroid *DisplayServerAndroid::get_singleton() { - return (DisplayServerAndroid *)DisplayServer::get_singleton(); + return static_cast<DisplayServerAndroid *>(DisplayServer::get_singleton()); } bool DisplayServerAndroid::has_feature(Feature p_feature) const { @@ -63,6 +64,7 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -73,9 +75,37 @@ String DisplayServerAndroid::get_name() const { return "Android"; } +bool DisplayServerAndroid::tts_is_speaking() const { + return TTS_Android::is_speaking(); +} + +bool DisplayServerAndroid::tts_is_paused() const { + return TTS_Android::is_paused(); +} + +Array DisplayServerAndroid::tts_get_voices() const { + return TTS_Android::get_voices(); +} + +void DisplayServerAndroid::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + TTS_Android::speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerAndroid::tts_pause() { + TTS_Android::pause(); +} + +void DisplayServerAndroid::tts_resume() { + TTS_Android::resume(); +} + +void DisplayServerAndroid::tts_stop() { + TTS_Android::stop(); +} + void DisplayServerAndroid::clipboard_set(const String &p_text) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND(!godot_java); + ERR_FAIL_NULL(godot_java); if (godot_java->has_set_clipboard()) { godot_java->set_clipboard(p_text); @@ -86,7 +116,7 @@ void DisplayServerAndroid::clipboard_set(const String &p_text) { String DisplayServerAndroid::clipboard_get() const { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND_V(!godot_java, String()); + ERR_FAIL_NULL_V(godot_java, String()); if (godot_java->has_get_clipboard()) { return godot_java->get_clipboard(); @@ -97,7 +127,7 @@ String DisplayServerAndroid::clipboard_get() const { bool DisplayServerAndroid::clipboard_has() const { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND_V(!godot_java, false); + ERR_FAIL_NULL_V(godot_java, false); if (godot_java->has_has_clipboard()) { return godot_java->has_clipboard(); @@ -106,9 +136,21 @@ bool DisplayServerAndroid::clipboard_has() const { } } +Array DisplayServerAndroid::get_display_cutouts() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Array()); + return godot_io_java->get_display_cutouts(); +} + +Rect2i DisplayServerAndroid::get_display_safe_area() const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, Rect2i()); + return godot_io_java->get_display_safe_area(); +} + void DisplayServerAndroid::screen_set_keep_on(bool p_enable) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND(!godot_java); + ERR_FAIL_NULL(godot_java); godot_java->set_keep_screen_on(p_enable); keep_screen_on = p_enable; @@ -120,14 +162,14 @@ bool DisplayServerAndroid::screen_is_kept_on() const { void DisplayServerAndroid::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); godot_io_java->set_screen_orientation(p_orientation); } DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, SCREEN_LANDSCAPE); + ERR_FAIL_NULL_V(godot_io_java, SCREEN_LANDSCAPE); const int orientation = godot_io_java->get_screen_orientation(); ERR_FAIL_INDEX_V_MSG(orientation, 7, SCREEN_LANDSCAPE, "Unrecognized screen orientation"); @@ -147,20 +189,24 @@ Size2i DisplayServerAndroid::screen_get_size(int p_screen) const { } Rect2i DisplayServerAndroid::screen_get_usable_rect(int p_screen) const { - GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, Rect2i()); - int xywh[4]; - godot_io_java->screen_get_usable_rect(xywh); - return Rect2i(xywh[0], xywh[1], xywh[2], xywh[3]); + Size2i display_size = OS_Android::get_singleton()->get_display_size(); + return Rect2i(0, 0, display_size.width, display_size.height); } int DisplayServerAndroid::screen_get_dpi(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, 0); + ERR_FAIL_NULL_V(godot_io_java, 0); return godot_io_java->get_screen_dpi(); } +float DisplayServerAndroid::screen_get_scale(int p_screen) const { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, 1.0f); + + return godot_io_java->get_scaled_density(); +} + float DisplayServerAndroid::screen_get_refresh_rate(int p_screen) const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); if (!godot_io_java) { @@ -177,7 +223,7 @@ bool DisplayServerAndroid::screen_is_touchscreen(int p_screen) const { void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); if (godot_io_java->has_vk()) { godot_io_java->show_vk(p_existing_text, p_multiline, p_max_length, p_cursor_start, p_cursor_end); @@ -188,7 +234,7 @@ void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, void DisplayServerAndroid::virtual_keyboard_hide() { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND(!godot_io_java); + ERR_FAIL_NULL(godot_io_java); if (godot_io_java->has_vk()) { godot_io_java->hide_vk(); @@ -199,7 +245,7 @@ void DisplayServerAndroid::virtual_keyboard_hide() { int DisplayServerAndroid::virtual_keyboard_get_height() const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); - ERR_FAIL_COND_V(!godot_io_java, 0); + ERR_FAIL_NULL_V(godot_io_java, 0); return godot_io_java->get_vk_height(); } @@ -270,7 +316,7 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type, return 0; // Not supported. } case WINDOW_HANDLE: { - return (int64_t)((OS_Android *)OS::get_singleton())->get_godot_java()->get_activity(); + return reinterpret_cast<int64_t>(static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity()); } case WINDOW_VIEW: { return 0; // Not supported. @@ -410,9 +456,9 @@ void DisplayServerAndroid::reset_window() { #if defined(VULKAN_ENABLED) if (rendering_driver == "vulkan") { ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_COND(!native_window); + ERR_FAIL_NULL(native_window); - ERR_FAIL_COND(!context_vulkan); + ERR_FAIL_NULL(context_vulkan); VSyncMode last_vsync_mode = context_vulkan->get_vsync_mode(MAIN_WINDOW_ID); context_vulkan->window_destroy(MAIN_WINDOW_ID); @@ -473,7 +519,7 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis if (rendering_driver == "vulkan") { ANativeWindow *native_window = OS_Android::get_singleton()->get_native_window(); - ERR_FAIL_COND(!native_window); + ERR_FAIL_NULL(native_window); context_vulkan = memnew(VulkanContextAndroid); if (context_vulkan->initialize() != OK) { diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 23077a6529..65bf2ec1a8 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -91,10 +91,22 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; virtual bool clipboard_has() const override; + virtual Array get_display_cutouts() const override; + virtual Rect2i get_display_safe_area() const override; + virtual void screen_set_keep_on(bool p_enable) override; virtual bool screen_is_kept_on() const override; @@ -106,6 +118,7 @@ public: virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index aa4b394965..560f188b82 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -36,11 +36,6 @@ #include "editor/editor_settings.h" void register_android_exporter() { - String exe_ext; - if (OS::get_singleton()->get_name() == "Windows") { - exe_ext = "*.exe"; - } - EDITOR_DEF("export/android/android_sdk_path", ""); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/debug_keystore", ""); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 2c431028b0..73c6fcc7e8 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -207,9 +207,9 @@ static const char *SPLASH_CONFIG_PATH = "res://android/build/res/drawable/splash static const char *GDNATIVE_LIBS_PATH = "res://android/build/libs/gdnativelibs.json"; static const int icon_densities_count = 6; -static const char *launcher_icon_option = "launcher_icons/main_192x192"; -static const char *launcher_adaptive_icon_foreground_option = "launcher_icons/adaptive_foreground_432x432"; -static const char *launcher_adaptive_icon_background_option = "launcher_icons/adaptive_background_432x432"; +static const char *launcher_icon_option = PNAME("launcher_icons/main_192x192"); +static const char *launcher_adaptive_icon_foreground_option = PNAME("launcher_icons/adaptive_foreground_432x432"); +static const char *launcher_adaptive_icon_background_option = PNAME("launcher_icons/adaptive_background_432x432"); static const LauncherIcon launcher_icons[icon_densities_count] = { { "res/mipmap-xxxhdpi-v4/icon.png", 192 }, @@ -249,7 +249,7 @@ static const int DEFAULT_TARGET_SDK_VERSION = 30; // Should match the value in ' const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION); void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { - EditorExportPlatformAndroid *ea = (EditorExportPlatformAndroid *)ud; + EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); while (!ea->quit_request.is_set()) { // Check for plugins updates @@ -409,7 +409,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { List<String> args; args.push_back("kill-server"); OS::get_singleton()->execute(adb, args); - }; + } } String EditorExportPlatformAndroid::get_project_name(const String &p_name) const { @@ -522,8 +522,8 @@ bool EditorExportPlatformAndroid::is_package_name_valid(const String &p_package, bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, const Vector<uint8_t> &p_data) { /* - * By not compressing files with little or not benefit in doing so, - * a performance gain is expected attime. Moreover, if the APK is + * By not compressing files with little or no benefit in doing so, + * a performance gain is expected at runtime. Moreover, if the APK is * zip-aligned, assets stored as they are can be efficiently read by * Android by memory-mapping them. */ @@ -543,7 +543,7 @@ bool EditorExportPlatformAndroid::_should_compress_asset(const String &p_path, c ".webp", // Same reasoning as .png ".cfb", // Don't let small config files slow-down startup ".scn", // Binary scenes are usually already compressed - ".stex", // Streamable textures are usually already compressed + ".ctex", // Streamable textures are usually already compressed // Trailer for easier processing nullptr }; @@ -596,8 +596,8 @@ Vector<String> EditorExportPlatformAndroid::get_abis() { /// List the gdap files in the directory specified by the p_path parameter. Vector<String> EditorExportPlatformAndroid::list_gdap_files(const String &p_path) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); @@ -685,7 +685,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj ERR_PRINT(err); return FAILED; } - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); Vector<String> abis = get_abis(); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { @@ -710,7 +710,7 @@ Error EditorExportPlatformAndroid::save_apk_so(void *p_userdata, const SharedObj } Error EditorExportPlatformAndroid::save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - APKExportData *ed = (APKExportData *)p_userdata; + APKExportData *ed = static_cast<APKExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", "assets/"); store_in_apk(ed, dst_path, p_data, _should_compress_asset(p_path, p_data) ? Z_DEFLATED : 0); @@ -725,7 +725,7 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared ERR_FAIL_COND_V_MSG(!p_so.path.get_file().begins_with("lib"), FAILED, "Android .so file names must start with \"lib\", but got: " + p_so.path); Vector<String> abis = get_abis(); - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); bool exported = false; for (int i = 0; i < p_so.tags.size(); ++i) { int abi_index = abis.find(p_so.tags[i]); @@ -807,7 +807,6 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres } manifest_text += _get_xr_features_tag(p_preset); - manifest_text += _get_instrumentation_tag(p_preset); manifest_text += _get_application_tag(p_preset, _has_storage_permission(perms)); manifest_text += "</manifest>\n"; String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); @@ -834,11 +833,9 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p uint32_t ofs = 8; uint32_t string_count = 0; - //uint32_t styles_count = 0; uint32_t string_flags = 0; uint32_t string_data_offset = 0; - //uint32_t styles_offset = 0; uint32_t string_table_begins = 0; uint32_t string_table_ends = 0; Vector<uint8_t> stable_extra; @@ -863,6 +860,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p bool classify_as_game = p_preset->get("package/classify_as_game"); bool retain_data_on_uninstall = p_preset->get("package/retain_data_on_uninstall"); bool exclude_from_recents = p_preset->get("package/exclude_from_recents"); + bool is_resizeable = bool(GLOBAL_GET("display/window/size/resizable")); Vector<String> perms; // Write permissions into the perms variable. @@ -878,10 +876,8 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p int iofs = ofs + 8; string_count = decode_uint32(&p_manifest[iofs]); - // iofs + 4 is `styles_count`. string_flags = decode_uint32(&p_manifest[iofs + 8]); string_data_offset = decode_uint32(&p_manifest[iofs + 12]); - // iofs + 16 is `styles_offset`. uint32_t st_offset = iofs + 20; string_table.resize(string_count); @@ -968,10 +964,6 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(retain_data_on_uninstall, &p_manifest.write[iofs + 16]); } - if (tname == "instrumentation" && attrname == "targetPackage") { - string_table.write[attr_value] = get_package_name(package_name); - } - if (tname == "activity" && attrname == "screenOrientation") { encode_uint32(screen_orientation, &p_manifest.write[iofs + 16]); } @@ -980,6 +972,10 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p encode_uint32(exclude_from_recents, &p_manifest.write[iofs + 16]); } + if (tname == "activity" && attrname == "resizeableActivity") { + encode_uint32(is_resizeable, &p_manifest.write[iofs + 16]); + } + if (tname == "supports-screens") { if (attrname == "smallScreens") { encode_uint32(screen_support_small ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); @@ -995,16 +991,23 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p } } - if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + // Hand tracking related configurations + if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_metadata_name") { string_table.write[attr_value] = "com.oculus.handtracking.frequency"; } - } - if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { - if (xr_mode_index == XR_MODE_OPENXR && hand_tracking_index > XR_HAND_TRACKING_NONE) { + if (tname == "meta-data" && attrname == "value" && value == "xr_hand_tracking_metadata_value") { string_table.write[attr_value] = (hand_tracking_frequency_index == XR_HAND_TRACKING_FREQUENCY_LOW ? "LOW" : "HIGH"); } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_name") { + string_table.write[attr_value] = "com.oculus.handtracking.version"; + } + + if (tname == "meta-data" && attrname == "name" && value == "xr_hand_tracking_version_value") { + string_table.write[attr_value] = "V2.0"; + } } iofs += 20; @@ -1387,6 +1390,7 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> & Vector<String> string_table; String package_name = p_preset->get("package/name"); + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); for (uint32_t i = 0; i < string_count; i++) { uint32_t offset = decode_uint32(&r_manifest[string_table_begins + i * 4]); @@ -1401,9 +1405,8 @@ void EditorExportPlatformAndroid::_fix_resources(const Ref<EditorExportPreset> & } else { String lang = str.substr(str.rfind("-") + 1, str.length()).replace("-", "_"); - String prop = "application/config/name_" + lang; - if (ProjectSettings::get_singleton()->has_setting(prop)) { - str = ProjectSettings::get_singleton()->get(prop); + if (appnames.has(lang)) { + str = appnames[lang]; } else { str = get_project_name(package_name); } @@ -1685,7 +1688,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { print_verbose("Found Android plugin " + plugins_configs[i].name); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "plugins/" + plugins_configs[i].name), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), plugins_configs[i].name)), false)); } plugins_changed.clear(); @@ -1695,7 +1698,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio // All Android devices supporting Vulkan run 64-bit Android, // so there is usually no point in exporting for 32-bit Android. const bool is_default = abi == "arm64-v8a"; - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + abi), is_default)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), abi)), is_default)); } r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); @@ -1746,7 +1749,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio const char **perms = android_perms; while (*perms) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "permissions/" + String(*perms).to_lower()), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("permissions"), String(*perms).to_lower())), false)); perms++; } } @@ -1815,7 +1818,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, String can_export_error; bool can_export_missing_templates; if (!can_export(p_preset, can_export_error, can_export_missing_templates)) { - EditorNode::add_io_error(can_export_error); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), can_export_error); return ERR_UNCONFIGURED; } @@ -1843,7 +1846,8 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, { \ DirAccess::remove_file_or_error(tmp_export_path); \ return m_err; \ - } + } \ + ((void)0) // Export to temporary APK before sending to device. Error err = export_project_helper(p_preset, true, tmp_export_path, EXPORT_FORMAT_APK, true, p_debug_flags); @@ -1893,7 +1897,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, err = OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); if (err || rv != 0) { - EditorNode::add_io_error(vformat(TTR("Could not install to device: %s"), output)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not install to device: %s"), output)); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } @@ -1971,7 +1975,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, err = OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); if (err || rv != 0) { - EditorNode::add_io_error(TTR("Could not execute on device.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), TTR("Could not execute on device.")); CLEANUP_AND_RETURN(ERR_CANT_CREATE); } @@ -2003,7 +2007,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { Error errn; String build_tools_dir = sdk_path.plus_file("build-tools"); - DirAccessRef da = DirAccess::open(build_tools_dir, &errn); + Ref<DirAccess> da = DirAccess::open(build_tools_dir, &errn); if (errn != OK) { print_error("Unable to open Android 'build-tools' directory."); return apksigner_path; @@ -2026,7 +2030,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { da->list_dir_end(); if (apksigner_path.is_empty()) { - EditorNode::get_singleton()->show_warning(TTR("Unable to find the 'apksigner' tool.")); + print_error("Unable to find the 'apksigner' tool."); } return apksigner_path; @@ -2120,7 +2124,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } else { Error errn; // Check for the platform-tools directory. - DirAccessRef da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); + Ref<DirAccess> da = DirAccess::open(sdk_path.plus_file("platform-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'platform-tools' directory!"); @@ -2138,7 +2142,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } // Check for the build-tools directory. - DirAccessRef build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); + Ref<DirAccess> build_tools_da = DirAccess::open(sdk_path.plus_file("build-tools"), &errn); if (errn != OK) { err += TTR("Invalid Android SDK path in Editor Settings."); err += TTR("Missing 'build-tools' directory!"); @@ -2256,9 +2260,9 @@ String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorE return fullpath; } -Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path) { +Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) { String fullpath = get_apk_expansion_fullpath(p_preset, p_path); - Error err = save_pack(p_preset, fullpath); + Error err = save_pack(p_preset, p_debug, fullpath); return err; } @@ -2331,7 +2335,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre String apksigner = get_apksigner_path(); print_verbose("Starting signing of the " + export_label + " binary using " + apksigner); if (!FileAccess::exists(apksigner)) { - EditorNode::add_io_error(vformat(TTR("'apksigner' could not be found.\nPlease check the command is available in the Android SDK build-tools directory.\nThe resulting %s is unsigned."), export_label)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label)); return OK; } @@ -2364,7 +2368,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } if (!FileAccess::exists(keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2385,10 +2389,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } int retval; output.clear(); - OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + return err; + } print_verbose(output); if (retval) { - EditorNode::add_io_error(vformat(TTR("'apksigner' returned with error #%d"), retval)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' returned with error #%d"), retval)); return ERR_CANT_CREATE; } @@ -2405,10 +2413,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } output.clear(); - OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); + return err; + } print_verbose(output); if (retval) { - EditorNode::add_io_error(vformat(TTR("'apksigner' verification of %s failed."), export_label)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label)); return ERR_CANT_CREATE; } @@ -2417,12 +2429,12 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } void EditorExportPlatformAndroid::_clear_assets_directory() { - DirAccessRef da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); // Clear the APK assets directory if (da_res->dir_exists(APK_ASSETS_DIRECTORY)) { print_verbose("Clearing APK assets directory.."); - DirAccessRef da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(APK_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(APK_ASSETS_DIRECTORY); } @@ -2430,7 +2442,7 @@ void EditorExportPlatformAndroid::_clear_assets_directory() { // Clear the AAB assets directory if (da_res->dir_exists(AAB_ASSETS_DIRECTORY)) { print_verbose("Clearing AAB assets directory.."); - DirAccessRef da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); + Ref<DirAccess> da_assets = DirAccess::open(AAB_ASSETS_DIRECTORY); da_assets->erase_contents_recursive(); da_res->remove(AAB_ASSETS_DIRECTORY); } @@ -2450,7 +2462,7 @@ void EditorExportPlatformAndroid::_remove_copied_libs() { ERR_FAIL_COND_MSG(error, "Error parsing \"" + libs_json + "\" on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); Vector<String> libs = json.get_data(); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); for (int i = 0; i < libs.size(); i++) { print_verbose("Removing previously installed library " + libs[i]); da->remove(libs[i]); @@ -2516,22 +2528,21 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (export_format == EXPORT_FORMAT_AAB) { if (!p_path.ends_with(".aab")) { - EditorNode::get_singleton()->show_warning(TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); return ERR_UNCONFIGURED; } if (apk_expansion) { - EditorNode::get_singleton()->show_warning(TTR("APK Expansion not compatible with Android App Bundle.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("APK Expansion not compatible with Android App Bundle.")); return ERR_UNCONFIGURED; } } if (export_format == EXPORT_FORMAT_APK && !p_path.ends_with(".apk")) { - EditorNode::get_singleton()->show_warning( - TTR("Invalid filename! Android APK requires the *.apk extension.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid filename! Android APK requires the *.apk extension.")); return ERR_UNCONFIGURED; } if (export_format > EXPORT_FORMAT_AAB || export_format < EXPORT_FORMAT_APK) { - EditorNode::add_io_error(TTR("Unsupported export format!\n")); - return ERR_UNCONFIGURED; //TODO: is this the right error? + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unsupported export format!")); + return ERR_UNCONFIGURED; } if (use_custom_build) { @@ -2539,16 +2550,15 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP //test that installed build version is alright { print_verbose("Checking build version.."); - FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); + Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); return ERR_UNCONFIGURED; } String version = f->get_line().strip_edges(); print_verbose("- build version: " + version); - f->close(); if (version != VERSION_FULL_CONFIG) { - EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Android build version mismatch: Template installed: %s, Godot version: %s. Please reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); return ERR_UNCONFIGURED; } } @@ -2561,7 +2571,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP String project_name = get_project_name(p_preset->get("package/name")); err = _create_project_name_strings_files(p_preset, project_name); //project name localization. if (err != OK) { - EditorNode::add_io_error(TTR("Unable to overwrite res://android/build/res/*.xml files with project name")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to overwrite res://android/build/res/*.xml files with project name.")); } // Copies the project icon files into the appropriate Gradle project directory. _copy_icons_to_gradle_project(p_preset, processed_splash_config_xml, splash_image, splash_bg_color_image, main_image, foreground, background); @@ -2576,22 +2586,21 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP CustomExportData user_data; user_data.assets_directory = assets_directory; user_data.debug = p_debug; - err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); if (err != OK) { - EditorNode::add_io_error(TTR("Could not export project files to gradle project\n")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); return err; } if (user_data.libs.size() > 0) { - FileAccessRef fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); + Ref<FileAccess> fa = FileAccess::open(GDNATIVE_LIBS_PATH, FileAccess::WRITE); JSON json; fa->store_string(json.stringify(user_data.libs, "\t")); - fa->close(); } } else { print_verbose("Saving apk expansion file.."); - err = save_apk_expansion_file(p_preset, p_path); + err = save_apk_expansion_file(p_preset, p_debug, p_path); if (err != OK) { - EditorNode::add_io_error(TTR("Could not write expansion package file!")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!")); return err; } } @@ -2676,7 +2685,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP debug_keystore = OS::get_singleton()->get_resource_dir().plus_file(debug_keystore).simplify_path(); } if (!FileAccess::exists(debug_keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2692,7 +2701,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP release_keystore = OS::get_singleton()->get_resource_dir().plus_file(release_keystore).simplify_path(); } if (!FileAccess::exists(release_keystore)) { - EditorNode::add_io_error(TTR("Could not find keystore, unable to export.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not find keystore, unable to export.")); return ERR_FILE_CANT_OPEN; } @@ -2704,7 +2713,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP int result = EditorNode::get_singleton()->execute_and_show_output(TTR("Building Android Project (gradle)"), build_command, cmdline); if (result != 0) { - EditorNode::get_singleton()->show_warning(TTR("Building of Android project failed, check output for the error.\nAlternatively visit docs.godotengine.org for Android build documentation.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Building of Android project failed, check output for the error. Alternatively visit docs.godotengine.org for Android build documentation.")); return ERR_CANT_CREATE; } @@ -2734,7 +2743,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("Copying Android binary using gradle command: " + String("\n") + build_command + " " + join_list(copy_args, String(" "))); int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args); if (copy_result != 0) { - EditorNode::get_singleton()->show_warning(TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); return ERR_CANT_CREATE; } @@ -2756,7 +2765,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP src_apk = find_export_template("android_release.apk"); } if (src_apk.is_empty()) { - EditorNode::add_io_error(vformat(TTR("Package not found: %s"), src_apk)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk)); return ERR_FILE_NOT_FOUND; } } @@ -2765,8 +2774,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2774,15 +2783,14 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unzFile pkg = unzOpen2(src_apk.utf8().get_data(), &io); if (!pkg) { - EditorNode::add_io_error(vformat(TTR("Could not find template APK to export:\n%s"), src_apk)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not find template APK to export: \"%s\"."), src_apk)); return ERR_FILE_NOT_FOUND; } int ret = unzGoToFirstFile(pkg); - zlib_filefunc_def io2 = io; - FileAccess *dst_f = nullptr; - io2.opaque = &dst_f; + Ref<FileAccess> io2_fa; + zlib_filefunc_def io2 = zipio_create_io(&io2_fa); String tmp_unaligned_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpexport-unaligned." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2790,7 +2798,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP { \ DirAccess::remove_file_or_error(tmp_unaligned_path); \ return m_err; \ - } + } \ + ((void)0) zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); @@ -2807,6 +2816,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } bool skip = false; @@ -2902,7 +2914,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (!invalid_abis.is_empty()) { String unsupported_arch = String(", ").join(invalid_abis); - EditorNode::add_io_error(vformat(TTR("Missing libraries in the export template for the selected architectures: %s.\nPlease build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Missing libraries in the export template for the selected architectures: %s. Please build a template with all required libraries, or uncheck the missing architectures in the export preset."), unsupported_arch)); CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND); } @@ -2915,25 +2927,25 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(p_preset, ignore_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, ignore_apk_file, &ed, save_apk_so); } else { if (apk_expansion) { - err = save_apk_expansion_file(p_preset, p_path); + err = save_apk_expansion_file(p_preset, p_debug, p_path); if (err != OK) { - EditorNode::add_io_error(TTR("Could not write expansion package file!")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not write expansion package file!")); return err; } } else { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, save_apk_file, &ed, save_apk_so); } } if (err != OK) { unzClose(pkg); - EditorNode::add_io_error(TTR("Could not export project files")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not export project files."))); CLEANUP_AND_RETURN(ERR_SKIP); } @@ -2969,15 +2981,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP unzFile tmp_unaligned = unzOpen2(tmp_unaligned_path.utf8().get_data(), &io); if (!tmp_unaligned) { - EditorNode::add_io_error(TTR("Could not unzip temporary unaligned APK.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not unzip temporary unaligned APK."))); CLEANUP_AND_RETURN(ERR_FILE_NOT_FOUND); } ret = unzGoToFirstFile(tmp_unaligned); - io2 = io; - dst_f = nullptr; - io2.opaque = &dst_f; + io2 = zipio_create_io(&io2_fa); zipFile final_apk = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); // Take files from the unaligned APK and write them out to the aligned one @@ -2991,6 +3001,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP char fname[16384]; char extra[16384]; ret = unzGetCurrentFileInfo(tmp_unaligned, &info, fname, 16384, extra, 16384 - ZIP_ALIGNMENT, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); @@ -3054,7 +3067,7 @@ void EditorExportPlatformAndroid::get_platform_features(List<String> *r_features r_features->push_back("android"); } -void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { +void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) { } EditorExportPlatformAndroid::EditorExportPlatformAndroid() { diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index a4eb608b19..eeb5aae0f1 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -28,6 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#ifndef ANDROID_EXPORT_PLUGIN_H +#define ANDROID_EXPORT_PLUGIN_H + #include "godot_plugin_config.h" #include "core/io/zip_io.h" @@ -210,7 +213,7 @@ public: String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path); + Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags); @@ -228,9 +231,11 @@ public: virtual void get_platform_features(List<String> *r_features) override; - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override; + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override; EditorExportPlatformAndroid(); ~EditorExportPlatformAndroid(); }; + +#endif // ANDROID_EXPORT_PLUGIN_H diff --git a/platform/android/export/godot_plugin_config.cpp b/platform/android/export/godot_plugin_config.cpp index 1210c09666..3daf6e44cd 100644 --- a/platform/android/export/godot_plugin_config.cpp +++ b/platform/android/export/godot_plugin_config.cpp @@ -71,7 +71,6 @@ PluginConfigAndroid PluginConfigAndroid::resolve_prebuilt_plugin(PluginConfigAnd Vector<PluginConfigAndroid> PluginConfigAndroid::get_prebuilt_plugins(String plugins_base_dir) { Vector<PluginConfigAndroid> prebuilt_plugins; - // prebuilt_plugins.push_back(resolve_prebuilt_plugin(MY_PREBUILT_PLUGIN, plugins_base_dir)); return prebuilt_plugins; } diff --git a/platform/android/export/godot_plugin_config.h b/platform/android/export/godot_plugin_config.h index 51cb4dea47..5188f615d4 100644 --- a/platform/android/export/godot_plugin_config.h +++ b/platform/android/export/godot_plugin_config.h @@ -103,4 +103,4 @@ struct PluginConfigAndroid { static String get_plugins_names(Vector<PluginConfigAndroid> plugins_configs); }; -#endif // GODOT_PLUGIN_CONFIG_H +#endif // ANDROID_GODOT_PLUGIN_CONFIG_H diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 9598d2f9fd..9a470edfdd 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -75,11 +75,10 @@ String _get_android_orientation_label(DisplayServer::ScreenOrientation screen_or // Utility method used to create a directory. Error create_directory(const String &p_dir) { if (!DirAccess::exists(p_dir)) { - DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); Error err = filesystem_da->make_dir_recursive(p_dir); ERR_FAIL_COND_V_MSG(err, ERR_CANT_CREATE, "Cannot create directory '" + p_dir + "'."); - memdelete(filesystem_da); } return OK; } @@ -92,10 +91,9 @@ Error store_file_at_path(const String &p_path, const Vector<uint8_t> &p_data) { if (err != OK) { return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_buffer(p_data.ptr(), p_data.size()); - memdelete(fa); return OK; } @@ -110,10 +108,9 @@ Error store_string_at_path(const String &p_path, const String &p_data) { } return err; } - FileAccess *fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); fa->store_string(p_data); - memdelete(fa); return OK; } @@ -123,7 +120,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - CustomExportData *export_data = (CustomExportData *)p_userdata; + CustomExportData *export_data = static_cast<CustomExportData *>(p_userdata); String dst_path = p_path.replace_first("res://", export_data->assets_directory + "/"); print_verbose("Saving project files from " + p_path + " into " + dst_path); Error err = store_file_at_path(dst_path, p_data); @@ -153,14 +150,15 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset store_string_at_path("res://android/build/res/values/godot_project_name_string.xml", processed_default_xml_string); // Searches the Gradle project res/ directory to find all supported locales - DirAccessRef da = DirAccess::open("res://android/build/res"); - if (!da) { + Ref<DirAccess> da = DirAccess::open("res://android/build/res"); + if (da.is_null()) { if (OS::get_singleton()->is_stdout_verbose()) { print_error("Unable to open Android resources directory."); } return ERR_CANT_OPEN; } da->list_dir_begin(); + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); while (true) { String file = da->get_next(); if (file.is_empty()) { @@ -171,10 +169,9 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset continue; } String locale = file.replace("values-", "").replace("-r", "_"); - String property_name = "application/config/name_" + locale; String locale_directory = "res://android/build/res/" + file + "/godot_project_name_string.xml"; - if (ProjectSettings::get_singleton()->has_setting(property_name)) { - String locale_project_name = ProjectSettings::get_singleton()->get(property_name); + if (appnames.has(locale)) { + String locale_project_name = appnames[locale]; String processed_xml_string = vformat(godot_project_name_xml_string, _android_xml_escape(locale_project_name)); print_verbose("Storing project name for locale " + locale + " under " + locale_directory); store_string_at_path(locale_directory, processed_xml_string); @@ -235,30 +232,19 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { return manifest_xr_features; } -String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset) { - String package_name = p_preset->get("package/unique_name"); - String manifest_instrumentation_text = vformat( - " <instrumentation\n" - " tools:node=\"replace\"\n" - " android:name=\".GodotInstrumentation\"\n" - " android:icon=\"@mipmap/icon\"\n" - " android:label=\"@string/godot_project_name_string\"\n" - " android:targetPackage=\"%s\" />\n", - package_name); - return manifest_instrumentation_text; -} - String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OPENXR; String orientation = _get_android_orientation_label(DisplayServer::ScreenOrientation(int(GLOBAL_GET("display/window/handheld/orientation")))); String manifest_activity_text = vformat( " <activity android:name=\"com.godot.game.GodotApp\" " - "tools:replace=\"android:screenOrientation,android:excludeFromRecents\" " + "tools:replace=\"android:screenOrientation,android:excludeFromRecents,android:resizeableActivity\" " "android:excludeFromRecents=\"%s\" " - "android:screenOrientation=\"%s\">\n", + "android:screenOrientation=\"%s\" " + "android:resizeableActivity=\"%s\">\n", bool_to_string(p_preset->get("package/exclude_from_recents")), - orientation); + orientation, + bool_to_string(bool(GLOBAL_GET("display/window/size/resizable")))); if (uses_xr) { manifest_activity_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"true\" />\n"; } else { @@ -280,6 +266,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ " android:requestLegacyExternalStorage=\"%s\"\n" " tools:replace=\"android:allowBackup,android:isGame,android:hasFragileUserData,android:requestLegacyExternalStorage\"\n" " tools:ignore=\"GoogleAppIndexingWarning\">\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_version_name\" />\n" " <meta-data tools:node=\"remove\" android:name=\"xr_hand_tracking_metadata_name\" />\n", bool_to_string(p_preset->get("user_data_backup/allow")), bool_to_string(p_preset->get("package/classify_as_game")), @@ -294,6 +281,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_ manifest_application_text += vformat( " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n", hand_tracking_frequency); + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.version\" android:value=\"V2.0\" />\n"; } } else { manifest_application_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n"; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 30a7f04729..109852bdfc 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -102,10 +102,8 @@ String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset); String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset); -String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset); - String _get_activity_tag(const Ref<EditorExportPreset> &p_preset); String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission); -#endif //GODOT_GRADLE_EXPORT_UTIL_H +#endif // GODOT_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index c84a919b6b..4bb8a13bb6 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,11 +34,13 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -FileAccess *FileAccessAndroid::create_android() { +Ref<FileAccess> FileAccessAndroid::create_android() { return memnew(FileAccessAndroid); } Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { + _close(); + String path = fix_path(p_path).simplify_path(); if (path.begins_with("/")) { path = path.substr(1, path.length()); @@ -47,33 +49,33 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { } ERR_FAIL_COND_V(p_mode_flags & FileAccess::WRITE, ERR_UNAVAILABLE); //can't write on android.. - a = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING); - if (!a) { + asset = AAssetManager_open(asset_manager, path.utf8().get_data(), AASSET_MODE_STREAMING); + if (!asset) { return ERR_CANT_OPEN; } - len = AAsset_getLength(a); + len = AAsset_getLength(asset); pos = 0; eof = false; return OK; } -void FileAccessAndroid::close() { - if (!a) { +void FileAccessAndroid::_close() { + if (!asset) { return; } - AAsset_close(a); - a = nullptr; + AAsset_close(asset); + asset = nullptr; } bool FileAccessAndroid::is_open() const { - return a != nullptr; + return asset != nullptr; } void FileAccessAndroid::seek(uint64_t p_position) { - ERR_FAIL_COND(!a); + ERR_FAIL_NULL(asset); - AAsset_seek(a, p_position, SEEK_SET); + AAsset_seek(asset, p_position, SEEK_SET); pos = p_position; if (pos > len) { pos = len; @@ -84,8 +86,8 @@ void FileAccessAndroid::seek(uint64_t p_position) { } void FileAccessAndroid::seek_end(int64_t p_position) { - ERR_FAIL_COND(!a); - AAsset_seek(a, p_position, SEEK_END); + ERR_FAIL_NULL(asset); + AAsset_seek(asset, p_position, SEEK_END); pos = len + p_position; } @@ -108,7 +110,7 @@ uint8_t FileAccessAndroid::get_8() const { } uint8_t byte; - AAsset_read(a, &byte, 1); + AAsset_read(asset, &byte, 1); pos++; return byte; } @@ -116,7 +118,7 @@ uint8_t FileAccessAndroid::get_8() const { uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); - int r = AAsset_read(a, p_dst, p_length); + int r = AAsset_read(asset, p_dst, p_length); if (pos + p_length > len) { eof = true; @@ -162,5 +164,5 @@ bool FileAccessAndroid::file_exists(const String &p_path) { } FileAccessAndroid::~FileAccessAndroid() { - close(); + _close(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index fb612cd008..c16f74ac43 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -35,45 +35,43 @@ #include <android/asset_manager.h> #include <android/log.h> #include <stdio.h> -//#include <android_native_app_glue.h> class FileAccessAndroid : public FileAccess { - static FileAccess *create_android(); - mutable AAsset *a = nullptr; + static Ref<FileAccess> create_android(); + mutable AAsset *asset = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; + void _close(); + public: static AAssetManager *asset_manager; - virtual Error _open(const String &p_path, int p_mode_flags); ///< open a file - virtual void close(); ///< close a file - virtual bool is_open() const; ///< true when file is open + virtual Error _open(const String &p_path, int p_mode_flags); // open a file + virtual bool is_open() const; // true when file is open - virtual void seek(uint64_t p_position); ///< seek to a given position - virtual void seek_end(int64_t p_position = 0); ///< seek from the end of file - virtual uint64_t get_position() const; ///< get position in the file - virtual uint64_t get_length() const; ///< get size of the file + virtual void seek(uint64_t p_position); // seek to a given position + virtual void seek_end(int64_t p_position = 0); // seek from the end of file + virtual uint64_t get_position() const; // get position in the file + virtual uint64_t get_length() const; // get size of the file - virtual bool eof_reached() const; ///< reading passed EOF + virtual bool eof_reached() const; // reading passed EOF - virtual uint8_t get_8() const; ///< get a byte + virtual uint8_t get_8() const; // get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const; - virtual Error get_error() const; ///< get last error + virtual Error get_error() const; // get last error virtual void flush(); - virtual void store_8(uint8_t p_dest); ///< store a byte + virtual void store_8(uint8_t p_dest); // store a byte - virtual bool file_exists(const String &p_path); ///< return true if a file exists + virtual bool file_exists(const String &p_path); // return true if a file exists virtual uint64_t _get_modified_time(const String &p_file) { return 0; } virtual uint32_t _get_unix_permissions(const String &p_file) { return 0; } virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) { return FAILED; } - //static void make_default(); - ~FileAccessAndroid(); }; diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 4c4501729d..c98e8f1d55 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -40,6 +40,13 @@ android:name="xr_hand_tracking_metadata_name" android:value="xr_hand_tracking_metadata_value"/> + <!-- XR hand tracking version --> + <!-- This is modified by the exporter based on the selected xr mode. DO NOT CHANGE the values here. --> + <!-- Removed at export time if the xr mode is not VR or hand tracking is disabled. --> + <meta-data + android:name="xr_hand_tracking_version_name" + android:value="xr_hand_tracking_version_value"/> + <!-- Supported Meta devices --> <!-- This is removed by the exporter if the xr mode is not VR. --> <meta-data diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 5d1a9d7b99..63b10e62b1 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -1,6 +1,4 @@ // Gradle build config for Godot Engine's Android port. -apply from: 'config.gradle' - buildscript { apply from: 'config.gradle' @@ -14,7 +12,12 @@ buildscript { } } -apply plugin: 'com.android.application' +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +apply from: 'config.gradle' allprojects { repositories { @@ -33,6 +36,11 @@ allprojects { } } +configurations { + // Initializes a placeholder for the devImplementation dependency configuration. + devImplementation {} +} + dependencies { implementation libraries.kotlinStdLib implementation libraries.androidxFragment @@ -45,6 +53,7 @@ dependencies { // Custom build mode. In this scenario this project is the only one around and the Godot // library is available through the pre-generated godot-lib.*.aar android archive files. debugImplementation fileTree(dir: 'libs/debug', include: ['*.jar', '*.aar']) + devImplementation fileTree(dir: 'libs/dev', include: ['*.jar', '*.aar']) releaseImplementation fileTree(dir: 'libs/release', include: ['*.jar', '*.aar']) } @@ -66,12 +75,17 @@ dependencies { android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion compileOptions { sourceCompatibility versions.javaVersion targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + assetPacks = [":assetPacks:installTime"] defaultConfig { @@ -93,6 +107,8 @@ android { versionName getExportVersionName() minSdkVersion getExportMinSdkVersion() targetSdkVersion getExportTargetSdkVersion() + + missingDimensionStrategy 'products', 'template' } lintOptions { @@ -146,6 +162,18 @@ android { } } + dev { + initWith debug + // Signing and zip-aligning are skipped for prebuilt builds, but + // performed for custom builds. + zipAlignEnabled shouldZipAlign() + if (shouldSign()) { + signingConfig signingConfigs.debug + } else { + signingConfig null + } + } + release { // Signing and zip-aligning are skipped for prebuilt builds, but // performed for custom builds. @@ -167,6 +195,7 @@ android { assets.srcDirs = ['assets'] } debug.jniLibs.srcDirs = ['libs/debug', 'libs/debug/vulkan_validation_layers'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] } @@ -183,6 +212,12 @@ task copyAndRenameDebugApk(type: Copy) { rename "android_debug.apk", getExportFilename() } +task copyAndRenameDevApk(type: Copy) { + from "$buildDir/outputs/apk/dev/android_dev.apk" + into getExportPath() + rename "android_dev.apk", getExportFilename() +} + task copyAndRenameReleaseApk(type: Copy) { from "$buildDir/outputs/apk/release/android_release.apk" into getExportPath() @@ -195,6 +230,12 @@ task copyAndRenameDebugAab(type: Copy) { rename "build-debug.aab", getExportFilename() } +task copyAndRenameDevAab(type: Copy) { + from "$buildDir/outputs/bundle/dev/build-dev.aab" + into getExportPath() + rename "build-dev.aab", getExportFilename() +} + task copyAndRenameReleaseAab(type: Copy) { from "$buildDir/outputs/bundle/release/build-release.aab" into getExportPath() diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 32e03998da..73a412a2b0 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,11 +1,12 @@ ext.versions = [ androidGradlePlugin: '7.0.3', - compileSdk : 30, + compileSdk : 31, minSdk : 19, // Also update 'platform/android/java/lib/AndroidManifest.xml#minSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION' targetSdk : 30, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' buildTools : '30.0.3', - kotlinVersion : '1.5.10', + kotlinVersion : '1.6.21', fragmentVersion : '1.3.6', + nexusPublishVersion: '1.1.0', javaVersion : 11, ndkVersion : '21.4.7075529' // Also update 'platform/android/detect.py#get_project_ndk_version()' when this is updated. @@ -14,7 +15,7 @@ ext.versions = [ ext.libraries = [ androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", - kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion", + kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion", androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion", ] @@ -76,7 +77,7 @@ ext.getGodotEditorVersion = { -> String editorVersion = project.hasProperty("godot_editor_version") ? project.property("godot_editor_version") : "" if (editorVersion == null || editorVersion.isEmpty()) { // Try the library version first - editorVersion = getGodotLibraryVersion() + editorVersion = getGodotLibraryVersionName() if (editorVersion.isEmpty()) { // Fallback value. @@ -86,13 +87,27 @@ ext.getGodotEditorVersion = { -> return editorVersion } -ext.getGodotLibraryVersion = { -> +ext.getGodotLibraryVersionCode = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionCode +} + +ext.getGodotLibraryVersionName = { -> + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = getGodotLibraryVersion() + return versionName +} + +ext.generateGodotLibraryVersion = { List<String> requiredKeys -> // Attempt to read the version from the `version.py` file. - String libraryVersion = "" + String libraryVersionName = "" + int libraryVersionCode = 0 File versionFile = new File("../../../version.py") if (versionFile.isFile()) { - List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"] def map = [:] List<String> lines = versionFile.readLines() @@ -110,15 +125,48 @@ ext.getGodotLibraryVersion = { -> } if (requiredKeys.empty) { - libraryVersion = map.values().join(".") + libraryVersionName = map.values().join(".") + try { + if (map.containsKey("patch")) { + libraryVersionCode = Integer.parseInt(map["patch"]) + } + + if (map.containsKey("minor")) { + libraryVersionCode += (Integer.parseInt(map["minor"]) * 100) + } + + if (map.containsKey("major")) { + libraryVersionCode += (Integer.parseInt(map["major"]) * 10000) + } + } catch (NumberFormatException ignore) { + libraryVersionCode = 1 + } } } - if (libraryVersion.isEmpty()) { + if (libraryVersionName.isEmpty()) { // Fallback value in case we're unable to read the file. - libraryVersion = "custom_build" + libraryVersionName = "custom_build" + } + + if (libraryVersionCode == 0) { + libraryVersionCode = 1 } - return libraryVersion + + return [libraryVersionName, libraryVersionCode] +} + +ext.getGodotLibraryVersion = { -> + List<String> requiredKeys = ["major", "minor", "patch", "status", "module_config"] + return generateGodotLibraryVersion(requiredKeys) +} + +ext.getGodotPublishVersion = { -> + List<String> requiredKeys = ["major", "minor", "patch", "status"] + String versionName = "" + int versionCode = 1 + (versionName, versionCode) = generateGodotLibraryVersion(requiredKeys) + return versionName } final String VALUE_SEPARATOR_REGEX = "\\|" diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml index 99f723f5ba..d64b50ca45 100644 --- a/platform/android/java/app/res/values/themes.xml +++ b/platform/android/java/app/res/values/themes.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/> + <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/> - <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme"> + <style name="GodotAppSplashTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"> <item name="android:windowBackground">@drawable/splash_drawable</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index e38d7b2ba6..ba53aefe7f 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -1,2 +1,15 @@ // This is the root directory of the Godot custom build. +pluginManagement { + apply from: 'config.gradle' + + plugins { + id 'com.android.application' version versions.androidGradlePlugin + id 'org.jetbrains.kotlin.android' version versions.kotlinVersion + } + repositories { + gradlePluginPortal() + google() + } +} + include ':assetPacks:installTime' diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index ac008edbed..da30bd3a95 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -1,18 +1,25 @@ -apply from: 'app/config.gradle' - buildscript { apply from: 'app/config.gradle' repositories { google() mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath libraries.androidGradlePlugin classpath libraries.kotlinGradlePlugin + classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' } } +plugins { + id 'io.github.gradle-nexus.publish-plugin' +} + +apply from: 'app/config.gradle' +apply from: 'scripts/publish-root.gradle' + allprojects { repositories { google() @@ -22,21 +29,22 @@ allprojects { ext { supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] - supportedTargets = ["release", "debug"] + supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"] + supportedFlavors = ["editor", "template"] - // Used by gradle to specify which architecture to build for by default when running `./gradlew build`. - // This command is usually used by Android Studio. + // Used by gradle to specify which architecture to build for by default when running + // `./gradlew build` (this command is usually used by Android Studio). // If building manually on the command line, it's recommended to use the - // `./gradlew generateGodotTemplates` build command instead after running the `scons` command. - // The defaultAbi must be one of the {supportedAbis} values. - defaultAbi = "arm64v8" + // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s). + // The {selectedAbis} values must be from the {supportedAbis} values. + selectedAbis = ["arm64v8"] } def rootDir = "../../.." def binDir = "$rootDir/bin/" -def getSconsTaskName(String buildType) { - return "compileGodotNativeLibs" + buildType.capitalize() +def getSconsTaskName(String flavor, String buildType, String abi) { + return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() } /** @@ -51,6 +59,17 @@ task copyDebugBinaryToBin(type: Copy) { } /** + * Copy the generated 'android_dev.apk' binary template into the Godot bin directory. + * Depends on the app build task to ensure the binary is generated prior to copying. + */ +task copyDevBinaryToBin(type: Copy) { + dependsOn ':app:assembleDev' + from('app/build/outputs/apk/dev') + into(binDir) + include('android_dev.apk') +} + +/** * Copy the generated 'android_release.apk' binary template into the Godot bin directory. * Depends on the app build task to ensure the binary is generated prior to copying. */ @@ -66,7 +85,7 @@ task copyReleaseBinaryToBin(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToAppModule(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into('app/libs/debug') include('godot-lib.debug.aar') @@ -77,18 +96,40 @@ task copyDebugAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyDebugAARToBin(type: Copy) { - dependsOn ':lib:assembleDebug' + dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into(binDir) include('godot-lib.debug.aar') } /** + * Copy the Godot android library archive dev file into the app module dev libs directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToAppModule(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into('app/libs/dev') + include('godot-lib.dev.aar') +} + +/** + * Copy the Godot android library archive dev file into the root bin directory. + * Depends on the library build task to ensure the AAR file is generated prior to copying. + */ +task copyDevAARToBin(type: Copy) { + dependsOn ':lib:assembleTemplateDev' + from('lib/build/outputs/aar') + into(binDir) + include('godot-lib.dev.aar') +} + +/** * Copy the Godot android library archive release file into the app module release libs directory. * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToAppModule(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into('app/libs/release') include('godot-lib.release.aar') @@ -99,7 +140,7 @@ task copyReleaseAARToAppModule(type: Copy) { * Depends on the library build task to ensure the AAR file is generated prior to copying. */ task copyReleaseAARToBin(type: Copy) { - dependsOn ':lib:assembleRelease' + dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into(binDir) include('godot-lib.release.aar') @@ -107,7 +148,7 @@ task copyReleaseAARToBin(type: Copy) { /** * Generate Godot custom build template by zipping the source files from the app directory, as well - * as the AAR files generated by 'copyDebugAAR' and 'copyReleaseAAR'. + * as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'. * The zip file also includes some gradle tools to allow building of the custom build. */ task zipCustomBuild(type: Zip) { @@ -126,8 +167,18 @@ def templateExcludedBuildTask() { def excludedTasks = [] if (!isAndroidStudio()) { logger.lifecycle("Excluding Android studio build tasks") - for (String buildType : supportedTargets) { - excludedTasks += ":lib:" + getSconsTaskName(buildType) + for (String flavor : supportedFlavors) { + for (String buildType : supportedTargetsMap.keySet()) { + if (buildType == "release" && flavor == "editor") { + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + continue + } + + for (String abi : selectedAbis) { + excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi) + } + } } } return excludedTasks @@ -137,7 +188,7 @@ def templateBuildTasks() { def tasks = [] // Only build the apks and aar files for which we have native shared libraries. - for (String target : supportedTargets) { + for (String target : supportedTargetsMap.keySet()) { File targetLibs = new File("lib/libs/" + target) if (targetLibs != null && targetLibs.isDirectory() @@ -163,6 +214,50 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } +task copyEditorDebugBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDebug' + from('editor/build/outputs/apk/debug') + into(binDir) + include('android_editor.apk') +} + +task copyEditorDevBinaryToBin(type: Copy) { + dependsOn ':editor:assembleDev' + from('editor/build/outputs/apk/dev') + into(binDir) + include('android_editor_dev.apk') +} + +/** + * Generate the Godot Editor Android apk. + * + * Note: The Godot 'tools' shared libraries must have been generated (via scons) prior to running + * this gradle task. The task will only build the apk(s) for which the shared libraries is + * available. + */ +task generateGodotEditor { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + + def tasks = [] + + for (String target : supportedTargetsMap.keySet()) { + if (target == "release") { + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + continue + } + File targetLibs = new File("lib/libs/tools/" + target) + if (targetLibs != null + && targetLibs.isDirectory() + && targetLibs.listFiles() != null + && targetLibs.listFiles().length > 0) { + tasks += "copyEditor${target.capitalize()}BinaryToBin" + } + } + + dependsOn = tasks +} + /** * Master task used to coordinate the tasks defined above to generate the set of Godot templates. */ @@ -187,7 +282,27 @@ task generateDevTemplate { } /** - * Clean the generated artifacts. + * Clean the generated editor artifacts. + */ +task cleanGodotEditor(type: Delete) { + // Delete the generated native tools libs + delete("lib/libs/tools") + + // Delete the library generated AAR files + delete("lib/build/outputs/aar") + + // Delete the generated binary apks + delete("editor/build/outputs/apk") + + // Delete the Godot editor apks in the Godot bin directory + delete("$binDir/android_editor.apk") + delete("$binDir/android_editor_dev.apk") + + finalizedBy getTasksByName("clean", true) +} + +/** + * Clean the generated template artifacts. */ task cleanGodotTemplates(type: Delete) { // Delete the generated native libs @@ -204,9 +319,11 @@ task cleanGodotTemplates(type: Delete) { // Delete the Godot templates in the Godot bin directory delete("$binDir/android_debug.apk") + delete("$binDir/android_dev.apk") delete("$binDir/android_release.apk") delete("$binDir/android_source.zip") delete("$binDir/godot-lib.debug.aar") + delete("$binDir/godot-lib.dev.aar") delete("$binDir/godot-lib.release.aar") finalizedBy getTasksByName("clean", true) diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle new file mode 100644 index 0000000000..dd167c3880 --- /dev/null +++ b/platform/android/java/editor/build.gradle @@ -0,0 +1,83 @@ +// Gradle build config for Godot Engine's Android port. +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +dependencies { + implementation libraries.kotlinStdLib + implementation libraries.androidxFragment + implementation project(":lib") + + implementation "androidx.window:window:1.0.0" +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion + + defaultConfig { + // The 'applicationId' suffix allows to install Godot 3.x(v3) and 4.x(v4) on the same device + applicationId "org.godotengine.editor.v4" + versionCode getGodotLibraryVersionCode() + versionName getGodotLibraryVersionName() + minSdkVersion versions.minSdk + //noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted + targetSdkVersion 29 // versions.targetSdk + + missingDimensionStrategy 'products', 'editor' + } + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = versions.javaVersion + } + + buildTypes { + dev { + initWith debug + applicationIdSuffix ".dev" + } + + debug { + initWith release + + // Need to swap with the release signing config when this is ready for public release. + signingConfig signingConfigs.debug + } + + release { + // This buildtype is disabled below. + // The editor can't be used with target=release only, as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + } + } + + packagingOptions { + // 'doNotStrip' is enabled for development within Android Studio + if (shouldNotStrip()) { + doNotStrip '**/*.so' + } + } + + // Disable 'release' buildtype. + // The editor can't be used with target=release only, as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + variantFilter { variant -> + if (variant.buildType.name == "release") { + setIgnore(true) + } + } + + applicationVariants.all { variant -> + variant.outputs.all { output -> + def suffix = variant.name == "dev" ? "_dev" : "" + output.outputFileName = "android_editor${suffix}.apk" + } + } +} diff --git a/platform/android/java/editor/src/dev/res/values/strings.xml b/platform/android/java/editor/src/dev/res/values/strings.xml new file mode 100644 index 0000000000..45fae3fd39 --- /dev/null +++ b/platform/android/java/editor/src/dev/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4.x (dev)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..659caf7ab4 --- /dev/null +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="org.godotengine.editor" + android:installLocation="auto"> + + <supports-screens + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" + android:xlargeScreens="true" /> + + <uses-feature + android:glEsVersion="0x00020000" + android:required="true" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.INTERNET" /> + + <application + android:allowBackup="false" + android:icon="@mipmap/icon" + android:label="@string/godot_editor_name_string" + tools:ignore="GoogleAppIndexingWarning" + android:requestLegacyExternalStorage="true"> + + <activity + android:name=".GodotProjectManager" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:exported="true" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:process=":GodotProjectManager"> + + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity + android:name=".GodotEditor" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:process=":GodotEditor" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + </activity> + + <activity + android:name=".GodotGame" + android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:label="@string/godot_project_name_string" + android:process=":GodotGame" + android:launchMode="singleTask" + android:screenOrientation="userLandscape" + android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"> + <layout android:defaultHeight="@dimen/editor_default_window_height" + android:defaultWidth="@dimen/editor_default_window_width" /> + </activity> + + </application> + +</manifest> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..a1ade722e8 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,146 @@ +/*************************************************************************/ +/* GodotEditor.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +package org.godotengine.editor + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Debug +import androidx.window.layout.WindowMetricsCalculator +import org.godotengine.godot.FullScreenGodotApp +import org.godotengine.godot.utils.PermissionsUtil +import java.util.* +import kotlin.math.min + +/** + * Base class for the Godot Android Editor activities. + * + * This provides the basic templates for the activities making up this application. + * Each derived activity runs in its own process, which enable up to have several instances of + * the Godot engine up and running at the same time. + * + * It also plays the role of the primary editor window. + */ +open class GodotEditor : FullScreenGodotApp() { + + companion object { + private const val WAIT_FOR_DEBUGGER = false + + private const val COMMAND_LINE_PARAMS = "command_line_params" + + private const val EDITOR_ARG = "--editor" + private const val PROJECT_MANAGER_ARG = "--project-manager" + } + + private val commandLineParams = ArrayList<String>() + + override fun onCreate(savedInstanceState: Bundle?) { + PermissionsUtil.requestManifestPermissions(this) + + val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS) + updateCommandLineParams(params) + + if (BuildConfig.BUILD_TYPE == "debug" && WAIT_FOR_DEBUGGER) { + Debug.waitForDebugger() + } + + super.onCreate(savedInstanceState) + } + + private fun updateCommandLineParams(args: Array<String>?) { + // Update the list of command line params with the new args + commandLineParams.clear() + if (args != null && args.isNotEmpty()) { + commandLineParams.addAll(listOf(*args)) + } + } + + override fun getCommandLine() = commandLineParams + + override fun onNewGodotInstanceRequested(args: Array<String>) { + // Parse the arguments to figure out which activity to start. + var targetClass: Class<*> = GodotGame::class.java + + // Whether we should launch the new godot instance in an adjacent window + // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT + var launchAdjacent = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (isInMultiWindowMode || isLargeScreen) + + for (arg in args) { + if (EDITOR_ARG == arg) { + targetClass = GodotEditor::class.java + launchAdjacent = false + break + } + + if (PROJECT_MANAGER_ARG == arg) { + targetClass = GodotProjectManager::class.java + launchAdjacent = false + break + } + } + + // Launch a new activity + val newInstance = Intent(this, targetClass) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(COMMAND_LINE_PARAMS, args) + if (launchAdjacent) { + newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) + } + startActivity(newInstance) + } + + // Get the screen's density scale + protected val isLargeScreen: Boolean + // Get the minimum window size // Correspond to the EXPANDED window size class. + get() { + val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) + + // Get the screen's density scale + val scale = resources.displayMetrics.density + + // Get the minimum window size + val minSize = min(metrics.bounds.width(), metrics.bounds.height()).toFloat() + val minSizeDp = minSize / scale + return minSizeDp >= 840f // Correspond to the EXPANDED window size class. + } + + override fun setRequestedOrientation(requestedOrientation: Int) { + if (!overrideOrientationRequest()) { + super.setRequestedOrientation(requestedOrientation) + } + } + + /** + * The Godot Android Editor sets its own orientation via its AndroidManifest + */ + protected open fun overrideOrientationRequest() = true +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 44f0a3eb3e..783095f93a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotInstrumentation.java */ +/* GodotGame.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,23 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.editor -import android.app.Instrumentation; -import android.content.Intent; -import android.os.Bundle; - -public class GodotInstrumentation extends Instrumentation { - private Intent intent; - - @Override - public void onCreate(Bundle arguments) { - intent = arguments.getParcelable("intent"); - start(); - } - - @Override - public void onStart() { - startActivitySync(intent); - } +/** + * Drives the 'run project' window of the Godot Editor. + */ +class GodotGame : GodotEditor() { + override fun overrideOrientationRequest() = false } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt new file mode 100644 index 0000000000..bcf4659603 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -0,0 +1,40 @@ +/*************************************************************************/ +/* GodotProjectManager.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +package org.godotengine.editor + +/** + * Launcher activity for the Godot Android Editor. + * + * It presents the user with the project manager interface. + * Upon selection of a project, this activity (via its parent logic) starts the + * [GodotEditor] activity. + */ +class GodotProjectManager : GodotEditor() diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..03fb6184d2 --- /dev/null +++ b/platform/android/java/editor/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="editor_default_window_height">600dp</dimen> + <dimen name="editor_default_window_width">800dp</dimen> +</resources> diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml new file mode 100644 index 0000000000..e8ce34f34d --- /dev/null +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4.x</string> +</resources> diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 2de62271c4..90dc61a6ac 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -16,12 +16,13 @@ <service android:name=".GodotDownloaderService" /> - </application> + <activity + android:name=".utils.ProcessPhoenix" + android:theme="@android:style/Theme.Translucent.NoTitleBar" + android:process=":phoenix" + android:exported="false" + /> - <instrumentation - android:icon="@mipmap/icon" - android:label="@string/godot_project_name_string" - android:name="org.godotengine.godot.GodotInstrumentation" - android:targetPackage="org.godotengine.godot" /> + </application> </manifest> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index fbed4ed078..6b82326a27 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -1,5 +1,14 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +ext { + PUBLISH_VERSION = getGodotPublishVersion() + PUBLISH_ARTIFACT_ID = 'godot' +} + +apply from: "../scripts/publish-module.gradle" dependencies { implementation libraries.kotlinStdLib @@ -11,21 +20,38 @@ def pathToRootDir = "../../../../" android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools - ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk - manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersion()] + manifestPlaceholders = [godotLibraryVersion: getGodotLibraryVersionName()] } + namespace = "org.godotengine.godot" + compileOptions { sourceCompatibility versions.javaVersion targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + + buildTypes { + dev { + initWith debug + } + } + + flavorDimensions "products" + productFlavors { + editor {} + template {} + } + lintOptions { abortOnError false disable 'MissingTranslation', 'UnusedResources' @@ -49,24 +75,50 @@ android { aidl.srcDirs = ['aidl'] assets.srcDirs = ['assets'] } + debug.jniLibs.srcDirs = ['libs/debug'] + dev.jniLibs.srcDirs = ['libs/dev'] release.jniLibs.srcDirs = ['libs/release'] + + // Editor jni library + editorDebug.jniLibs.srcDirs = ['libs/tools/debug'] + editorDev.jniLibs.srcDirs = ['libs/tools/dev'] + } + + // Disable 'editorRelease'. + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + variantFilter { variant -> + if (variant.name == "editorRelease") { + setIgnore(true) + } } libraryVariants.all { variant -> - variant.outputs.all { output -> - output.outputFileName = "godot-lib.${variant.name}.aar" + def flavorName = variant.getFlavorName() + if (flavorName == null || flavorName == "") { + throw new GradleException("Invalid product flavor: $flavorName") } - def buildType = variant.buildType.name.capitalize() + boolean toolsFlag = flavorName == "editor" + + def buildType = variant.buildType.name + if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) { + throw new GradleException("Invalid build type: $buildType") + } - def releaseTarget = buildType.toLowerCase() - if (releaseTarget == null || releaseTarget == "") { - throw new GradleException("Invalid build type: " + buildType) + def sconsTarget = supportedTargetsMap[buildType] + if (sconsTarget == null || sconsTarget == "") { + throw new GradleException("Invalid scons target: $sconsTarget") } - if (!supportedAbis.contains(defaultAbi)) { - throw new GradleException("Invalid default abi: " + defaultAbi) + // Update the name of the generated library + def outputSuffix = "${buildType}.aar" + if (toolsFlag) { + outputSuffix = "tools.$outputSuffix" + } + variant.outputs.all { output -> + output.outputFileName = "godot-lib.${outputSuffix}" } // Find scons' executable path @@ -79,13 +131,11 @@ android { for (ext in sconsExts) { String sconsNameExt = sconsName + ext logger.lifecycle("Checking $sconsNameExt") - sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! break } - // Check all the options in path List<File> allOptions = org.gradle.internal.os.OperatingSystem.current().findAllInPath(sconsNameExt) if (!allOptions.isEmpty()) { @@ -94,21 +144,34 @@ android { break } } - if (sconsExecutableFile == null) { throw new GradleException("Unable to find executable path for the '$sconsName' command.") } else { logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") } - // Creating gradle task to generate the native libraries for the default abi. - def taskName = getSconsTaskName(buildType) - tasks.create(name: taskName, type: Exec) { - executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "target=${releaseTarget}", "android_arch=${defaultAbi}", "-j" + Runtime.runtime.availableProcessors() - } + for (String selectedAbi : selectedAbis) { + if (!supportedAbis.contains(selectedAbi)) { + throw new GradleException("Invalid selected abi: $selectedAbi") + } - // Schedule the tasks so the generated libs are present before the aar file is packaged. - tasks["merge${buildType}JniLibFolders"].dependsOn taskName + // Creating gradle task to generate the native libraries for the selected abi. + def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) + tasks.create(name: taskName, type: Exec) { + executable sconsExecutableFile.absolutePath + args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + } + + // Schedule the tasks so the generated libs are present before the aar file is packaged. + tasks["merge${flavorName.capitalize()}${buildType.capitalize()}JniLibFolders"].dependsOn taskName + } } + + // TODO: Enable when issues with AGP 7.1+ are resolved (https://github.com/GodotVR/godot_openxr/issues/187). +// publishing { +// singleVariant("templateRelease") { +// withSourcesJar() +// withJavadocJar() +// } +// } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index e8ffbb9481..f21f88db0a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -30,7 +30,8 @@ package org.godotengine.godot; -import android.content.ComponentName; +import org.godotengine.godot.utils.ProcessPhoenix; + import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -65,16 +66,13 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God } else { Log.v(TAG, "Creating new Godot fragment instance."); godotFragment = initGodotInstance(); - if (godotFragment == null) { - throw new IllegalStateException("Godot instance must be non-null."); - } - getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } } @Override public void onDestroy() { + Log.v(TAG, "Destroying Godot app..."); super.onDestroy(); onGodotForceQuit(godotFragment); } @@ -82,27 +80,21 @@ public abstract class FullScreenGodotApp extends FragmentActivity implements God @Override public final void onGodotForceQuit(Godot instance) { if (instance == godotFragment) { - System.exit(0); + Log.v(TAG, "Force quitting Godot instance"); + ProcessPhoenix.forceQuit(this); } } @Override public final void onGodotRestartRequested(Godot instance) { if (instance == godotFragment) { - // HACK: - // - // Currently it's very hard to properly deinitialize Godot on Android to restart the game + // It's very hard to properly de-initialize Godot on Android to restart the game // from scratch. Therefore, we need to kill the whole app process and relaunch it. // // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including // releasing and reloading native libs or resetting their state somehow and clearing statics). - // - // Using instrumentation is a way of making the whole app process restart, because Android - // will kill any process of the same package which was already running. - // - Bundle args = new Bundle(); - args.putParcelable("intent", getIntent()); - startInstrumentation(new ComponentName(this, GodotInstrumentation.class), null, args); + Log.v(TAG, "Restarting Godot instance..."); + ProcessPhoenix.triggerRebirth(this); } } 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 78848c109a..cafae94d62 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -36,6 +36,7 @@ import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import org.godotengine.godot.tts.GodotTTS; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -47,7 +48,6 @@ import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -166,6 +166,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public static GodotIO io; public static GodotNetUtils netUtils; + public static GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -333,9 +334,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public void restart() { - if (godotHost != null) { - godotHost.onGodotRestartRequested(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotRestartRequested(this); + } + }); } public void alert(final String message, final String title) { @@ -457,15 +460,12 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC io = new GodotIO(activity); GodotLib.io = io; netUtils = new GodotNetUtils(activity); + tts = new GodotTTS(activity); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME); mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion); @@ -508,17 +508,14 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC use_debug_opengl = true; } else if (command_line[i].equals("--use_immersive")) { use_immersive = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ - window.getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - UiChangeListener(); - } + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | // hide nav bar + View.SYSTEM_UI_FLAG_FULLSCREEN | // hide status bar + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + UiChangeListener(); } else if (command_line[i].equals("--use_apk_expansion")) { use_apk_expansion = true; } else if (has_extra && command_line[i].equals("--apk_expansion_md5")) { @@ -665,14 +662,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } public String getClipboard() { - String copiedText = ""; - - if (mClipboard.hasPrimaryClip()) { - ClipData.Item item = mClipboard.getPrimaryClip().getItemAt(0); - copiedText = item.getText().toString(); - } - - return copiedText; + ClipData clipData = mClipboard.getPrimaryClip(); + if (clipData == null) + return ""; + CharSequence text = clipData.getItemAt(0).getText(); + if (text == null) + return ""; + return text.toString(); } public void setClipboard(String p_text) { @@ -698,7 +694,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME); - if (use_immersive && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // check if the application runs on an android 4.4+ + if (use_immersive) { Window window = getActivity().getWindow(); window.getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | @@ -718,15 +714,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final View decorView = getActivity().getWindow().getDecorView(); decorView.setOnSystemUiVisibilityChangeListener(visibility -> { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - } + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); } }); } @@ -859,9 +853,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void forceQuit() { // TODO: This is a temp solution. The proper fix will involve tracking down and properly shutting down each // native Godot components that is started in Godot#onVideoInit. - if (godotHost != null) { - godotHost.onGodotForceQuit(this); - } + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onGodotForceQuit(this); + } + }); } private boolean obbIsCorrupted(String f, String main_pack_md5) { @@ -885,9 +881,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) { - String s = Integer.toHexString(0xFF & messageDigest[i]); - + for (byte b : messageDigest) { + String s = Integer.toHexString(0xFF & b); if (s.length() == 1) { s = "0" + s; } @@ -1010,6 +1005,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mProgressFraction.setText(Helpers.getDownloadProgressString(progress.mOverallProgress, progress.mOverallTotal)); } + public void initInputDevices() { mRenderView.initInputDevices(); } @@ -1018,4 +1014,13 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private GodotRenderView getRenderView() { // used by native side to get renderView return mRenderView; } + + @Keep + private void createNewGodotInstance(String[] args) { + runOnUiThread(() -> { + if (godotHost != null) { + godotHost.onNewGodotInstanceRequested(args); + } + }); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 61093d54de..08da1b1832 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -29,6 +29,8 @@ /*************************************************************************/ package org.godotengine.godot; +import org.godotengine.godot.gl.GLSurfaceView; +import org.godotengine.godot.gl.GodotRenderer; import org.godotengine.godot.input.GodotGestureHandler; import org.godotengine.godot.input.GodotInputHandler; import org.godotengine.godot.utils.GLUtils; @@ -43,7 +45,6 @@ import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PixelFormat; -import android.opengl.GLSurfaceView; import android.os.Build; import android.view.GestureDetector; import android.view.KeyEvent; diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 8e8f993369..2e7b67194f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -60,8 +60,16 @@ public interface GodotHost { default void onGodotForceQuit(Godot instance) {} /** - * Invoked on the GL thread when the Godot instance wants to be restarted. It's up to the host + * Invoked on the UI thread when the Godot instance wants to be restarted. It's up to the host * to perform the appropriate action(s). */ default void onGodotRestartRequested(Godot instance) {} + + /** + * Invoked on the UI thread when a new Godot instance is requested. It's up to the host to + * perform the appropriate action(s). + * + * @param args Arguments used to initialize the new instance. + */ + default void onNewGodotInstanceRequested(String[] args) {} } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index b151e7eec1..a8e3669ac6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -38,6 +38,7 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; import android.graphics.Point; +import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Environment; @@ -51,6 +52,7 @@ import android.view.DisplayCutout; import android.view.WindowInsets; import java.io.IOException; +import java.util.List; import java.util.Locale; // Wrapper for native library @@ -222,8 +224,30 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = activity.getApplicationContext().getResources().getDisplayMetrics(); - return (int)(metrics.density * 160f); + return activity.getResources().getDisplayMetrics().densityDpi; + } + + /** + * Returns bucketized density values. + */ + public float getScaledDensity() { + int densityDpi = activity.getResources().getDisplayMetrics().densityDpi; + float selectedScaledDensity; + if (densityDpi >= DisplayMetrics.DENSITY_XXXHIGH) { + selectedScaledDensity = 4.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) { + selectedScaledDensity = 3.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) { + selectedScaledDensity = 2.0f; + } else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) { + selectedScaledDensity = 1.5f; + } else if (densityDpi >= DisplayMetrics.DENSITY_MEDIUM) { + selectedScaledDensity = 1.0f; + } else { + selectedScaledDensity = 0.75f; + } + Log.d(TAG, "Selected scaled density: " + selectedScaledDensity); + return selectedScaledDensity; } public double getScreenRefreshRate(double fallback) { @@ -234,7 +258,7 @@ public class GodotIO { return fallback; } - public int[] screenGetUsableRect() { + public int[] getDisplaySafeArea() { DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); Display display = activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -256,6 +280,25 @@ public class GodotIO { return result; } + public int[] getDisplayCutouts() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + return new int[0]; + DisplayCutout cutout = activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + if (cutout == null) + return new int[0]; + List<Rect> rects = cutout.getBoundingRects(); + int cutouts = rects.size(); + int[] result = new int[cutouts * 4]; + int index = 0; + for (Rect rect : rects) { + result[index++] = rect.left; + result[index++] = rect.top; + result[index++] = rect.width(); + result[index++] = rect.height(); + } + return result; + } + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (edit != null) edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 29e4b4b29e..1f8f8c82a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -30,6 +30,8 @@ package org.godotengine.godot; +import org.godotengine.godot.gl.GodotRenderer; + import android.app.Activity; import android.hardware.SensorEvent; import android.view.Surface; @@ -68,7 +70,7 @@ public class GodotLib { * @param p_surface * @param p_width * @param p_height - * @see android.opengl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onSurfaceChanged(GL10, int, int) */ public static native void resize(Surface p_surface, int p_width, int p_height); @@ -85,9 +87,14 @@ public class GodotLib { /** * Invoked on the GL thread to draw the current frame. - * @see android.opengl.GLSurfaceView.Renderer#onDrawFrame(GL10) + * @see org.godotengine.godot.gl.GLSurfaceView.Renderer#onDrawFrame(GL10) + */ + public static native boolean step(); + + /** + * TTS callback. */ - public static native void step(); + public static native void ttsCallback(int event, int id, int pos); /** * Forward touch events from the main thread to the GL thread. diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java new file mode 100644 index 0000000000..af16cfce74 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper.java @@ -0,0 +1,566 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.gl; + +import android.opengl.GLDebugHelper; +import android.opengl.GLException; + +import java.io.IOException; +import java.io.Writer; + +import javax.microedition.khronos.egl.EGL; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +class EGLLogWrapper implements EGL11 { + private EGL10 mEgl10; + Writer mLog; + boolean mLogArgumentNames; + boolean mCheckError; + private int mArgCount; + + + public EGLLogWrapper(EGL egl, int configFlags, Writer log) { + mEgl10 = (EGL10) egl; + mLog = log; + mLogArgumentNames = + (GLDebugHelper.CONFIG_LOG_ARGUMENT_NAMES & configFlags) != 0; + mCheckError = + (GLDebugHelper.CONFIG_CHECK_GL_ERROR & configFlags) != 0; + } + + public boolean eglChooseConfig(EGLDisplay display, int[] attrib_list, + EGLConfig[] configs, int config_size, int[] num_config) { + begin("eglChooseConfig"); + arg("display", display); + arg("attrib_list", attrib_list); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglChooseConfig(display, attrib_list, configs, + config_size, num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public boolean eglCopyBuffers(EGLDisplay display, EGLSurface surface, + Object native_pixmap) { + begin("eglCopyBuffers"); + arg("display", display); + arg("surface", surface); + arg("native_pixmap", native_pixmap); + end(); + + boolean result = mEgl10.eglCopyBuffers(display, surface, native_pixmap); + returns(result); + checkError(); + return result; + } + + public EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, + EGLContext share_context, int[] attrib_list) { + begin("eglCreateContext"); + arg("display", display); + arg("config", config); + arg("share_context", share_context); + arg("attrib_list", attrib_list); + end(); + + EGLContext result = mEgl10.eglCreateContext(display, config, + share_context, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePbufferSurface(EGLDisplay display, + EGLConfig config, int[] attrib_list) { + begin("eglCreatePbufferSurface"); + arg("display", display); + arg("config", config); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePbufferSurface(display, config, + attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePixmapSurface(EGLDisplay display, + EGLConfig config, Object native_pixmap, int[] attrib_list) { + begin("eglCreatePixmapSurface"); + arg("display", display); + arg("config", config); + arg("native_pixmap", native_pixmap); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreatePixmapSurface(display, config, + native_pixmap, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreateWindowSurface(EGLDisplay display, + EGLConfig config, Object native_window, int[] attrib_list) { + begin("eglCreateWindowSurface"); + arg("display", display); + arg("config", config); + arg("native_window", native_window); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = mEgl10.eglCreateWindowSurface(display, config, + native_window, attrib_list); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroyContext(EGLDisplay display, EGLContext context) { + begin("eglDestroyContext"); + arg("display", display); + arg("context", context); + end(); + + boolean result = mEgl10.eglDestroyContext(display, context); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroySurface(EGLDisplay display, EGLSurface surface) { + begin("eglDestroySurface"); + arg("display", display); + arg("surface", surface); + end(); + + boolean result = mEgl10.eglDestroySurface(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, + int attribute, int[] value) { + begin("eglGetConfigAttrib"); + arg("display", display); + arg("config", config); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglGetConfigAttrib(display, config, attribute, + value); + arg("value", value); + returns(result); + checkError(); + return false; + } + + public boolean eglGetConfigs(EGLDisplay display, EGLConfig[] configs, + int config_size, int[] num_config) { + begin("eglGetConfigs"); + arg("display", display); + arg("config_size", config_size); + end(); + + boolean result = mEgl10.eglGetConfigs(display, configs, config_size, + num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public EGLContext eglGetCurrentContext() { + begin("eglGetCurrentContext"); + end(); + + EGLContext result = mEgl10.eglGetCurrentContext(); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetCurrentDisplay() { + begin("eglGetCurrentDisplay"); + end(); + + EGLDisplay result = mEgl10.eglGetCurrentDisplay(); + returns(result); + + checkError(); + return result; + } + + public EGLSurface eglGetCurrentSurface(int readdraw) { + begin("eglGetCurrentSurface"); + arg("readdraw", readdraw); + end(); + + EGLSurface result = mEgl10.eglGetCurrentSurface(readdraw); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetDisplay(Object native_display) { + begin("eglGetDisplay"); + arg("native_display", native_display); + end(); + + EGLDisplay result = mEgl10.eglGetDisplay(native_display); + returns(result); + + checkError(); + return result; + } + + public int eglGetError() { + begin("eglGetError"); + end(); + + int result = mEgl10.eglGetError(); + returns(getErrorString(result)); + + return result; + } + + public boolean eglInitialize(EGLDisplay display, int[] major_minor) { + begin("eglInitialize"); + arg("display", display); + end(); + boolean result = mEgl10.eglInitialize(display, major_minor); + returns(result); + arg("major_minor", major_minor); + checkError(); + return result; + } + + public boolean eglMakeCurrent(EGLDisplay display, EGLSurface draw, + EGLSurface read, EGLContext context) { + begin("eglMakeCurrent"); + arg("display", display); + arg("draw", draw); + arg("read", read); + arg("context", context); + end(); + boolean result = mEgl10.eglMakeCurrent(display, draw, read, context); + returns(result); + checkError(); + return result; + } + + public boolean eglQueryContext(EGLDisplay display, EGLContext context, + int attribute, int[] value) { + begin("eglQueryContext"); + arg("display", display); + arg("context", context); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQueryContext(display, context, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public String eglQueryString(EGLDisplay display, int name) { + begin("eglQueryString"); + arg("display", display); + arg("name", name); + end(); + String result = mEgl10.eglQueryString(display, name); + returns(result); + checkError(); + return result; + } + + public boolean eglQuerySurface(EGLDisplay display, EGLSurface surface, + int attribute, int[] value) { + begin("eglQuerySurface"); + arg("display", display); + arg("surface", surface); + arg("attribute", attribute); + end(); + boolean result = mEgl10.eglQuerySurface(display, surface, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public boolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) { + begin("eglSwapBuffers"); + arg("display", display); + arg("surface", surface); + end(); + boolean result = mEgl10.eglSwapBuffers(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglTerminate(EGLDisplay display) { + begin("eglTerminate"); + arg("display", display); + end(); + boolean result = mEgl10.eglTerminate(display); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitGL() { + begin("eglWaitGL"); + end(); + boolean result = mEgl10.eglWaitGL(); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitNative(int engine, Object bindTarget) { + begin("eglWaitNative"); + arg("engine", engine); + arg("bindTarget", bindTarget); + end(); + boolean result = mEgl10.eglWaitNative(engine, bindTarget); + returns(result); + checkError(); + return result; + } + + private void checkError() { + int eglError; + if ((eglError = mEgl10.eglGetError()) != EGL_SUCCESS) { + String errorMessage = "eglError: " + getErrorString(eglError); + logLine(errorMessage); + if (mCheckError) { + throw new GLException(eglError, errorMessage); + } + } + } + + private void logLine(String message) { + log(message + '\n'); + } + + private void log(String message) { + try { + mLog.write(message); + } catch (IOException e) { + // Ignore exception, keep on trying + } + } + + private void begin(String name) { + log(name + '('); + mArgCount = 0; + } + + private void arg(String name, String value) { + if (mArgCount++ > 0) { + log(", "); + } + if (mLogArgumentNames) { + log(name + "="); + } + log(value); + } + + private void end() { + log(");\n"); + flush(); + } + + private void flush() { + try { + mLog.flush(); + } catch (IOException e) { + mLog = null; + } + } + + private void arg(String name, int value) { + arg(name, Integer.toString(value)); + } + + private void arg(String name, Object object) { + arg(name, toString(object)); + } + + private void arg(String name, EGLDisplay object) { + if (object == EGL10.EGL_DEFAULT_DISPLAY) { + arg(name, "EGL10.EGL_DEFAULT_DISPLAY"); + } else if (object == EGL_NO_DISPLAY) { + arg(name, "EGL10.EGL_NO_DISPLAY"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLContext object) { + if (object == EGL10.EGL_NO_CONTEXT) { + arg(name, "EGL10.EGL_NO_CONTEXT"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLSurface object) { + if (object == EGL10.EGL_NO_SURFACE) { + arg(name, "EGL10.EGL_NO_SURFACE"); + } else { + arg(name, toString(object)); + } + } + + private void returns(String result) { + log(" returns " + result + ";\n"); + flush(); + } + + private void returns(int result) { + returns(Integer.toString(result)); + } + + private void returns(boolean result) { + returns(Boolean.toString(result)); + } + + private void returns(Object result) { + returns(toString(result)); + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } else { + return obj.toString(); + } + } + + private void arg(String name, int[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private void arg(String name, Object[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private String toString(int n, int[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private String toString(int n, Object[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private static String getHex(int value) { + return "0x" + Integer.toHexString(value); + } + + public static String getErrorString(int error) { + switch (error) { + case EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return getHex(error); + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java new file mode 100644 index 0000000000..8449c08b88 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -0,0 +1,1939 @@ +// clang-format off + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.godotengine.godot.gl; + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + * <p> + * A GLSurfaceView provides the following features: + * <p> + * <ul> + * <li>Manages a surface, which is a special piece of memory that can be + * composited into the Android view system. + * <li>Manages an EGL display, which enables OpenGL to render into a surface. + * <li>Accepts a user-provided Renderer object that does the actual rendering. + * <li>Renders on a dedicated thread to decouple rendering performance from the + * UI thread. + * <li>Supports both on-demand and continuous rendering. + * <li>Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls. + * </ul> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Using GLSurfaceView</h3> + * <p> + * Typically you use GLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLSurfaceView can be used as-is. For the most part + * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLSurfaceView + * using the {@link #setRenderer(Renderer)} call. + * <p> + * <h3>Initializing GLSurfaceView</h3> + * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}. + * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * <ul> + * <li>{@link #setDebugFlags(int)} + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * <li>{@link #setGLWrapper(GLWrapper)} + * </ul> + * <p> + * <h4>Specifying the android.view.Surface</h4> + * By default GLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + * <p> + * <h4>Choosing an EGL Configuration</h4> + * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how many channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. + * <p> + * By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + * <p> + * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + * <p> + * <h4>Debug Behavior</h4> + * You can optionally modify the behavior of GLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + * <p> + * <h4>Setting a Renderer</h4> + * Finally, you must call {@link #setRenderer} to register a {@link Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + * <p> + * <h3>Rendering Mode</h3> + * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + * <p> + * <h3>Activity Life-cycle</h3> + * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients + * are required to call {@link #onPause()} when the activity stops and + * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate + * the OpenGL display. + * <p> + * <h3>Handling events</h3> + * <p> + * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + * <pre class="prettyprint"> + * class MyGLSurfaceView extends GLSurfaceView { + * + * private MyRenderer mMyRenderer; + * + * public void start() { + * mMyRenderer = ...; + * setRenderer(mMyRenderer); + * } + * + * public boolean onKeyDown(int keyCode, KeyEvent event) { + * if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + * queueEvent(new Runnable() { + * // This method will be called on the rendering + * // thread: + * public void run() { + * mMyRenderer.handleDpadCenter(); + * }}); + * return true; + * } + * return super.onKeyDown(keyCode, event); + * } + * } + * </pre> + * + */ +public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private final static String TAG = "GLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + * <p> + * Wrapping is typically used for debugging purposes. + * <p> + * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + * <p> + * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + * <p> + * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + * <p> + * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + * <p> + * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + * <p>This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + * <p>The following GLSurfaceView methods can only be called <em>before</em> + * setRenderer is called: + * <ul> + * <li>{@link #setEGLConfigChooser(boolean)} + * <li>{@link #setEGLConfigChooser(EGLConfigChooser)} + * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)} + * </ul> + * <p> + * The following GLSurfaceView methods can only be called <em>after</em> + * setRenderer is called: + * <ul> + * <li>{@link #getRenderMode()} + * <li>{@link #onPause()} + * <li>{@link #onResume()} + * <li>{@link #queueEvent(Runnable)} + * <li>{@link #requestRender()} + * <li>{@link #setRenderMode(int)} + * </ul> + * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + * <p>If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p> + * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + * <p>Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + * <pre class="prettyprint"> + * public MyView(Context context) { + * super(context); + * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context. + * setRenderer(new MyRenderer()); + * } + * </pre> + * <p>Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + * <p>If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + * <p>This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + * <p> + * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + * <p> + * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceCreated(SurfaceHolder holder) { + mGLThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + mGLThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (mGLThread != null) { + mGLThread.requestRenderAndNotify(finishDrawing); + } + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Deprecated + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Since we are part of the framework we know only surfaceRedrawNeededAsync + // will be called. + } + + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + * + * This method should be called when it is no longer desirable for the + * GLSurfaceView to continue rendering, such as in response to + * {@link android.app.Activity#onStop Activity.onStop}. + * + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + * + * This method should typically be called in + * {@link android.app.Activity#onStart Activity.onStart}. + * + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + * <p>Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + * <pre class="prettyprint"> + * class MyGLWrapper implements GLWrapper { + * GL wrap(GL gl) { + * return new MyGLImplementation(gl); + * } + * static class MyGLImplementation implements GL,GL10,GL11,... { + * ... + * } + * } + * </pre> + * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + * <p> + * The renderer is responsible for making OpenGL calls to render a frame. + * <p> + * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link GLSurfaceView#setRenderer} to + * register the renderer with the GLSurfaceView. + * <p> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about how to use OpenGL, read the + * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p> + * </div> + * + * <h3>Threading</h3> + * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. + * <p> + * <h3>EGL Context Lost</h3> + * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + * <p> + * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + * <p> + * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + * <p> + * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + * <p> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + * <p> + * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + * <p> + * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + * <pre class="prettyprint"> + * void onSurfaceChanged(GL10 gl, int width, int height) { + * gl.glViewport(0, 0, width, height); + * // for a fixed camera, set the projection too + * float ratio = (float) width / height; + * gl.glMatrixMode(GL10.GL_PROJECTION); + * gl.glLoadIdentity(); + * gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + // -- GODOT start -- + /** + * Called to draw the current frame. + * <p> + * This method is responsible for drawing the current frame. + * <p> + * The implementation of this method typically looks like this: + * <pre class="prettyprint"> + * boolean onDrawFrame(GL10 gl) { + * gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + * //... other gl calls to render the scene ... + * return true; + * } + * </pre> + * @param gl the GL interface. Use <code>instanceof</code> to + * test if the interface supports GL11 or higher interfaces. + * + * @return true if the buffers should be swapped, false otherwise. + */ + boolean onDrawFrame(GL10 gl); + // -- GODOT end -- + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + * <p> + * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + EGLLogWrapper.getErrorString(error); + } + + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference<GLSurfaceView> glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + // -- GODOT start -- + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } finally { + } + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + view.mRenderer.onSurfaceChanged(gl, w, h); + } finally { + } + } + sizeChanged = false; + } + + boolean swapBuffers = false; + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + swapBuffers = view.mRenderer.onDrawFrame(gl); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } finally {} + } + } + if (swapBuffers) { + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + // -- GODOT end -- + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference<GLSurfaceView> mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference<GLSurfaceView> mThisWeakRef = + new WeakReference<GLSurfaceView>(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} + diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java index e3956ac459..5c4fd00f6d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java @@ -28,38 +28,38 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +package org.godotengine.godot.gl; +import org.godotengine.godot.GodotLib; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; -import org.godotengine.godot.utils.GLUtils; - -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Godot's renderer implementation. + * Godot's GL renderer implementation. */ -class GodotRenderer implements GLSurfaceView.Renderer { +public class GodotRenderer implements GLSurfaceView.Renderer { private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; - GodotRenderer() { + public GodotRenderer() { this.pluginRegistry = GodotPluginRegistry.getPluginRegistry(); } - public void onDrawFrame(GL10 gl) { + public boolean onDrawFrame(GL10 gl) { if (activityJustResumed) { GodotLib.onRendererResumed(); activityJustResumed = false; } - GodotLib.step(); + boolean swapBuffers = GodotLib.step(); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { plugin.onGLDrawFrame(gl); } + + return swapBuffers; } public void onSurfaceChanged(GL10 gl, int width, int height) { @@ -76,13 +76,13 @@ class GodotRenderer implements GLSurfaceView.Renderer { } } - void onActivityResumed() { + public void onActivityResumed() { // We defer invoking GodotLib.onRendererResumed() until the first draw frame call. // This ensures we have a valid GL context and surface when we do so. activityJustResumed = true; } - void onActivityPaused() { + public void onActivityPaused() { GodotLib.onRendererPaused(); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index c06d89b843..8694bb91e1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -34,8 +34,9 @@ import static org.godotengine.godot.utils.GLUtils.DEBUG; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; -import org.godotengine.godot.input.InputManagerCompat.InputDeviceListener; +import android.content.Context; +import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; import android.util.SparseArray; @@ -53,9 +54,9 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener { private final GodotRenderView mRenderView; - private final InputManagerCompat mInputManager; + private final InputManager mInputManager; private final String tag = this.getClass().getSimpleName(); @@ -64,7 +65,7 @@ public class GodotInputHandler implements InputDeviceListener { public GodotInputHandler(GodotRenderView godotView) { mRenderView = godotView; - mInputManager = InputManagerCompat.Factory.getInputManager(mRenderView.getView().getContext()); + mInputManager = (InputManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java deleted file mode 100644 index 21fdc658bb..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.content.Context; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -public interface InputManagerCompat { - /** - * Gets information about the input device with the specified id. - * - * @param id The device id - * @return The input device or null if not found - */ - InputDevice getInputDevice(int id); - - /** - * Gets the ids of all input devices in the system. - * - * @return The input device ids. - */ - int[] getInputDeviceIds(); - - /** - * Registers an input device listener to receive notifications about when - * input devices are added, removed or changed. - * - * @param listener The listener to register. - * @param handler The handler on which the listener should be invoked, or - * null if the listener should be invoked on the calling thread's - * looper. - */ - void registerInputDeviceListener(InputManagerCompat.InputDeviceListener listener, - Handler handler); - - /** - * Unregisters an input device listener. - * - * @param listener The listener to unregister. - */ - void unregisterInputDeviceListener(InputManagerCompat.InputDeviceListener listener); - - /* - * The following three calls are to simulate V16 behavior on pre-Jellybean - * devices. If you don't call them, your callback will never be called - * pre-API 16. - */ - - /** - * Pass the motion events to the InputManagerCompat. This is used to - * optimize for polling for controllers. If you do not pass these events in, - * polling will cause regular object creation. - * - * @param event the motion event from the app - */ - void onGenericMotionEvent(MotionEvent event); - - /** - * Tell the V9 input manager that it should stop polling for disconnected - * devices. You can call this during onPause in your activity, although you - * might want to call it whenever your game is not active (or whenever you - * don't care about being notified of new input devices) - */ - void onPause(); - - /** - * Tell the V9 input manager that it should start polling for disconnected - * devices. You can call this during onResume in your activity, although you - * might want to call it less often (only when the gameplay is actually - * active) - */ - void onResume(); - - interface InputDeviceListener { - /** - * Called whenever the input manager detects that a device has been - * added. This will only be called in the V9 version when a motion event - * is detected. - * - * @param deviceId The id of the input device that was added. - */ - void onInputDeviceAdded(int deviceId); - - /** - * Called whenever the properties of an input device have changed since - * they were last queried. This will not be called for the V9 version of - * the API. - * - * @param deviceId The id of the input device that changed. - */ - void onInputDeviceChanged(int deviceId); - - /** - * Called whenever the input manager detects that a device has been - * removed. For the V9 version, this can take some time depending on the - * poll rate. - * - * @param deviceId The id of the input device that was removed. - */ - void onInputDeviceRemoved(int deviceId); - } - - /** - * Use this to construct a compatible InputManager. - */ - class Factory { - /** - * Constructs and returns a compatible InputManger - * - * @param context the Context that will be used to get the system - * service from - * @return a compatible implementation of InputManager - */ - public static InputManagerCompat getInputManager(Context context) { - return new InputManagerV16(context); - } - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java deleted file mode 100644 index 0dbc13c77b..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/input/InputManagerV16.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.godotengine.godot.input; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.input.InputManager; -import android.os.Build; -import android.os.Handler; -import android.view.InputDevice; -import android.view.MotionEvent; - -import java.util.HashMap; -import java.util.Map; - -@TargetApi(Build.VERSION_CODES.JELLY_BEAN) -public class InputManagerV16 implements InputManagerCompat { - private final InputManager mInputManager; - private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> mListeners; - - public InputManagerV16(Context context) { - mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); - mListeners = new HashMap<>(); - } - - @Override - public InputDevice getInputDevice(int id) { - return mInputManager.getInputDevice(id); - } - - @Override - public int[] getInputDeviceIds() { - return mInputManager.getInputDeviceIds(); - } - - static class V16InputDeviceListener implements InputManager.InputDeviceListener { - final InputManagerCompat.InputDeviceListener mIDL; - - public V16InputDeviceListener(InputDeviceListener idl) { - mIDL = idl; - } - - @Override - public void onInputDeviceAdded(int deviceId) { - mIDL.onInputDeviceAdded(deviceId); - } - - @Override - public void onInputDeviceChanged(int deviceId) { - mIDL.onInputDeviceChanged(deviceId); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - mIDL.onInputDeviceRemoved(deviceId); - } - } - - @Override - public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { - V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); - mInputManager.registerInputDeviceListener(v16Listener, handler); - mListeners.put(listener, v16Listener); - } - - @Override - public void unregisterInputDeviceListener(InputDeviceListener listener) { - V16InputDeviceListener curListener = mListeners.remove(listener); - if (null != curListener) { - mInputManager.unregisterInputDeviceListener(curListener); - } - } - - @Override - public void onGenericMotionEvent(MotionEvent event) { - // unused in V16 - } - - @Override - public void onPause() { - // unused in V16 - } - - @Override - public void onResume() { - // unused in V16 - } -} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java new file mode 100644 index 0000000000..2239ddac8e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -0,0 +1,298 @@ +/*************************************************************************/ +/* GodotTTS.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +import org.godotengine.godot.GodotLib; + +import android.app.Activity; +import android.os.Bundle; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.Voice; + +import androidx.annotation.Keep; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Set; + +/** + * Wrapper for Android Text to Speech API and custom utterance query implementation. + * <p> + * A [GodotTTS] provides the following features: + * <p> + * <ul> + * <li>Access to the Android Text to Speech API. + * <li>Utterance pause / resume functions, unsupported by Android TTS API. + * </ul> + */ +@Keep +public class GodotTTS extends UtteranceProgressListener { + // Note: These constants must be in sync with DisplayServer::TTSUtteranceEvent enum from "servers/display_server.h". + final private static int EVENT_START = 0; + final private static int EVENT_END = 1; + final private static int EVENT_CANCEL = 2; + final private static int EVENT_BOUNDARY = 3; + + final private TextToSpeech synth; + final private LinkedList<GodotUtterance> queue; + final private Object lock = new Object(); + private GodotUtterance lastUtterance; + + private boolean speaking; + private boolean paused; + + public GodotTTS(Activity p_activity) { + synth = new TextToSpeech(p_activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + private void updateTTS() { + if (!speaking && queue.size() > 0) { + int mode = TextToSpeech.QUEUE_FLUSH; + GodotUtterance message = queue.pollFirst(); + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(message.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(message.pitch); + synth.setSpeechRate(message.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, message.volume / 100.f); + + lastUtterance = message; + lastUtterance.start = 0; + lastUtterance.offset = 0; + paused = false; + + synth.speak(message.text, mode, params, String.valueOf(message.id)); + speaking = true; + } + } + + /** + * Called by TTS engine when the TTS service is about to speak the specified range. + */ + @Override + public void onRangeStart(String utteranceId, int start, int end, int frame) { + synchronized (lock) { + if (lastUtterance != null && Integer.parseInt(utteranceId) == lastUtterance.id) { + lastUtterance.offset = start; + GodotLib.ttsCallback(EVENT_BOUNDARY, lastUtterance.id, start + lastUtterance.start); + } + } + } + + /** + * Called by TTS engine when an utterance was canceled in progress. + */ + @Override + public void onStop(String utteranceId, boolean interrupted) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an utterance has begun to be spoken.. + */ + @Override + public void onStart(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && lastUtterance.start == 0 && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_START, lastUtterance.id, 0); + } + } + } + + /** + * Called by TTS engine when an utterance was successfully finished. + */ + @Override + public void onDone(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_END, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing. + */ + @Override + public void onError(String utteranceId, int errorCode) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Called by TTS engine when an error has occurred during processing (pre API level 21 version). + */ + @Override + public void onError(String utteranceId) { + synchronized (lock) { + if (lastUtterance != null && !paused && Integer.parseInt(utteranceId) == lastUtterance.id) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + speaking = false; + updateTTS(); + } + } + } + + /** + * Adds an utterance to the queue. + */ + public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { + synchronized (lock) { + GodotUtterance message = new GodotUtterance(text, voice, volume, pitch, rate, utterance_id); + queue.addLast(message); + + if (isPaused()) { + resumeSpeaking(); + } else { + updateTTS(); + } + } + } + + /** + * Puts the synthesizer into a paused state. + */ + public void pauseSpeaking() { + synchronized (lock) { + if (!paused) { + paused = true; + synth.stop(); + } + } + } + + /** + * Resumes the synthesizer if it was paused. + */ + public void resumeSpeaking() { + synchronized (lock) { + if (lastUtterance != null && paused) { + int mode = TextToSpeech.QUEUE_FLUSH; + + Set<Voice> voices = synth.getVoices(); + for (Voice v : voices) { + if (v.getName().equals(lastUtterance.voice)) { + synth.setVoice(v); + break; + } + } + synth.setPitch(lastUtterance.pitch); + synth.setSpeechRate(lastUtterance.rate); + + Bundle params = new Bundle(); + params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, lastUtterance.volume / 100.f); + + lastUtterance.start = lastUtterance.offset; + lastUtterance.offset = 0; + paused = false; + + synth.speak(lastUtterance.text.substring(lastUtterance.start), mode, params, String.valueOf(lastUtterance.id)); + speaking = true; + } else { + paused = false; + } + } + } + + /** + * Stops synthesis in progress and removes all utterances from the queue. + */ + public void stopSpeaking() { + synchronized (lock) { + for (GodotUtterance u : queue) { + GodotLib.ttsCallback(EVENT_CANCEL, u.id, 0); + } + queue.clear(); + + if (lastUtterance != null) { + GodotLib.ttsCallback(EVENT_CANCEL, lastUtterance.id, 0); + } + lastUtterance = null; + + paused = false; + speaking = false; + + synth.stop(); + } + } + + /** + * Returns voice information. + */ + public String[] getVoices() { + Set<Voice> voices = synth.getVoices(); + String[] list = new String[voices.size()]; + int i = 0; + for (Voice v : voices) { + list[i++] = v.getLocale().toString() + ";" + v.getName(); + } + return list; + } + + /** + * Returns true if the synthesizer is generating speech, or have utterance waiting in the queue. + */ + public boolean isSpeaking() { + return speaking; + } + + /** + * Returns true if the synthesizer is in a paused state. + */ + public boolean isPaused() { + return paused; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java new file mode 100644 index 0000000000..bde37e7315 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotUtterance.java @@ -0,0 +1,55 @@ +/*************************************************************************/ +/* GodotUtterance.java */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +package org.godotengine.godot.tts; + +/** + * A speech request for GodotTTS. + */ +class GodotUtterance { + final String text; + final String voice; + final int volume; + final float pitch; + final float rate; + final int id; + + int offset = -1; + int start = 0; + + GodotUtterance(String text, String voice, int volume, float pitch, float rate, int id) { + this.text = text; + this.voice = voice; + this.volume = volume; + this.pitch = pitch; + this.rate = rate; + this.id = id; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java index 39a57f587a..47df23fe1a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/Crypt.java @@ -43,8 +43,8 @@ public class Crypt { // Create Hex String StringBuilder hexString = new StringBuilder(); - for (int i = 0; i < messageDigest.length; i++) - hexString.append(Integer.toHexString(0xFF & messageDigest[i])); + for (byte b : messageDigest) + hexString.append(Integer.toHexString(0xFF & b)); return hexString.toString(); } catch (Exception e) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java new file mode 100644 index 0000000000..2cc37b627a --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -0,0 +1,141 @@ +// clang-format off + +/* Third-party library. + * Upstream: https://github.com/JakeWharton/ProcessPhoenix + * Commit: 12cb27c2cc9c3fc555e97f2db89e571667de82c4 + */ + +/* + * Copyright (C) 2014 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.godotengine.godot.utils; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Process; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + +/** + * Process Phoenix facilitates restarting your application process. This should only be used for + * things like fundamental state changes in your debug builds (e.g., changing from staging to + * production). + * <p> + * Trigger process recreation by calling {@link #triggerRebirth} with a {@link Context} instance. + */ +public final class ProcessPhoenix extends Activity { + private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; + + /** + * Call to restart the application process using the {@linkplain Intent#CATEGORY_DEFAULT default} + * activity as an intent. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context) { + triggerRebirth(context, getRestartIntent(context)); + } + + /** + * Call to restart the application process using the specified intents. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Intent... nextIntents) { + if (nextIntents.length < 1) { + throw new IllegalArgumentException("intents cannot be empty"); + } + // create a new task for the first activity. + nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + + Intent intent = new Intent(context, ProcessPhoenix.class); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context. + intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents))); + intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()); + context.startActivity(intent); + } + + // -- GODOT start -- + /** + * Finish the activity and kill its process + */ + public static void forceQuit(Activity activity) { + forceQuit(activity, Process.myPid()); + } + + /** + * Finish the activity and kill its process + * @param activity + * @param pid + */ + public static void forceQuit(Activity activity, int pid) { + Process.killProcess(pid); // Kill original main process + activity.finish(); + Runtime.getRuntime().exit(0); // Kill kill kill! + } + + // -- GODOT end -- + + private static Intent getRestartIntent(Context context) { + String packageName = context.getPackageName(); + Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + if (defaultIntent != null) { + return defaultIntent; + } + + throw new IllegalStateException("Unable to determine default activity for " + + packageName + + ". Does an activity specify the DEFAULT category in its intent filter?"); + } + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // -- GODOT start -- + ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS); + startActivities(intents.toArray(new Intent[intents.size()])); + forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); + // -- GODOT end -- + } + + /** + * Checks if the current process is a temporary Phoenix Process. + * This can be used to avoid initialisation of unused resources or to prevent running code that + * is not multi-process ready. + * + * @return true if the current process is a temporary Phoenix Process + */ + public static boolean isPhoenixProcess(Context context) { + int currentPid = Process.myPid(); + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RunningAppProcessInfo> runningProcesses = manager.getRunningAppProcesses(); + if (runningProcesses != null) { + for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) { + if (processInfo.pid == currentPid && processInfo.processName.endsWith(":phoenix")) { + return true; + } + } + } + return false; + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java index 4c1c84affb..e35d4f5828 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrConfigChooser.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGLExt; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java index 2b4369b8a6..deb9c4bb1d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrContextFactory.java @@ -30,8 +30,9 @@ package org.godotengine.godot.xr.ovr; +import org.godotengine.godot.gl.GLSurfaceView; + import android.opengl.EGL14; -import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java index fbfe0a3a75..f087b7dc74 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/ovr/OvrWindowSurfaceFactory.java @@ -30,7 +30,7 @@ package org.godotengine.godot.xr.ovr; -import android.opengl.GLSurfaceView; +import org.godotengine.godot.gl.GLSurfaceView; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java index 9fde1961ea..445238b1c2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularConfigChooser.java @@ -30,10 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; - import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index ce1184a75c..5d62723170 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -30,9 +30,9 @@ package org.godotengine.godot.xr.regular; +import org.godotengine.godot.gl.GLSurfaceView; import org.godotengine.godot.utils.GLUtils; -import android.opengl.GLSurfaceView; import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java index 420dda45a0..68329c5c49 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularFallbackConfigChooser.java @@ -30,8 +30,6 @@ package org.godotengine.godot.xr.regular; -import org.godotengine.godot.utils.GLUtils; - import android.util.Log; import javax.microedition.khronos.egl.EGL10; diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index 34925684da..711f7cd502 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -15,5 +15,6 @@ file(GLOB_RECURSE HEADERS ${GODOT_ROOT_DIR}/*.h**) add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC - ${GODOT_ROOT_DIR} - ${GODOT_ROOT_DIR}/modules/gdnative/include) + ${GODOT_ROOT_DIR}) + +add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED) diff --git a/platform/android/java/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle index 158bb2b98e..5e810ae1ba 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -1,11 +1,13 @@ // Non functional android library used to provide Android Studio editor support to the project. plugins { id 'com.android.library' + id 'org.jetbrains.kotlin.android' } android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion defaultConfig { minSdkVersion versions.minSdk @@ -17,6 +19,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + packagingOptions { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' @@ -28,8 +34,6 @@ android { } } - ndkVersion versions.ndkVersion - externalNativeBuild { cmake { path "CMakeLists.txt" diff --git a/platform/android/java/scripts/publish-module.gradle b/platform/android/java/scripts/publish-module.gradle new file mode 100644 index 0000000000..32b749e493 --- /dev/null +++ b/platform/android/java/scripts/publish-module.gradle @@ -0,0 +1,69 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +group = ossrhGroupId +version = PUBLISH_VERSION + +afterEvaluate { + publishing { + publications { + templateRelease(MavenPublication) { + from components.templateRelease + + // The coordinates of the library, being set from variables that + // we'll set up later + groupId ossrhGroupId + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION + + // Mostly self-explanatory metadata + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Godot Engine Android Library' + url = 'https://godotengine.org/' + licenses { + license { + name = 'MIT License' + url = 'https://github.com/godotengine/godot/blob/master/LICENSE.txt' + } + } + developers { + developer { + id = 'm4gr3d' + name = 'Fredia Huya-Kouadio' + email = 'fhuyakou@gmail.com' + } + developer { + id = 'reduz' + name = 'Juan Linietsky' + email = 'reduzio@gmail.com' + } + developer { + id = 'akien-mga' + name = 'Rémi Verschelde' + email = 'rverschelde@gmail.com' + } + // Add all other devs here... + } + + // Version control info - if you're using GitHub, follow the + // format as seen here + scm { + connection = 'scm:git:github.com/godotengine/godot.git' + developerConnection = 'scm:git:ssh://github.com/godotengine/godot.git' + url = 'https://github.com/godotengine/godot/tree/master' + } + } + } + } + } +} + +signing { + useInMemoryPgpKeys( + rootProject.ext["signing.keyId"], + rootProject.ext["signing.key"], + rootProject.ext["signing.password"], + ) + sign publishing.publications +} diff --git a/platform/android/java/scripts/publish-root.gradle b/platform/android/java/scripts/publish-root.gradle new file mode 100644 index 0000000000..ae88487c34 --- /dev/null +++ b/platform/android/java/scripts/publish-root.gradle @@ -0,0 +1,39 @@ +// Create variables with empty default values +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.key"] = '' +ext["ossrhGroupId"] = '' +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' + +File secretPropsFile = project.rootProject.file('local.properties') +if (secretPropsFile.exists()) { + // Read local.properties file first if it exists + Properties p = new Properties() + new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else { + // Use system environment variables + ext["ossrhGroupId"] = System.getenv('OSSRH_GROUP_ID') + ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') + ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') + ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') + ext["signing.password"] = System.getenv('SIGNING_PASSWORD') + ext["signing.key"] = System.getenv('SIGNING_KEY') +} + +// Set up Sonatype repository +nexusPublishing { + repositories { + sonatype { + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 584b626900..466ffebf22 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -1,9 +1,25 @@ // Configure the root project. +pluginManagement { + apply from: 'app/config.gradle' + + plugins { + id 'com.android.application' version versions.androidGradlePlugin + id 'com.android.library' version versions.androidGradlePlugin + id 'org.jetbrains.kotlin.android' version versions.kotlinVersion + id 'io.github.gradle-nexus.publish-plugin' version versions.nexusPublishVersion + } + repositories { + gradlePluginPortal() + google() + } +} + rootProject.name = "Godot" include ':app' include ':lib' include ':nativeSrcsConfigs' +include ':editor' include ':assetPacks:installTime' project(':assetPacks:installTime').projectDir = file("app/assetPacks/installTime") diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 7c788b4dc4..349ae704f9 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -34,16 +34,16 @@ #include "thread_jandroid.h" bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret) { - Map<StringName, List<MethodInfo>>::Element *M = methods.find(p_method); + HashMap<StringName, List<MethodInfo>>::Iterator M = methods.find(p_method); if (!M) { return false; } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_NULL_V(env, false); MethodInfo *method = nullptr; - for (MethodInfo &E : M->get()) { + for (MethodInfo &E : M->value) { if (!p_instance && !E._static) { r_error.error = Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL; continue; @@ -485,14 +485,14 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, return success; } -Variant JavaClass::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { +Variant JavaClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { Variant ret; bool found = _call_method(nullptr, p_method, p_args, p_argcount, r_error, ret); if (found) { return ret; } - return RefCounted::call(p_method, p_args, p_argcount, r_error); + return RefCounted::callp(p_method, p_args, p_argcount, r_error); } JavaClass::JavaClass() { @@ -500,7 +500,7 @@ JavaClass::JavaClass() { ///////////////////// -Variant JavaObject::call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { +Variant JavaObject::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { return Variant(); } @@ -971,14 +971,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, Ref<JavaClass>()); + ERR_FAIL_NULL_V(env, Ref<JavaClass>()); jclass bclass = env->FindClass(p_class.utf8().get_data()); - ERR_FAIL_COND_V(!bclass, Ref<JavaClass>()); + ERR_FAIL_NULL_V(bclass, Ref<JavaClass>()); jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, getDeclaredMethods); - ERR_FAIL_COND_V(!methods, Ref<JavaClass>()); + ERR_FAIL_NULL_V(methods, Ref<JavaClass>()); Ref<JavaClass> java_class = memnew(JavaClass); @@ -1061,7 +1061,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { if (E->get().param_types.size() != mi.param_types.size()) { continue; } - bool valid = true; + bool this_valid = true; for (int j = 0; j < E->get().param_types.size(); j++) { Variant::Type _new; float new_l; @@ -1070,14 +1070,14 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { JavaClass::_convert_to_variant_type(E->get().param_types[j], existing, existing_l); JavaClass::_convert_to_variant_type(mi.param_types[j], _new, new_l); if (_new != existing) { - valid = false; + this_valid = false; break; } new_likeliness += new_l; existing_likeliness = existing_l; } - if (!valid) { + if (!this_valid) { continue; } @@ -1155,10 +1155,10 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { singleton = this; JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); - jclass activityClass = env->FindClass("android/app/Activity"); - jmethodID getClassLoader = env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); + jclass activity = env->FindClass("android/app/Activity"); + jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;"); classLoader = env->CallObjectMethod(p_activity, getClassLoader); classLoader = (jclass)env->NewGlobalRef(classLoader); jclass classLoaderClass = env->FindClass("java/lang/ClassLoader"); @@ -1168,18 +1168,18 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); - // + bclass = env->FindClass("java/lang/reflect/Method"); getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;"); getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); - /// + bclass = env->FindClass("java/lang/reflect/Field"); Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - // each + bclass = env->FindClass("java/lang/Boolean"); Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z"); diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index d6e3ad90b1..b71c6ef1e6 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -31,6 +31,8 @@ #include "java_godot_io_wrapper.h" #include "core/error/error_list.h" +#include "core/math/rect2.h" +#include "core/variant/variant.h" // JNIEnv is only valid within the thread it belongs to, in a multi threading environment // we can't cache it. @@ -51,11 +53,13 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _open_URI = p_env->GetMethodID(cls, "openURI", "(Ljava/lang/String;)I"); _get_cache_dir = p_env->GetMethodID(cls, "getCacheDir", "()Ljava/lang/String;"); _get_data_dir = p_env->GetMethodID(cls, "getDataDir", "()Ljava/lang/String;"); + _get_display_cutouts = p_env->GetMethodID(cls, "getDisplayCutouts", "()[I"), + _get_display_safe_area = p_env->GetMethodID(cls, "getDisplaySafeArea", "()[I"), _get_locale = p_env->GetMethodID(cls, "getLocale", "()Ljava/lang/String;"); _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); + _get_scaled_density = p_env->GetMethodID(cls, "getScaledDensity", "()F"); _get_screen_refresh_rate = p_env->GetMethodID(cls, "getScreenRefreshRate", "(D)D"); - _screen_get_usable_rect = p_env->GetMethodID(cls, "screenGetUsableRect", "()[I"), _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); _hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V"); @@ -76,7 +80,7 @@ jobject GodotIOJavaWrapper::get_instance() { Error GodotIOJavaWrapper::open_uri(const String &p_uri) { if (_open_URI) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, ERR_UNAVAILABLE); + ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE); jstring jStr = env->NewStringUTF(p_uri.utf8().get_data()); return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK; } else { @@ -87,7 +91,7 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) { String GodotIOJavaWrapper::get_cache_dir() { if (_get_cache_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_cache_dir); return jstring_to_string(s, env); } else { @@ -98,7 +102,7 @@ String GodotIOJavaWrapper::get_cache_dir() { String GodotIOJavaWrapper::get_user_data_dir() { if (_get_data_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_data_dir); return jstring_to_string(s, env); } else { @@ -109,7 +113,7 @@ String GodotIOJavaWrapper::get_user_data_dir() { String GodotIOJavaWrapper::get_locale() { if (_get_locale) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_locale); return jstring_to_string(s, env); } else { @@ -120,7 +124,7 @@ String GodotIOJavaWrapper::get_locale() { String GodotIOJavaWrapper::get_model() { if (_get_model) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_model); return jstring_to_string(s, env); } else { @@ -131,13 +135,23 @@ String GodotIOJavaWrapper::get_model() { int GodotIOJavaWrapper::get_screen_dpi() { if (_get_screen_DPI) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 160); + ERR_FAIL_NULL_V(env, 160); return env->CallIntMethod(godot_io_instance, _get_screen_DPI); } else { return 160; } } +float GodotIOJavaWrapper::get_scaled_density() { + if (_get_scaled_density) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, 1.0f); + return env->CallFloatMethod(godot_io_instance, _get_scaled_density); + } else { + return 1.0f; + } +} + float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { if (_get_screen_refresh_rate) { JNIEnv *env = get_jni_env(); @@ -151,24 +165,44 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { return fallback; } -void GodotIOJavaWrapper::screen_get_usable_rect(int (&p_rect_xywh)[4]) { - if (_screen_get_usable_rect) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _screen_get_usable_rect); - ERR_FAIL_COND(env->GetArrayLength(returnArray) != 4); - jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); - for (int i = 0; i < 4; i++) { - p_rect_xywh[i] = arrayBody[i]; - } - env->ReleaseIntArrayElements(returnArray, arrayBody, 0); +Array GodotIOJavaWrapper::get_display_cutouts() { + Array result; + ERR_FAIL_NULL_V(_get_display_cutouts, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_cutouts); + jint arrayLength = env->GetArrayLength(returnArray); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + int cutouts = arrayLength / 4; + for (int i = 0; i < cutouts; i++) { + int x = arrayBody[i * 4]; + int y = arrayBody[i * 4 + 1]; + int width = arrayBody[i * 4 + 2]; + int height = arrayBody[i * 4 + 3]; + Rect2 cutout(x, y, width, height); + result.append(cutout); } + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; +} + +Rect2i GodotIOJavaWrapper::get_display_safe_area() { + Rect2i result; + ERR_FAIL_NULL_V(_get_display_safe_area, result); + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, result); + jintArray returnArray = (jintArray)env->CallObjectMethod(godot_io_instance, _get_display_safe_area); + ERR_FAIL_COND_V(env->GetArrayLength(returnArray) != 4, result); + jint *arrayBody = env->GetIntArrayElements(returnArray, JNI_FALSE); + result = Rect2i(arrayBody[0], arrayBody[1], arrayBody[2], arrayBody[3]); + env->ReleaseIntArrayElements(returnArray, arrayBody, 0); + return result; } String GodotIOJavaWrapper::get_unique_id() { if (_get_unique_id) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_unique_id); return jstring_to_string(s, env); } else { @@ -177,13 +211,13 @@ String GodotIOJavaWrapper::get_unique_id() { } bool GodotIOJavaWrapper::has_vk() { - return (_show_keyboard != 0) && (_hide_keyboard != 0); + return (_show_keyboard != nullptr) && (_hide_keyboard != nullptr); } void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (_show_keyboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_existing.utf8().get_data()); env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); } @@ -192,7 +226,7 @@ void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int void GodotIOJavaWrapper::hide_vk() { if (_hide_keyboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_io_instance, _hide_keyboard); } } @@ -200,7 +234,7 @@ void GodotIOJavaWrapper::hide_vk() { void GodotIOJavaWrapper::set_screen_orientation(int p_orient) { if (_set_screen_orientation) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_io_instance, _set_screen_orientation, p_orient); } } @@ -208,7 +242,7 @@ void GodotIOJavaWrapper::set_screen_orientation(int p_orient) { int GodotIOJavaWrapper::get_screen_orientation() { if (_get_screen_orientation) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_NULL_V(env, 0); return env->CallIntMethod(godot_io_instance, _get_screen_orientation); } else { return 0; @@ -218,7 +252,7 @@ int GodotIOJavaWrapper::get_screen_orientation() { String GodotIOJavaWrapper::get_system_dir(int p_dir, bool p_shared_storage) { if (_get_system_dir) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String(".")); + ERR_FAIL_NULL_V(env, String(".")); jstring s = (jstring)env->CallObjectMethod(godot_io_instance, _get_system_dir, p_dir, p_shared_storage); return jstring_to_string(s, env); } else { diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 38a2b710a9..aec7d1db97 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -37,6 +37,8 @@ #include <android/log.h> #include <jni.h> +#include "core/math/rect2i.h" +#include "core/variant/array.h" #include "string_android.h" // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ @@ -48,11 +50,13 @@ private: jmethodID _open_URI = 0; jmethodID _get_cache_dir = 0; jmethodID _get_data_dir = 0; + jmethodID _get_display_cutouts = 0; + jmethodID _get_display_safe_area = 0; jmethodID _get_locale = 0; jmethodID _get_model = 0; jmethodID _get_screen_DPI = 0; + jmethodID _get_scaled_density = 0; jmethodID _get_screen_refresh_rate = 0; - jmethodID _screen_get_usable_rect = 0; jmethodID _get_unique_id = 0; jmethodID _show_keyboard = 0; jmethodID _hide_keyboard = 0; @@ -72,8 +76,10 @@ public: String get_locale(); String get_model(); int get_screen_dpi(); + float get_scaled_density(); float get_screen_refresh_rate(float fallback); - void screen_get_usable_rect(int (&p_rect_xywh)[4]); + Array get_display_cutouts(); + Rect2i get_display_safe_area(); String get_unique_id(); bool has_vk(); void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end); @@ -85,4 +91,4 @@ public: String get_system_dir(int p_dir, bool p_shared_storage); }; -#endif /* !JAVA_GODOT_IO_WRAPPER_H */ +#endif // JAVA_GODOT_IO_WRAPPER_H diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index dd4fa9de7b..de666f1b11 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -49,6 +49,7 @@ #include "os_android.h" #include "string_android.h" #include "thread_jandroid.h" +#include "tts_android.h" #include <android/input.h> #include <unistd.h> @@ -61,7 +62,6 @@ static AndroidInputHandler *input_handler = nullptr; static GodotJavaWrapper *godot_java = nullptr; static GodotIOJavaWrapper *godot_io_java = nullptr; -static bool initialized = false; static SafeNumeric<int> step; // Shared between UI and render threads static Size2 new_size; @@ -79,8 +79,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion) { - initialized = true; - JavaVM *jvm; env->GetJavaVM(&jvm); @@ -96,17 +94,18 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en DirAccessJAndroid::setup(godot_io_java->get_instance()); NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env)); + TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/godotengine/godot/tts/GodotTTS;", env)); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); - char wd[500]; - getcwd(wd, 500); - godot_java->on_video_init(env); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz) { // lets cleanup + if (java_class_wrapper) { + memdelete(java_class_wrapper); + } if (godot_io_java) { delete godot_io_java; } @@ -117,6 +116,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env delete input_handler; } if (os_android) { + os_android->main_loop_end(); delete os_android; } } @@ -138,7 +138,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc for (int i = 0; i < cmdlen; i++) { jstring string = (jstring)env->GetObjectArrayElement(p_cmdline, i); - const char *rawString = env->GetStringUTFChars(string, 0); + const char *rawString = env->GetStringUTFChars(string, nullptr); cmdline[i] = rawString; j_cmdline[i] = string; @@ -146,7 +146,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc } } - Error err = Main::setup("apk", cmdlen, (char **)cmdline, false); + Error err = Main::setup(OS_Android::ANDROID_EXEC_PATH, cmdlen, (char **)cmdline, false); if (cmdline) { if (j_cmdline) { for (int i = 0; i < cmdlen; ++i) { @@ -209,9 +209,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jcl } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos) { + TTS_Android::_java_utterance_callback(event, id, pos); +} + +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz) { if (step.get() == -1) { - return; + return true; } if (step.get() == 0) { @@ -220,12 +224,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl Main::setup2(Thread::get_caller_id()); input_handler = new AndroidInputHandler(); step.increment(); - return; + return true; } if (step.get() == 1) { if (!Main::start()) { - return; // should exit instead and print the error + return true; // should exit instead and print the error } godot_java->on_godot_setup_completed(env); @@ -239,9 +243,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl DisplayServerAndroid::get_singleton()->process_magnetometer(magnetometer); DisplayServerAndroid::get_singleton()->process_gyroscope(gyroscope); - if (os_android->main_loop_iterate()) { + bool should_swap_buffers = false; + if (os_android->main_loop_iterate(&should_swap_buffers)) { godot_java->force_quit(env); } + + return should_swap_buffers; } void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask, jfloat vertical_factor, jfloat horizontal_factor) { @@ -423,7 +430,7 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv * JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); - ERR_FAIL_COND(!obj); + ERR_FAIL_NULL(obj); int res = env->PushLocalFrame(16); ERR_FAIL_COND(res != 0); @@ -434,27 +441,26 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en Variant *vlist = (Variant *)alloca(sizeof(Variant) * count); Variant **vptr = (Variant **)alloca(sizeof(Variant *) * count); for (int i = 0; i < count; i++) { - jobject obj = env->GetObjectArrayElement(params, i); + jobject jobj = env->GetObjectArrayElement(params, i); Variant v; - if (obj) { - v = _jobject_to_variant(env, obj); + if (jobj) { + v = _jobject_to_variant(env, jobj); } memnew_placement(&vlist[i], Variant); vlist[i] = v; vptr[i] = &vlist[i]; - env->DeleteLocalRef(obj); + env->DeleteLocalRef(jobj); } Callable::CallError err; - obj->call(str_method, (const Variant **)vptr, count, err); - // something + obj->callp(str_method, (const Variant **)vptr, count, err); env->PopLocalFrame(nullptr); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) { Object *obj = ObjectDB::get_instance(ObjectID(ID)); - ERR_FAIL_COND(!obj); + ERR_FAIL_NULL(obj); int res = env->PushLocalFrame(16); ERR_FAIL_COND(res != 0); @@ -462,19 +468,21 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); - Variant args[VARIANT_ARG_MAX]; - for (int i = 0; i < MIN(count, VARIANT_ARG_MAX); i++) { - jobject obj = env->GetObjectArrayElement(params, i); - if (obj) { - args[i] = _jobject_to_variant(env, obj); + Variant *args = (Variant *)alloca(sizeof(Variant) * count); + const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count); + + for (int i = 0; i < count; i++) { + jobject jobj = env->GetObjectArrayElement(params, i); + if (jobj) { + args[i] = _jobject_to_variant(env, jobj); } - env->DeleteLocalRef(obj); + env->DeleteLocalRef(jobj); + argptrs[i] = &args[i]; } - static_assert(VARIANT_ARG_MAX == 8, "This code needs to be updated if VARIANT_ARG_MAX != 8"); - obj->call_deferred(str_method, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - // something + MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count); + env->PopLocalFrame(nullptr); } @@ -494,6 +502,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI return; } + // We force redraw to ensure we render at least once when resuming the app. + Main::force_redraw(); if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_RESUMED); } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 927b44ddb6..e1d30eea77 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -42,7 +42,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jobject p_surface, jint p_width, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_newcontext(JNIEnv *env, jclass clazz, jobject p_surface); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ttsCallback(JNIEnv *env, jclass clazz, jint event, jint id, jint pos); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jclass clazz); void touch_preprocessing(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions, jint buttons_mask = 0, jfloat vertical_factor = 0, jfloat horizontal_factor = 0); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_touch__IIII_3F(JNIEnv *env, jclass clazz, jint input_device, jint ev, jint pointer, jint pointer_count, jfloatArray positions); @@ -71,4 +72,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); } -#endif /* !JAVA_GODOT_LIB_JNI_H */ +#endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index 5605d834fa..0153ba96fc 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -34,7 +34,7 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); _godot_view = env->NewGlobalRef(godot_view); @@ -48,27 +48,27 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { } void GodotJavaViewWrapper::request_pointer_capture() { - if (_request_pointer_capture != 0) { + if (_request_pointer_capture != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _request_pointer_capture); } } void GodotJavaViewWrapper::release_pointer_capture() { - if (_request_pointer_capture != 0) { + if (_request_pointer_capture != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _release_pointer_capture); } } void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) { - if (_set_pointer_icon != 0) { + if (_set_pointer_icon != nullptr) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->CallVoidMethod(_godot_view, _set_pointer_icon, pointer_type); } @@ -76,7 +76,7 @@ void GodotJavaViewWrapper::set_pointer_icon(int pointer_type) { GodotJavaViewWrapper::~GodotJavaViewWrapper() { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); + ERR_FAIL_NULL(env); env->DeleteGlobalRef(_godot_view); env->DeleteGlobalRef(_cls); diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index 6e02c26ae2..b1f258bbb5 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -57,4 +57,4 @@ public: ~GodotJavaViewWrapper(); }; -#endif //GODOT_JAVA_GODOT_VIEW_WRAPPER_H +#endif // GODOT_JAVA_GODOT_VIEW_WRAPPER_H diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 754267c834..e3456fe4e4 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -77,6 +77,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); + _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); @@ -95,9 +96,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl if (p_env == nullptr) { p_env = get_jni_env(); } - - ERR_FAIL_COND_V(p_env == nullptr, nullptr); - + ERR_FAIL_NULL_V(p_env, nullptr); jfieldID fid = p_env->GetStaticFieldID(godot_class, p_name, p_class); return p_env->GetStaticObjectField(godot_class, fid); } else { @@ -108,8 +107,7 @@ jobject GodotJavaWrapper::get_member_object(const char *p_name, const char *p_cl jobject GodotJavaWrapper::get_class_loader() { if (_get_class_loader) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); return env->CallObjectMethod(activity, _get_class_loader); } else { return nullptr; @@ -121,8 +119,7 @@ GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { return _godot_view; } JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); jmethodID godot_view_getter = env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); _godot_view = new GodotJavaViewWrapper(env->CallObjectMethod(godot_instance, godot_view_getter)); return _godot_view; @@ -133,8 +130,7 @@ void GodotJavaWrapper::on_video_init(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _on_video_init); } } @@ -153,7 +149,7 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started); } } @@ -163,8 +159,7 @@ void GodotJavaWrapper::restart(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _restart); } } @@ -174,8 +169,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) { if (p_env == nullptr) { p_env = get_jni_env(); } - ERR_FAIL_COND(p_env == nullptr); - + ERR_FAIL_NULL(p_env); p_env->CallVoidMethod(godot_instance, _finish); } } @@ -183,8 +177,7 @@ void GodotJavaWrapper::force_quit(JNIEnv *p_env) { void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) { if (_set_keep_screen_on) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_instance, _set_keep_screen_on, p_enabled); } } @@ -192,8 +185,7 @@ void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) { void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { if (_alert) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data()); jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data()); env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle); @@ -202,24 +194,21 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { int GodotJavaWrapper::get_gles_version_code() { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 0); - + ERR_FAIL_NULL_V(env, 0); if (_get_GLES_version_code) { return env->CallIntMethod(godot_instance, _get_GLES_version_code); } - return 0; } bool GodotJavaWrapper::has_get_clipboard() { - return _get_clipboard != 0; + return _get_clipboard != nullptr; } String GodotJavaWrapper::get_clipboard() { if (_get_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); - + ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard); return jstring_to_string(s, env); } else { @@ -230,8 +219,7 @@ String GodotJavaWrapper::get_clipboard() { String GodotJavaWrapper::get_input_fallback_mapping() { if (_get_input_fallback_mapping) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, String()); - + ERR_FAIL_NULL_V(env, String()); jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping); return jstring_to_string(fallback_mapping, env); } else { @@ -240,28 +228,26 @@ String GodotJavaWrapper::get_input_fallback_mapping() { } bool GodotJavaWrapper::has_set_clipboard() { - return _set_clipboard != 0; + return _set_clipboard != nullptr; } void GodotJavaWrapper::set_clipboard(const String &p_text) { if (_set_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_text.utf8().get_data()); env->CallVoidMethod(godot_instance, _set_clipboard, jStr); } } bool GodotJavaWrapper::has_has_clipboard() { - return _has_clipboard != 0; + return _has_clipboard != nullptr; } bool GodotJavaWrapper::has_clipboard() { if (_has_clipboard) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _has_clipboard); } else { return false; @@ -271,8 +257,7 @@ bool GodotJavaWrapper::has_clipboard() { bool GodotJavaWrapper::request_permission(const String &p_name) { if (_request_permission) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); jstring jStrName = env->NewStringUTF(p_name.utf8().get_data()); return env->CallBooleanMethod(godot_instance, _request_permission, jStrName); } else { @@ -283,8 +268,7 @@ bool GodotJavaWrapper::request_permission(const String &p_name) { bool GodotJavaWrapper::request_permissions() { if (_request_permissions) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _request_permissions); } else { return false; @@ -295,14 +279,12 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { Vector<String> permissions_list; if (_get_granted_permissions) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, permissions_list); - + ERR_FAIL_NULL_V(env, permissions_list); jobject permissions_object = env->CallObjectMethod(godot_instance, _get_granted_permissions); jobjectArray *arr = reinterpret_cast<jobjectArray *>(&permissions_object); - int i = 0; jsize len = env->GetArrayLength(*arr); - for (i = 0; i < len; i++) { + for (int i = 0; i < len; i++) { jstring jstr = (jstring)env->GetObjectArrayElement(*arr, i); String str = jstring_to_string(jstr, env); permissions_list.push_back(str); @@ -315,8 +297,7 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { void GodotJavaWrapper::init_input_devices() { if (_init_input_devices) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_instance, _init_input_devices); } } @@ -324,8 +305,7 @@ void GodotJavaWrapper::init_input_devices() { jobject GodotJavaWrapper::get_surface() { if (_get_surface) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, nullptr); - + ERR_FAIL_NULL_V(env, nullptr); return env->CallObjectMethod(godot_instance, _get_surface); } else { return nullptr; @@ -335,8 +315,7 @@ jobject GodotJavaWrapper::get_surface() { bool GodotJavaWrapper::is_activity_resumed() { if (_is_activity_resumed) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, false); - + ERR_FAIL_NULL_V(env, false); return env->CallBooleanMethod(godot_instance, _is_activity_resumed); } else { return false; @@ -346,8 +325,19 @@ bool GodotJavaWrapper::is_activity_resumed() { void GodotJavaWrapper::vibrate(int p_duration_ms) { if (_vibrate) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + ERR_FAIL_NULL(env); env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms); } } + +void GodotJavaWrapper::create_new_godot_instance(List<String> args) { + if (_create_new_godot_instance) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); + for (int i = 0; i < args.size(); i++) { + env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data())); + } + env->CallVoidMethod(godot_instance, _create_new_godot_instance, jargs); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 42ae91480f..bbf7c0ae33 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -37,6 +37,7 @@ #include <android/log.h> #include <jni.h> +#include "core/templates/list.h" #include "java_godot_view_wrapper.h" #include "string_android.h" @@ -50,26 +51,27 @@ private: GodotJavaViewWrapper *_godot_view = nullptr; - jmethodID _on_video_init = 0; - jmethodID _restart = 0; - jmethodID _finish = 0; - jmethodID _set_keep_screen_on = 0; - jmethodID _alert = 0; - jmethodID _get_GLES_version_code = 0; - jmethodID _get_clipboard = 0; - jmethodID _set_clipboard = 0; - jmethodID _has_clipboard = 0; - jmethodID _request_permission = 0; - jmethodID _request_permissions = 0; - jmethodID _get_granted_permissions = 0; - jmethodID _init_input_devices = 0; - jmethodID _get_surface = 0; - jmethodID _is_activity_resumed = 0; - jmethodID _vibrate = 0; - jmethodID _get_input_fallback_mapping = 0; - jmethodID _on_godot_setup_completed = 0; - jmethodID _on_godot_main_loop_started = 0; - jmethodID _get_class_loader = 0; + jmethodID _on_video_init = nullptr; + jmethodID _restart = nullptr; + jmethodID _finish = nullptr; + jmethodID _set_keep_screen_on = nullptr; + jmethodID _alert = nullptr; + jmethodID _get_GLES_version_code = nullptr; + jmethodID _get_clipboard = nullptr; + jmethodID _set_clipboard = nullptr; + jmethodID _has_clipboard = nullptr; + jmethodID _request_permission = nullptr; + jmethodID _request_permissions = nullptr; + jmethodID _get_granted_permissions = nullptr; + jmethodID _init_input_devices = nullptr; + jmethodID _get_surface = nullptr; + jmethodID _is_activity_resumed = nullptr; + jmethodID _vibrate = nullptr; + jmethodID _get_input_fallback_mapping = nullptr; + jmethodID _on_godot_setup_completed = nullptr; + jmethodID _on_godot_main_loop_started = nullptr; + jmethodID _get_class_loader = nullptr; + jmethodID _create_new_godot_instance = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -103,6 +105,7 @@ public: bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); + void create_new_godot_instance(List<String> args); }; -#endif /* !JAVA_GODOT_WRAPPER_H */ +#endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/jni_utils.cpp b/platform/android/jni_utils.cpp index e2573d10f8..193ef61264 100644 --- a/platform/android/jni_utils.cpp +++ b/platform/android/jni_utils.cpp @@ -123,10 +123,10 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a for (int j = 0; j < keys.size(); j++) { Variant var = dict[keys[j]]; - jvalret v = _variant_to_jvalue(env, var.get_type(), &var, true); - env->SetObjectArrayElement(jvalues, j, v.val.l); - if (v.obj) { - env->DeleteLocalRef(v.obj); + jvalret valret = _variant_to_jvalue(env, var.get_type(), &var, true); + env->SetObjectArrayElement(jvalues, j, valret.val.l); + if (valret.obj) { + env->DeleteLocalRef(valret.obj); } } @@ -186,7 +186,7 @@ String _get_class_name(JNIEnv *env, jclass cls, bool *array) { if (array) { jmethodID isArray = env->GetMethodID(cclass, "isArray", "()Z"); jboolean isarr = env->CallBooleanMethod(cls, isArray); - (*array) = isarr ? true : false; + (*array) = isarr != 0; } String name = jstring_to_string(clsName, env); env->DeleteLocalRef(clsName); diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp index a65e7c6724..225a1132fe 100644 --- a/platform/android/net_socket_android.cpp +++ b/platform/android/net_socket_android.cpp @@ -32,10 +32,10 @@ #include "thread_jandroid.h" -jobject NetSocketAndroid::net_utils = 0; -jclass NetSocketAndroid::cls = 0; -jmethodID NetSocketAndroid::_multicast_lock_acquire = 0; -jmethodID NetSocketAndroid::_multicast_lock_release = 0; +jobject NetSocketAndroid::net_utils = nullptr; +jclass NetSocketAndroid::cls = nullptr; +jmethodID NetSocketAndroid::_multicast_lock_acquire = nullptr; +jmethodID NetSocketAndroid::_multicast_lock_release = nullptr; void NetSocketAndroid::setup(jobject p_net_utils) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h index 3e919497ea..97a611cb04 100644 --- a/platform/android/net_socket_android.h +++ b/platform/android/net_socket_android.h @@ -74,4 +74,4 @@ public: ~NetSocketAndroid(); }; -#endif +#endif // NET_SOCKET_ANDROID_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index b17b0f3139..6674428de8 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -35,6 +35,8 @@ #include "drivers/unix/file_access_unix.h" #include "main/main.h" #include "platform/android/display_server_android.h" +#include "scene/main/scene_tree.h" +#include "servers/rendering_server.h" #include "dir_access_jandroid.h" #include "file_access_android.h" @@ -45,6 +47,8 @@ #include "java_godot_io_wrapper.h" #include "java_godot_wrapper.h" +const char *OS_Android::ANDROID_EXEC_PATH = "apk"; + String _remove_symlink(const String &dir) { // Workaround for Android 6.0+ using a symlink. // Save the current directory. @@ -72,27 +76,34 @@ public: }; void OS_Android::alert(const String &p_alert, const String &p_title) { - GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); - ERR_FAIL_COND(!godot_java); - + ERR_FAIL_NULL(godot_java); godot_java->alert(p_alert, p_title); } void OS_Android::initialize_core() { OS_Unix::initialize_core(); +#ifdef TOOLS_ENABLED + FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); +#else if (use_apk_expansion) { FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES); } else { FileAccess::make_default<FileAccessAndroid>(FileAccess::ACCESS_RESOURCES); } +#endif FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA); FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM); + +#ifdef TOOLS_ENABLED + DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES); +#else if (use_apk_expansion) { DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES); } else { DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES); } +#endif DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA); DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM); @@ -125,7 +136,7 @@ void OS_Android::finalize() { } OS_Android *OS_Android::get_singleton() { - return (OS_Android *)OS::get_singleton(); + return static_cast<OS_Android *>(OS::get_singleton()); } GodotJavaWrapper *OS_Android::get_godot_java() { @@ -148,9 +159,14 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { p_library_handle = dlopen(p_path.utf8().get_data(), RTLD_NOW); - ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = p_path; + } + return OK; } @@ -168,16 +184,27 @@ void OS_Android::main_loop_begin() { } } -bool OS_Android::main_loop_iterate() { +bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) { if (!main_loop) { return false; } DisplayServerAndroid::get_singleton()->process_events(); - return Main::iteration(); + uint64_t current_frames_drawn = Engine::get_singleton()->get_frames_drawn(); + bool exit = Main::iteration(); + + if (r_should_swap_buffers) { + *r_should_swap_buffers = !is_in_low_processor_usage_mode() || RenderingServer::get_singleton()->has_changed() || current_frames_drawn != Engine::get_singleton()->get_frames_drawn(); + } + + return exit; } void OS_Android::main_loop_end() { if (main_loop) { + SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop); + if (scene_tree) { + scene_tree->quit(); + } main_loop->finalize(); } } @@ -197,7 +224,11 @@ Error OS_Android::shell_open(String p_uri) { } String OS_Android::get_resource_dir() const { +#ifdef TOOLS_ENABLED + return OS_Unix::get_resource_dir(); +#else return "/"; //android has its own filesystem for resources inside the APK +#endif } String OS_Android::get_locale() const { @@ -222,6 +253,14 @@ String OS_Android::get_data_path() const { return get_user_data_dir(); } +String OS_Android::get_executable_path() const { + // Since unix process creation is restricted on Android, we bypass + // OS_Unix::get_executable_path() so we can return ANDROID_EXEC_PATH. + // Detection of ANDROID_EXEC_PATH allows to handle process creation in an Android compliant + // manner. + return OS::get_executable_path(); +} + String OS_Android::get_user_data_dir() const { if (!data_dir_cache.is_empty()) { return data_dir_cache; @@ -271,7 +310,7 @@ Size2i OS_Android::get_display_size() const { void OS_Android::set_opengl_extensions(const char *p_gl_extensions) { #if defined(GLES3_ENABLED) - ERR_FAIL_COND(!p_gl_extensions); + ERR_FAIL_NULL(p_gl_extensions); gl_extensions = p_gl_extensions; #endif } @@ -294,6 +333,10 @@ void OS_Android::vibrate_handheld(int p_duration_ms) { godot_java->vibrate(p_duration_ms); } +String OS_Android::get_config_path() const { + return get_user_data_dir().plus_file("config"); +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "mobile") { return true; @@ -343,5 +386,26 @@ OS_Android::OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_god DisplayServerAndroid::register_android_driver(); } +Error OS_Android::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments); + } else { + return OS_Unix::execute(p_path, p_arguments, r_pipe, r_exitcode, read_stderr, p_pipe_mutex, p_open_console); + } +} + +Error OS_Android::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { + if (p_path == ANDROID_EXEC_PATH) { + return create_instance(p_arguments, r_child_id); + } else { + return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); + } +} + +Error OS_Android::create_instance(const List<String> &p_arguments, ProcessID *r_child_id) { + godot_java->create_new_godot_instance(p_arguments); + return OK; +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index f523f172c6..3f607eac48 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -52,7 +52,7 @@ private: #endif #if defined(VULKAN_ENABLED) - ANativeWindow *native_window; + ANativeWindow *native_window = nullptr; #endif mutable String data_dir_cache; @@ -60,12 +60,14 @@ private: AudioDriverOpenSL audio_driver_android; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; - GodotJavaWrapper *godot_java; - GodotIOJavaWrapper *godot_io_java; + GodotJavaWrapper *godot_java = nullptr; + GodotIOJavaWrapper *godot_io_java = nullptr; public: + static const char *ANDROID_EXEC_PATH; + virtual void initialize_core() override; virtual void initialize() override; @@ -88,13 +90,13 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual String get_name() const override; virtual MainLoop *get_main_loop() const override; void main_loop_begin(); - bool main_loop_iterate(); + bool main_loop_iterate(bool *r_should_swap_buffers = nullptr); void main_loop_end(); void main_loop_focusout(); void main_loop_focusin(); @@ -108,6 +110,7 @@ public: ANativeWindow *get_native_window() const; virtual Error shell_open(String p_uri) override; + virtual String get_executable_path() const override; virtual String get_user_data_dir() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; @@ -121,9 +124,15 @@ public: void vibrate_handheld(int p_duration_ms) override; + virtual String get_config_path() const override; + + virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; + virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; + virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; + virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); ~OS_Android(); }; -#endif +#endif // OS_ANDROID_H diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 48aeb3d070..7a39e2003d 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -114,19 +114,18 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS String signal_name = jstring_to_string(j_signal_name, env); int count = env->GetArrayLength(j_signal_params); - ERR_FAIL_COND_MSG(count > VARIANT_ARG_MAX, "Maximum argument count exceeded!"); - Variant variant_params[VARIANT_ARG_MAX]; - const Variant *args[VARIANT_ARG_MAX]; + Variant *variant_params = (Variant *)alloca(sizeof(Variant) * count); + const Variant **args = (const Variant **)alloca(sizeof(Variant *) * count); for (int i = 0; i < count; i++) { jobject j_param = env->GetObjectArrayElement(j_signal_params, i); variant_params[i] = _jobject_to_variant(env, j_param); args[i] = &variant_params[i]; env->DeleteLocalRef(j_param); - }; + } - singleton->emit_signal(StringName(signal_name), args, count); + singleton->emit_signalp(StringName(signal_name), args, count); } JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jclass clazz, jobjectArray gdnlib_paths) { diff --git a/platform/android/string_android.h b/platform/android/string_android.h index 2afaa86fec..79c71b5d04 100644 --- a/platform/android/string_android.h +++ b/platform/android/string_android.h @@ -30,6 +30,7 @@ #ifndef STRING_ANDROID_H #define STRING_ANDROID_H + #include "core/string/ustring.h" #include "thread_jandroid.h" #include <jni.h> diff --git a/platform/android/thread_jandroid.h b/platform/android/thread_jandroid.h index 2073423f8d..3b000517fd 100644 --- a/platform/android/thread_jandroid.h +++ b/platform/android/thread_jandroid.h @@ -38,4 +38,4 @@ void init_thread_jandroid(JavaVM *p_jvm, JNIEnv *p_env); void setup_android_thread(); JNIEnv *get_jni_env(); -#endif +#endif // THREAD_JANDROID_H diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp new file mode 100644 index 0000000000..27ba8da448 --- /dev/null +++ b/platform/android/tts_android.cpp @@ -0,0 +1,189 @@ +/*************************************************************************/ +/* tts_android.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "tts_android.h" + +#include "java_godot_wrapper.h" +#include "os_android.h" +#include "string_android.h" +#include "thread_jandroid.h" + +jobject TTS_Android::tts = nullptr; +jclass TTS_Android::cls = nullptr; + +jmethodID TTS_Android::_is_speaking = nullptr; +jmethodID TTS_Android::_is_paused = nullptr; +jmethodID TTS_Android::_get_voices = nullptr; +jmethodID TTS_Android::_speak = nullptr; +jmethodID TTS_Android::_pause_speaking = nullptr; +jmethodID TTS_Android::_resume_speaking = nullptr; +jmethodID TTS_Android::_stop_speaking = nullptr; + +HashMap<int, Char16String> TTS_Android::ids; + +void TTS_Android::setup(jobject p_tts) { + JNIEnv *env = get_jni_env(); + + tts = env->NewGlobalRef(p_tts); + + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); + + _is_speaking = env->GetMethodID(cls, "isSpeaking", "()Z"); + _is_paused = env->GetMethodID(cls, "isPaused", "()Z"); + _get_voices = env->GetMethodID(cls, "getVoices", "()[Ljava/lang/String;"); + _speak = env->GetMethodID(cls, "speak", "(Ljava/lang/String;Ljava/lang/String;IFFIZ)V"); + _pause_speaking = env->GetMethodID(cls, "pauseSpeaking", "()V"); + _resume_speaking = env->GetMethodID(cls, "resumeSpeaking", "()V"); + _stop_speaking = env->GetMethodID(cls, "stopSpeaking", "()V"); +} + +void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + if (ids.has(p_id)) { + int pos = 0; + if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-16 to UTF-32. + const Char16String &string = ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + } else if ((DisplayServer::TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ids.erase(p_id); + } + DisplayServer::get_singleton()->tts_post_utterance_event((DisplayServer::TTSUtteranceEvent)p_event, p_id, pos); + } +} + +bool TTS_Android::is_speaking() { + if (_is_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_speaking); + } else { + return false; + } +} + +bool TTS_Android::is_paused() { + if (_is_paused) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(tts, _is_paused); + } else { + return false; + } +} + +Array TTS_Android::get_voices() { + Array list; + if (_get_voices) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, list); + + jobject voices_object = env->CallObjectMethod(tts, _get_voices); + jobjectArray *arr = reinterpret_cast<jobjectArray *>(&voices_object); + + jsize len = env->GetArrayLength(*arr); + for (int i = 0; i < len; i++) { + jstring jStr = (jstring)env->GetObjectArrayElement(*arr, i); + String str = jstring_to_string(jStr, env); + Vector<String> tokens = str.split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + list.push_back(voice_d); + } + env->DeleteLocalRef(jStr); + } + } + return list; +} + +void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + ids[p_utterance_id] = p_text.utf16(); + + if (_speak) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); + jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); + env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt); + } +} + +void TTS_Android::pause() { + if (_pause_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _pause_speaking); + } +} + +void TTS_Android::resume() { + if (_resume_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _resume_speaking); + } +} + +void TTS_Android::stop() { + for (const KeyValue<int, Char16String> &E : ids) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); + } + ids.clear(); + + if (_stop_speaking) { + JNIEnv *env = get_jni_env(); + + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(tts, _stop_speaking); + } +} diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h new file mode 100644 index 0000000000..bc0cdb8d55 --- /dev/null +++ b/platform/android/tts_android.h @@ -0,0 +1,67 @@ +/*************************************************************************/ +/* tts_android.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TTS_ANDROID_H +#define TTS_ANDROID_H + +#include "core/string/ustring.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <jni.h> + +class TTS_Android { + static jobject tts; + static jclass cls; + + static jmethodID _is_speaking; + static jmethodID _is_paused; + static jmethodID _get_voices; + static jmethodID _speak; + static jmethodID _pause_speaking; + static jmethodID _resume_speaking; + static jmethodID _stop_speaking; + + static HashMap<int, Char16String> ids; + +public: + static void setup(jobject p_tts); + static void _java_utterance_callback(int p_event, int p_id, int p_pos); + + static bool is_speaking(); + static bool is_paused(); + static Array get_voices(); + static void speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt); + static void pause(); + static void resume(); + static void stop(); +}; + +#endif // TTS_ANDROID_H diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 58b574a72f..5e10bf5646 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -13,6 +13,7 @@ iphone_lib = [ "display_server_iphone.mm", "joypad_iphone.mm", "godot_view.mm", + "tts_ios.mm", "display_layer.mm", "godot_app_delegate.m", "godot_view_renderer.mm", diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index f442235e7c..392a0151be 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -47,8 +47,11 @@ def configure(env): if env["target"].startswith("release"): env.Append(CPPDEFINES=["NDEBUG", ("NS_BLOCK_ASSERTIONS", 1)]) if env["optimize"] == "speed": # optimize for speed (default) - env.Append(CCFLAGS=["-O2", "-ftree-vectorize", "-fomit-frame-pointer"]) - env.Append(LINKFLAGS=["-O2"]) + # `-O2` is more friendly to debuggers than `-O3`, leading to better crash backtraces + # when using `target=release_debug`. + opt = "-O3" if env["target"] == "release" else "-O2" + env.Append(CCFLAGS=[opt, "-ftree-vectorize", "-fomit-frame-pointer"]) + env.Append(LINKFLAGS=[opt]) elif env["optimize"] == "size": # optimize for size env.Append(CCFLAGS=["-Os", "-ftree-vectorize"]) env.Append(LINKFLAGS=["-Os"]) diff --git a/platform/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h index 7441550f67..7af222e3f8 100644 --- a/platform/iphone/display_server_iphone.h +++ b/platform/iphone/display_server_iphone.h @@ -58,6 +58,8 @@ class DisplayServerIPhone : public DisplayServer { RenderingDeviceVulkan *rendering_device_vulkan = nullptr; #endif + id tts = nullptr; + DisplayServer::ScreenOrientation screen_orientation; ObjectID window_attached_instance_id; @@ -123,6 +125,17 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + + virtual Rect2i get_display_safe_area() const override; + virtual int get_screen_count() const override; virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index a0f8daf5a0..573ee9b7a8 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -38,6 +38,7 @@ #include "ios.h" #import "keyboard_input_view.h" #include "os_iphone.h" +#include "tts_ios.h" #import "view_controller.h" #import <Foundation/Foundation.h> @@ -52,6 +53,9 @@ DisplayServerIPhone *DisplayServerIPhone::get_singleton() { DisplayServerIPhone::DisplayServerIPhone(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { rendering_driver = p_rendering_driver; + // Init TTS + tts = [[TTS_IOS alloc] init]; + #if defined(GLES3_ENABLED) // FIXME: Add support for both OpenGL and Vulkan when OpenGL is implemented // again, @@ -310,6 +314,7 @@ bool DisplayServerIPhone::has_feature(Feature p_feature) const { case FEATURE_ORIENTATION: case FEATURE_TOUCHSCREEN: case FEATURE_VIRTUAL_KEYBOARD: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -320,6 +325,57 @@ String DisplayServerIPhone::get_name() const { return "iPhone"; } +bool DisplayServerIPhone::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isSpeaking]; +} + +bool DisplayServerIPhone::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isPaused]; +} + +Array DisplayServerIPhone::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return [tts getVoices]; +} + +void DisplayServerIPhone::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt]; +} + +void DisplayServerIPhone::tts_pause() { + ERR_FAIL_COND(!tts); + [tts pauseSpeaking]; +} + +void DisplayServerIPhone::tts_resume() { + ERR_FAIL_COND(!tts); + [tts resumeSpeaking]; +} + +void DisplayServerIPhone::tts_stop() { + ERR_FAIL_COND(!tts); + [tts stopSpeaking]; +} + +Rect2i DisplayServerIPhone::get_display_safe_area() const { + if (@available(iOS 11, *)) { + UIEdgeInsets insets = UIEdgeInsetsZero; + UIView *view = AppDelegate.viewController.godotView; + if ([view respondsToSelector:@selector(safeAreaInsets)]) { + insets = [view safeAreaInsets]; + } + float scale = screen_get_scale(); + Size2i insets_position = Size2i(insets.left, insets.top) * scale; + Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; + return Rect2i(screen_get_position() + insets_position, screen_get_size() - insets_size); + } else { + return Rect2i(screen_get_position(), screen_get_size()); + } +} + int DisplayServerIPhone::get_screen_count() const { return 1; } @@ -339,22 +395,7 @@ Size2i DisplayServerIPhone::screen_get_size(int p_screen) const { } Rect2i DisplayServerIPhone::screen_get_usable_rect(int p_screen) const { - if (@available(iOS 11, *)) { - UIEdgeInsets insets = UIEdgeInsetsZero; - UIView *view = AppDelegate.viewController.godotView; - - if ([view respondsToSelector:@selector(safeAreaInsets)]) { - insets = [view safeAreaInsets]; - } - - float scale = screen_get_scale(p_screen); - Size2i insets_position = Size2i(insets.left, insets.top) * scale; - Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; - - return Rect2i(screen_get_position(p_screen) + insets_position, screen_get_size(p_screen) - insets_size); - } else { - return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); - } + return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); } int DisplayServerIPhone::screen_get_dpi(int p_screen) const { diff --git a/platform/iphone/export/export_plugin.cpp b/platform/iphone/export/export_plugin.cpp index 69c6df8a38..4cf1c279eb 100644 --- a/platform/iphone/export/export_plugin.cpp +++ b/platform/iphone/export/export_plugin.cpp @@ -58,18 +58,18 @@ struct LoadingScreenInfo { }; static const LoadingScreenInfo loading_screen_infos[] = { - { "landscape_launch_screens/iphone_2436x1125", "Default-Landscape-X.png", 2436, 1125, false }, - { "landscape_launch_screens/iphone_2208x1242", "Default-Landscape-736h@3x.png", 2208, 1242, false }, - { "landscape_launch_screens/ipad_1024x768", "Default-Landscape.png", 1024, 768, false }, - { "landscape_launch_screens/ipad_2048x1536", "Default-Landscape@2x.png", 2048, 1536, false }, - - { "portrait_launch_screens/iphone_640x960", "Default-480h@2x.png", 640, 960, true }, - { "portrait_launch_screens/iphone_640x1136", "Default-568h@2x.png", 640, 1136, true }, - { "portrait_launch_screens/iphone_750x1334", "Default-667h@2x.png", 750, 1334, true }, - { "portrait_launch_screens/iphone_1125x2436", "Default-Portrait-X.png", 1125, 2436, true }, - { "portrait_launch_screens/ipad_768x1024", "Default-Portrait.png", 768, 1024, true }, - { "portrait_launch_screens/ipad_1536x2048", "Default-Portrait@2x.png", 1536, 2048, true }, - { "portrait_launch_screens/iphone_1242x2208", "Default-Portrait-736h@3x.png", 1242, 2208, true } + { PNAME("landscape_launch_screens/iphone_2436x1125"), "Default-Landscape-X.png", 2436, 1125, false }, + { PNAME("landscape_launch_screens/iphone_2208x1242"), "Default-Landscape-736h@3x.png", 2208, 1242, false }, + { PNAME("landscape_launch_screens/ipad_1024x768"), "Default-Landscape.png", 1024, 768, false }, + { PNAME("landscape_launch_screens/ipad_2048x1536"), "Default-Landscape@2x.png", 2048, 1536, false }, + + { PNAME("portrait_launch_screens/iphone_640x960"), "Default-480h@2x.png", 640, 960, true }, + { PNAME("portrait_launch_screens/iphone_640x1136"), "Default-568h@2x.png", 640, 1136, true }, + { PNAME("portrait_launch_screens/iphone_750x1334"), "Default-667h@2x.png", 750, 1334, true }, + { PNAME("portrait_launch_screens/iphone_1125x2436"), "Default-Portrait-X.png", 1125, 2436, true }, + { PNAME("portrait_launch_screens/ipad_768x1024"), "Default-Portrait.png", 768, 1024, true }, + { PNAME("portrait_launch_screens/ipad_1536x2048"), "Default-Portrait@2x.png", 1536, 2048, true }, + { PNAME("portrait_launch_screens/iphone_1242x2208"), "Default-Portrait-736h@3x.png", 1242, 2208, true } }; void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) { @@ -78,7 +78,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) Vector<ExportArchitecture> architectures = _get_supported_architectures(); for (int i = 0; i < architectures.size(); ++i) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "architectures/" + architectures[i].name), architectures[i].is_default)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("architectures"), architectures[i].name)), architectures[i].is_default)); } r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_store_team_id"), "")); @@ -92,34 +92,28 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/targeted_device_family", PROPERTY_HINT_ENUM, "iPhone,iPad,iPhone & iPad"), 2)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/info"), "Made with Godot Engine")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); Vector<PluginConfigIOS> found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "plugins/" + found_plugins[i].name), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), found_plugins[i].name)), false)); } - Set<String> plist_keys; + HashSet<String> plist_keys; for (int i = 0; i < found_plugins.size(); i++) { // Editable plugin plist values PluginConfigIOS plugin = found_plugins[i]; - const String *K = nullptr; - while ((K = plugin.plist.next(K))) { - String key = *K; - PluginConfigIOS::PlistItem item = plugin.plist[key]; - switch (item.type) { + for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) { + switch (E.value.type) { case PluginConfigIOS::PlistItemType::STRING_INPUT: { - String preset_name = "plugins_plist/" + key; + String preset_name = "plugins_plist/" + E.key; if (!plist_keys.has(preset_name)) { - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), item.value)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), E.value.value)); plist_keys.insert(preset_name); } } break; @@ -139,8 +133,11 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data/accessible_from_itunes_sharing"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photolibrary_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_120x120", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone/iPod Touch with Retina display r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_180x180", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone with Retina HD display @@ -200,8 +197,6 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$modules_buildgrp", p_config.modules_buildgrp) + "\n"; } else if (lines[i].find("$name") != -1) { strnew += lines[i].replace("$name", p_config.pkg_name) + "\n"; - } else if (lines[i].find("$info") != -1) { - strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n"; } else if (lines[i].find("$bundle_identifier") != -1) { strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n"; } else if (lines[i].find("$short_version") != -1) { @@ -210,8 +205,6 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$version", p_preset->get("application/version")) + "\n"; } else if (lines[i].find("$signature") != -1) { strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n"; - } else if (lines[i].find("$copyright") != -1) { - strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n"; } else if (lines[i].find("$team_id") != -1) { strnew += lines[i].replace("$team_id", p_preset->get("application/app_store_team_id")) + "\n"; } else if (lines[i].find("$default_build_config") != -1) { @@ -389,6 +382,36 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ String value = value_format.format(value_dictionary, "$_"); strnew += lines[i].replace("$launch_screen_background_color", value) + "\n"; + } else if (lines[i].find("$pbx_locale_file_reference") != -1) { + String locale_files; + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + int index = 0; + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = " + lang + "; path = " + lang + ".lproj/InfoPlist.strings; sourceTree = \"<group>\"; };"; + } + index++; + } + } + strnew += lines[i].replace("$pbx_locale_file_reference", locale_files); + } else if (lines[i].find("$pbx_locale_build_reference") != -1) { + String locale_files; + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + int index = 0; + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */,"; + } + index++; + } + } + strnew += lines[i].replace("$pbx_locale_build_reference", locale_files); } else { strnew += lines[i] + "\n"; } @@ -504,8 +527,8 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr String json_description = "{\"images\":["; String sizes; - DirAccess *da = DirAccess::open(p_iconset_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_iconset_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) { IconInfo info = icon_infos[i]; @@ -544,7 +567,6 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr } if (err) { - memdelete(da); String err_str = String("Failed to export icon(" + String(info.preset_key) + "): '" + icon_path + "'."); ERR_PRINT(err_str.utf8().get_data()); return err; @@ -562,19 +584,16 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr json_description += String("}"); } json_description += "]}"; - memdelete(da); - FileAccess *json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); - ERR_FAIL_COND_V(!json_file, ERR_CANT_CREATE); + Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); + ERR_FAIL_COND_V(json_file.is_null(), ERR_CANT_CREATE); CharString json_utf8 = json_description.utf8(); json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length()); - memdelete(json_file); - FileAccess *sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); - ERR_FAIL_COND_V(!sizes_file, ERR_CANT_CREATE); + Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); + ERR_FAIL_COND_V(sizes_file.is_null(), ERR_CANT_CREATE); CharString sizes_utf8 = sizes.utf8(); sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length()); - memdelete(sizes_file); return OK; } @@ -648,8 +667,8 @@ Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExpor } Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { - DirAccess *da = DirAccess::open(p_dest_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_dest_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) { LoadingScreenInfo info = loading_screen_infos[i]; @@ -686,7 +705,6 @@ Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExp err = da->copy(loading_screen_file, p_dest_dir + info.export_name); } if (err) { - memdelete(da); String err_str = String("Failed to export loading screen (") + info.preset_key + ") from path '" + loading_screen_file + "'."; ERR_PRINT(err_str.utf8().get_data()); return err; @@ -734,12 +752,11 @@ Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExp } } } - memdelete(da); return OK; } -Error EditorExportPlatformIOS::_walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata) { +Error EditorExportPlatformIOS::_walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata) { Vector<String> dirs; String current_dir = p_da->get_current_dir(); p_da->list_dir_begin(); @@ -785,7 +802,7 @@ struct CodesignData { Error EditorExportPlatformIOS::_codesign(String p_file, void *p_userdata) { if (p_file.ends_with(".dylib")) { - CodesignData *data = (CodesignData *)p_userdata; + CodesignData *data = static_cast<CodesignData *>(p_userdata); print_line(String("Signing ") + p_file); String sign_id; @@ -800,7 +817,11 @@ Error EditorExportPlatformIOS::_codesign(String p_file, void *p_userdata) { codesign_args.push_back("-s"); codesign_args.push_back(sign_id); codesign_args.push_back(p_file); - return OS::get_singleton()->execute("codesign", codesign_args); + String str; + Error err = OS::get_singleton()->execute("codesign", codesign_args, &str, nullptr, true); + print_verbose("codesign (" + p_file + "):\n" + str); + + return err; } return OK; } @@ -940,21 +961,15 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { - DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); - String binary_name = p_out_dir.get_file().get_basename(); - DirAccess *da = DirAccess::create_for_path(p_asset); - if (!da) { - memdelete(filesystem_da); + Ref<DirAccess> da = DirAccess::create_for_path(p_asset); + if (da.is_null()) { ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + p_asset + "."); } bool file_exists = da->file_exists(p_asset); bool dir_exists = da->dir_exists(p_asset); if (!file_exists && !dir_exists) { - memdelete(da); - memdelete(filesystem_da); return ERR_FILE_NOT_FOUND; } @@ -1014,19 +1029,18 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String destination = p_out_dir.plus_file(asset_path); } + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); + if (!filesystem_da->dir_exists(destination_dir)) { Error make_dir_err = filesystem_da->make_dir_recursive(destination_dir); if (make_dir_err) { - memdelete(da); - memdelete(filesystem_da); return make_dir_err; } } Error err = dir_exists ? da->copy_dir(p_asset, destination) : da->copy(p_asset, destination); - memdelete(da); if (err) { - memdelete(filesystem_da); return err; } IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; @@ -1082,17 +1096,13 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String String info_plist = info_plist_format.replace("$name", file_name); - FileAccess *f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); - if (f) { + Ref<FileAccess> f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); + if (f.is_valid()) { f->store_string(info_plist); - f->close(); - memdelete(f); } } } - memdelete(filesystem_da); - return OK; } @@ -1172,7 +1182,7 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> Vector<String> added_embedded_dependenciy_names; HashMap<String, String> plist_values; - Set<String> plugin_linker_flags; + HashSet<String> plugin_linker_flags; Error err; @@ -1249,11 +1259,10 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> // Plist // Using hash map container to remove duplicates - const String *K = nullptr; - while ((K = plugin.plist.next(K))) { - String key = *K; - PluginConfigIOS::PlistItem item = plugin.plist[key]; + for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) { + String key = E.key; + const PluginConfigIOS::PlistItem &item = E.value; String value; @@ -1292,10 +1301,9 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> // Updating `Info.plist` { - const String *K = nullptr; - while ((K = plist_values.next(K))) { - String key = *K; - String value = plist_values[key]; + for (const KeyValue<String, String> &E : plist_values) { + String key = E.key; + String value = E.value; if (key.is_empty() || value.is_empty()) { continue; @@ -1346,8 +1354,8 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> // Update Linker Flag Values { String result_linker_flags = " "; - for (Set<String>::Element *E = plugin_linker_flags.front(); E; E = E->next()) { - const String &flag = E->get(); + for (const String &E : plugin_linker_flags) { + const String &flag = E; if (flag.length() == 0) { continue; @@ -1388,7 +1396,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p String err; src_pkg_name = find_export_template("iphone.zip", &err); if (src_pkg_name.is_empty()) { - EditorNode::add_io_error(err); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found.")); return ERR_FILE_NOT_FOUND; } } @@ -1397,29 +1405,29 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return ERR_FILE_BAD_PATH; } - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (da) { - String current_dir = da->get_current_dir(); - - // remove leftovers from last export so they don't interfere - // in case some files are no longer needed - if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) { - da->erase_contents_recursive(); - } - if (da->change_dir(dest_dir + binary_name) == OK) { - da->erase_contents_recursive(); - } + { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_valid()) { + String current_dir = da->get_current_dir(); + + // remove leftovers from last export so they don't interfere + // in case some files are no longer needed + if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) { + da->erase_contents_recursive(); + } + if (da->change_dir(dest_dir + binary_name) == OK) { + da->erase_contents_recursive(); + } - da->change_dir(current_dir); + da->change_dir(current_dir); - if (!da->dir_exists(dest_dir + binary_name)) { - Error err = da->make_dir(dest_dir + binary_name); - if (err) { - memdelete(da); - return err; + if (!da->dir_exists(dest_dir + binary_name)) { + Error err = da->make_dir(dest_dir + binary_name); + if (err) { + return err; + } } } - memdelete(da); } if (ep.step("Making .pck", 0)) { @@ -1427,7 +1435,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p } String pack_path = dest_dir + binary_name + ".pck"; Vector<SharedObject> libraries; - Error err = save_pack(p_preset, pack_path, &libraries); + Error err = save_pack(p_preset, p_debug, pack_path, &libraries); if (err) { return err; } @@ -1440,9 +1448,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p print_line("Static framework: " + library_to_use); String pkg_name; - if (p_preset->get("application/name") != "") { - pkg_name = p_preset->get("application/name"); // app_name - } else if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { + if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name")); } else { pkg_name = "Unnamed"; @@ -1451,7 +1457,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p bool found_library = false; const String project_file = "godot_ios.xcodeproj/project.pbxproj"; - Set<String> files_to_parse; + HashSet<String> files_to_parse; files_to_parse.insert("godot_ios/godot_ios-Info.plist"); files_to_parse.insert(project_file); files_to_parse.insert("godot_ios/export_options.plist"); @@ -1477,15 +1483,15 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p Vector<IOSExportAsset> assets; - DirAccess *tmp_app_path = DirAccess::create_for_path(dest_dir); - ERR_FAIL_COND_V(!tmp_app_path, ERR_CANT_CREATE); + Ref<DirAccess> tmp_app_path = DirAccess::create_for_path(dest_dir); + ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE); print_line("Unzipping..."); - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { - EditorNode::add_io_error("Could not open export template (not a zip file?):\n" + src_pkg_name); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Could not open export template (not a zip file?): \"%s\".", src_pkg_name)); return ERR_CANT_OPEN; } @@ -1504,6 +1510,9 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); @@ -1556,22 +1565,20 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (dir_err) { ERR_PRINT("Can't create '" + dir_name + "'."); unzClose(src_pkg_zip); - memdelete(tmp_app_path); return ERR_CANT_CREATE; } } /* write the file */ - FileAccess *f = FileAccess::open(file, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + file + "'."); - unzClose(src_pkg_zip); - memdelete(tmp_app_path); - return ERR_CANT_CREATE; - }; - f->store_buffer(data.ptr(), data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + file + "'."); + unzClose(src_pkg_zip); + return ERR_CANT_CREATE; + }; + f->store_buffer(data.ptr(), data.size()); + } #if defined(OSX_ENABLED) || defined(X11_ENABLED) if (is_execute) { @@ -1589,10 +1596,53 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (!found_library) { ERR_PRINT("Requested template library '" + library_to_use + "' not found. It might be missing from your template archive."); - memdelete(tmp_app_path); return ERR_FILE_NOT_FOUND; } + Dictionary appnames = ProjectSettings::get_singleton()->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"); + Dictionary photolibrary_usage_descriptions = p_preset->get("privacy/photolibrary_usage_description_localized"); + + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + { + String fname = dest_dir + binary_name + "/en.lproj"; + tmp_app_path->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";"); + f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";"); + f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";"); + f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";"); + } + + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + String fname = dest_dir + binary_name + "/" + lang + ".lproj"; + tmp_app_path->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + if (appnames.has(lang)) { + f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); + } + if (camera_usage_descriptions.has(lang)) { + f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); + } + if (microphone_usage_descriptions.has(lang)) { + f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); + } + if (photolibrary_usage_descriptions.has(lang)) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + photolibrary_usage_descriptions[lang].operator String() + "\";"); + } + } + } + } + // Copy project static libs to the project Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { @@ -1603,7 +1653,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p Error lib_copy_err = tmp_app_path->copy(static_lib_path, dest_lib_file_path); if (lib_copy_err != OK) { ERR_PRINT("Can't copy '" + static_lib_path + "'."); - memdelete(tmp_app_path); return lib_copy_err; } } @@ -1614,7 +1663,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (!tmp_app_path->dir_exists(iconset_dir)) { err = tmp_app_path->make_dir_recursive(iconset_dir); } - memdelete(tmp_app_path); if (err) { return err; } @@ -1624,43 +1672,42 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return err; } - bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard"); + { + bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard"); - String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; - String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; + String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; + String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; - DirAccess *launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (launch_screen_da.is_null()) { + return ERR_CANT_CREATE; + } - if (!launch_screen_da) { - return ERR_CANT_CREATE; - } + if (use_storyboard) { + print_line("Using Launch Storyboard"); - if (use_storyboard) { - print_line("Using Launch Storyboard"); + if (launch_screen_da->change_dir(launch_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(launch_image_path); + } - if (launch_screen_da->change_dir(launch_image_path) == OK) { - launch_screen_da->erase_contents_recursive(); - launch_screen_da->remove(launch_image_path); - } + err = _export_loading_screen_file(p_preset, splash_image_path); + } else { + print_line("Using Launch Images"); - err = _export_loading_screen_file(p_preset, splash_image_path); - } else { - print_line("Using Launch Images"); + const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard"; - const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard"; + launch_screen_da->remove(launch_screen_path); - launch_screen_da->remove(launch_screen_path); + if (launch_screen_da->change_dir(splash_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(splash_image_path); + } - if (launch_screen_da->change_dir(splash_image_path) == OK) { - launch_screen_da->erase_contents_recursive(); - launch_screen_da->remove(splash_image_path); + err = _export_loading_screen_images(p_preset, launch_image_path); } - - err = _export_loading_screen_images(p_preset, launch_image_path); } - memdelete(launch_screen_da); - if (err) { return err; } @@ -1669,25 +1716,26 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p _export_additional_assets(dest_dir + binary_name, libraries, assets); _add_assets_to_project(p_preset, project_file_data, assets); String project_file_name = dest_dir + binary_name + ".xcodeproj/project.pbxproj"; - FileAccess *f = FileAccess::open(project_file_name, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + project_file_name + "'."); - return ERR_CANT_CREATE; - }; - f->store_buffer(project_file_data.ptr(), project_file_data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(project_file_name, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + project_file_name + "'."); + return ERR_CANT_CREATE; + }; + f->store_buffer(project_file_data.ptr(), project_file_data.size()); + } #ifdef OSX_ENABLED - if (ep.step("Code-signing dylibs", 2)) { - return ERR_SKIP; + { + if (ep.step("Code-signing dylibs", 2)) { + return ERR_SKIP; + } + Ref<DirAccess> dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); + ERR_FAIL_COND_V(dylibs_dir.is_null(), ERR_CANT_OPEN); + CodesignData codesign_data(p_preset, p_debug); + err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data); + ERR_FAIL_COND_V(err, err); } - DirAccess *dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); - ERR_FAIL_COND_V(!dylibs_dir, ERR_CANT_OPEN); - CodesignData codesign_data(p_preset, p_debug); - err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data); - memdelete(dylibs_dir); - ERR_FAIL_COND_V(err, err); if (ep.step("Making .xcarchive", 3)) { return ERR_SKIP; diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index c01983e39f..1db7418e3f 100644 --- a/platform/iphone/export/export_plugin.h +++ b/platform/iphone/export/export_plugin.h @@ -53,8 +53,6 @@ class EditorExportPlatformIOS : public EditorExportPlatform { GDCLASS(EditorExportPlatformIOS, EditorExportPlatform); - int version_code; - Ref<ImageTexture> logo; // Plugins @@ -65,7 +63,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<PluginConfigIOS> plugins; typedef Error (*FileHandler)(String p_file, void *p_userdata); - static Error _walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata); + static Error _walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata); static Error _codesign(String p_file, void *p_userdata); void _blend_and_rotate(Ref<Image> &p_dst, Ref<Image> &p_src, bool p_rot); @@ -141,7 +139,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } static void _check_for_changes_poll_thread(void *ud) { - EditorExportPlatformIOS *ea = (EditorExportPlatformIOS *)ud; + EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud); while (!ea->quit_request.is_set()) { // Nothing to do if we already know the plugins have changed. @@ -206,7 +204,7 @@ public: r_features->push_back("ios"); } - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override { } EditorExportPlatformIOS(); @@ -215,8 +213,8 @@ public: /// List the gdip files in the directory specified by the p_path parameter. static Vector<String> list_plugin_config_files(const String &p_path, bool p_check_directories) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); diff --git a/platform/iphone/godot_view_gesture_recognizer.mm b/platform/iphone/godot_view_gesture_recognizer.mm index c8137f35ff..49a92add5e 100644 --- a/platform/iphone/godot_view_gesture_recognizer.mm +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -70,6 +70,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.cancelsTouchesInView = YES; self.delaysTouchesBegan = YES; self.delaysTouchesEnded = YES; + self.requiresExclusiveTouchType = NO; self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); diff --git a/platform/iphone/ios.h b/platform/iphone/ios.h index 03e663b05c..0607d7b395 100644 --- a/platform/iphone/ios.h +++ b/platform/iphone/ios.h @@ -32,15 +32,26 @@ #define IOS_H #include "core/object/class_db.h" +#import <CoreHaptics/CoreHaptics.h> class iOS : public Object { GDCLASS(iOS, Object); static void _bind_methods(); +private: + CHHapticEngine *haptic_engine API_AVAILABLE(ios(13)) = nullptr; + + CHHapticEngine *get_haptic_engine_instance() API_AVAILABLE(ios(13)); + void start_haptic_engine(); + void stop_haptic_engine(); + public: static void alert(const char *p_alert, const char *p_title); + bool supports_haptic_engine(); + void vibrate_haptic_engine(float p_duration_seconds); + String get_model() const; String get_rate_url(int p_app_id) const; diff --git a/platform/iphone/ios.mm b/platform/iphone/ios.mm index ad1ea70c10..79baae028a 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -32,11 +32,110 @@ #import "app_delegate.h" #import "view_controller.h" + +#import <CoreHaptics/CoreHaptics.h> #import <UIKit/UIKit.h> #include <sys/sysctl.h> void iOS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rate_url", "app_id"), &iOS::get_rate_url); + ClassDB::bind_method(D_METHOD("supports_haptic_engine"), &iOS::supports_haptic_engine); + ClassDB::bind_method(D_METHOD("start_haptic_engine"), &iOS::start_haptic_engine); + ClassDB::bind_method(D_METHOD("stop_haptic_engine"), &iOS::stop_haptic_engine); +}; + +bool iOS::supports_haptic_engine() { + if (@available(iOS 13, *)) { + id<CHHapticDeviceCapability> capabilities = [CHHapticEngine capabilitiesForHardware]; + return capabilities.supportsHaptics; + } + + return false; +} + +CHHapticEngine *iOS::get_haptic_engine_instance() API_AVAILABLE(ios(13)) { + if (haptic_engine == nullptr) { + NSError *error = nullptr; + haptic_engine = [[CHHapticEngine alloc] initAndReturnError:&error]; + + if (!error) { + [haptic_engine setAutoShutdownEnabled:true]; + } else { + haptic_engine = nullptr; + NSLog(@"Could not initialize haptic engine: %@", error); + } + } + + return haptic_engine; +} + +void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) { + if (@available(iOS 13, *)) { // We need the @available check every time to make the compiler happy... + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + NSDictionary *hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds) + }, + }, + ], + }; + + NSError *error; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; + + [[haptic_engine createPlayerWithPattern:pattern error:&error] startAtTime:0 error:&error]; + + NSLog(@"Could not vibrate using haptic engine: %@", error); + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); +} + +void iOS::start_haptic_engine() { + if (@available(iOS 13, *)) { + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + [haptic_engine startWithCompletionHandler:^(NSError *returnedError) { + if (returnedError) { + NSLog(@"Could not start haptic engine: %@", returnedError); + } + }]; + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); +} + +void iOS::stop_haptic_engine() { + if (@available(iOS 13, *)) { + if (supports_haptic_engine()) { + CHHapticEngine *haptic_engine = get_haptic_engine_instance(); + if (haptic_engine) { + [haptic_engine stopWithCompletionHandler:^(NSError *returnedError) { + if (returnedError) { + NSLog(@"Could not stop haptic engine: %@", returnedError); + } + }]; + } + + return; + } + } + + NSLog(@"Haptic engine is not supported in this version of iOS"); } void iOS::alert(const char *p_alert, const char *p_title) { diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index 3281ff0cdb..d03403bbb4 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -52,11 +52,11 @@ private: AudioDriverCoreAudio audio_driver; - iOS *ios; + iOS *ios = nullptr; - JoypadIPhone *joypad_iphone; + JoypadIPhone *joypad_iphone = nullptr; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; virtual void initialize_core() override; virtual void initialize() override; @@ -92,7 +92,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index 56cb49318c..95b06b728e 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -204,12 +204,17 @@ void OSIPhone::finalize() { // MARK: Dynamic Libraries -Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { if (p_path.length() == 0) { p_library_handle = RTLD_SELF; + + if (r_resolved_path != nullptr) { + *r_resolved_path = p_path; + } + return OK; } - return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path); + return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path, r_resolved_path); } Error OSIPhone::close_dynamic_library(void *p_library_handle) { @@ -259,11 +264,9 @@ Error OSIPhone::shell_open(String p_uri) { } void OSIPhone::set_user_data_dir(String p_dir) { - DirAccess *da = DirAccess::open(p_dir); - + Ref<DirAccess> da = DirAccess::open(p_dir); user_data_dir = da->get_current_dir(); printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); - memdelete(da); } String OSIPhone::get_user_data_dir() const { @@ -300,8 +303,12 @@ String OSIPhone::get_processor_name() const { } void OSIPhone::vibrate_handheld(int p_duration_ms) { - // iOS does not support duration for vibration - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); + if (ios->supports_haptic_engine()) { + ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f); + } else { + // iOS <13 does not support duration for vibration + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); + } } bool OSIPhone::_check_internal_feature_support(const String &p_feature) { diff --git a/platform/iphone/tts_ios.h b/platform/iphone/tts_ios.h new file mode 100644 index 0000000000..064316b0b2 --- /dev/null +++ b/platform/iphone/tts_ios.h @@ -0,0 +1,63 @@ +/*************************************************************************/ +/* tts_ios.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TTS_IOS_H +#define TTS_IOS_H + +#if __has_include(<AVFAudio/AVSpeechSynthesis.h>) +#import <AVFAudio/AVSpeechSynthesis.h> +#else +#import <AVFoundation/AVFoundation.h> +#endif + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/rb_map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +@interface TTS_IOS : NSObject <AVSpeechSynthesizerDelegate> { + bool speaking; + HashMap<id, int> ids; + + AVSpeechSynthesizer *av_synth; + List<DisplayServer::TTSUtterance> queue; +} + +- (void)pauseSpeaking; +- (void)resumeSpeaking; +- (void)stopSpeaking; +- (bool)isSpeaking; +- (bool)isPaused; +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt; +- (Array)getVoices; +@end + +#endif // TTS_IOS_H diff --git a/platform/iphone/tts_ios.mm b/platform/iphone/tts_ios.mm new file mode 100644 index 0000000000..a079d02add --- /dev/null +++ b/platform/iphone/tts_ios.mm @@ -0,0 +1,164 @@ +/*************************************************************************/ +/* tts_ios.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "tts_ios.h" + +@implementation TTS_IOS + +- (id)init { + self = [super init]; + self->speaking = false; + self->av_synth = [[AVSpeechSynthesizer alloc] init]; + [self->av_synth setDelegate:self]; + print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized."); + return self; +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance { + NSString *string = [utterance speechString]; + + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos); +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +- (void)update { + if (!speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]]; + if (message.rate > 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)]; + } else if (message.rate < 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)]; + } + [new_utterance setPitchMultiplier:message.pitch]; + [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + + ids[new_utterance] = message.id; + [av_synth speakUtterance:new_utterance]; + + queue.pop_front(); + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + speaking = true; + } +} + +- (void)pauseSpeaking { + [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate]; +} + +- (void)resumeSpeaking { + [av_synth continueSpeaking]; +} + +- (void)stopSpeaking { + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + speaking = false; +} + +- (bool)isSpeaking { + return speaking || (queue.size() > 0); +} + +- (bool)isPaused { + return [av_synth isPaused]; +} + +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt { + if (interrupt) { + [self stopSpeaking]; + } + + if (text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = text; + message.voice = voice; + message.volume = CLAMP(volume, 0, 100); + message.pitch = CLAMP(pitch, 0.f, 2.f); + message.rate = CLAMP(rate, 0.1f, 10.f); + message.id = utterance_id; + queue.push_back(message); + + if ([self isPaused]) { + [self resumeSpeaking]; + } else { + [self update]; + } +} + +- (Array)getVoices { + Array list; + for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) { + NSString *voiceIdentifierString = [voice identifier]; + NSString *voiceLocaleIdentifier = [voice language]; + NSString *voiceName = [voice name]; + Dictionary voice_d; + voice_d["name"] = String::utf8([voiceName UTF8String]); + voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]); + voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + return list; +} + +@end diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 8d9ba82fd4..4827dc4627 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -37,6 +37,8 @@ for ext in env["JS_EXTERNS"]: build = [] if env["gdnative_enabled"]: build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] + if env["threads_enabled"]: + build_targets.append("#bin/godot${PROGSUFFIX}.worker.js") # Reset libraries. The main runtime will only link emscripten libraries, not godot ones. sys_env["LIBS"] = [] # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. @@ -58,7 +60,7 @@ if env["gdnative_enabled"]: wasm_env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"]) wasm_env.Append(LINKFLAGS=["-s", "SIDE_MODULE=2"]) wasm = wasm_env.add_program("#bin/godot.side${PROGSUFFIX}.wasm", javascript_files) - build = [sys[0], sys[1], wasm[0]] + build = sys + [wasm[0]] else: build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] if env["threads_enabled"]: @@ -87,5 +89,5 @@ wrap_list = [ js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js") # Extra will be the thread worker, or the GDNative side, or None -extra = build[2] if len(build) > 2 else None +extra = build[2:] if len(build) > 2 else None env.CreateTemplateZip(js_wrapped, build[1], extra) diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 4190b24b8e..46a0a816bf 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -37,8 +37,8 @@ static JavaScript *javascript_eval; void register_javascript_api() { JavaScriptToolsEditorPlugin::initialize(); - GDREGISTER_VIRTUAL_CLASS(JavaScriptObject); - GDREGISTER_VIRTUAL_CLASS(JavaScript); + GDREGISTER_ABSTRACT_CLASS(JavaScriptObject); + GDREGISTER_ABSTRACT_CLASS(JavaScript); javascript_eval = memnew(JavaScript); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval)); } diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index bea54ae1cb..1507f32375 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -64,8 +64,8 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { } String resource_path = ProjectSettings::get_singleton()->get_resource_path(); - FileAccess *src_f; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); // Name the downloaded ZIP file to contain the project name and download date for easier organization. // Replace characters not allowed (or risky) in Windows file names with safe characters. @@ -82,22 +82,22 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { const String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, nullptr); - FileAccess *f = FileAccess::open(output_path, FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Unable to create ZIP file."); - Vector<uint8_t> buf; - buf.resize(f->get_length()); - f->get_buffer(buf.ptrw(), buf.size()); - godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + { + Ref<FileAccess> f = FileAccess::open(output_path, FileAccess::READ); + ERR_FAIL_COND_MSG(f.is_null(), "Unable to create ZIP file."); + Vector<uint8_t> buf; + buf.resize(f->get_length()); + f->get_buffer(buf.ptrw(), buf.size()); + godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + } - f->close(); - memdelete(f); // Remove the temporary file since it was sent to the user's native filesystem as a download. DirAccess::remove_file_or_error(output_path); } void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { - FileAccess *f = FileAccess::open(p_path, FileAccess::READ); - if (!f) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { WARN_PRINT("Unable to open file for zipping: " + p_path); return; } @@ -105,8 +105,6 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z uint64_t len = f->get_length(); data.resize(len); f->get_buffer(data.ptrw(), len); - f->close(); - memdelete(f); String path = p_path.replace_first(p_base_path, ""); zipOpenNewFileInZip(p_zip, @@ -124,8 +122,8 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z } void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { - DirAccess *dir = DirAccess::open(p_path); - if (!dir) { + Ref<DirAccess> dir = DirAccess::open(p_path); + if (dir.is_null()) { WARN_PRINT("Unable to open directory for zipping: " + p_path); return; } diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index b6be44fbb2..a769260f01 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -48,11 +48,6 @@ def get_flags(): return [ ("tools", False), ("builtin_pcre2_with_jit", False), - # Disabling the mbedtls module reduces file size. - # The module has little use due to the limited networking functionality - # in this platform. For the available networking methods, the browser - # manages TLS. - ("module_mbedtls_enabled", False), ("vulkan", False), ] @@ -190,10 +185,6 @@ def configure(env): if env["javascript_eval"]: env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"]) - if env["threads_enabled"] and env["gdnative_enabled"]: - print("Threads and GDNative support can't be both enabled due to WebAssembly limitations") - sys.exit(255) - # Thread support (via SharedArrayBuffer). if env["threads_enabled"]: env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"]) @@ -206,12 +197,19 @@ def configure(env): env.Append(CPPDEFINES=["NO_THREADS"]) if env["gdnative_enabled"]: - major, minor, patch = get_compiler_version(env) - if major < 2 or (major == 2 and minor == 0 and patch < 10): - print("GDNative support requires emscripten >= 2.0.10, detected: %s.%s.%s" % (major, minor, patch)) + cc_version = get_compiler_version(env) + cc_semver = (int(cc_version["major"]), int(cc_version["minor"]), int(cc_version["patch"])) + if cc_semver < (2, 0, 10): + print("GDNative support requires emscripten >= 2.0.10, detected: %s.%s.%s" % cc_semver) + sys.exit(255) + + if env["threads_enabled"] and cc_semver < (3, 1, 14): + print("Threads and GDNative requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver) sys.exit(255) env.Append(CCFLAGS=["-s", "RELOCATABLE=1"]) env.Append(LINKFLAGS=["-s", "RELOCATABLE=1"]) + # Weak symbols are broken upstream: https://github.com/emscripten-core/emscripten/issues/12819 + env.Append(CPPDEFINES=["ZSTD_HAVE_WEAK_SYMBOLS=0"]) env.extra_suffix = ".gdnative" + env.extra_suffix # Reduce code size by generating less support code (e.g. skip NodeJS support). diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 2caf369354..a96c539a1f 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -34,7 +34,7 @@ #include "drivers/gles3/rasterizer_gles3.h" #endif #include "platform/javascript/os_javascript.h" -#include "servers/rendering/rasterizer_dummy.h" +#include "servers/rendering/dummy/rasterizer_dummy.h" #include <emscripten.h> #include <png.h> @@ -244,9 +244,9 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape case DisplayServer::CURSOR_CROSS: return "crosshair"; case DisplayServer::CURSOR_WAIT: - return "progress"; - case DisplayServer::CURSOR_BUSY: return "wait"; + case DisplayServer::CURSOR_BUSY: + return "progress"; case DisplayServer::CURSOR_DRAG: return "grab"; case DisplayServer::CURSOR_CAN_DROP: @@ -274,6 +274,90 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape } } +bool DisplayServerJavaScript::tts_is_speaking() const { + return godot_js_tts_is_speaking(); +} + +bool DisplayServerJavaScript::tts_is_paused() const { + return godot_js_tts_is_paused(); +} + +void DisplayServerJavaScript::update_voices_callback(int p_size, const char **p_voice) { + get_singleton()->voices.clear(); + for (int i = 0; i < p_size; i++) { + Vector<String> tokens = String::utf8(p_voice[i]).split(";", true, 2); + if (tokens.size() == 2) { + Dictionary voice_d; + voice_d["name"] = tokens[1]; + voice_d["id"] = tokens[1]; + voice_d["language"] = tokens[0]; + get_singleton()->voices.push_back(voice_d); + } + } +} + +Array DisplayServerJavaScript::tts_get_voices() const { + godot_js_tts_get_voices(update_voices_callback); + return voices; +} + +void DisplayServerJavaScript::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + if (p_interrupt) { + tts_stop(); + } + + if (p_text.is_empty()) { + tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + CharString string = p_text.utf8(); + utterance_ids[p_utterance_id] = string; + + godot_js_tts_speak(string.get_data(), p_voice.utf8().get_data(), CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, DisplayServerJavaScript::_js_utterance_callback); +} + +void DisplayServerJavaScript::tts_pause() { + godot_js_tts_pause(); +} + +void DisplayServerJavaScript::tts_resume() { + godot_js_tts_resume(); +} + +void DisplayServerJavaScript::tts_stop() { + for (const KeyValue<int, CharString> &E : utterance_ids) { + tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); + } + utterance_ids.clear(); + godot_js_tts_stop(); +} + +void DisplayServerJavaScript::_js_utterance_callback(int p_event, int p_id, int p_pos) { + DisplayServerJavaScript *ds = (DisplayServerJavaScript *)DisplayServer::get_singleton(); + if (ds->utterance_ids.has(p_id)) { + int pos = 0; + if ((TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { + // Convert position from UTF-8 to UTF-32. + const CharString &string = ds->utterance_ids[p_id]; + for (int i = 0; i < MIN(p_pos, string.length()); i++) { + uint8_t c = string[i]; + if ((c & 0xe0) == 0xc0) { + i += 1; + } else if ((c & 0xf0) == 0xe0) { + i += 2; + } else if ((c & 0xf8) == 0xf0) { + i += 3; + } + pos++; + } + } else if ((TTSUtteranceEvent)p_event != DisplayServer::TTS_UTTERANCE_STARTED) { + ds->utterance_ids.erase(p_id); + } + ds->tts_post_utterance_event((TTSUtteranceEvent)p_event, p_id, pos); + } +} + void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); if (cursor_shape == p_shape) { @@ -287,7 +371,7 @@ DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { return cursor_shape; } -void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerJavaScript::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { if (p_cursor.is_valid()) { Ref<Texture2D> texture = p_cursor; Ref<AtlasTexture> atlas_texture = p_cursor; @@ -755,6 +839,8 @@ bool DisplayServerJavaScript::has_feature(Feature p_feature) const { //case FEATURE_ORIENTATION: case FEATURE_VIRTUAL_KEYBOARD: return godot_js_display_vk_available() != 0; + case FEATURE_TEXT_TO_SPEECH: + return godot_js_display_tts_available() != 0; default: return false; } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index b50956d91c..79b0fbb652 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -55,6 +55,8 @@ private: EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_ctx = 0; #endif + HashMap<int, CharString> utterance_ids; + WindowMode window_mode = WINDOW_MODE_WINDOWED; ObjectID window_attached_instance_id = {}; @@ -66,6 +68,8 @@ private: String clipboard; Point2 touches[32]; + Array voices; + char canvas_id[256] = { 0 }; bool cursor_inside_canvas = true; CursorShape cursor_shape = CURSOR_ARROW; @@ -89,6 +93,7 @@ private: static void vk_input_text_callback(const char *p_text, int p_cursor); static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); void process_joypads(); + static void _js_utterance_callback(int p_event, int p_id, int p_pos); static Vector<String> get_rendering_drivers_func(); static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); @@ -97,6 +102,7 @@ private: static void request_quit_callback(); static void window_blur_callback(); + static void update_voices_callback(int p_size, const char **p_voice); static void update_clipboard_callback(const char *p_text); static void send_window_event_callback(int p_notification); static void drop_files_js_callback(char **p_filev, int p_filec); @@ -115,10 +121,20 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + // tts + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + // cursor virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; // mouse virtual void mouse_set_mode(MouseMode p_mode) override; diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index 4dad2d5204..3cb1d75e52 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -52,10 +52,10 @@ def create_template_zip(env, js, wasm, extra): ] # GDNative/Threads specific if env["gdnative_enabled"]: - in_files.append(extra) # Runtime + in_files.append(extra.pop()) # Runtime out_files.append(zip_dir.File(binary_name + ".side.wasm")) - elif env["threads_enabled"]: - in_files.append(extra) # Worker + if env["threads_enabled"]: + in_files.append(extra.pop()) # Worker out_files.append(zip_dir.File(binary_name + ".worker.js")) service_worker = "#misc/dist/html/service-worker.js" diff --git a/platform/javascript/export/export_plugin.cpp b/platform/javascript/export/export_plugin.cpp index c7e503732d..901580c140 100644 --- a/platform/javascript/export/export_plugin.cpp +++ b/platform/javascript/export/export_plugin.cpp @@ -33,17 +33,17 @@ #include "core/config/project_settings.h" Error EditorExportPlatformJavaScript::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) { - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io); if (!pkg) { - EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + p_template); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template)); return ERR_FILE_NOT_FOUND; } if (unzGoToFirstFile(pkg) != UNZ_OK) { - EditorNode::get_singleton()->show_warning(TTR("Invalid export template:") + "\n" + p_template); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template)); unzClose(pkg); return ERR_FILE_CORRUPT; } @@ -56,6 +56,11 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template String file = String::utf8(fname); + // Skip folders. + if (file.ends_with("/")) { + continue; + } + // Skip service worker and offline page if not exporting pwa. if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) { continue; @@ -70,14 +75,13 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template //write String dst = p_dir.plus_file(file.replace("godot", p_name)); - FileAccess *f = FileAccess::open(dst, FileAccess::WRITE); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst); + Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst)); unzClose(pkg); return ERR_FILE_CANT_WRITE; } f->store_buffer(data.ptr(), data.size()); - memdelete(f); } while (unzGoToNextFile(pkg) == UNZ_OK); unzClose(pkg); @@ -85,17 +89,16 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template } Error EditorExportPlatformJavaScript::_write_or_error(const uint8_t *p_content, int p_size, String p_path) { - FileAccess *f = FileAccess::open(p_path, FileAccess::WRITE); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path)); return ERR_FILE_CANT_WRITE; } f->store_buffer(p_content, p_size); - memdelete(f); return OK; } -void EditorExportPlatformJavaScript::_replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template) { +void EditorExportPlatformJavaScript::_replace_strings(HashMap<String, String> p_replaces, Vector<uint8_t> &r_template) { String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size()); String out; Vector<String> lines = str_template.split("\n"); @@ -147,7 +150,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re // Replaces HTML string const String str_config = Variant(config).to_json_string(); const String custom_head_include = p_preset->get("html/head_include"); - Map<String, String> replaces; + HashMap<String, String> replaces; replaces["$GODOT_URL"] = p_name + ".js"; replaces["$GODOT_PROJECT_NAME"] = ProjectSettings::get_singleton()->get_setting("application/config/name"); replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include; @@ -165,7 +168,7 @@ Error EditorExportPlatformJavaScript::_add_manifest_icon(const String &p_path, c icon.instantiate(); const Error err = ImageLoader::load_image(p_icon, icon); if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + p_icon); + add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon)); return err; } if (icon->get_width() != p_size || icon->get_height() != p_size) { @@ -177,7 +180,7 @@ Error EditorExportPlatformJavaScript::_add_manifest_icon(const String &p_path, c } const Error err = icon->save_png(icon_dest); if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + icon_dest); + add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest)); return err; } Dictionary icon_dict; @@ -198,7 +201,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & const String dir = p_path.get_base_dir(); const String name = p_path.get_file().get_basename(); const ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); - Map<String, String> replaces; + HashMap<String, String> replaces; replaces["@GODOT_VERSION@"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec()); replaces["@GODOT_NAME@"] = proj_name.substr(0, 16); replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html"; @@ -212,7 +215,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & cache_files.push_back(name + ".icon.png"); cache_files.push_back(name + ".apple-touch-icon.png"); } - if (mode == EXPORT_MODE_THREADS) { + if (mode & EXPORT_MODE_THREADS) { cache_files.push_back(name + ".worker.js"); cache_files.push_back(name + ".audio.worklet.js"); } @@ -222,7 +225,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & Array opt_cache_files; opt_cache_files.push_back(name + ".wasm"); opt_cache_files.push_back(name + ".pck"); - if (mode == EXPORT_MODE_GDNATIVE) { + if (mode & EXPORT_MODE_GDNATIVE) { opt_cache_files.push_back(name + ".side.wasm"); for (int i = 0; i < p_shared_objects.size(); i++) { opt_cache_files.push_back(p_shared_objects[i].path.get_file()); @@ -233,15 +236,13 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & const String sw_path = dir.plus_file(name + ".service.worker.js"); Vector<uint8_t> sw; { - FileAccess *f = FileAccess::open(sw_path, FileAccess::READ); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + sw_path); + Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path)); return ERR_FILE_CANT_READ; } sw.resize(f->get_length()); f->get_buffer(sw.ptrw(), sw.size()); - memdelete(f); - f = nullptr; } _replace_strings(replaces, sw); Error err = _write_or_error(sw.ptr(), sw.size(), dir.plus_file(name + ".service.worker.js")); @@ -252,11 +253,11 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & // Custom offline page const String offline_page = p_preset->get("progressive_web_app/offline_page"); if (!offline_page.is_empty()) { - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); const String offline_dest = dir.plus_file(name + ".offline.html"); err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest); if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + offline_dest); + add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest)); return err; } } @@ -316,9 +317,10 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP } } ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); - if (mode == EXPORT_MODE_THREADS) { + if (mode & EXPORT_MODE_THREADS) { r_features->push_back("threads"); - } else if (mode == EXPORT_MODE_GDNATIVE) { + } + if (mode & EXPORT_MODE_GDNATIVE) { r_features->push_back("wasm32"); } } @@ -360,6 +362,15 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const { } bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +#ifndef DEV_ENABLED + // We don't provide export templates for the HTML5 platform currently as there + // is no suitable renderer to use with them. So we forbid exporting and tell + // users why. This is skipped in DEV_ENABLED so that contributors can still test + // the pipeline once we start having WebGL or WebGPU support. + r_error = "The HTML5 platform is currently not supported in Godot 4.0, as there is no suitable renderer for it.\n"; + return false; +#endif + String err; bool valid = false; ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); @@ -433,30 +444,30 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } if (!template_path.is_empty() && !FileAccess::exists(template_path)) { - EditorNode::get_singleton()->show_warning(TTR("Template file not found:") + "\n" + template_path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path)); return ERR_FILE_NOT_FOUND; } // Export pck and shared objects Vector<SharedObject> shared_objects; String pck_path = base_path + ".pck"; - Error error = save_pack(p_preset, pck_path, &shared_objects); + Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects); if (error != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + pck_path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path)); return error; } - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - for (int i = 0; i < shared_objects.size(); i++) { - String dst = base_dir.plus_file(shared_objects[i].path.get_file()); - error = da->copy(shared_objects[i].path, dst); - if (error != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + shared_objects[i].path.get_file()); - memdelete(da); - return error; + + { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < shared_objects.size(); i++) { + String dst = base_dir.plus_file(shared_objects[i].path.get_file()); + error = da->copy(shared_objects[i].path, dst); + if (error != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file())); + return error; + } } } - memdelete(da); - da = nullptr; // Extract templates. error = _extract_template(template_path, base_dir, base_name, pwa); @@ -466,32 +477,25 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar). Dictionary file_sizes; - FileAccess *f = nullptr; - f = FileAccess::open(pck_path, FileAccess::READ); - if (f) { + Ref<FileAccess> f = FileAccess::open(pck_path, FileAccess::READ); + if (f.is_valid()) { file_sizes[pck_path.get_file()] = (uint64_t)f->get_length(); - memdelete(f); - f = nullptr; } f = FileAccess::open(base_path + ".wasm", FileAccess::READ); - if (f) { + if (f.is_valid()) { file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length(); - memdelete(f); - f = nullptr; } // Read the HTML shell file (custom or from template). const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html; Vector<uint8_t> html; f = FileAccess::open(html_path, FileAccess::READ); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Could not read HTML shell:") + "\n" + html_path); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path)); return ERR_FILE_CANT_READ; } html.resize(f->get_length()); f->get_buffer(html.ptrw(), html.size()); - memdelete(f); - f = nullptr; // Generate HTML file with replaced strings. _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes); @@ -505,7 +509,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese Ref<Image> splash = _get_project_splash(); const String splash_png_path = base_path + ".png"; if (splash->save_png(splash_png_path) != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + splash_png_path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path)); return ERR_FILE_CANT_WRITE; } @@ -515,13 +519,13 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese Ref<Image> favicon = _get_project_icon(); const String favicon_png_path = base_path + ".icon.png"; if (favicon->save_png(favicon_png_path) != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + favicon_png_path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path)); return ERR_FILE_CANT_WRITE; } favicon->resize(180, 180); const String apple_icon_png_path = base_path + ".apple-touch-icon.png"; if (favicon->save_png(apple_icon_png_path) != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + apple_icon_png_path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path)); return ERR_FILE_CANT_WRITE; } } @@ -577,14 +581,15 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese } const String dest = EditorPaths::get_singleton()->get_cache_dir().plus_file("web"); - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { Error err = da->make_dir_recursive(dest); if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Could not create HTTP server directory:") + "\n" + dest); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest)); return err; } } + const String basepath = dest.plus_file("tmp_js_export"); Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags); if (err != OK) { @@ -627,7 +632,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese err = server->listen(bind_port, bind_ip, use_ssl, ssl_key, ssl_cert); } if (err != OK) { - EditorNode::get_singleton()->show_warning(TTR("Error starting HTTP server:") + "\n" + itos(err)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err)); return err; } @@ -642,7 +647,7 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const { } void EditorExportPlatformJavaScript::_server_thread_poll(void *data) { - EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data; + EditorExportPlatformJavaScript *ej = static_cast<EditorExportPlatformJavaScript *>(data); while (!ej->server_quit) { OS::get_singleton()->delay_usec(6900); { diff --git a/platform/javascript/export/export_plugin.h b/platform/javascript/export/export_plugin.h index d17fd2f674..1aaec5454d 100644 --- a/platform/javascript/export/export_plugin.h +++ b/platform/javascript/export/export_plugin.h @@ -61,19 +61,16 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform { EXPORT_MODE_NORMAL = 0, EXPORT_MODE_THREADS = 1, EXPORT_MODE_GDNATIVE = 2, + EXPORT_MODE_THREADS_GDNATIVE = 3, }; String _get_template_name(ExportMode p_mode, bool p_debug) const { String name = "webassembly"; - switch (p_mode) { - case EXPORT_MODE_THREADS: - name += "_threads"; - break; - case EXPORT_MODE_GDNATIVE: - name += "_gdnative"; - break; - default: - break; + if (p_mode & EXPORT_MODE_GDNATIVE) { + name += "_gdnative"; + } + if (p_mode & EXPORT_MODE_THREADS) { + name += "_threads"; } if (p_debug) { name += "_debug.zip"; @@ -104,7 +101,7 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform { } Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa); - void _replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template); + void _replace_strings(HashMap<String, String> p_replaces, Vector<uint8_t> &r_template); void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes); Error _add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr); Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects); @@ -138,7 +135,7 @@ public: r_features->push_back(get_os_name().to_lower()); } - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override { } String get_debug_protocol() const override { return "ws://"; } diff --git a/platform/javascript/export/export_server.h b/platform/javascript/export/export_server.h index 1b908ad9b0..a831b76076 100644 --- a/platform/javascript/export/export_server.h +++ b/platform/javascript/export/export_server.h @@ -41,7 +41,7 @@ class EditorHTTPServer : public RefCounted { private: Ref<TCPServer> server; - Map<String, String> mimes; + HashMap<String, String> mimes; Ref<StreamPeerTCP> tcp; Ref<StreamPeerSSL> ssl; Ref<StreamPeer> peer; @@ -153,8 +153,8 @@ public: } const String ctype = mimes[req_ext]; - FileAccess *f = FileAccess::open(filepath, FileAccess::READ); - ERR_FAIL_COND(!f); + Ref<FileAccess> f = FileAccess::open(filepath, FileAccess::READ); + ERR_FAIL_COND(f.is_null()); String s = "HTTP/1.1 200 OK\r\n"; s += "Connection: Close\r\n"; s += "Content-Type: " + ctype + "\r\n"; @@ -166,7 +166,6 @@ public: CharString cs = s.utf8(); Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); if (err != OK) { - memdelete(f); ERR_FAIL(); } @@ -178,11 +177,9 @@ public: } err = peer->put_data(bytes, read); if (err != OK) { - memdelete(f); ERR_FAIL(); } } - memdelete(f); } void poll() { diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h index 2cb5c3025c..1a383c9799 100644 --- a/platform/javascript/godot_js.h +++ b/platform/javascript/godot_js.h @@ -67,6 +67,15 @@ extern int godot_js_input_gamepad_sample_get(int p_idx, float r_btns[16], int32_ extern void godot_js_input_paste_cb(void (*p_callback)(const char *p_text)); extern void godot_js_input_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); +// TTS +extern int godot_js_tts_is_speaking(); +extern int godot_js_tts_is_paused(); +extern int godot_js_tts_get_voices(void (*p_callback)(int p_size, const char **p_voices)); +extern void godot_js_tts_speak(const char *p_text, const char *p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, void (*p_callback)(int p_event, int p_id, int p_pos)); +extern void godot_js_tts_pause(); +extern void godot_js_tts_resume(); +extern void godot_js_tts_stop(); + // Display extern int godot_js_display_screen_dpi_get(); extern double godot_js_display_pixel_ratio_get(); @@ -109,6 +118,7 @@ extern void godot_js_display_notification_cb(void (*p_callback)(int p_notificati // Display Virtual Keyboard extern int godot_js_display_vk_available(); +extern int godot_js_display_tts_available(); extern void godot_js_display_vk_cb(void (*p_input)(const char *p_text, int p_cursor)); extern void godot_js_display_vk_show(const char *p_text, int p_multiline, int p_start, int p_end); extern void godot_js_display_vk_hide(); diff --git a/platform/javascript/godot_webgl2.h b/platform/javascript/godot_webgl2.h new file mode 100644 index 0000000000..7c357ff66d --- /dev/null +++ b/platform/javascript/godot_webgl2.h @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* godot_webgl2.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 GODOT_WEBGL2_H +#define GODOT_WEBGL2_H + +#include "GLES3/gl3.h" +#include "webgl/webgl2.h" + +#endif diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index c946302862..32bdfed4c7 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -76,7 +76,7 @@ void HTTPClientJavaScript::set_connection(const Ref<StreamPeer> &p_connection) { } Ref<StreamPeer> HTTPClientJavaScript::get_connection() const { - ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform."); + ERR_FAIL_V_MSG(Ref<RefCounted>(), "Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform."); } Error HTTPClientJavaScript::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len) { diff --git a/platform/javascript/javascript_singleton.cpp b/platform/javascript/javascript_singleton.cpp index eb5c02822f..8dc7aba5f8 100644 --- a/platform/javascript/javascript_singleton.cpp +++ b/platform/javascript/javascript_singleton.cpp @@ -81,7 +81,7 @@ protected: public: Variant getvar(const Variant &p_key, bool *r_valid = nullptr) const override; void setvar(const Variant &p_key, const Variant &p_value, bool *r_valid = nullptr) override; - Variant call(const StringName &p_method, const Variant **p_args, int p_argc, Callable::CallError &r_error) override; + Variant callp(const StringName &p_method, const Variant **p_args, int p_argc, Callable::CallError &r_error) override; JavaScriptObjectImpl() {} JavaScriptObjectImpl(int p_id) { _js_id = p_id; } ~JavaScriptObjectImpl() { @@ -207,7 +207,7 @@ int JavaScriptObjectImpl::_variant2js(const void **p_args, int p_pos, godot_js_w break; case Variant::INT: { const int64_t tmp = v->operator int64_t(); - if (tmp >= 1 << 31) { + if (tmp >= 1LL << 31) { r_val->r = (double)tmp; return Variant::FLOAT; } @@ -231,7 +231,7 @@ int JavaScriptObjectImpl::_variant2js(const void **p_args, int p_pos, godot_js_w return type; } -Variant JavaScriptObjectImpl::call(const StringName &p_method, const Variant **p_args, int p_argc, Callable::CallError &r_error) { +Variant JavaScriptObjectImpl::callp(const StringName &p_method, const Variant **p_args, int p_argc, Callable::CallError &r_error) { godot_js_wrapper_ex exchange; const String method = p_method; void *lock = nullptr; diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index 2bdf4e56ad..5997631bf8 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -330,6 +330,91 @@ const GodotDisplay = { return 0; }, + godot_js_tts_is_speaking__sig: 'i', + godot_js_tts_is_speaking: function () { + return window.speechSynthesis.speaking; + }, + + godot_js_tts_is_paused__sig: 'i', + godot_js_tts_is_paused: function () { + return window.speechSynthesis.paused; + }, + + godot_js_tts_get_voices__sig: 'vi', + godot_js_tts_get_voices: function (p_callback) { + const func = GodotRuntime.get_func(p_callback); + try { + const arr = []; + const voices = window.speechSynthesis.getVoices(); + for (let i = 0; i < voices.length; i++) { + arr.push(`${voices[i].lang};${voices[i].name}`); + } + const c_ptr = GodotRuntime.allocStringArray(arr); + func(arr.length, c_ptr); + GodotRuntime.freeStringArray(c_ptr, arr.length); + } catch (e) { + // Fail graciously. + } + }, + + godot_js_tts_speak__sig: 'viiiffii', + godot_js_tts_speak: function (p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) { + const func = GodotRuntime.get_func(p_callback); + + function listener_end(evt) { + evt.currentTarget.cb(1 /*TTS_UTTERANCE_ENDED*/, evt.currentTarget.id, 0); + } + + function listener_start(evt) { + evt.currentTarget.cb(0 /*TTS_UTTERANCE_STARTED*/, evt.currentTarget.id, 0); + } + + function listener_error(evt) { + evt.currentTarget.cb(2 /*TTS_UTTERANCE_CANCELED*/, evt.currentTarget.id, 0); + } + + function listener_bound(evt) { + evt.currentTarget.cb(3 /*TTS_UTTERANCE_BOUNDARY*/, evt.currentTarget.id, evt.charIndex); + } + + const utterance = new SpeechSynthesisUtterance(GodotRuntime.parseString(p_text)); + utterance.rate = p_rate; + utterance.pitch = p_pitch; + utterance.volume = p_volume / 100.0; + utterance.addEventListener('end', listener_end); + utterance.addEventListener('start', listener_start); + utterance.addEventListener('error', listener_error); + utterance.addEventListener('boundary', listener_bound); + utterance.id = p_utterance_id; + utterance.cb = func; + const voice = GodotRuntime.parseString(p_voice); + const voices = window.speechSynthesis.getVoices(); + for (let i = 0; i < voices.length; i++) { + if (voices[i].name === voice) { + utterance.voice = voices[i]; + break; + } + } + window.speechSynthesis.resume(); + window.speechSynthesis.speak(utterance); + }, + + godot_js_tts_pause__sig: 'v', + godot_js_tts_pause: function () { + window.speechSynthesis.pause(); + }, + + godot_js_tts_resume__sig: 'v', + godot_js_tts_resume: function () { + window.speechSynthesis.resume(); + }, + + godot_js_tts_stop__sig: 'v', + godot_js_tts_stop: function () { + window.speechSynthesis.cancel(); + window.speechSynthesis.resume(); + }, + godot_js_display_alert__sig: 'vi', godot_js_display_alert: function (p_text) { window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert @@ -377,6 +462,7 @@ const GodotDisplay = { GodotRuntime.setHeapValue(height, window.screen.height * scale, 'i32'); }, + godot_js_display_window_size_get__sig: 'vii', godot_js_display_window_size_get: function (p_width, p_height) { GodotRuntime.setHeapValue(p_width, GodotConfig.canvas.width, 'i32'); GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32'); @@ -625,6 +711,11 @@ const GodotDisplay = { return GodotDisplayVK.available(); }, + godot_js_display_tts_available__sig: 'i', + godot_js_display_tts_available: function () { + return 'speechSynthesis' in window; + }, + godot_js_display_vk_cb__sig: 'vi', godot_js_display_vk_cb: function (p_input_cb) { const input_cb = GodotRuntime.get_func(p_input_cb); diff --git a/platform/javascript/js/libs/library_godot_fetch.js b/platform/javascript/js/libs/library_godot_fetch.js index 007e7b70f5..285e50a035 100644 --- a/platform/javascript/js/libs/library_godot_fetch.js +++ b/platform/javascript/js/libs/library_godot_fetch.js @@ -89,7 +89,6 @@ const GodotFetch = { method: method, headers: headers, body: body, - credentials: 'include', }; obj.request = fetch(url, init); obj.request.then(GodotFetch.onresponse.bind(null, id)).catch(GodotFetch.onerror.bind(null, id)); diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 12d06a8d51..377eec3234 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -305,7 +305,9 @@ const GodotOS = { godot_js_os_hw_concurrency_get__sig: 'i', godot_js_os_hw_concurrency_get: function () { - return navigator.hardwareConcurrency || 1; + // TODO Godot core needs fixing to avoid spawning too many threads (> 24). + const concurrency = navigator.hardwareConcurrency || 1; + return concurrency < 2 ? concurrency : 2; }, godot_js_os_download_buffer__sig: 'viiii', diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index da88ea18b0..1686353229 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -131,6 +131,10 @@ int OS_JavaScript::get_process_id() const { ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform."); } +bool OS_JavaScript::is_process_running(const ProcessID &p_pid) const { + return false; +} + int OS_JavaScript::get_processor_count() const { return godot_js_os_hw_concurrency_get(); } @@ -221,10 +225,15 @@ bool OS_JavaScript::is_userfs_persistent() const { return idb_available; } -Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = p_path.get_file(); p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror()); + + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 9e272f9aa1..0c672111cc 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -79,6 +79,7 @@ public: Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; Error kill(const ProcessID &p_pid) override; int get_process_id() const override; + bool is_process_running(const ProcessID &p_pid) const override; int get_processor_count() const override; int get_default_thread_pool_size() const override { return 1; } @@ -99,7 +100,7 @@ public: void alert(const String &p_alert, const String &p_title = "ALERT!") override; - Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override; + Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; void resume_audio(); diff --git a/platform/javascript/package-lock.json b/platform/javascript/package-lock.json index 35f864f01a..f8c67b206f 100644 --- a/platform/javascript/package-lock.json +++ b/platform/javascript/package-lock.json @@ -109,6 +109,28 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, "node_modules/@zeit/schemas": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz", @@ -658,10 +680,13 @@ } }, "node_modules/entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/error-ex": { "version": "1.3.2", @@ -1637,34 +1662,35 @@ } }, "node_modules/js2xmlparser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz", - "integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", "dev": true, "dependencies": { - "xmlcreate": "^2.0.3" + "xmlcreate": "^2.0.4" } }, "node_modules/jsdoc": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.7.tgz", - "integrity": "sha512-sxKt7h0vzCd+3Y81Ey2qinupL6DpRSZJclS04ugHDNmRUXGzqicMJ6iwayhSA0S0DwwX30c5ozyUthr1QKF6uw==", + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.10.tgz", + "integrity": "sha512-IdQ8ppSo5LKZ9o3M+LKIIK8i00DIe5msDvG3G81Km+1dhy0XrOWD0Ji8H61ElgyEj/O9KRLokgKbAM9XX9CJAg==", "dev": true, "dependencies": { "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.1", - "klaw": "^3.0.0", - "markdown-it": "^10.0.0", - "markdown-it-anchor": "^5.2.7", - "marked": "^2.0.3", + "js2xmlparser": "^4.0.2", + "klaw": "^4.0.1", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "taffydb": "2.6.2", - "underscore": "~1.13.1" + "underscore": "~1.13.2" }, "bin": { "jsdoc": "jsdoc.js" @@ -1713,12 +1739,12 @@ } }, "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-4.0.1.tgz", + "integrity": "sha512-pgsE40/SvC7st04AHiISNewaIMUbY5V/K8b21ekiPiFoYs/EYSdsGa+FJArB1d441uq4Q8zZyIxvAzkGNlBdRw==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.9" + "engines": { + "node": ">=14.14.0" } }, "node_modules/levn": { @@ -1735,9 +1761,9 @@ } }, "node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "dependencies": { "uc.micro": "^1.0.1" @@ -1808,14 +1834,14 @@ } }, "node_modules/markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, @@ -1824,24 +1850,31 @@ } }, "node_modules/markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", + "version": "8.6.4", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", + "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", "dev": true, "peerDependencies": { + "@types/markdown-it": "*", "markdown-it": "*" } }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/marked": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.7.tgz", - "integrity": "sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.16.tgz", + "integrity": "sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA==", "dev": true, "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 8.16.2" + "node": ">= 12" } }, "node_modules/mdurl": { @@ -1884,9 +1917,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/mkdirp": { @@ -2834,9 +2867,9 @@ } }, "node_modules/underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", + "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==", "dev": true }, "node_modules/update-check": { @@ -2992,9 +3025,9 @@ "dev": true }, "node_modules/xmlcreate": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", - "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "dev": true }, "node_modules/yallist": { @@ -3079,6 +3112,28 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, "@zeit/schemas": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz", @@ -3493,9 +3548,9 @@ } }, "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true }, "error-ex": { @@ -4239,34 +4294,35 @@ } }, "js2xmlparser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz", - "integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", "dev": true, "requires": { - "xmlcreate": "^2.0.3" + "xmlcreate": "^2.0.4" } }, "jsdoc": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.7.tgz", - "integrity": "sha512-sxKt7h0vzCd+3Y81Ey2qinupL6DpRSZJclS04ugHDNmRUXGzqicMJ6iwayhSA0S0DwwX30c5ozyUthr1QKF6uw==", + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.10.tgz", + "integrity": "sha512-IdQ8ppSo5LKZ9o3M+LKIIK8i00DIe5msDvG3G81Km+1dhy0XrOWD0Ji8H61ElgyEj/O9KRLokgKbAM9XX9CJAg==", "dev": true, "requires": { "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.1", - "klaw": "^3.0.0", - "markdown-it": "^10.0.0", - "markdown-it-anchor": "^5.2.7", - "marked": "^2.0.3", + "js2xmlparser": "^4.0.2", + "klaw": "^4.0.1", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "taffydb": "2.6.2", - "underscore": "~1.13.1" + "underscore": "~1.13.2" }, "dependencies": { "escape-string-regexp": { @@ -4305,13 +4361,10 @@ } }, "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-4.0.1.tgz", + "integrity": "sha512-pgsE40/SvC7st04AHiISNewaIMUbY5V/K8b21ekiPiFoYs/EYSdsGa+FJArB1d441uq4Q8zZyIxvAzkGNlBdRw==", + "dev": true }, "levn": { "version": "0.4.1", @@ -4324,9 +4377,9 @@ } }, "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", "dev": true, "requires": { "uc.micro": "^1.0.1" @@ -4388,29 +4441,37 @@ } }, "markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "dev": true, "requires": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + } } }, "markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", + "version": "8.6.4", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", + "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", "dev": true, "requires": {} }, "marked": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.7.tgz", - "integrity": "sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.16.tgz", + "integrity": "sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA==", "dev": true }, "mdurl": { @@ -4444,9 +4505,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { @@ -5188,9 +5249,9 @@ } }, "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", + "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==", "dev": true }, "update-check": { @@ -5315,9 +5376,9 @@ "dev": true }, "xmlcreate": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", - "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "dev": true }, "yallist": { diff --git a/platform/javascript/package.json b/platform/javascript/package.json index 2ff1544837..8c38bc89e8 100644 --- a/platform/javascript/package.json +++ b/platform/javascript/package.json @@ -15,7 +15,7 @@ "format:libs": "npm run lint:libs -- --fix", "format:modules": "npm run lint:modules -- --fix", "format:tools": "npm run lint:tools -- --fix", - "serve": "serve" + "serve": "serve" }, "author": "Godot Engine contributors", "license": "MIT", diff --git a/platform/javascript/platform_config.h b/platform/javascript/platform_config.h index ba1b0d459e..1970fe0fa0 100644 --- a/platform/javascript/platform_config.h +++ b/platform/javascript/platform_config.h @@ -29,3 +29,5 @@ /*************************************************************************/ #include <alloca.h> + +#define OPENGL_INCLUDE_H "platform/javascript/godot_webgl2.h" diff --git a/platform/linuxbsd/SCsub b/platform/linuxbsd/SCsub index cec8706fbc..09a432eae2 100644 --- a/platform/linuxbsd/SCsub +++ b/platform/linuxbsd/SCsub @@ -20,6 +20,9 @@ if "x11" in env and env["x11"]: "key_mapping_x11.cpp", ] +if "speechd" in env and env["speechd"]: + common_linuxbsd.append(["speechd-so_wrap.c", "tts_linux.cpp"]) + if "vulkan" in env and env["vulkan"]: common_linuxbsd.append("vulkan_context_x11.cpp") diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index b4ec7924f6..33da094860 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" @@ -61,21 +62,22 @@ static void handle_crash(int sig) { msg = proj_settings->get("debug/settings/crash_handler/message"); } - // Dump the backtrace to stderr with a message to the user - fprintf(stderr, "\n================================================================\n"); - fprintf(stderr, "%s: Program crashed with signal %d\n", __FUNCTION__, sig); - + // Tell MainLoop about the crash. This can be handled by users too in Node. if (OS::get_singleton()->get_main_loop()) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Dump the backtrace to stderr with a message to the user + print_error("\n================================================================"); + print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, sig)); + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. if (String(VERSION_HASH).is_empty()) { - fprintf(stderr, "Engine version: %s\n", VERSION_FULL_NAME); + print_error(vformat("Engine version: %s", VERSION_FULL_NAME)); } else { - fprintf(stderr, "Engine version: %s (%s)\n", VERSION_FULL_NAME, VERSION_HASH); + print_error(vformat("Engine version: %s (%s)", VERSION_FULL_NAME, VERSION_HASH)); } - fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); + print_error(vformat("Dumping the backtrace. %s", msg)); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { for (size_t i = 1; i < size; i++) { @@ -117,13 +119,13 @@ static void handle_crash(int sig) { output = output.substr(0, output.length() - 1); } - fprintf(stderr, "[%ld] %s (%s)\n", (long int)i, fname, output.utf8().get_data()); + print_error(vformat("[%d] %s (%s)", (int64_t)i, fname, output)); } free(strings); } - fprintf(stderr, "-- END OF BACKTRACE --\n"); - fprintf(stderr, "================================================================\n"); + print_error("-- END OF BACKTRACE --"); + print_error("================================================================"); // Abort to pass the error to the OS abort(); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index ab643b254a..19cf341c85 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -75,6 +75,7 @@ def get_opts(): BoolVariable("use_msan", "Use LLVM compiler memory sanitizer (MSAN)", False), BoolVariable("pulseaudio", "Detect and use PulseAudio", True), BoolVariable("dbus", "Detect and use D-Bus to handle screensaver", True), + BoolVariable("speechd", "Detect and use Speech Dispatcher for Text-to-Speech support", True), BoolVariable("udev", "Use udev for gamepad connection callbacks", True), BoolVariable("x11", "Enable X11 display", True), BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True), @@ -115,7 +116,7 @@ def configure(env): ## Architecture - is64 = sys.maxsize > 2 ** 32 + is64 = sys.maxsize > 2**32 if env["bits"] == "default": env["bits"] = "64" if is64 else "32" @@ -162,6 +163,7 @@ def configure(env): if env["use_ubsan"] or env["use_asan"] or env["use_lsan"] or env["use_tsan"] or env["use_msan"]: env.extra_suffix += ".san" + env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"]) if env["use_ubsan"]: env.Append( @@ -261,27 +263,6 @@ def configure(env): if not env["builtin_libpng"]: env.ParseConfig("pkg-config libpng16 --cflags --libs") - if not env["builtin_bullet"]: - # We need at least version 2.90 - min_bullet_version = "2.90" - - import subprocess - - bullet_version = subprocess.check_output(["pkg-config", "bullet", "--modversion"]).strip() - if str(bullet_version) < min_bullet_version: - # Abort as system bullet was requested but too old - print( - "Bullet: System version {0} does not match minimal requirements ({1}). Aborting.".format( - bullet_version, min_bullet_version - ) - ) - sys.exit(255) - env.ParseConfig("pkg-config bullet --cflags --libs") - - if False: # not env['builtin_assimp']: - # FIXME: Add min version check - env.ParseConfig("pkg-config assimp --cflags --libs") - if not env["builtin_enet"]: env.ParseConfig("pkg-config libenet --cflags --libs") @@ -339,29 +320,41 @@ def configure(env): if os.system("pkg-config --exists alsa") == 0: # 0 means found env["alsa"] = True env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) + env.ParseConfig("pkg-config alsa --cflags") # Only cflags, we dlopen the library. else: print("Warning: ALSA libraries not found. Disabling the ALSA audio driver.") if env["pulseaudio"]: if os.system("pkg-config --exists libpulse") == 0: # 0 means found env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) - env.ParseConfig("pkg-config --cflags libpulse") + env.ParseConfig("pkg-config libpulse --cflags") # Only cflags, we dlopen the library. else: + env["pulseaudio"] = False print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") if env["dbus"]: if os.system("pkg-config --exists dbus-1") == 0: # 0 means found env.Append(CPPDEFINES=["DBUS_ENABLED"]) - env.ParseConfig("pkg-config --cflags --libs dbus-1") + env.ParseConfig("pkg-config dbus-1 --cflags --libs") else: print("Warning: D-Bus development libraries not found. Disabling screensaver prevention.") + if env["speechd"]: + if os.system("pkg-config --exists speech-dispatcher") == 0: # 0 means found + env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) + env.ParseConfig("pkg-config speech-dispatcher --cflags") # Only cflags, we dlopen the library. + else: + env["speechd"] = False + print("Warning: Speech Dispatcher development libraries not found. Disabling Text-to-Speech support.") + if platform.system() == "Linux": env.Append(CPPDEFINES=["JOYDEV_ENABLED"]) if env["udev"]: if os.system("pkg-config --exists libudev") == 0: # 0 means found env.Append(CPPDEFINES=["UDEV_ENABLED"]) + env.ParseConfig("pkg-config libudev --cflags") # Only cflags, we dlopen the library. else: + env["udev"] = False print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") else: env["udev"] = False # Linux specific @@ -386,11 +379,11 @@ def configure(env): if not env["use_volk"]: env.ParseConfig("pkg-config vulkan --cflags --libs") if not env["builtin_glslang"]: - # No pkgconfig file for glslang so far + # No pkgconfig file so far, hardcode expected lib name. env.Append(LIBS=["glslang", "SPIRV"]) env.Append(CPPDEFINES=["GLES3_ENABLED"]) - env.Append(LIBS=["GL"]) + env.ParseConfig("pkg-config gl --cflags --libs") env.Append(LIBS=["pthread"]) diff --git a/platform/linuxbsd/detect_prime_x11.cpp b/platform/linuxbsd/detect_prime_x11.cpp index 63531d33fa..42b7f68a5e 100644 --- a/platform/linuxbsd/detect_prime_x11.cpp +++ b/platform/linuxbsd/detect_prime_x11.cpp @@ -55,7 +55,7 @@ typedef GLXContext (*GLXCREATECONTEXTATTRIBSARBPROC)(Display *, GLXFBConfig, GLXContext, Bool, const int *); struct vendor { - const char *glxvendor; + const char *glxvendor = nullptr; int priority = 0; }; diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index bf3bfe9553..4aec111022 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -109,6 +109,15 @@ struct Hints { unsigned long status = 0; }; +static String get_atom_name(Display *p_disp, Atom p_atom) { + char *name = XGetAtomName(p_disp, p_atom); + ERR_FAIL_NULL_V_MSG(name, String(), "Atom is invalid."); + String ret; + ret.parse_utf8(name); + XFree(name); + return ret; +} + bool DisplayServerX11::has_feature(Feature p_feature) const { switch (p_feature) { case FEATURE_SUBWINDOWS: @@ -130,6 +139,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { case FEATURE_KEEP_SCREEN_ON: #endif case FEATURE_CLIPBOARD_PRIMARY: + case FEATURE_TEXT_TO_SPEECH: return true; default: { } @@ -298,6 +308,45 @@ void DisplayServerX11::_flush_mouse_motion() { xi.relative_motion.y = 0; } +#ifdef SPEECHD_ENABLED + +bool DisplayServerX11::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_speaking(); +} + +bool DisplayServerX11::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_paused(); +} + +Array DisplayServerX11::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return tts->get_voices(); +} + +void DisplayServerX11::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerX11::tts_pause() { + ERR_FAIL_COND(!tts); + tts->pause(); +} + +void DisplayServerX11::tts_resume() { + ERR_FAIL_COND(!tts); + tts->resume(); +} + +void DisplayServerX11::tts_stop() { + ERR_FAIL_COND(!tts); + tts->stop(); +} + +#endif + void DisplayServerX11::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ @@ -354,15 +403,15 @@ DisplayServerX11::MouseMode DisplayServerX11::mouse_get_mode() const { return mouse_mode; } -void DisplayServerX11::mouse_warp_to_position(const Point2i &p_to) { +void DisplayServerX11::warp_mouse(const Point2i &p_position) { _THREAD_SAFE_METHOD_ if (mouse_mode == MOUSE_MODE_CAPTURED) { - last_mouse_pos = p_to; + last_mouse_pos = p_position; } else { WindowID window_id = windows.has(last_focused_window) ? last_focused_window : MAIN_WINDOW_ID; XWarpPointer(x11_display, None, windows[window_id].x11_window, - 0, 0, 0, 0, (int)p_to.x, (int)p_to.y); + 0, 0, 0, 0, (int)p_position.x, (int)p_position.y); } } @@ -435,7 +484,7 @@ String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, A Window selection_owner = XGetSelectionOwner(x11_display, p_source); if (selection_owner == x11_window) { static const char *target_type = "PRIMARY"; - if (p_source != None && String(XGetAtomName(x11_display, p_source)) == target_type) { + if (p_source != None && get_atom_name(x11_display, p_source) == target_type) { return internal_clipboard_primary; } else { return internal_clipboard; @@ -661,17 +710,17 @@ void DisplayServerX11::_clipboard_transfer_ownership(Atom p_source, Window x11_w int DisplayServerX11::get_screen_count() const { _THREAD_SAFE_METHOD_ + int count = 0; // Using Xinerama Extension int event_base, error_base; - const Bool ext_okay = XineramaQueryExtension(x11_display, &event_base, &error_base); - if (!ext_okay) { - return 0; + if (XineramaQueryExtension(x11_display, &event_base, &error_base)) { + XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &count); + XFree(xsi); + } else { + count = XScreenCount(x11_display); } - int count; - XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &count); - XFree(xsi); return count; } @@ -703,6 +752,19 @@ Rect2i DisplayServerX11::_screen_get_rect(int p_screen) const { if (xsi) { XFree(xsi); } + } else { + int count = XScreenCount(x11_display); + if (p_screen < count) { + Window root = XRootWindow(x11_display, p_screen); + XWindowAttributes xwa; + XGetWindowAttributes(x11_display, root, &xwa); + rect.position.x = xwa.x; + rect.position.y = xwa.y; + rect.size.width = xwa.width; + rect.size.height = xwa.height; + } else { + ERR_PRINT("Invalid screen index: " + itos(p_screen) + "(count: " + itos(count) + ")."); + } } return rect; @@ -1193,7 +1255,7 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { DEBUG_LOG_X11("delete_sub_window: %lu (%u) \n", wd.x11_window, p_id); while (wd.transient_children.size()) { - window_set_transient(wd.transient_children.front()->get(), INVALID_WINDOW_ID); + window_set_transient(*wd.transient_children.begin(), INVALID_WINDOW_ID); } if (wd.transient_parent != INVALID_WINDOW_ID) { @@ -1723,8 +1785,15 @@ bool DisplayServerX11::_window_maximize_check(WindowID p_window, const char *p_a if (result == Success && data) { Atom *atoms = (Atom *)data; - Atom wm_act_max_horz = XInternAtom(x11_display, "_NET_WM_ACTION_MAXIMIZE_HORZ", False); - Atom wm_act_max_vert = XInternAtom(x11_display, "_NET_WM_ACTION_MAXIMIZE_VERT", False); + Atom wm_act_max_horz; + Atom wm_act_max_vert; + if (strcmp(p_atom_name, "_NET_WM_STATE") == 0) { + wm_act_max_horz = XInternAtom(x11_display, "_NET_WM_STATE_MAXIMIZED_HORZ", False); + wm_act_max_vert = XInternAtom(x11_display, "_NET_WM_STATE_MAXIMIZED_VERT", False); + } else { + wm_act_max_horz = XInternAtom(x11_display, "_NET_WM_ACTION_MAXIMIZE_HORZ", False); + wm_act_max_vert = XInternAtom(x11_display, "_NET_WM_ACTION_MAXIMIZE_VERT", False); + } bool found_wm_act_max_horz = false; bool found_wm_act_max_vert = false; @@ -2085,7 +2154,7 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo XGetWindowAttributes(x11_display, wd.x11_window, &xwa); ERR_FAIL_COND_MSG(p_window == MAIN_WINDOW_ID, "Main window can't be popup."); - ERR_FAIL_COND_MSG((xwa.map_state == IsViewable) && (wd.is_popup != p_enabled), "Pupup flag can't changed while window is opened."); + ERR_FAIL_COND_MSG((xwa.map_state == IsViewable) && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened."); wd.is_popup = p_enabled; } break; default: { @@ -2114,7 +2183,7 @@ bool DisplayServerX11::window_get_flag(WindowFlags p_flag, WindowID p_window) co unsigned char *data = nullptr; if (XGetWindowProperty(x11_display, wd.x11_window, prop, 0, sizeof(Hints), False, AnyPropertyType, &type, &format, &len, &remaining, &data) == Success) { if (data && (format == 32) && (len >= 5)) { - borderless = !((Hints *)data)->decorations; + borderless = !(reinterpret_cast<Hints *>(data)->decorations); } if (data) { XFree(data); @@ -2146,7 +2215,7 @@ void DisplayServerX11::window_request_attention(WindowID p_window) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; + const WindowData &wd = windows[p_window]; // Using EWMH -- Extended Window Manager Hints // // Sets the _NET_WM_STATE_DEMANDS_ATTENTION atom for WM_STATE @@ -2172,7 +2241,7 @@ void DisplayServerX11::window_move_to_foreground(WindowID p_window) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; + const WindowData &wd = windows[p_window]; XEvent xev; Atom net_active_window = XInternAtom(x11_display, "_NET_ACTIVE_WINDOW", False); @@ -2287,14 +2356,14 @@ DisplayServerX11::CursorShape DisplayServerX11::cursor_get_shape() const { return current_cursor; } -void DisplayServerX11::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); + HashMap<CursorShape, Vector<Variant>>::Iterator cursor_c = cursors_cache.find(p_shape); if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { + if (cursor_c->value[0] == p_cursor && cursor_c->value[1] == p_hotspot) { cursor_set_shape(p_shape); return; } @@ -2448,8 +2517,7 @@ String DisplayServerX11::keyboard_get_layout_language(int p_index) const { Atom names = kbd->names->symbols; if (names != None) { - char *name = XGetAtomName(x11_display, names); - Vector<String> info = String(name).split("+"); + Vector<String> info = get_atom_name(x11_display, names).split("+"); if (p_index >= 0 && p_index < _group_count) { if (p_index + 1 < info.size()) { ret = info[p_index + 1]; // Skip "pc" at the start and "inet"/"group" at the end of symbols. @@ -2459,7 +2527,6 @@ String DisplayServerX11::keyboard_get_layout_language(int p_index) const { } else { ERR_PRINT("Index " + itos(p_index) + "is out of bounds (" + itos(_group_count) + ")."); } - XFree(name); } XkbFreeKeyboard(kbd, 0, true); } @@ -2486,9 +2553,7 @@ String DisplayServerX11::keyboard_get_layout_name(int p_index) const { } if (p_index >= 0 && p_index < _group_count) { - char *full_name = XGetAtomName(x11_display, groups[p_index]); - ret.parse_utf8(full_name); - XFree(full_name); + ret = get_atom_name(x11_display, groups[p_index]); } else { ERR_PRINT("Index " + itos(p_index) + "is out of bounds (" + itos(_group_count) + ")."); } @@ -2522,10 +2587,9 @@ DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, unsigned long bytes_after = 0; unsigned char *ret = nullptr; - int read_bytes = 1024; - // Keep trying to read the property until there are no bytes unread. if (p_property != None) { + int read_bytes = 1024; do { if (ret != nullptr) { XFree(ret); @@ -2545,13 +2609,13 @@ DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, return p; } -static Atom pick_target_from_list(Display *p_display, Atom *p_list, int p_count) { +static Atom pick_target_from_list(Display *p_display, const Atom *p_list, int p_count) { static const char *target_type = "text/uri-list"; for (int i = 0; i < p_count; i++) { Atom atom = p_list[i]; - if (atom != None && String(XGetAtomName(p_display, atom)) == target_type) { + if (atom != None && get_atom_name(p_display, atom) == target_type) { return atom; } } @@ -2560,15 +2624,15 @@ static Atom pick_target_from_list(Display *p_display, Atom *p_list, int p_count) static Atom pick_target_from_atoms(Display *p_disp, Atom p_t1, Atom p_t2, Atom p_t3) { static const char *target_type = "text/uri-list"; - if (p_t1 != None && String(XGetAtomName(p_disp, p_t1)) == target_type) { + if (p_t1 != None && get_atom_name(p_disp, p_t1) == target_type) { return p_t1; } - if (p_t2 != None && String(XGetAtomName(p_disp, p_t2)) == target_type) { + if (p_t2 != None && get_atom_name(p_disp, p_t2) == target_type) { return p_t2; } - if (p_t3 != None && String(XGetAtomName(p_disp, p_t3)) == target_type) { + if (p_t3 != None && get_atom_name(p_disp, p_t3) == target_type) { return p_t3; } @@ -2890,7 +2954,7 @@ Atom DisplayServerX11::_process_selection_request_target(Atom p_target, Window p // is the owner during a selection request. CharString clip; static const char *target_type = "PRIMARY"; - if (p_selection != None && String(XGetAtomName(x11_display, p_selection)) == target_type) { + if (p_selection != None && get_atom_name(x11_display, p_selection) == target_type) { clip = internal_clipboard_primary.utf8(); } else { clip = internal_clipboard.utf8(); @@ -3040,7 +3104,7 @@ void DisplayServerX11::_window_changed(XEvent *event) { } void DisplayServerX11::_dispatch_input_events(const Ref<InputEvent> &p_event) { - ((DisplayServerX11 *)(get_singleton()))->_dispatch_input_event(p_event); + static_cast<DisplayServerX11 *>(get_singleton())->_dispatch_input_event(p_event); } void DisplayServerX11::_dispatch_input_event(const Ref<InputEvent> &p_event) { @@ -3050,7 +3114,7 @@ void DisplayServerX11::_dispatch_input_event(const Ref<InputEvent> &p_event) { Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -3094,7 +3158,7 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev } void DisplayServerX11::_poll_events_thread(void *ud) { - DisplayServerX11 *display_server = (DisplayServerX11 *)ud; + DisplayServerX11 *display_server = static_cast<DisplayServerX11 *>(ud); display_server->_poll_events(); } @@ -3196,20 +3260,24 @@ Rect2i DisplayServerX11::window_get_popup_safe_rect(WindowID p_window) const { } void DisplayServerX11::popup_open(WindowID p_window) { + _THREAD_SAFE_METHOD_ + WindowData &wd = windows[p_window]; if (wd.is_popup) { - // Close all popups, up to current popup parent, or every popup if new window is not transient. + // Find current popup parent, or root popup if new window is not transient. + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); while (E) { if (wd.transient_parent != E->get() || wd.transient_parent == INVALID_WINDOW_ID) { - _send_window_event(windows[E->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } else { break; } } + if (C) { + _send_window_event(windows[C->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); + } time_since_popup = OS::get_singleton()->get_ticks_msec(); popup_list.push_back(p_window); @@ -3217,26 +3285,33 @@ void DisplayServerX11::popup_open(WindowID p_window) { } void DisplayServerX11::popup_close(WindowID p_window) { + _THREAD_SAFE_METHOD_ + List<WindowID>::Element *E = popup_list.find(p_window); while (E) { - _send_window_event(windows[E->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); List<WindowID>::Element *F = E->next(); + WindowID win_id = E->get(); popup_list.erase(E); + + _send_window_event(windows[win_id], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); E = F; } } -void DisplayServerX11::mouse_process_popups() { +bool DisplayServerX11::mouse_process_popups() { + _THREAD_SAFE_METHOD_ + if (popup_list.is_empty()) { - return; + return false; } uint64_t delta = OS::get_singleton()->get_ticks_msec() - time_since_popup; if (delta < 250) { - return; + return false; } int number_of_screens = XScreenCount(x11_display); + bool closed = false; for (int i = 0; i < number_of_screens; i++) { Window root, child; int root_x, root_y, win_x, win_y; @@ -3247,7 +3322,9 @@ void DisplayServerX11::mouse_process_popups() { Vector2i pos = Vector2i(root_attrs.x + root_x, root_attrs.y + root_y); if ((pos != last_mouse_monitor_pos) || (mask != last_mouse_monitor_mask)) { if (((mask & Button1Mask) || (mask & Button2Mask) || (mask & Button3Mask) || (mask & Button4Mask) || (mask & Button5Mask))) { + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); + // Find top popup to close. while (E) { // Popup window area. Rect2i win_rect = Rect2i(window_get_position(E->get()), window_get_size(E->get())); @@ -3258,18 +3335,21 @@ void DisplayServerX11::mouse_process_popups() { } else if (safe_rect != Rect2i() && safe_rect.has_point(pos)) { break; } else { - _send_window_event(windows[E->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } } + if (C) { + _send_window_event(windows[C->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); + closed = true; + } } } last_mouse_monitor_mask = mask; last_mouse_monitor_pos = pos; } } + return closed; } void DisplayServerX11::process_events() { @@ -3280,7 +3360,7 @@ void DisplayServerX11::process_events() { ++frame; #endif - mouse_process_popups(); + bool ignore_events = mouse_process_popups(); if (app_focused) { //verify that one of the windows has focus, else send focus out notification @@ -3330,6 +3410,10 @@ void DisplayServerX11::process_events() { for (uint32_t event_index = 0; event_index < events.size(); ++event_index) { XEvent &event = events[event_index]; + if (ignore_events) { + XFreeEventData(x11_display, &event.xcookie); + continue; + } WindowID window_id = MAIN_WINDOW_ID; @@ -3379,9 +3463,9 @@ void DisplayServerX11::process_events() { } if (XIMaskIsSet(raw_event->valuators.mask, VALUATOR_PRESSURE)) { - Map<int, Vector2>::Element *pen_pressure = xi.pen_pressure_range.find(device_id); + HashMap<int, Vector2>::Iterator pen_pressure = xi.pen_pressure_range.find(device_id); if (pen_pressure) { - Vector2 pen_pressure_range = pen_pressure->value(); + Vector2 pen_pressure_range = pen_pressure->value; if (pen_pressure_range != Vector2()) { xi.pressure_supported = true; xi.pressure = (*values - pen_pressure_range[0]) / @@ -3393,9 +3477,9 @@ void DisplayServerX11::process_events() { } if (XIMaskIsSet(raw_event->valuators.mask, VALUATOR_TILTX)) { - Map<int, Vector2>::Element *pen_tilt_x = xi.pen_tilt_x_range.find(device_id); + HashMap<int, Vector2>::Iterator pen_tilt_x = xi.pen_tilt_x_range.find(device_id); if (pen_tilt_x) { - Vector2 pen_tilt_x_range = pen_tilt_x->value(); + Vector2 pen_tilt_x_range = pen_tilt_x->value; if (pen_tilt_x_range[0] != 0 && *values < 0) { xi.tilt.x = *values / -pen_tilt_x_range[0]; } else if (pen_tilt_x_range[1] != 0) { @@ -3407,9 +3491,9 @@ void DisplayServerX11::process_events() { } if (XIMaskIsSet(raw_event->valuators.mask, VALUATOR_TILTY)) { - Map<int, Vector2>::Element *pen_tilt_y = xi.pen_tilt_y_range.find(device_id); + HashMap<int, Vector2>::Iterator pen_tilt_y = xi.pen_tilt_y_range.find(device_id); if (pen_tilt_y) { - Vector2 pen_tilt_y_range = pen_tilt_y->value(); + Vector2 pen_tilt_y_range = pen_tilt_y->value; if (pen_tilt_y_range[0] != 0 && *values < 0) { xi.tilt.y = *values / -pen_tilt_y_range[0]; } else if (pen_tilt_y_range[1] != 0) { @@ -3431,11 +3515,11 @@ void DisplayServerX11::process_events() { xi.raw_pos.x = rel_x; xi.raw_pos.y = rel_y; - Map<int, Vector2>::Element *abs_info = xi.absolute_devices.find(device_id); + HashMap<int, Vector2>::Iterator abs_info = xi.absolute_devices.find(device_id); if (abs_info) { // Absolute mode device - Vector2 mult = abs_info->value(); + Vector2 mult = abs_info->value; xi.relative_motion.x += (xi.raw_pos.x - xi.old_raw_pos.x) * mult.x; xi.relative_motion.y += (xi.raw_pos.y - xi.old_raw_pos.y) * mult.y; @@ -3480,21 +3564,21 @@ void DisplayServerX11::process_events() { } break; case XI_TouchUpdate: { - Map<int, Vector2>::Element *curr_pos_elem = xi.state.find(index); + HashMap<int, Vector2>::Iterator curr_pos_elem = xi.state.find(index); if (!curr_pos_elem) { // Defensive break; } - if (curr_pos_elem->value() != pos) { + if (curr_pos_elem->value != pos) { Ref<InputEventScreenDrag> sd; sd.instantiate(); sd->set_window_id(window_id); sd->set_index(index); sd->set_position(pos); - sd->set_relative(pos - curr_pos_elem->value()); + sd->set_relative(pos - curr_pos_elem->value); Input::get_singleton()->parse_input_event(sd); - curr_pos_elem->value() = pos; + curr_pos_elem->value = pos; } } break; #endif @@ -3655,10 +3739,14 @@ void DisplayServerX11::process_events() { const WindowData &wd = windows[window_id]; + XWindowAttributes xwa; + XSync(x11_display, False); + XGetWindowAttributes(x11_display, wd.x11_window, &xwa); + // Set focus when menu window is re-used. // RevertToPointerRoot is used to make sure we don't lose all focus in case // a subwindow and its parent are both destroyed. - if (!wd.no_focus && !wd.is_popup) { + if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup) { XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); } @@ -3914,6 +4002,7 @@ void DisplayServerX11::process_events() { Property p = _read_property(x11_display, windows[window_id].x11_window, XInternAtom(x11_display, "PRIMARY", 0)); Vector<String> files = String((char *)p.data).split("\n", false); + XFree(p.data); for (int i = 0; i < files.size(); i++) { files.write[i] = files[i].replace("file://", "").uri_decode().strip_edges(); } @@ -3956,6 +4045,7 @@ void DisplayServerX11::process_events() { if (more_than_3) { Property p = _read_property(x11_display, source, XInternAtom(x11_display, "XdndTypeList", False)); requested = pick_target_from_list(x11_display, (Atom *)p.data, p.nitems); + XFree(p.data); } else { requested = pick_target_from_atoms(x11_display, event.xclient.data.l[2], event.xclient.data.l[3], event.xclient.data.l[4]); } @@ -4029,13 +4119,13 @@ void DisplayServerX11::process_events() { void DisplayServerX11::release_rendering_thread() { #if defined(GLES3_ENABLED) -// gl_manager->release_current(); + gl_manager->release_current(); #endif } void DisplayServerX11::make_rendering_thread() { #if defined(GLES3_ENABLED) -// gl_manager->make_current(); + gl_manager->make_current(); #endif } @@ -4448,24 +4538,11 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode r_error = OK; - current_cursor = CURSOR_ARROW; - mouse_mode = MOUSE_MODE_VISIBLE; - for (int i = 0; i < CURSOR_MAX; i++) { cursors[i] = None; img[i] = nullptr; } - xmbstring = nullptr; - - last_click_ms = 0; - last_click_button_index = MouseButton::NONE; - last_click_pos = Point2i(-100, -100); - - last_timestamp = 0; - last_mouse_pos_valid = false; - last_keyrelease_time = 0; - XInitThreads(); //always use threads /** XLIB INITIALIZATION **/ @@ -4500,8 +4577,6 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } const char *err; - xrr_get_monitors = nullptr; - xrr_free_monitors = nullptr; int xrandr_major = 0; int xrandr_minor = 0; int event_base, error_base; @@ -4577,11 +4652,10 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode XFree(imvalret); } - /* Atorm internment */ + /* Atom internment */ wm_delete = XInternAtom(x11_display, "WM_DELETE_WINDOW", true); - //Set Xdnd (drag & drop) support + // Set Xdnd (drag & drop) support. xdnd_aware = XInternAtom(x11_display, "XdndAware", False); - xdnd_version = 5; xdnd_enter = XInternAtom(x11_display, "XdndEnter", False); xdnd_position = XInternAtom(x11_display, "XdndPosition", False); xdnd_status = XInternAtom(x11_display, "XdndStatus", False); @@ -4590,6 +4664,11 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode xdnd_finished = XInternAtom(x11_display, "XdndFinished", False); xdnd_selection = XInternAtom(x11_display, "XdndSelection", False); +#ifdef SPEECHD_ENABLED + // Init TTS + tts = memnew(TTS_Linux); +#endif + //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; @@ -4663,11 +4742,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } driver_found = true; - // gl_manager->set_use_vsync(current_videomode.use_vsync); - if (true) { - // if (RasterizerGLES3::is_viable() == OK) { - // RasterizerGLES3::register_config(); RasterizerGLES3::make_current(); } else { memdelete(gl_manager); @@ -4842,12 +4917,6 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } cursor_set_shape(CURSOR_BUSY); - requested = None; - - /*if (p_desired.layered) { - set_window_per_pixel_transparency_enabled(true); - }*/ - XEvent xevent; while (XPending(x11_display) > 0) { XNextEvent(x11_display, &xevent); @@ -4942,6 +5011,10 @@ DisplayServerX11::~DisplayServerX11() { memfree(xmbstring); } +#ifdef SPEECHD_ENABLED + memdelete(tts); +#endif + #ifdef DBUS_ENABLED memdelete(screensaver); #endif diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index 63d32d939d..4beeddd3a8 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -46,6 +46,10 @@ #include "servers/rendering/renderer_compositor.h" #include "servers/rendering_server.h" +#if defined(SPEECHD_ENABLED) +#include "tts_linux.h" +#endif + #if defined(GLES3_ENABLED) #include "gl_manager_x11.h" #endif @@ -96,8 +100,8 @@ class DisplayServerX11 : public DisplayServer { Atom xdnd_finished; Atom xdnd_selection; Atom xdnd_aware; - Atom requested; - int xdnd_version; + Atom requested = None; + int xdnd_version = 5; #if defined(GLES3_ENABLED) GLManager_X11 *gl_manager = nullptr; @@ -108,10 +112,14 @@ class DisplayServerX11 : public DisplayServer { #endif #if defined(DBUS_ENABLED) - FreeDesktopScreenSaver *screensaver; + FreeDesktopScreenSaver *screensaver = nullptr; bool keep_screen_on = false; #endif +#ifdef SPEECHD_ENABLED + TTS_Linux *tts = nullptr; +#endif + struct WindowData { Window x11_window; ::XIC xic; @@ -129,7 +137,7 @@ class DisplayServerX11 : public DisplayServer { Callable drop_files_callback; WindowID transient_parent = INVALID_WINDOW_ID; - Set<WindowID> transient_children; + HashSet<WindowID> transient_children; ObjectID instance_id; @@ -151,7 +159,7 @@ class DisplayServerX11 : public DisplayServer { unsigned int focus_order = 0; }; - Map<WindowID, WindowData> windows; + HashMap<WindowID, WindowData> windows; unsigned int last_mouse_monitor_mask = 0; Vector2i last_mouse_monitor_pos; @@ -166,21 +174,21 @@ class DisplayServerX11 : public DisplayServer { String internal_clipboard; String internal_clipboard_primary; - Window xdnd_source_window; + Window xdnd_source_window = 0; ::Display *x11_display; - char *xmbstring; - int xmblen; - unsigned long last_timestamp; - ::Time last_keyrelease_time; + char *xmbstring = nullptr; + int xmblen = 0; + unsigned long last_timestamp = 0; + ::Time last_keyrelease_time = 0; ::XIM xim; ::XIMStyle xim_style; static void _xim_destroy_callback(::XIM im, ::XPointer client_data, ::XPointer call_data); Point2i last_mouse_pos; - bool last_mouse_pos_valid; - Point2i last_click_pos; - uint64_t last_click_ms; + bool last_mouse_pos_valid = false; + Point2i last_click_pos = Point2i(-100, -100); + uint64_t last_click_ms = 0; MouseButton last_click_button_index = MouseButton::NONE; MouseButton last_button_state = MouseButton::NONE; bool app_focused = false; @@ -189,12 +197,12 @@ class DisplayServerX11 : public DisplayServer { struct { int opcode; Vector<int> touch_devices; - Map<int, Vector2> absolute_devices; - Map<int, Vector2> pen_pressure_range; - Map<int, Vector2> pen_tilt_x_range; - Map<int, Vector2> pen_tilt_y_range; + HashMap<int, Vector2> absolute_devices; + HashMap<int, Vector2> pen_pressure_range; + HashMap<int, Vector2> pen_tilt_x_range; + HashMap<int, Vector2> pen_tilt_y_range; XIEventMask all_event_mask; - Map<int, Vector2> state; + HashMap<int, Vector2> state; double pressure; bool pressure_supported; Vector2 tilt; @@ -213,7 +221,7 @@ class DisplayServerX11 : public DisplayServer { void _get_key_modifier_state(unsigned int p_x11_state, Ref<InputEventWithModifiers> state); void _flush_mouse_motion(); - MouseMode mouse_mode; + MouseMode mouse_mode = MOUSE_MODE_VISIBLE; Point2i center; void _handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); @@ -225,31 +233,27 @@ class DisplayServerX11 : public DisplayServer { String _clipboard_get(Atom p_source, Window x11_window) const; void _clipboard_transfer_ownership(Atom p_source, Window x11_window) const; - //bool minimized; - //bool window_has_focus; - bool do_mouse_warp; + bool do_mouse_warp = false; - const char *cursor_theme; - int cursor_size; + const char *cursor_theme = nullptr; + int cursor_size = 0; XcursorImage *img[CURSOR_MAX]; Cursor cursors[CURSOR_MAX]; Cursor null_cursor; - CursorShape current_cursor; - Map<CursorShape, Vector<Variant>> cursors_cache; + CursorShape current_cursor = CURSOR_ARROW; + HashMap<CursorShape, Vector<Variant>> cursors_cache; - bool layered_window; + bool layered_window = false; String rendering_driver; - //bool window_focused; - //void set_wm_border(bool p_enabled); void set_wm_fullscreen(bool p_enabled); void set_wm_above(bool p_enabled); typedef xrr_monitor_info *(*xrr_get_monitors_t)(Display *dpy, Window window, Bool get_active, int *nmonitors); typedef void (*xrr_free_monitors_t)(xrr_monitor_info *monitors); - xrr_get_monitors_t xrr_get_monitors; - xrr_free_monitors_t xrr_free_monitors; - void *xrandr_handle; + xrr_get_monitors_t xrr_get_monitors = nullptr; + xrr_free_monitors_t xrr_free_monitors = nullptr; + void *xrandr_handle = nullptr; Bool xrandr_ext_ok; struct Property { @@ -291,17 +295,28 @@ protected: void _window_changed(XEvent *event); public: - void mouse_process_popups(); + bool mouse_process_popups(); void popup_open(WindowID p_window); void popup_close(WindowID p_window); virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; +#ifdef SPEECHD_ENABLED + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; +#endif + virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; - virtual void mouse_warp_to_position(const Point2i &p_to) override; + virtual void warp_mouse(const Point2i &p_position) override; virtual Point2i mouse_get_position() const override; virtual MouseButton mouse_get_button_state() const override; @@ -392,7 +407,7 @@ public: virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override; virtual int keyboard_get_layout_count() const override; virtual int keyboard_get_current_layout() const override; diff --git a/platform/linuxbsd/export/export.cpp b/platform/linuxbsd/export/export.cpp index f05d2faa11..965b969ba8 100644 --- a/platform/linuxbsd/export/export.cpp +++ b/platform/linuxbsd/export/export.cpp @@ -30,15 +30,10 @@ #include "export.h" -#include "core/io/file_access.h" -#include "editor/editor_export.h" -#include "platform/linuxbsd/logo.gen.h" -#include "scene/resources/texture.h" - -static Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size); +#include "export_plugin.h" void register_linuxbsd_exporter() { - Ref<EditorExportPlatformPC> platform; + Ref<EditorExportPlatformLinuxBSD> platform; platform.instantiate(); Ref<Image> img = memnew(Image(_linuxbsd_logo)); @@ -47,120 +42,10 @@ void register_linuxbsd_exporter() { logo->create_from_image(img); platform->set_logo(logo); platform->set_name("Linux/X11"); - platform->set_extension("x86"); + platform->set_extension("x86_32"); platform->set_extension("x86_64", "binary_format/64_bits"); - platform->set_release_32("linux_x11_32_release"); - platform->set_debug_32("linux_x11_32_debug"); - platform->set_release_64("linux_x11_64_release"); - platform->set_debug_64("linux_x11_64_debug"); - platform->set_os_name("LinuxBSD"); + platform->set_os_name("Linux"); platform->set_chmod_flags(0755); - platform->set_fixup_embedded_pck_func(&fixup_embedded_pck); EditorExport::get_singleton()->add_export_platform(platform); } - -static Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { - // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data - - FileAccess *f = FileAccess::open(p_path, FileAccess::READ_WRITE); - if (!f) { - return ERR_CANT_OPEN; - } - - // Read and check ELF magic number - { - uint32_t magic = f->get_32(); - if (magic != 0x464c457f) { // 0x7F + "ELF" - f->close(); - return ERR_FILE_CORRUPT; - } - } - - // Read program architecture bits from class field - - int bits = f->get_8() * 32; - - if (bits == 32 && p_embedded_size >= 0x100000000) { - f->close(); - ERR_FAIL_V_MSG(ERR_INVALID_DATA, "32-bit executables cannot have embedded data >= 4 GiB."); - } - - // Get info about the section header table - - int64_t section_table_pos; - int64_t section_header_size; - if (bits == 32) { - section_header_size = 40; - f->seek(0x20); - section_table_pos = f->get_32(); - f->seek(0x30); - } else { // 64 - section_header_size = 64; - f->seek(0x28); - section_table_pos = f->get_64(); - f->seek(0x3c); - } - int num_sections = f->get_16(); - int string_section_idx = f->get_16(); - - // Load the strings table - uint8_t *strings; - { - // Jump to the strings section header - f->seek(section_table_pos + string_section_idx * section_header_size); - - // Read strings data size and offset - int64_t string_data_pos; - int64_t string_data_size; - if (bits == 32) { - f->seek(f->get_position() + 0x10); - string_data_pos = f->get_32(); - string_data_size = f->get_32(); - } else { // 64 - f->seek(f->get_position() + 0x18); - string_data_pos = f->get_64(); - string_data_size = f->get_64(); - } - - // Read strings data - f->seek(string_data_pos); - strings = (uint8_t *)memalloc(string_data_size); - if (!strings) { - f->close(); - return ERR_OUT_OF_MEMORY; - } - f->get_buffer(strings, string_data_size); - } - - // Search for the "pck" section - - bool found = false; - for (int i = 0; i < num_sections; ++i) { - int64_t section_header_pos = section_table_pos + i * section_header_size; - f->seek(section_header_pos); - - uint32_t name_offset = f->get_32(); - if (strcmp((char *)strings + name_offset, "pck") == 0) { - // "pck" section found, let's patch! - - if (bits == 32) { - f->seek(section_header_pos + 0x10); - f->store_32(p_embedded_start); - f->store_32(p_embedded_size); - } else { // 64 - f->seek(section_header_pos + 0x18); - f->store_64(p_embedded_start); - f->store_64(p_embedded_size); - } - - found = true; - break; - } - } - - memfree(strings); - f->close(); - - return found ? OK : ERR_FILE_CORRUPT; -} diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp new file mode 100644 index 0000000000..4e14920e79 --- /dev/null +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -0,0 +1,212 @@ +/*************************************************************************/ +/* export_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "export_plugin.h" + +#include "core/config/project_settings.h" +#include "editor/editor_node.h" + +Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); + return ERR_CANT_CREATE; + } + + f->store_line("#!/bin/sh"); + f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'"); + f->store_line("base_path=\"$(dirname \"$(realpath \"$0\")\")\""); + f->store_line("\"$base_path/" + p_pkg_name + "\" \"$@\""); + + return OK; +} + +Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { + Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, p_path, p_flags); + + if (err != OK) { + return err; + } + + String app_name; + if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { + app_name = String(ProjectSettings::get_singleton()->get("application/config/name")); + } else { + app_name = "Unnamed"; + } + app_name = OS::get_singleton()->get_safe_dir_name(app_name); + + // Save console script. + if (err == OK) { + int con_scr = p_preset->get("debug/export_console_script"); + if ((con_scr == 1 && p_debug) || (con_scr == 2)) { + String scr_path = p_path.get_basename() + ".sh"; + err = _export_debug_script(p_preset, app_name, p_path.get_file(), scr_path); + FileAccess::set_unix_permissions(scr_path, 0755); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), TTR("Could not create console script.")); + } + } + } + + return err; +} + +void EditorExportPlatformLinuxBSD::set_extension(const String &p_extension, const String &p_feature_key) { + extensions[p_feature_key] = p_extension; +} + +String EditorExportPlatformLinuxBSD::get_template_file_name(const String &p_target, const String &p_arch) const { + return "linux_x11_" + p_arch + "_" + p_target; +} + +List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { + List<String> list; + for (const KeyValue<String, String> &E : extensions) { + if (p_preset->get(E.key)) { + list.push_back(extensions[E.key]); + return list; + } + } + + if (extensions.has("default")) { + list.push_back(extensions["default"]); + return list; + } + + return list; +} + +Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { + // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data + + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); + return ERR_CANT_OPEN; + } + + // Read and check ELF magic number + { + uint32_t magic = f->get_32(); + if (magic != 0x464c457f) { // 0x7F + "ELF" + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); + return ERR_FILE_CORRUPT; + } + } + + // Read program architecture bits from class field + + int bits = f->get_8() * 32; + + if (bits == 32 && p_embedded_size >= 0x100000000) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB.")); + } + + // Get info about the section header table + + int64_t section_table_pos; + int64_t section_header_size; + if (bits == 32) { + section_header_size = 40; + f->seek(0x20); + section_table_pos = f->get_32(); + f->seek(0x30); + } else { // 64 + section_header_size = 64; + f->seek(0x28); + section_table_pos = f->get_64(); + f->seek(0x3c); + } + int num_sections = f->get_16(); + int string_section_idx = f->get_16(); + + // Load the strings table + uint8_t *strings; + { + // Jump to the strings section header + f->seek(section_table_pos + string_section_idx * section_header_size); + + // Read strings data size and offset + int64_t string_data_pos; + int64_t string_data_size; + if (bits == 32) { + f->seek(f->get_position() + 0x10); + string_data_pos = f->get_32(); + string_data_size = f->get_32(); + } else { // 64 + f->seek(f->get_position() + 0x18); + string_data_pos = f->get_64(); + string_data_size = f->get_64(); + } + + // Read strings data + f->seek(string_data_pos); + strings = (uint8_t *)memalloc(string_data_size); + if (!strings) { + return ERR_OUT_OF_MEMORY; + } + f->get_buffer(strings, string_data_size); + } + + // Search for the "pck" section + + bool found = false; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * section_header_size; + f->seek(section_header_pos); + + uint32_t name_offset = f->get_32(); + if (strcmp((char *)strings + name_offset, "pck") == 0) { + // "pck" section found, let's patch! + + if (bits == 32) { + f->seek(section_header_pos + 0x10); + f->store_32(p_embedded_start); + f->store_32(p_embedded_size); + } else { // 64 + f->seek(section_header_pos + 0x18); + f->store_64(p_embedded_start); + f->store_64(p_embedded_size); + } + + found = true; + break; + } + } + + memfree(strings); + + if (!found) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); + return ERR_FILE_CORRUPT; + } + return OK; +} diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h new file mode 100644 index 0000000000..e04bcc20f9 --- /dev/null +++ b/platform/linuxbsd/export/export_plugin.h @@ -0,0 +1,52 @@ +/*************************************************************************/ +/* export_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 LINUXBSD_EXPORT_PLUGIN_H +#define LINUXBSD_EXPORT_PLUGIN_H + +#include "core/io/file_access.h" +#include "editor/editor_export.h" +#include "editor/editor_settings.h" +#include "platform/linuxbsd/logo.gen.h" +#include "scene/resources/texture.h" + +class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { + HashMap<String, String> extensions; + Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); + +public: + void set_extension(const String &p_extension, const String &p_feature_key = "default"); + virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; + virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; +}; + +#endif diff --git a/platform/linuxbsd/gl_manager_x11.h b/platform/linuxbsd/gl_manager_x11.h index 0bb0a446ab..fb2c74a2b6 100644 --- a/platform/linuxbsd/gl_manager_x11.h +++ b/platform/linuxbsd/gl_manager_x11.h @@ -52,21 +52,20 @@ public: private: // any data specific to the window struct GLWindow { - GLWindow() { in_use = false; } - bool in_use; + bool in_use = false; // the external ID .. should match the GL window number .. unused I think - DisplayServer::WindowID window_id; - int width; - int height; + DisplayServer::WindowID window_id = DisplayServer::INVALID_WINDOW_ID; + int width = 0; + int height = 0; ::Window x11_window; - int gldisplay_id; + int gldisplay_id = 0; }; struct GLDisplay { GLDisplay() { context = nullptr; } ~GLDisplay(); - GLManager_X11_Private *context; + GLManager_X11_Private *context = nullptr; ::Display *x11_display; XVisualInfo x_vi; XSetWindowAttributes x_swa; @@ -82,7 +81,7 @@ private: LocalVector<GLWindow> _windows; LocalVector<GLDisplay> _displays; - GLWindow *_current_window; + GLWindow *_current_window = nullptr; void _internal_set_current_window(GLWindow *p_win); diff --git a/platform/linuxbsd/godot_linuxbsd.cpp b/platform/linuxbsd/godot_linuxbsd.cpp index 7c9f81bd3f..9fe00568fb 100644 --- a/platform/linuxbsd/godot_linuxbsd.cpp +++ b/platform/linuxbsd/godot_linuxbsd.cpp @@ -33,10 +33,20 @@ #include <stdlib.h> #include <unistd.h> +#if defined(SANITIZERS_ENABLED) +#include <sys/resource.h> +#endif + #include "main/main.h" #include "os_linuxbsd.h" int main(int argc, char *argv[]) { +#if defined(SANITIZERS_ENABLED) + // Note: Set stack size to be at least 30 MB (vs 8 MB default) to avoid overflow, address sanitizer can increase stack usage up to 3 times. + struct rlimit stack_lim = { 0x1E00000, 0x1E00000 }; + setrlimit(RLIMIT_STACK, &stack_lim); +#endif + OS_LinuxBSD os; setlocale(LC_CTYPE, ""); diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 65d53b266f..bc018e366b 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -65,6 +65,7 @@ void JoypadLinux::Joypad::reset() { abs_map[i] = -1; curr_axis[i] = 0; } + events.clear(); } JoypadLinux::JoypadLinux(Input *in) { @@ -84,23 +85,26 @@ JoypadLinux::JoypadLinux(Input *in) { print_verbose("JoypadLinux: udev disabled, parsing /dev/input to detect joypads."); #endif input = in; - joy_thread.start(joy_thread_func, this); + monitor_joypads_thread.start(monitor_joypads_thread_func, this); + joypad_events_thread.start(joypad_events_thread_func, this); } JoypadLinux::~JoypadLinux() { - exit_monitor.set(); - joy_thread.wait_to_finish(); - close_joypad(); + monitor_joypads_exit.set(); + joypad_events_exit.set(); + monitor_joypads_thread.wait_to_finish(); + joypad_events_thread.wait_to_finish(); + close_joypads(); } -void JoypadLinux::joy_thread_func(void *p_user) { +void JoypadLinux::monitor_joypads_thread_func(void *p_user) { if (p_user) { - JoypadLinux *joy = (JoypadLinux *)p_user; - joy->run_joypad_thread(); + JoypadLinux *joy = static_cast<JoypadLinux *>(p_user); + joy->monitor_joypads_thread_run(); } } -void JoypadLinux::run_joypad_thread() { +void JoypadLinux::monitor_joypads_thread_run() { #ifdef UDEV_ENABLED if (use_udev) { udev *_udev = udev_new(); @@ -140,7 +144,6 @@ void JoypadLinux::enumerate_joypads(udev *p_udev) { if (devnode) { String devnode_str = devnode; if (devnode_str.find(ignore_str) == -1) { - MutexLock lock(joy_mutex); open_joypad(devnode); } } @@ -156,7 +159,7 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { udev_monitor_enable_receiving(mon); int fd = udev_monitor_get_fd(mon); - while (!exit_monitor.is_set()) { + while (!monitor_joypads_exit.is_set()) { fd_set fds; struct timeval tv; int ret; @@ -175,7 +178,6 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { dev = udev_monitor_receive_device(mon); if (dev && udev_device_get_devnode(dev) != nullptr) { - MutexLock lock(joy_mutex); String action = udev_device_get_action(dev); const char *devnode = udev_device_get_devnode(dev); if (devnode) { @@ -184,11 +186,10 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { if (action == "add") { open_joypad(devnode); } else if (String(action) == "remove") { - close_joypad(get_joy_from_path(devnode)); + close_joypad(devnode); } } } - udev_device_unref(dev); } } @@ -199,59 +200,54 @@ void JoypadLinux::monitor_joypads(udev *p_udev) { #endif void JoypadLinux::monitor_joypads() { - while (!exit_monitor.is_set()) { - { - MutexLock lock(joy_mutex); - - DIR *input_directory; - input_directory = opendir("/dev/input"); - if (input_directory) { - struct dirent *current; - char fname[64]; - - while ((current = readdir(input_directory)) != nullptr) { - if (strncmp(current->d_name, "event", 5) != 0) { - continue; - } - sprintf(fname, "/dev/input/%.*s", 16, current->d_name); - if (attached_devices.find(fname) == -1) { - open_joypad(fname); - } + while (!monitor_joypads_exit.is_set()) { + DIR *input_directory; + input_directory = opendir("/dev/input"); + if (input_directory) { + struct dirent *current; + char fname[64]; + + while ((current = readdir(input_directory)) != nullptr) { + if (strncmp(current->d_name, "event", 5) != 0) { + continue; + } + sprintf(fname, "/dev/input/%.*s", 16, current->d_name); + if (attached_devices.find(fname) == -1) { + open_joypad(fname); } } - closedir(input_directory); } - usleep(1000000); // 1s + closedir(input_directory); } + usleep(1000000); // 1s } -int JoypadLinux::get_joy_from_path(String p_path) const { +void JoypadLinux::close_joypads() { for (int i = 0; i < JOYPADS_MAX; i++) { - if (joypads[i].devpath == p_path) { - return i; - } + MutexLock lock(joypads_mutex[i]); + Joypad &joypad = joypads[i]; + close_joypad(joypad, i); } - return -2; } -void JoypadLinux::close_joypad(int p_id) { - if (p_id == -1) { - for (int i = 0; i < JOYPADS_MAX; i++) { - close_joypad(i); +void JoypadLinux::close_joypad(const char *p_devpath) { + for (int i = 0; i < JOYPADS_MAX; i++) { + MutexLock lock(joypads_mutex[i]); + Joypad &joypad = joypads[i]; + if (joypads[i].devpath == p_devpath) { + close_joypad(joypad, i); } - return; - } else if (p_id < 0) { - return; } +} - Joypad &joy = joypads[p_id]; - - if (joy.fd != -1) { - close(joy.fd); - joy.fd = -1; - attached_devices.remove_at(attached_devices.find(joy.devpath)); +void JoypadLinux::close_joypad(Joypad &p_joypad, int p_id) { + if (p_joypad.fd != -1) { + close(p_joypad.fd); + p_joypad.fd = -1; + attached_devices.erase(p_joypad.devpath); input->joy_connection_changed(p_id, false, ""); } + p_joypad.events.clear(); } static String _hex_str(uint8_t p_byte) { @@ -265,27 +261,25 @@ static String _hex_str(uint8_t p_byte) { return ret; } -void JoypadLinux::setup_joypad_properties(int p_id) { - Joypad *joy = &joypads[p_id]; - +void JoypadLinux::setup_joypad_properties(Joypad &p_joypad) { unsigned long keybit[NBITS(KEY_MAX)] = { 0 }; unsigned long absbit[NBITS(ABS_MAX)] = { 0 }; int num_buttons = 0; int num_axes = 0; - if ((ioctl(joy->fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit) < 0) || - (ioctl(joy->fd, EVIOCGBIT(EV_ABS, sizeof(absbit)), absbit) < 0)) { + if ((ioctl(p_joypad.fd, EVIOCGBIT(EV_KEY, sizeof(keybit)), keybit) < 0) || + (ioctl(p_joypad.fd, EVIOCGBIT(EV_ABS, sizeof(absbit)), absbit) < 0)) { return; } for (int i = BTN_JOYSTICK; i < KEY_MAX; ++i) { if (test_bit(i, keybit)) { - joy->key_map[i] = num_buttons++; + p_joypad.key_map[i] = num_buttons++; } } for (int i = BTN_MISC; i < BTN_JOYSTICK; ++i) { if (test_bit(i, keybit)) { - joy->key_map[i] = num_buttons++; + p_joypad.key_map[i] = num_buttons++; } } for (int i = 0; i < ABS_MISC; ++i) { @@ -295,21 +289,21 @@ void JoypadLinux::setup_joypad_properties(int p_id) { continue; } if (test_bit(i, absbit)) { - joy->abs_map[i] = num_axes++; - joy->abs_info[i] = memnew(input_absinfo); - if (ioctl(joy->fd, EVIOCGABS(i), joy->abs_info[i]) < 0) { - memdelete(joy->abs_info[i]); - joy->abs_info[i] = nullptr; + p_joypad.abs_map[i] = num_axes++; + p_joypad.abs_info[i] = memnew(input_absinfo); + if (ioctl(p_joypad.fd, EVIOCGABS(i), p_joypad.abs_info[i]) < 0) { + memdelete(p_joypad.abs_info[i]); + p_joypad.abs_info[i] = nullptr; } } } - joy->force_feedback = false; - joy->ff_effect_timestamp = 0; + p_joypad.force_feedback = false; + p_joypad.ff_effect_timestamp = 0; unsigned long ffbit[NBITS(FF_CNT)]; - if (ioctl(joy->fd, EVIOCGBIT(EV_FF, sizeof(ffbit)), ffbit) != -1) { + if (ioctl(p_joypad.fd, EVIOCGBIT(EV_FF, sizeof(ffbit)), ffbit) != -1) { if (test_bit(FF_RUMBLE, ffbit)) { - joy->force_feedback = true; + p_joypad.force_feedback = true; } } } @@ -353,12 +347,12 @@ void JoypadLinux::open_joypad(const char *p_path) { return; } - joypads[joy_num].reset(); - - Joypad &joy = joypads[joy_num]; - joy.fd = fd; - joy.devpath = String(p_path); - setup_joypad_properties(joy_num); + MutexLock lock(joypads_mutex[joy_num]); + Joypad &joypad = joypads[joy_num]; + joypad.reset(); + joypad.fd = fd; + joypad.devpath = String(p_path); + setup_joypad_properties(joypad); sprintf(uid, "%04x%04x", BSWAP16(inpid.bustype), 0); if (inpid.vendor && inpid.product && inpid.version) { uint16_t vendor = BSWAP16(inpid.vendor); @@ -379,13 +373,12 @@ void JoypadLinux::open_joypad(const char *p_path) { } } -void JoypadLinux::joypad_vibration_start(int p_id, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { - Joypad &joy = joypads[p_id]; - if (!joy.force_feedback || joy.fd == -1 || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { +void JoypadLinux::joypad_vibration_start(Joypad &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { + if (!p_joypad.force_feedback || p_joypad.fd == -1 || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { return; } - if (joy.ff_effect_id != -1) { - joypad_vibration_stop(p_id, p_timestamp); + if (p_joypad.ff_effect_id != -1) { + joypad_vibration_stop(p_joypad, p_timestamp); } struct ff_effect effect; @@ -396,7 +389,7 @@ void JoypadLinux::joypad_vibration_start(int p_id, float p_weak_magnitude, float effect.replay.length = floor(p_duration * 1000); effect.replay.delay = 0; - if (ioctl(joy.fd, EVIOCSFF, &effect) < 0) { + if (ioctl(p_joypad.fd, EVIOCSFF, &effect) < 0) { return; } @@ -404,26 +397,25 @@ void JoypadLinux::joypad_vibration_start(int p_id, float p_weak_magnitude, float play.type = EV_FF; play.code = effect.id; play.value = 1; - if (write(joy.fd, (const void *)&play, sizeof(play)) == -1) { + if (write(p_joypad.fd, (const void *)&play, sizeof(play)) == -1) { print_verbose("Couldn't write to Joypad device."); } - joy.ff_effect_id = effect.id; - joy.ff_effect_timestamp = p_timestamp; + p_joypad.ff_effect_id = effect.id; + p_joypad.ff_effect_timestamp = p_timestamp; } -void JoypadLinux::joypad_vibration_stop(int p_id, uint64_t p_timestamp) { - Joypad &joy = joypads[p_id]; - if (!joy.force_feedback || joy.fd == -1 || joy.ff_effect_id == -1) { +void JoypadLinux::joypad_vibration_stop(Joypad &p_joypad, uint64_t p_timestamp) { + if (!p_joypad.force_feedback || p_joypad.fd == -1 || p_joypad.ff_effect_id == -1) { return; } - if (ioctl(joy.fd, EVIOCRMFF, joy.ff_effect_id) < 0) { + if (ioctl(p_joypad.fd, EVIOCRMFF, p_joypad.ff_effect_id) < 0) { return; } - joy.ff_effect_id = -1; - joy.ff_effect_timestamp = p_timestamp; + p_joypad.ff_effect_id = -1; + p_joypad.ff_effect_timestamp = p_timestamp; } float JoypadLinux::axis_correct(const input_absinfo *p_abs, int p_value) const { @@ -433,104 +425,124 @@ float JoypadLinux::axis_correct(const input_absinfo *p_abs, int p_value) const { return 2.0f * (p_value - min) / (max - min) - 1.0f; } -void JoypadLinux::process_joypads() { - if (joy_mutex.try_lock() != OK) { - return; +void JoypadLinux::joypad_events_thread_func(void *p_user) { + if (p_user) { + JoypadLinux *joy = (JoypadLinux *)p_user; + joy->joypad_events_thread_run(); } +} + +void JoypadLinux::joypad_events_thread_run() { + while (!joypad_events_exit.is_set()) { + bool no_events = true; + for (int i = 0; i < JOYPADS_MAX; i++) { + MutexLock lock(joypads_mutex[i]); + Joypad &joypad = joypads[i]; + if (joypad.fd == -1) { + continue; + } + input_event event; + while (read(joypad.fd, &event, sizeof(event)) > 0) { + no_events = false; + JoypadEvent joypad_event; + joypad_event.type = event.type; + joypad_event.code = event.code; + joypad_event.value = event.value; + joypad.events.push_back(joypad_event); + } + if (errno != EAGAIN) { + close_joypad(joypad, i); + } + } + if (no_events) { + usleep(10000); // 10ms + } + } +} + +void JoypadLinux::process_joypads() { for (int i = 0; i < JOYPADS_MAX; i++) { - if (joypads[i].fd == -1) { + MutexLock lock(joypads_mutex[i]); + Joypad &joypad = joypads[i]; + if (joypad.fd == -1) { continue; } + for (uint32_t j = 0; j < joypad.events.size(); j++) { + const JoypadEvent &joypad_event = joypad.events[j]; + // joypad_event may be tainted and out of MAX_KEY range, which will cause + // joypad.key_map[joypad_event.code] to crash + if (joypad_event.code >= MAX_KEY) { + return; + } - input_event events[32]; - Joypad *joy = &joypads[i]; - - int len; - - while ((len = read(joy->fd, events, (sizeof events))) > 0) { - len /= sizeof(events[0]); - for (int j = 0; j < len; j++) { - input_event &ev = events[j]; - - // ev may be tainted and out of MAX_KEY range, which will cause - // joy->key_map[ev.code] to crash - if (ev.code >= MAX_KEY) { - return; - } - - switch (ev.type) { - case EV_KEY: - input->joy_button(i, (JoyButton)joy->key_map[ev.code], ev.value); - break; - - case EV_ABS: - - switch (ev.code) { - case ABS_HAT0X: - if (ev.value != 0) { - if (ev.value < 0) { - joy->dpad = (HatMask)((joy->dpad | HatMask::LEFT) & ~HatMask::RIGHT); - } else { - joy->dpad = (HatMask)((joy->dpad | HatMask::RIGHT) & ~HatMask::LEFT); - } + switch (joypad_event.type) { + case EV_KEY: + input->joy_button(i, (JoyButton)joypad.key_map[joypad_event.code], joypad_event.value); + break; + + case EV_ABS: + switch (joypad_event.code) { + case ABS_HAT0X: + if (joypad_event.value != 0) { + if (joypad_event.value < 0) { + joypad.dpad = (HatMask)((joypad.dpad | HatMask::LEFT) & ~HatMask::RIGHT); } else { - joy->dpad &= ~(HatMask::LEFT | HatMask::RIGHT); + joypad.dpad = (HatMask)((joypad.dpad | HatMask::RIGHT) & ~HatMask::LEFT); } - - input->joy_hat(i, (HatMask)joy->dpad); - break; - - case ABS_HAT0Y: - if (ev.value != 0) { - if (ev.value < 0) { - joy->dpad = (HatMask)((joy->dpad | HatMask::UP) & ~HatMask::DOWN); - } else { - joy->dpad = (HatMask)((joy->dpad | HatMask::DOWN) & ~HatMask::UP); - } + } else { + joypad.dpad &= ~(HatMask::LEFT | HatMask::RIGHT); + } + input->joy_hat(i, (HatMask)joypad.dpad); + break; + + case ABS_HAT0Y: + if (joypad_event.value != 0) { + if (joypad_event.value < 0) { + joypad.dpad = (HatMask)((joypad.dpad | HatMask::UP) & ~HatMask::DOWN); } else { - joy->dpad &= ~(HatMask::UP | HatMask::DOWN); - } - - input->joy_hat(i, (HatMask)joy->dpad); - break; - - default: - if (ev.code >= MAX_ABS) { - return; - } - if (joy->abs_map[ev.code] != -1 && joy->abs_info[ev.code]) { - float value = axis_correct(joy->abs_info[ev.code], ev.value); - joy->curr_axis[joy->abs_map[ev.code]] = value; + joypad.dpad = (HatMask)((joypad.dpad | HatMask::DOWN) & ~HatMask::UP); } - break; - } - break; - } + } else { + joypad.dpad &= ~(HatMask::UP | HatMask::DOWN); + } + input->joy_hat(i, (HatMask)joypad.dpad); + break; + + default: + if (joypad_event.code >= MAX_ABS) { + return; + } + if (joypad.abs_map[joypad_event.code] != -1 && joypad.abs_info[joypad_event.code]) { + float value = axis_correct(joypad.abs_info[joypad_event.code], joypad_event.value); + joypad.curr_axis[joypad.abs_map[joypad_event.code]] = value; + } + break; + } + break; } } + joypad.events.clear(); + for (int j = 0; j < MAX_ABS; j++) { - int index = joy->abs_map[j]; + int index = joypad.abs_map[j]; if (index != -1) { - input->joy_axis(i, (JoyAxis)index, joy->curr_axis[index]); + input->joy_axis(i, (JoyAxis)index, joypad.curr_axis[index]); } } - if (len == 0 || (len < 0 && errno != EAGAIN)) { - close_joypad(i); - } - if (joy->force_feedback) { + if (joypad.force_feedback) { uint64_t timestamp = input->get_joy_vibration_timestamp(i); - if (timestamp > joy->ff_effect_timestamp) { + if (timestamp > joypad.ff_effect_timestamp) { Vector2 strength = input->get_joy_vibration_strength(i); float duration = input->get_joy_vibration_duration(i); if (strength.x == 0 && strength.y == 0) { - joypad_vibration_stop(i, timestamp); + joypad_vibration_stop(joypad, timestamp); } else { - joypad_vibration_start(i, strength.x, strength.y, duration, timestamp); + joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); } } } } - joy_mutex.unlock(); } -#endif + +#endif // JOYDEV_ENABLED diff --git a/platform/linuxbsd/joypad_linux.h b/platform/linuxbsd/joypad_linux.h index 9177465547..4afc261ce7 100644 --- a/platform/linuxbsd/joypad_linux.h +++ b/platform/linuxbsd/joypad_linux.h @@ -35,6 +35,7 @@ #include "core/input/input.h" #include "core/os/mutex.h" #include "core/os/thread.h" +#include "core/templates/local_vector.h" struct input_absinfo; @@ -51,6 +52,12 @@ private: MAX_KEY = 767, // Hack because <linux/input.h> can't be included here }; + struct JoypadEvent { + uint16_t type; + uint16_t code; + int32_t value; + }; + struct Joypad { float curr_axis[MAX_ABS]; int key_map[MAX_KEY]; @@ -65,6 +72,8 @@ private: int ff_effect_id = 0; uint64_t ff_effect_timestamp = 0; + LocalVector<JoypadEvent> events; + ~Joypad(); void reset(); }; @@ -72,29 +81,39 @@ private: #ifdef UDEV_ENABLED bool use_udev = false; #endif - SafeFlag exit_monitor; - Mutex joy_mutex; - Thread joy_thread; Input *input = nullptr; + + SafeFlag monitor_joypads_exit; + SafeFlag joypad_events_exit; + Thread monitor_joypads_thread; + Thread joypad_events_thread; + Joypad joypads[JOYPADS_MAX]; + Mutex joypads_mutex[JOYPADS_MAX]; + Vector<String> attached_devices; - static void joy_thread_func(void *p_user); + static void monitor_joypads_thread_func(void *p_user); + void monitor_joypads_thread_run(); + + void open_joypad(const char *p_path); + void setup_joypad_properties(Joypad &p_joypad); - int get_joy_from_path(String p_path) const; + void close_joypads(); + void close_joypad(const char *p_devpath); + void close_joypad(Joypad &p_joypad, int p_id); - void setup_joypad_properties(int p_id); - void close_joypad(int p_id = -1); #ifdef UDEV_ENABLED void enumerate_joypads(struct udev *p_udev); void monitor_joypads(struct udev *p_udev); #endif void monitor_joypads(); - void run_joypad_thread(); - void open_joypad(const char *p_path); - void joypad_vibration_start(int p_id, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); - void joypad_vibration_stop(int p_id, uint64_t p_timestamp); + void joypad_vibration_start(Joypad &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); + void joypad_vibration_stop(Joypad &p_joypad, uint64_t p_timestamp); + + static void joypad_events_thread_func(void *p_user); + void joypad_events_thread_run(); float axis_correct(const input_absinfo *p_abs, int p_value) const; }; diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index d876932a83..b73d4dc626 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -32,6 +32,7 @@ #include "core/io/dir_access.h" #include "main/main.h" +#include "servers/display_server.h" #ifdef X11_ENABLED #include "display_server_x11.h" @@ -130,20 +131,19 @@ void OS_LinuxBSD::initialize_joypads() { String OS_LinuxBSD::get_unique_id() const { static String machine_id; if (machine_id.is_empty()) { - if (FileAccess *f = FileAccess::open("/etc/machine-id", FileAccess::READ)) { + Ref<FileAccess> f = FileAccess::open("/etc/machine-id", FileAccess::READ); + if (f.is_valid()) { while (machine_id.is_empty() && !f->eof_reached()) { machine_id = f->get_line().strip_edges(); } - f->close(); - memdelete(f); } } return machine_id; } String OS_LinuxBSD::get_processor_name() const { - FileAccessRef f = FileAccess::open("/proc/cpuinfo", FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, "", String("Couldn't open `/proc/cpuinfo` to get the CPU model name. Returning an empty string.")); + Ref<FileAccess> f = FileAccess::open("/proc/cpuinfo", FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), "", String("Couldn't open `/proc/cpuinfo` to get the CPU model name. Returning an empty string.")); while (!f->eof_reached()) { const String line = f->get_line(); @@ -242,6 +242,91 @@ bool OS_LinuxBSD::_check_internal_feature_support(const String &p_feature) { return p_feature == "pc"; } +uint64_t OS_LinuxBSD::get_embedded_pck_offset() const { + Ref<FileAccess> f = FileAccess::open(get_executable_path(), FileAccess::READ); + if (f.is_null()) { + return 0; + } + + // Read and check ELF magic number. + { + uint32_t magic = f->get_32(); + if (magic != 0x464c457f) { // 0x7F + "ELF" + return 0; + } + } + + // Read program architecture bits from class field. + int bits = f->get_8() * 32; + + // Get info about the section header table. + int64_t section_table_pos; + int64_t section_header_size; + if (bits == 32) { + section_header_size = 40; + f->seek(0x20); + section_table_pos = f->get_32(); + f->seek(0x30); + } else { // 64 + section_header_size = 64; + f->seek(0x28); + section_table_pos = f->get_64(); + f->seek(0x3c); + } + int num_sections = f->get_16(); + int string_section_idx = f->get_16(); + + // Load the strings table. + uint8_t *strings; + { + // Jump to the strings section header. + f->seek(section_table_pos + string_section_idx * section_header_size); + + // Read strings data size and offset. + int64_t string_data_pos; + int64_t string_data_size; + if (bits == 32) { + f->seek(f->get_position() + 0x10); + string_data_pos = f->get_32(); + string_data_size = f->get_32(); + } else { // 64 + f->seek(f->get_position() + 0x18); + string_data_pos = f->get_64(); + string_data_size = f->get_64(); + } + + // Read strings data. + f->seek(string_data_pos); + strings = (uint8_t *)memalloc(string_data_size); + if (!strings) { + return 0; + } + f->get_buffer(strings, string_data_size); + } + + // Search for the "pck" section. + int64_t off = 0; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * section_header_size; + f->seek(section_header_pos); + + uint32_t name_offset = f->get_32(); + if (strcmp((char *)strings + name_offset, "pck") == 0) { + if (bits == 32) { + f->seek(section_header_pos + 0x10); + off = f->get_32(); + } else { // 64 + f->seek(section_header_pos + 0x18); + off = f->get_64(); + } + break; + } + } + memfree(strings); + + return off; +} + String OS_LinuxBSD::get_config_path() const { if (has_environment("XDG_CONFIG_HOME")) { if (get_environment("XDG_CONFIG_HOME").is_absolute_path()) { @@ -398,9 +483,11 @@ static String get_mountpoint(const String &p_path) { } Error OS_LinuxBSD::move_to_trash(const String &p_path) { + String path = p_path.rstrip("/"); // Strip trailing slash when path points to a directory + int err_code; List<String> args; - args.push_back(p_path); + args.push_back(path); args.push_front("trash"); // The command is `gio trash <file_name>` so we need to add it to args. Error result = execute("gio", args, nullptr, &err_code); // For GNOME based machines. if (result == OK && !err_code) { @@ -430,14 +517,14 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // If the commands `kioclient5`, `gio` or `gvfs-trash` don't exist on the system we do it manually. String trash_path = ""; - String mnt = get_mountpoint(p_path); + String mnt = get_mountpoint(path); // If there is a directory "[Mountpoint]/.Trash-[UID], use it as the trash can. if (!mnt.is_empty()) { - String path(mnt + "/.Trash-" + itos(getuid())); + String mountpoint_trash_path(mnt + "/.Trash-" + itos(getuid())); struct stat s; - if (!stat(path.utf8().get_data(), &s)) { - trash_path = path; + if (!stat(mountpoint_trash_path.utf8().get_data(), &s)) { + trash_path = mountpoint_trash_path; } } @@ -462,24 +549,21 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // Create needed directories for decided trash can location. { - DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); Error err = dir_access->make_dir_recursive(trash_path); // Issue an error if trash can is not created properly. ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\""); err = dir_access->make_dir_recursive(trash_path + "/files"); - ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\"/files"); + ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "/files\""); err = dir_access->make_dir_recursive(trash_path + "/info"); - ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "\"/info"); + ERR_FAIL_COND_V_MSG(err != OK, err, "Could not create the trash path \"" + trash_path + "/info\""); } // The trash can is successfully created, now we check that we don't exceed our file name length limit. // If the file name is too long trim it so we can add the identifying number and ".trashinfo". // Assumes that the file name length limit is 255 characters. - String file_name = p_path.get_file(); - if (file_name.length() == 0) { - file_name = p_path.get_base_dir().get_file(); - } + String file_name = path.get_file(); if (file_name.length() > 240) { file_name = file_name.substr(0, file_name.length() - 15); } @@ -502,29 +586,32 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { } file_name = fn; + String renamed_path = path.get_base_dir() + "/" + file_name; + // Generates the .trashinfo file OS::Date date = OS::get_singleton()->get_date(false); OS::Time time = OS::get_singleton()->get_time(false); String timestamp = vformat("%04d-%02d-%02dT%02d:%02d:", date.year, (int)date.month, date.day, time.hour, time.minute); timestamp = vformat("%s%02d", timestamp, time.second); // vformat only supports up to 6 arguments. - String trash_info = "[Trash Info]\nPath=" + p_path.uri_encode() + "\nDeletionDate=" + timestamp + "\n"; + String trash_info = "[Trash Info]\nPath=" + path.uri_encode() + "\nDeletionDate=" + timestamp + "\n"; { Error err; - FileAccessRef file = FileAccess::open(trash_path + "/info/" + file_name + ".trashinfo", FileAccess::WRITE, &err); - ERR_FAIL_COND_V_MSG(err != OK, err, "Can't create trashinfo file:" + trash_path + "/info/" + file_name + ".trashinfo"); - file->store_string(trash_info); - file->close(); + { + Ref<FileAccess> file = FileAccess::open(trash_path + "/info/" + file_name + ".trashinfo", FileAccess::WRITE, &err); + ERR_FAIL_COND_V_MSG(err != OK, err, "Can't create trashinfo file: \"" + trash_path + "/info/" + file_name + ".trashinfo\""); + file->store_string(trash_info); + } // Rename our resource before moving it to the trash can. - DirAccessRef dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - err = dir_access->rename(p_path, p_path.get_base_dir() + "/" + file_name); - ERR_FAIL_COND_V_MSG(err != OK, err, "Can't rename file \"" + p_path + "\""); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + err = dir_access->rename(path, renamed_path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Can't rename file \"" + path + "\" to \"" + renamed_path + "\""); } // Move the given resource to the trash can. // Do not use DirAccess:rename() because it can't move files across multiple mountpoints. List<String> mv_args; - mv_args.push_back(p_path.get_base_dir() + "/" + file_name); + mv_args.push_back(renamed_path); mv_args.push_back(trash_path + "/files"); { int retval; @@ -532,11 +619,10 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { // Issue an error if "mv" failed to move the given resource to the trash can. if (err != OK || retval != 0) { - ERR_PRINT("move_to_trash: Could not move the resource \"" + p_path + "\" to the trash can \"" + trash_path + "/files\""); - DirAccess *dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - err = dir_access->rename(p_path.get_base_dir() + "/" + file_name, p_path); - memdelete(dir_access); - ERR_FAIL_COND_V_MSG(err != OK, err, "Could not rename " + p_path.get_base_dir() + "/" + file_name + " back to its original name:" + p_path); + ERR_PRINT("move_to_trash: Could not move the resource \"" + path + "\" to the trash can \"" + trash_path + "/files\""); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + err = dir_access->rename(renamed_path, path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Could not rename \"" + renamed_path + "\" back to its original name: \"" + path + "\""); return FAILED; } } diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index d3857e85f8..3f97b86eae 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -63,7 +63,7 @@ class OS_LinuxBSD : public OS_Unix { CrashHandler crash_handler; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; protected: virtual void initialize() override; @@ -78,6 +78,8 @@ public: virtual MainLoop *get_main_loop() const override; + virtual uint64_t get_embedded_pck_offset() const override; + virtual String get_config_path() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; diff --git a/platform/linuxbsd/speechd-so_wrap.c b/platform/linuxbsd/speechd-so_wrap.c new file mode 100644 index 0000000000..749474e181 --- /dev/null +++ b/platform/linuxbsd/speechd-so_wrap.c @@ -0,0 +1,881 @@ +// This file is generated. Do not edit! +// see https://github.com/hpvb/dynload-wrapper for details +// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21 +// flags: ./dynload-wrapper/generate-wrapper.py --sys-include <libspeechd.h> --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c +// +#include <stdint.h> + +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd +#define spd_open spd_open_dylibloader_orig_speechd +#define spd_open2 spd_open2_dylibloader_orig_speechd +#define spd_close spd_close_dylibloader_orig_speechd +#define spd_say spd_say_dylibloader_orig_speechd +#define spd_sayf spd_sayf_dylibloader_orig_speechd +#define spd_stop spd_stop_dylibloader_orig_speechd +#define spd_stop_all spd_stop_all_dylibloader_orig_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd +#define spd_cancel spd_cancel_dylibloader_orig_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd +#define spd_pause spd_pause_dylibloader_orig_speechd +#define spd_pause_all spd_pause_all_dylibloader_orig_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd +#define spd_resume spd_resume_dylibloader_orig_speechd +#define spd_resume_all spd_resume_all_dylibloader_orig_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd +#define spd_key spd_key_dylibloader_orig_speechd +#define spd_char spd_char_dylibloader_orig_speechd +#define spd_wchar spd_wchar_dylibloader_orig_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd +#define spd_set_notification spd_set_notification_dylibloader_orig_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd +#define spd_set_volume spd_set_volume_dylibloader_orig_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd +#define spd_get_volume spd_get_volume_dylibloader_orig_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd +#define spd_set_language spd_set_language_dylibloader_orig_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd +#define spd_get_language spd_get_language_dylibloader_orig_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd +#define spd_list_modules spd_list_modules_dylibloader_orig_speechd +#define free_spd_modules free_spd_modules_dylibloader_orig_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd +#define spd_list_voices spd_list_voices_dylibloader_orig_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd +#define free_spd_voices free_spd_voices_dylibloader_orig_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd +#define spd_execute_command spd_execute_command_dylibloader_orig_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd +#define spd_send_data spd_send_data_dylibloader_orig_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd +#include <libspeechd.h> +#undef SPDConnectionAddress__free +#undef spd_get_default_address +#undef spd_open +#undef spd_open2 +#undef spd_close +#undef spd_say +#undef spd_sayf +#undef spd_stop +#undef spd_stop_all +#undef spd_stop_uid +#undef spd_cancel +#undef spd_cancel_all +#undef spd_cancel_uid +#undef spd_pause +#undef spd_pause_all +#undef spd_pause_uid +#undef spd_resume +#undef spd_resume_all +#undef spd_resume_uid +#undef spd_key +#undef spd_char +#undef spd_wchar +#undef spd_sound_icon +#undef spd_set_voice_type +#undef spd_set_voice_type_all +#undef spd_set_voice_type_uid +#undef spd_get_voice_type +#undef spd_set_synthesis_voice +#undef spd_set_synthesis_voice_all +#undef spd_set_synthesis_voice_uid +#undef spd_set_data_mode +#undef spd_set_notification_on +#undef spd_set_notification_off +#undef spd_set_notification +#undef spd_set_voice_rate +#undef spd_set_voice_rate_all +#undef spd_set_voice_rate_uid +#undef spd_get_voice_rate +#undef spd_set_voice_pitch +#undef spd_set_voice_pitch_all +#undef spd_set_voice_pitch_uid +#undef spd_get_voice_pitch +#undef spd_set_voice_pitch_range +#undef spd_set_voice_pitch_range_all +#undef spd_set_voice_pitch_range_uid +#undef spd_set_volume +#undef spd_set_volume_all +#undef spd_set_volume_uid +#undef spd_get_volume +#undef spd_set_punctuation +#undef spd_set_punctuation_all +#undef spd_set_punctuation_uid +#undef spd_set_capital_letters +#undef spd_set_capital_letters_all +#undef spd_set_capital_letters_uid +#undef spd_set_spelling +#undef spd_set_spelling_all +#undef spd_set_spelling_uid +#undef spd_set_language +#undef spd_set_language_all +#undef spd_set_language_uid +#undef spd_get_language +#undef spd_set_output_module +#undef spd_set_output_module_all +#undef spd_set_output_module_uid +#undef spd_get_message_list_fd +#undef spd_list_modules +#undef free_spd_modules +#undef spd_get_output_module +#undef spd_list_voices +#undef spd_list_synthesis_voices +#undef free_spd_voices +#undef spd_execute_command_with_list_reply +#undef spd_execute_command +#undef spd_execute_command_with_reply +#undef spd_execute_command_wo_mutex +#undef spd_send_data +#undef spd_send_data_wo_mutex +#include <dlfcn.h> +#include <stdio.h> +void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*); +SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**); +SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode); +SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**); +void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...); +int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t); +int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int); +SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode); +int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*); +int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int); +int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int); +int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int); +int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*); +int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**); +char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*); +void (*free_spd_modules_dylibloader_wrapper_speechd)( char**); +char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*); +char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*); +SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*); +void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**); +char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*); +int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*); +int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**); +int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*); +char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +int initialize_speechd(int verbose) { + void *handle; + char *error; + handle = dlopen("libspeechd.so.2", RTLD_LAZY); + if (!handle) { + if (verbose) { + fprintf(stderr, "%s\n", dlerror()); + } + return(1); + } + dlerror(); +// SPDConnectionAddress__free + *(void **) (&SPDConnectionAddress__free_dylibloader_wrapper_speechd) = dlsym(handle, "SPDConnectionAddress__free"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_default_address + *(void **) (&spd_get_default_address_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_default_address"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_open + *(void **) (&spd_open_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_open2 + *(void **) (&spd_open2_dylibloader_wrapper_speechd) = dlsym(handle, "spd_open2"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_close + *(void **) (&spd_close_dylibloader_wrapper_speechd) = dlsym(handle, "spd_close"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_say + *(void **) (&spd_say_dylibloader_wrapper_speechd) = dlsym(handle, "spd_say"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_sayf + *(void **) (&spd_sayf_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sayf"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop + *(void **) (&spd_stop_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop_all + *(void **) (&spd_stop_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_stop_uid + *(void **) (&spd_stop_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_stop_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel + *(void **) (&spd_cancel_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel_all + *(void **) (&spd_cancel_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_cancel_uid + *(void **) (&spd_cancel_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_cancel_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause + *(void **) (&spd_pause_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause_all + *(void **) (&spd_pause_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_pause_uid + *(void **) (&spd_pause_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_pause_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume + *(void **) (&spd_resume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume_all + *(void **) (&spd_resume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_resume_uid + *(void **) (&spd_resume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_resume_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_key + *(void **) (&spd_key_dylibloader_wrapper_speechd) = dlsym(handle, "spd_key"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_char + *(void **) (&spd_char_dylibloader_wrapper_speechd) = dlsym(handle, "spd_char"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_wchar + *(void **) (&spd_wchar_dylibloader_wrapper_speechd) = dlsym(handle, "spd_wchar"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_sound_icon + *(void **) (&spd_sound_icon_dylibloader_wrapper_speechd) = dlsym(handle, "spd_sound_icon"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type + *(void **) (&spd_set_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type_all + *(void **) (&spd_set_voice_type_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_type_uid + *(void **) (&spd_set_voice_type_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_type_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_type + *(void **) (&spd_get_voice_type_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_type"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice + *(void **) (&spd_set_synthesis_voice_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice_all + *(void **) (&spd_set_synthesis_voice_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_synthesis_voice_uid + *(void **) (&spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_synthesis_voice_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_data_mode + *(void **) (&spd_set_data_mode_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_data_mode"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification_on + *(void **) (&spd_set_notification_on_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_on"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification_off + *(void **) (&spd_set_notification_off_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification_off"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_notification + *(void **) (&spd_set_notification_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_notification"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate + *(void **) (&spd_set_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate_all + *(void **) (&spd_set_voice_rate_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_rate_uid + *(void **) (&spd_set_voice_rate_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_rate_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_rate + *(void **) (&spd_get_voice_rate_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_rate"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch + *(void **) (&spd_set_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_all + *(void **) (&spd_set_voice_pitch_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_uid + *(void **) (&spd_set_voice_pitch_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_voice_pitch + *(void **) (&spd_get_voice_pitch_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_voice_pitch"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range + *(void **) (&spd_set_voice_pitch_range_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range_all + *(void **) (&spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_voice_pitch_range_uid + *(void **) (&spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_voice_pitch_range_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume + *(void **) (&spd_set_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume_all + *(void **) (&spd_set_volume_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_volume_uid + *(void **) (&spd_set_volume_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_volume_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_volume + *(void **) (&spd_get_volume_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_volume"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation + *(void **) (&spd_set_punctuation_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation_all + *(void **) (&spd_set_punctuation_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_punctuation_uid + *(void **) (&spd_set_punctuation_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_punctuation_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters + *(void **) (&spd_set_capital_letters_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters_all + *(void **) (&spd_set_capital_letters_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_capital_letters_uid + *(void **) (&spd_set_capital_letters_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_capital_letters_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling + *(void **) (&spd_set_spelling_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling_all + *(void **) (&spd_set_spelling_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_spelling_uid + *(void **) (&spd_set_spelling_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_spelling_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language + *(void **) (&spd_set_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language_all + *(void **) (&spd_set_language_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_language_uid + *(void **) (&spd_set_language_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_language_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_language + *(void **) (&spd_get_language_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_language"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module + *(void **) (&spd_set_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module_all + *(void **) (&spd_set_output_module_all_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_all"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_set_output_module_uid + *(void **) (&spd_set_output_module_uid_dylibloader_wrapper_speechd) = dlsym(handle, "spd_set_output_module_uid"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_message_list_fd + *(void **) (&spd_get_message_list_fd_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_message_list_fd"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_modules + *(void **) (&spd_list_modules_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_modules"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// free_spd_modules + *(void **) (&free_spd_modules_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_modules"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_get_output_module + *(void **) (&spd_get_output_module_dylibloader_wrapper_speechd) = dlsym(handle, "spd_get_output_module"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_voices + *(void **) (&spd_list_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_list_synthesis_voices + *(void **) (&spd_list_synthesis_voices_dylibloader_wrapper_speechd) = dlsym(handle, "spd_list_synthesis_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// free_spd_voices + *(void **) (&free_spd_voices_dylibloader_wrapper_speechd) = dlsym(handle, "free_spd_voices"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_with_list_reply + *(void **) (&spd_execute_command_with_list_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_list_reply"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command + *(void **) (&spd_execute_command_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_with_reply + *(void **) (&spd_execute_command_with_reply_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_with_reply"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_execute_command_wo_mutex + *(void **) (&spd_execute_command_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_execute_command_wo_mutex"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_send_data + *(void **) (&spd_send_data_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +// spd_send_data_wo_mutex + *(void **) (&spd_send_data_wo_mutex_dylibloader_wrapper_speechd) = dlsym(handle, "spd_send_data_wo_mutex"); + if (verbose) { + error = dlerror(); + if (error != NULL) { + fprintf(stderr, "%s\n", error); + } + } +return 0; +} diff --git a/platform/linuxbsd/speechd-so_wrap.h b/platform/linuxbsd/speechd-so_wrap.h new file mode 100644 index 0000000000..8e1c053348 --- /dev/null +++ b/platform/linuxbsd/speechd-so_wrap.h @@ -0,0 +1,330 @@ +#ifndef DYLIBLOAD_WRAPPER_SPEECHD +#define DYLIBLOAD_WRAPPER_SPEECHD +// This file is generated. Do not edit! +// see https://github.com/hpvb/dynload-wrapper for details +// generated by ./dynload-wrapper/generate-wrapper.py 0.3 on 2022-04-28 14:34:21 +// flags: ./dynload-wrapper/generate-wrapper.py --sys-include <libspeechd.h> --include /usr/include/speech-dispatcher/libspeechd.h --soname libspeechd.so.2 --init-name speechd --omit-prefix spd_get_client_list --output-header speechd-so_wrap.h --output-implementation speechd-so_wrap.c +// +#include <stdint.h> + +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_orig_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_orig_speechd +#define spd_open spd_open_dylibloader_orig_speechd +#define spd_open2 spd_open2_dylibloader_orig_speechd +#define spd_close spd_close_dylibloader_orig_speechd +#define spd_say spd_say_dylibloader_orig_speechd +#define spd_sayf spd_sayf_dylibloader_orig_speechd +#define spd_stop spd_stop_dylibloader_orig_speechd +#define spd_stop_all spd_stop_all_dylibloader_orig_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_orig_speechd +#define spd_cancel spd_cancel_dylibloader_orig_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_orig_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_orig_speechd +#define spd_pause spd_pause_dylibloader_orig_speechd +#define spd_pause_all spd_pause_all_dylibloader_orig_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_orig_speechd +#define spd_resume spd_resume_dylibloader_orig_speechd +#define spd_resume_all spd_resume_all_dylibloader_orig_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_orig_speechd +#define spd_key spd_key_dylibloader_orig_speechd +#define spd_char spd_char_dylibloader_orig_speechd +#define spd_wchar spd_wchar_dylibloader_orig_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_orig_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_orig_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_orig_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_orig_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_orig_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_orig_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_orig_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_orig_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_orig_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_orig_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_orig_speechd +#define spd_set_notification spd_set_notification_dylibloader_orig_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_orig_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_orig_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_orig_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_orig_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_orig_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_orig_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_orig_speechd +#define spd_set_volume spd_set_volume_dylibloader_orig_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_orig_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_orig_speechd +#define spd_get_volume spd_get_volume_dylibloader_orig_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_orig_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_orig_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_orig_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_orig_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_orig_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_orig_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_orig_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_orig_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_orig_speechd +#define spd_set_language spd_set_language_dylibloader_orig_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_orig_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_orig_speechd +#define spd_get_language spd_get_language_dylibloader_orig_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_orig_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_orig_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_orig_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_orig_speechd +#define spd_list_modules spd_list_modules_dylibloader_orig_speechd +#define free_spd_modules free_spd_modules_dylibloader_orig_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_orig_speechd +#define spd_list_voices spd_list_voices_dylibloader_orig_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_orig_speechd +#define free_spd_voices free_spd_voices_dylibloader_orig_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_orig_speechd +#define spd_execute_command spd_execute_command_dylibloader_orig_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_orig_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_orig_speechd +#define spd_send_data spd_send_data_dylibloader_orig_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_orig_speechd +#include <libspeechd.h> +#undef SPDConnectionAddress__free +#undef spd_get_default_address +#undef spd_open +#undef spd_open2 +#undef spd_close +#undef spd_say +#undef spd_sayf +#undef spd_stop +#undef spd_stop_all +#undef spd_stop_uid +#undef spd_cancel +#undef spd_cancel_all +#undef spd_cancel_uid +#undef spd_pause +#undef spd_pause_all +#undef spd_pause_uid +#undef spd_resume +#undef spd_resume_all +#undef spd_resume_uid +#undef spd_key +#undef spd_char +#undef spd_wchar +#undef spd_sound_icon +#undef spd_set_voice_type +#undef spd_set_voice_type_all +#undef spd_set_voice_type_uid +#undef spd_get_voice_type +#undef spd_set_synthesis_voice +#undef spd_set_synthesis_voice_all +#undef spd_set_synthesis_voice_uid +#undef spd_set_data_mode +#undef spd_set_notification_on +#undef spd_set_notification_off +#undef spd_set_notification +#undef spd_set_voice_rate +#undef spd_set_voice_rate_all +#undef spd_set_voice_rate_uid +#undef spd_get_voice_rate +#undef spd_set_voice_pitch +#undef spd_set_voice_pitch_all +#undef spd_set_voice_pitch_uid +#undef spd_get_voice_pitch +#undef spd_set_voice_pitch_range +#undef spd_set_voice_pitch_range_all +#undef spd_set_voice_pitch_range_uid +#undef spd_set_volume +#undef spd_set_volume_all +#undef spd_set_volume_uid +#undef spd_get_volume +#undef spd_set_punctuation +#undef spd_set_punctuation_all +#undef spd_set_punctuation_uid +#undef spd_set_capital_letters +#undef spd_set_capital_letters_all +#undef spd_set_capital_letters_uid +#undef spd_set_spelling +#undef spd_set_spelling_all +#undef spd_set_spelling_uid +#undef spd_set_language +#undef spd_set_language_all +#undef spd_set_language_uid +#undef spd_get_language +#undef spd_set_output_module +#undef spd_set_output_module_all +#undef spd_set_output_module_uid +#undef spd_get_message_list_fd +#undef spd_list_modules +#undef free_spd_modules +#undef spd_get_output_module +#undef spd_list_voices +#undef spd_list_synthesis_voices +#undef free_spd_voices +#undef spd_execute_command_with_list_reply +#undef spd_execute_command +#undef spd_execute_command_with_reply +#undef spd_execute_command_wo_mutex +#undef spd_send_data +#undef spd_send_data_wo_mutex +#ifdef __cplusplus +extern "C" { +#endif +#define SPDConnectionAddress__free SPDConnectionAddress__free_dylibloader_wrapper_speechd +#define spd_get_default_address spd_get_default_address_dylibloader_wrapper_speechd +#define spd_open spd_open_dylibloader_wrapper_speechd +#define spd_open2 spd_open2_dylibloader_wrapper_speechd +#define spd_close spd_close_dylibloader_wrapper_speechd +#define spd_say spd_say_dylibloader_wrapper_speechd +#define spd_sayf spd_sayf_dylibloader_wrapper_speechd +#define spd_stop spd_stop_dylibloader_wrapper_speechd +#define spd_stop_all spd_stop_all_dylibloader_wrapper_speechd +#define spd_stop_uid spd_stop_uid_dylibloader_wrapper_speechd +#define spd_cancel spd_cancel_dylibloader_wrapper_speechd +#define spd_cancel_all spd_cancel_all_dylibloader_wrapper_speechd +#define spd_cancel_uid spd_cancel_uid_dylibloader_wrapper_speechd +#define spd_pause spd_pause_dylibloader_wrapper_speechd +#define spd_pause_all spd_pause_all_dylibloader_wrapper_speechd +#define spd_pause_uid spd_pause_uid_dylibloader_wrapper_speechd +#define spd_resume spd_resume_dylibloader_wrapper_speechd +#define spd_resume_all spd_resume_all_dylibloader_wrapper_speechd +#define spd_resume_uid spd_resume_uid_dylibloader_wrapper_speechd +#define spd_key spd_key_dylibloader_wrapper_speechd +#define spd_char spd_char_dylibloader_wrapper_speechd +#define spd_wchar spd_wchar_dylibloader_wrapper_speechd +#define spd_sound_icon spd_sound_icon_dylibloader_wrapper_speechd +#define spd_set_voice_type spd_set_voice_type_dylibloader_wrapper_speechd +#define spd_set_voice_type_all spd_set_voice_type_all_dylibloader_wrapper_speechd +#define spd_set_voice_type_uid spd_set_voice_type_uid_dylibloader_wrapper_speechd +#define spd_get_voice_type spd_get_voice_type_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice spd_set_synthesis_voice_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice_all spd_set_synthesis_voice_all_dylibloader_wrapper_speechd +#define spd_set_synthesis_voice_uid spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd +#define spd_set_data_mode spd_set_data_mode_dylibloader_wrapper_speechd +#define spd_set_notification_on spd_set_notification_on_dylibloader_wrapper_speechd +#define spd_set_notification_off spd_set_notification_off_dylibloader_wrapper_speechd +#define spd_set_notification spd_set_notification_dylibloader_wrapper_speechd +#define spd_set_voice_rate spd_set_voice_rate_dylibloader_wrapper_speechd +#define spd_set_voice_rate_all spd_set_voice_rate_all_dylibloader_wrapper_speechd +#define spd_set_voice_rate_uid spd_set_voice_rate_uid_dylibloader_wrapper_speechd +#define spd_get_voice_rate spd_get_voice_rate_dylibloader_wrapper_speechd +#define spd_set_voice_pitch spd_set_voice_pitch_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_all spd_set_voice_pitch_all_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_uid spd_set_voice_pitch_uid_dylibloader_wrapper_speechd +#define spd_get_voice_pitch spd_get_voice_pitch_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range spd_set_voice_pitch_range_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range_all spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd +#define spd_set_voice_pitch_range_uid spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd +#define spd_set_volume spd_set_volume_dylibloader_wrapper_speechd +#define spd_set_volume_all spd_set_volume_all_dylibloader_wrapper_speechd +#define spd_set_volume_uid spd_set_volume_uid_dylibloader_wrapper_speechd +#define spd_get_volume spd_get_volume_dylibloader_wrapper_speechd +#define spd_set_punctuation spd_set_punctuation_dylibloader_wrapper_speechd +#define spd_set_punctuation_all spd_set_punctuation_all_dylibloader_wrapper_speechd +#define spd_set_punctuation_uid spd_set_punctuation_uid_dylibloader_wrapper_speechd +#define spd_set_capital_letters spd_set_capital_letters_dylibloader_wrapper_speechd +#define spd_set_capital_letters_all spd_set_capital_letters_all_dylibloader_wrapper_speechd +#define spd_set_capital_letters_uid spd_set_capital_letters_uid_dylibloader_wrapper_speechd +#define spd_set_spelling spd_set_spelling_dylibloader_wrapper_speechd +#define spd_set_spelling_all spd_set_spelling_all_dylibloader_wrapper_speechd +#define spd_set_spelling_uid spd_set_spelling_uid_dylibloader_wrapper_speechd +#define spd_set_language spd_set_language_dylibloader_wrapper_speechd +#define spd_set_language_all spd_set_language_all_dylibloader_wrapper_speechd +#define spd_set_language_uid spd_set_language_uid_dylibloader_wrapper_speechd +#define spd_get_language spd_get_language_dylibloader_wrapper_speechd +#define spd_set_output_module spd_set_output_module_dylibloader_wrapper_speechd +#define spd_set_output_module_all spd_set_output_module_all_dylibloader_wrapper_speechd +#define spd_set_output_module_uid spd_set_output_module_uid_dylibloader_wrapper_speechd +#define spd_get_message_list_fd spd_get_message_list_fd_dylibloader_wrapper_speechd +#define spd_list_modules spd_list_modules_dylibloader_wrapper_speechd +#define free_spd_modules free_spd_modules_dylibloader_wrapper_speechd +#define spd_get_output_module spd_get_output_module_dylibloader_wrapper_speechd +#define spd_list_voices spd_list_voices_dylibloader_wrapper_speechd +#define spd_list_synthesis_voices spd_list_synthesis_voices_dylibloader_wrapper_speechd +#define free_spd_voices free_spd_voices_dylibloader_wrapper_speechd +#define spd_execute_command_with_list_reply spd_execute_command_with_list_reply_dylibloader_wrapper_speechd +#define spd_execute_command spd_execute_command_dylibloader_wrapper_speechd +#define spd_execute_command_with_reply spd_execute_command_with_reply_dylibloader_wrapper_speechd +#define spd_execute_command_wo_mutex spd_execute_command_wo_mutex_dylibloader_wrapper_speechd +#define spd_send_data spd_send_data_dylibloader_wrapper_speechd +#define spd_send_data_wo_mutex spd_send_data_wo_mutex_dylibloader_wrapper_speechd +extern void (*SPDConnectionAddress__free_dylibloader_wrapper_speechd)( SPDConnectionAddress*); +extern SPDConnectionAddress* (*spd_get_default_address_dylibloader_wrapper_speechd)( char**); +extern SPDConnection* (*spd_open_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode); +extern SPDConnection* (*spd_open2_dylibloader_wrapper_speechd)(const char*,const char*,const char*, SPDConnectionMode, SPDConnectionAddress*, int, char**); +extern void (*spd_close_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_say_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_sayf_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*,...); +extern int (*spd_stop_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_stop_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_stop_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_cancel_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_cancel_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_cancel_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_pause_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_pause_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_pause_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_resume_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_resume_all_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_resume_uid_dylibloader_wrapper_speechd)( SPDConnection*, int); +extern int (*spd_key_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_char_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_wchar_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority, wchar_t); +extern int (*spd_sound_icon_dylibloader_wrapper_speechd)( SPDConnection*, SPDPriority,const char*); +extern int (*spd_set_voice_type_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +extern int (*spd_set_voice_type_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType); +extern int (*spd_set_voice_type_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDVoiceType, unsigned int); +extern SPDVoiceType (*spd_get_voice_type_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_synthesis_voice_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_synthesis_voice_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_synthesis_voice_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern int (*spd_set_data_mode_dylibloader_wrapper_speechd)( SPDConnection*, SPDDataMode); +extern int (*spd_set_notification_on_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +extern int (*spd_set_notification_off_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification); +extern int (*spd_set_notification_dylibloader_wrapper_speechd)( SPDConnection*, SPDNotification,const char*); +extern int (*spd_set_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_rate_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_rate_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_voice_rate_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_voice_pitch_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_voice_pitch_range_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_range_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_voice_pitch_range_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_set_volume_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_volume_all_dylibloader_wrapper_speechd)( SPDConnection*, signed int); +extern int (*spd_set_volume_uid_dylibloader_wrapper_speechd)( SPDConnection*, signed int, unsigned int); +extern int (*spd_get_volume_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_punctuation_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +extern int (*spd_set_punctuation_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation); +extern int (*spd_set_punctuation_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDPunctuation, unsigned int); +extern int (*spd_set_capital_letters_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +extern int (*spd_set_capital_letters_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters); +extern int (*spd_set_capital_letters_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDCapitalLetters, unsigned int); +extern int (*spd_set_spelling_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +extern int (*spd_set_spelling_all_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling); +extern int (*spd_set_spelling_uid_dylibloader_wrapper_speechd)( SPDConnection*, SPDSpelling, unsigned int); +extern int (*spd_set_language_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_language_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_language_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern char* (*spd_get_language_dylibloader_wrapper_speechd)( SPDConnection*); +extern int (*spd_set_output_module_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_output_module_all_dylibloader_wrapper_speechd)( SPDConnection*,const char*); +extern int (*spd_set_output_module_uid_dylibloader_wrapper_speechd)( SPDConnection*,const char*, unsigned int); +extern int (*spd_get_message_list_fd_dylibloader_wrapper_speechd)( SPDConnection*, int, int*, char**); +extern char** (*spd_list_modules_dylibloader_wrapper_speechd)( SPDConnection*); +extern void (*free_spd_modules_dylibloader_wrapper_speechd)( char**); +extern char* (*spd_get_output_module_dylibloader_wrapper_speechd)( SPDConnection*); +extern char** (*spd_list_voices_dylibloader_wrapper_speechd)( SPDConnection*); +extern SPDVoice** (*spd_list_synthesis_voices_dylibloader_wrapper_speechd)( SPDConnection*); +extern void (*free_spd_voices_dylibloader_wrapper_speechd)( SPDVoice**); +extern char** (*spd_execute_command_with_list_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern int (*spd_execute_command_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern int (*spd_execute_command_with_reply_dylibloader_wrapper_speechd)( SPDConnection*, char*, char**); +extern int (*spd_execute_command_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*, char*); +extern char* (*spd_send_data_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +extern char* (*spd_send_data_wo_mutex_dylibloader_wrapper_speechd)( SPDConnection*,const char*, int); +int initialize_speechd(int verbose); +#ifdef __cplusplus +} +#endif +#endif diff --git a/platform/linuxbsd/tts_linux.cpp b/platform/linuxbsd/tts_linux.cpp new file mode 100644 index 0000000000..aea1183d3d --- /dev/null +++ b/platform/linuxbsd/tts_linux.cpp @@ -0,0 +1,261 @@ +/*************************************************************************/ +/* tts_linux.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "tts_linux.h" + +#include "core/config/project_settings.h" +#include "servers/text_server.h" + +TTS_Linux *TTS_Linux::singleton = nullptr; + +void TTS_Linux::speech_init_thread_func(void *p_userdata) { + TTS_Linux *tts = (TTS_Linux *)p_userdata; + if (tts) { + MutexLock thread_safe_method(tts->_thread_safe_); +#ifdef DEBUG_ENABLED + int dylibloader_verbose = 1; +#else + int dylibloader_verbose = 0; +#endif + if (initialize_speechd(dylibloader_verbose) == 0) { + CharString class_str; + String config_name = GLOBAL_GET("application/config/name"); + if (config_name.length() == 0) { + class_str = "Godot_Engine"; + } else { + class_str = config_name.utf8(); + } + tts->synth = spd_open(class_str, "Godot_Engine_Speech_API", "Godot_Engine", SPD_MODE_THREADED); + if (tts->synth) { + tts->synth->callback_end = &speech_event_callback; + tts->synth->callback_cancel = &speech_event_callback; + tts->synth->callback_im = &speech_event_index_mark; + spd_set_notification_on(tts->synth, SPD_END); + spd_set_notification_on(tts->synth, SPD_CANCEL); + + print_verbose("Text-to-Speech: Speech Dispatcher initialized."); + } else { + print_verbose("Text-to-Speech: Cannot initialize Speech Dispatcher synthesizer!"); + } + } else { + print_verbose("Text-to-Speech: Cannot load Speech Dispatcher library!"); + } + } +} + +void TTS_Linux::speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark) { + TTS_Linux *tts = TTS_Linux::get_singleton(); + if (tts && tts->ids.has(p_msg_id)) { + MutexLock thread_safe_method(tts->_thread_safe_); + // Get word offset from the index mark injected to the text stream. + String mark = String::utf8(p_index_mark); + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[p_msg_id], mark.to_int()); + } +} + +void TTS_Linux::speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type) { + TTS_Linux *tts = TTS_Linux::get_singleton(); + if (tts) { + MutexLock thread_safe_method(tts->_thread_safe_); + List<DisplayServer::TTSUtterance> &queue = tts->queue; + if (!tts->paused && tts->ids.has(p_msg_id)) { + if (p_type == SPD_EVENT_END) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[p_msg_id]); + tts->ids.erase(p_msg_id); + tts->last_msg_id = -1; + tts->speaking = false; + } else if (p_type == SPD_EVENT_CANCEL) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, tts->ids[p_msg_id]); + tts->ids.erase(p_msg_id); + tts->last_msg_id = -1; + tts->speaking = false; + } + } + if (!tts->speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + // Inject index mark after each word. + String text; + String language; + SPDVoice **voices = spd_list_synthesis_voices(tts->synth); + if (voices != nullptr) { + SPDVoice **voices_ptr = voices; + while (*voices_ptr != nullptr) { + if (String::utf8((*voices_ptr)->name) == message.voice) { + language = String::utf8((*voices_ptr)->language); + break; + } + voices_ptr++; + } + free_spd_voices(voices); + } + PackedInt32Array breaks = TS->string_get_word_breaks(message.text, language); + int prev = 0; + for (int i = 0; i < breaks.size(); i++) { + text += message.text.substr(prev, breaks[i] - prev); + text += "<mark name=\"" + String::num_int64(breaks[i], 10) + "\"/>"; + prev = breaks[i]; + } + text += message.text.substr(prev, -1); + + spd_set_synthesis_voice(tts->synth, message.voice.utf8().get_data()); + spd_set_volume(tts->synth, message.volume * 2 - 100); + spd_set_voice_pitch(tts->synth, (message.pitch - 1) * 100); + float rate = 0; + if (message.rate > 1.f) { + rate = log10(MIN(message.rate, 2.5f)) / log10(2.5f) * 100; + } else if (message.rate < 1.f) { + rate = log10(MAX(message.rate, 0.5f)) / log10(0.5f) * -100; + } + spd_set_voice_rate(tts->synth, rate); + spd_set_data_mode(tts->synth, SPD_DATA_SSML); + tts->last_msg_id = spd_say(tts->synth, SPD_TEXT, text.utf8().get_data()); + tts->ids[tts->last_msg_id] = message.id; + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + + queue.pop_front(); + tts->speaking = true; + } + } +} + +bool TTS_Linux::is_speaking() const { + return speaking; +} + +bool TTS_Linux::is_paused() const { + return paused; +} + +Array TTS_Linux::get_voices() const { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND_V(!synth, Array()); + Array list; + SPDVoice **voices = spd_list_synthesis_voices(synth); + if (voices != nullptr) { + SPDVoice **voices_ptr = voices; + while (*voices_ptr != nullptr) { + Dictionary voice_d; + voice_d["name"] = String::utf8((*voices_ptr)->name); + voice_d["id"] = String::utf8((*voices_ptr)->name); + voice_d["language"] = String::utf8((*voices_ptr)->language) + "_" + String::utf8((*voices_ptr)->variant); + list.push_back(voice_d); + + voices_ptr++; + } + free_spd_voices(voices); + } + return list; +} + +void TTS_Linux::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = p_text; + message.voice = p_voice; + message.volume = CLAMP(p_volume, 0, 100); + message.pitch = CLAMP(p_pitch, 0.f, 2.f); + message.rate = CLAMP(p_rate, 0.1f, 10.f); + message.id = p_utterance_id; + queue.push_back(message); + + if (is_paused()) { + resume(); + } else { + speech_event_callback(0, 0, SPD_EVENT_BEGIN); + } +} + +void TTS_Linux::pause() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + if (spd_pause(synth) == 0) { + paused = true; + } +} + +void TTS_Linux::resume() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + spd_resume(synth); + paused = false; +} + +void TTS_Linux::stop() { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!synth); + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + if ((last_msg_id != -1) && ids.has(last_msg_id)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[last_msg_id]); + } + queue.clear(); + ids.clear(); + last_msg_id = -1; + spd_cancel(synth); + spd_resume(synth); + speaking = false; + paused = false; +} + +TTS_Linux *TTS_Linux::get_singleton() { + return singleton; +} + +TTS_Linux::TTS_Linux() { + singleton = this; + // Speech Dispatcher init can be slow, it might wait for helper process to start on background, so run it in the thread. + init_thread.start(speech_init_thread_func, this); +} + +TTS_Linux::~TTS_Linux() { + init_thread.wait_to_finish(); + if (synth) { + spd_close(synth); + } + + singleton = nullptr; +} diff --git a/platform/linuxbsd/tts_linux.h b/platform/linuxbsd/tts_linux.h new file mode 100644 index 0000000000..4e3f348ae4 --- /dev/null +++ b/platform/linuxbsd/tts_linux.h @@ -0,0 +1,78 @@ +/*************************************************************************/ +/* tts_linux.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TTS_LINUX_H +#define TTS_LINUX_H + +#include "core/os/thread.h" +#include "core/os/thread_safe.h" +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/rb_map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include "speechd-so_wrap.h" + +class TTS_Linux { + _THREAD_SAFE_CLASS_ + + List<DisplayServer::TTSUtterance> queue; + SPDConnection *synth = nullptr; + bool speaking = false; + bool paused = false; + int last_msg_id = -1; + HashMap<int, int> ids; + + Thread init_thread; + + static void speech_init_thread_func(void *p_userdata); + static void speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type); + static void speech_event_index_mark(size_t p_msg_id, size_t p_client_id, SPDNotificationType p_type, char *p_index_mark); + + static TTS_Linux *singleton; + +public: + static TTS_Linux *get_singleton(); + + bool is_speaking() const; + bool is_paused() const; + Array get_voices() const; + + void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false); + void pause(); + void resume(); + void stop(); + + TTS_Linux(); + ~TTS_Linux(); +}; + +#endif // TTS_LINUX_H diff --git a/platform/osx/SCsub b/platform/osx/SCsub index d72a75af04..3a4c95613d 100644 --- a/platform/osx/SCsub +++ b/platform/osx/SCsub @@ -18,6 +18,7 @@ files = [ "key_mapping_osx.mm", "godot_main_osx.mm", "dir_access_osx.mm", + "tts_osx.mm", "joypad_osx.cpp", "vulkan_context_osx.mm", "gl_manager_osx_legacy.mm", diff --git a/platform/osx/crash_handler_osx.mm b/platform/osx/crash_handler_osx.mm index 06ed91907c..a798ba3b46 100644 --- a/platform/osx/crash_handler_osx.mm +++ b/platform/osx/crash_handler_osx.mm @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" @@ -85,21 +86,22 @@ static void handle_crash(int sig) { msg = proj_settings->get("debug/settings/crash_handler/message"); } - // Dump the backtrace to stderr with a message to the user - fprintf(stderr, "\n================================================================\n"); - fprintf(stderr, "%s: Program crashed with signal %d\n", __FUNCTION__, sig); - + // Tell MainLoop about the crash. This can be handled by users too in Node. if (OS::get_singleton()->get_main_loop()) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + // Dump the backtrace to stderr with a message to the user + print_error("\n================================================================"); + print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, sig)); + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. if (String(VERSION_HASH).is_empty()) { - fprintf(stderr, "Engine version: %s\n", VERSION_FULL_NAME); + print_error(vformat("Engine version: %s", VERSION_FULL_NAME)); } else { - fprintf(stderr, "Engine version: %s (%s)\n", VERSION_FULL_NAME, VERSION_HASH); + print_error(vformat("Engine version: %s (%s)", VERSION_FULL_NAME, VERSION_HASH)); } - fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); + print_error(vformat("Dumping the backtrace. %s", msg)); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { void *load_addr = (void *)load_address(); @@ -157,13 +159,13 @@ static void handle_crash(int sig) { } } - fprintf(stderr, "[%zu] %s\n", i, output.utf8().get_data()); + print_error(vformat("[%d] %s", (int64_t)i, output)); } free(strings); } - fprintf(stderr, "-- END OF BACKTRACE --\n"); - fprintf(stderr, "================================================================\n"); + print_error("-- END OF BACKTRACE --"); + print_error("================================================================"); // Abort to pass the error to the OS abort(); diff --git a/platform/osx/detect.py b/platform/osx/detect.py index 0ff93bedb4..8d848d2094 100644 --- a/platform/osx/detect.py +++ b/platform/osx/detect.py @@ -127,6 +127,7 @@ def configure(env): if env["use_ubsan"] or env["use_asan"] or env["use_tsan"]: env.extra_suffix += ".san" + env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"]) if env["use_ubsan"]: env.Append( diff --git a/platform/osx/dir_access_osx.mm b/platform/osx/dir_access_osx.mm index d26f35e847..6bafb9470d 100644 --- a/platform/osx/dir_access_osx.mm +++ b/platform/osx/dir_access_osx.mm @@ -34,8 +34,8 @@ #include <errno.h> -#include <AppKit/NSWorkspace.h> -#include <Foundation/Foundation.h> +#import <AppKit/NSWorkspace.h> +#import <Foundation/Foundation.h> String DirAccessOSX::fix_unicode_name(const char *p_name) const { String fname; diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index 036e74c47c..9575cb29a2 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -45,10 +45,11 @@ #include "platform/osx/vulkan_context_osx.h" #endif // VULKAN_ENABLED -#include <AppKit/AppKit.h> -#include <AppKit/NSCursor.h> -#include <ApplicationServices/ApplicationServices.h> -#include <CoreVideo/CoreVideo.h> +#import <AppKit/AppKit.h> +#import <AppKit/NSCursor.h> +#import <ApplicationServices/ApplicationServices.h> +#import <CoreVideo/CoreVideo.h> +#import <Foundation/Foundation.h> #undef BitMap #undef CursorShape @@ -96,7 +97,7 @@ public: WindowID transient_parent = INVALID_WINDOW_ID; bool exclusive = false; - Set<WindowID> transient_children; + HashSet<WindowID> transient_children; bool layered_window = false; bool fullscreen = false; @@ -124,7 +125,7 @@ private: NSMenu *apple_menu = nullptr; NSMenu *dock_menu = nullptr; - Map<String, NSMenu *> submenu; + HashMap<String, NSMenu *> submenu; struct WarpEvent { NSTimeInterval timestamp; @@ -137,6 +138,8 @@ private: Vector<KeyEvent> key_event_buffer; int key_event_pos = 0; + id tts = nullptr; + Point2i im_selection; String im_text; @@ -160,12 +163,13 @@ private: float display_max_scale = 1.f; Point2i origin; bool displays_arrangement_dirty = true; + bool is_resizing = false; CursorShape cursor_shape = CURSOR_ARROW; NSCursor *cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; + HashMap<CursorShape, Vector<Variant>> cursors_cache; - Map<WindowID, WindowData> windows; + HashMap<WindowID, WindowData> windows; const NSMenu *_get_menu_root(const String &p_menu_root) const; NSMenu *_get_menu_root(const String &p_menu_root); @@ -185,6 +189,7 @@ private: void _process_key_events(); void _update_keyboard_layouts(); static void _keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info); + NSImage *_convert_to_nsimg(Ref<Image> &p_image) const; static NSCursor *_cursor_from_selector(SEL p_selector, SEL p_fallback = nil); @@ -203,9 +208,11 @@ public: void push_to_key_event_buffer(const KeyEvent &p_event); void update_im_text(const Point2i &p_selection, const String &p_text); void set_last_focused_window(WindowID p_window); - void mouse_process_popups(bool p_close = false); + bool mouse_process_popups(bool p_close = false); void popup_open(WindowID p_window); void popup_close(WindowID p_window); + void set_is_resizing(bool p_is_resizing); + bool get_is_resizing() const; void window_update(WindowID p_window); void window_destroy(WindowID p_window); @@ -214,30 +221,61 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; - virtual void global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag = Variant()) override; - virtual void global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag = Variant()) override; - virtual void global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu) override; - virtual void global_menu_add_separator(const String &p_menu_root) override; + virtual void global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual void global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; + virtual void global_menu_add_separator(const String &p_menu_root, int p_index = -1) override; + + virtual int global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const override; + virtual int global_menu_get_item_index_from_tag(const String &p_menu_root, const Variant &p_tag) const override; virtual bool global_menu_is_item_checked(const String &p_menu_root, int p_idx) const override; virtual bool global_menu_is_item_checkable(const String &p_menu_root, int p_idx) const override; - virtual Callable global_menu_get_item_callback(const String &p_menu_root, int p_idx) override; - virtual Variant global_menu_get_item_tag(const String &p_menu_root, int p_idx) override; - virtual String global_menu_get_item_text(const String &p_menu_root, int p_idx) override; - virtual String global_menu_get_item_submenu(const String &p_menu_root, int p_idx) override; + virtual bool global_menu_is_item_radio_checkable(const String &p_menu_root, int p_idx) const override; + virtual Callable global_menu_get_item_callback(const String &p_menu_root, int p_idx) const override; + virtual Variant global_menu_get_item_tag(const String &p_menu_root, int p_idx) const override; + virtual String global_menu_get_item_text(const String &p_menu_root, int p_idx) const override; + virtual String global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const override; + virtual Key global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const override; + virtual bool global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const override; + virtual String global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const override; + virtual int global_menu_get_item_state(const String &p_menu_root, int p_idx) const override; + virtual int global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const override; + virtual Ref<Texture2D> global_menu_get_item_icon(const String &p_menu_root, int p_idx) const override; virtual void global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) override; virtual void global_menu_set_item_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; + virtual void global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; virtual void global_menu_set_item_callback(const String &p_menu_root, int p_idx, const Callable &p_callback) override; virtual void global_menu_set_item_tag(const String &p_menu_root, int p_idx, const Variant &p_tag) override; virtual void global_menu_set_item_text(const String &p_menu_root, int p_idx, const String &p_text) override; virtual void global_menu_set_item_submenu(const String &p_menu_root, int p_idx, const String &p_submenu) override; + virtual void global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode) override; + virtual void global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled) override; + virtual void global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) override; + virtual void global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) override; + virtual void global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) override; + virtual void global_menu_set_item_icon(const String &p_menu_root, int p_idx, const Ref<Texture2D> &p_icon) override; virtual int global_menu_get_item_count(const String &p_menu_root) const override; virtual void global_menu_remove_item(const String &p_menu_root, int p_idx) override; virtual void global_menu_clear(const String &p_menu_root) override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override; virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; @@ -245,7 +283,7 @@ public: virtual MouseMode mouse_get_mode() const override; bool update_mouse_wrap(WindowData &p_wd, NSPoint &r_delta, NSPoint &r_mpos, NSTimeInterval p_timestamp); - virtual void mouse_warp_to_position(const Point2i &p_to) override; + virtual void warp_mouse(const Point2i &p_position) override; virtual Point2i mouse_get_position() const override; void mouse_set_button_state(MouseButton p_state); virtual MouseButton mouse_get_button_state() const override; @@ -335,7 +373,7 @@ public: void cursor_update_shape(); virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; virtual bool get_swap_cancel_ok() override; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index 23f37a8e18..b6a5813bd0 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -37,18 +37,20 @@ #include "key_mapping_osx.h" #include "os_osx.h" +#include "tts_osx.h" + #include "core/io/marshalls.h" #include "core/math/geometry_2d.h" #include "core/os/keyboard.h" #include "main/main.h" #include "scene/resources/texture.h" -#include <Carbon/Carbon.h> -#include <Cocoa/Cocoa.h> -#include <IOKit/IOCFPlugIn.h> -#include <IOKit/IOKitLib.h> -#include <IOKit/hid/IOHIDKeys.h> -#include <IOKit/hid/IOHIDLib.h> +#import <Carbon/Carbon.h> +#import <Cocoa/Cocoa.h> +#import <IOKit/IOCFPlugIn.h> +#import <IOKit/IOKitLib.h> +#import <IOKit/hid/IOHIDKeys.h> +#import <IOKit/hid/IOHIDLib.h> #if defined(GLES3_ENABLED) #include "drivers/gles3/rasterizer_gles3.h" @@ -91,6 +93,7 @@ NSMenu *DisplayServerOSX::_get_menu_root(const String &p_menu_root) { // Submenu. if (!submenu.has(p_menu_root)) { NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; + [n_menu setAutoenablesItems:NO]; submenu[p_menu_root] = n_menu; } menu = submenu[p_menu_root]; @@ -143,7 +146,7 @@ DisplayServerOSX::WindowID DisplayServerOSX::_create_window(WindowMode p_mode, V [wd.window_object setTabbingMode:NSWindowTabbingModeDisallowed]; } - CALayer *layer = [wd.window_view layer]; + CALayer *layer = [(NSView *)wd.window_view layer]; if (layer) { layer.contentsScale = scale; } @@ -171,7 +174,7 @@ DisplayServerOSX::WindowID DisplayServerOSX::_create_window(WindowMode p_mode, V wd.size.width = contentRect.size.width * scale; wd.size.height = contentRect.size.height * scale; - CALayer *layer = [wd.window_view layer]; + CALayer *layer = [(NSView *)wd.window_view layer]; if (layer) { layer.contentsScale = scale; } @@ -206,16 +209,16 @@ void DisplayServerOSX::_update_window_style(WindowData p_wd) { if (borderless_full) { // If the window covers up the screen set the level to above the main menu and hide on deactivate. - [p_wd.window_object setLevel:NSMainMenuWindowLevel + 1]; - [p_wd.window_object setHidesOnDeactivate:YES]; + [(NSWindow *)p_wd.window_object setLevel:NSMainMenuWindowLevel + 1]; + [(NSWindow *)p_wd.window_object setHidesOnDeactivate:YES]; } else { // Reset these when our window is not a borderless window that covers up the screen. if (p_wd.on_top && !p_wd.fullscreen) { - [p_wd.window_object setLevel:NSFloatingWindowLevel]; + [(NSWindow *)p_wd.window_object setLevel:NSFloatingWindowLevel]; } else { - [p_wd.window_object setLevel:NSNormalWindowLevel]; + [(NSWindow *)p_wd.window_object setLevel:NSNormalWindowLevel]; } - [p_wd.window_object setHidesOnDeactivate:NO]; + [(NSWindow *)p_wd.window_object setHidesOnDeactivate:NO]; } } @@ -231,7 +234,7 @@ void DisplayServerOSX::_set_window_per_pixel_transparency_enabled(bool p_enabled [wd.window_object setBackgroundColor:[NSColor clearColor]]; [wd.window_object setOpaque:NO]; [wd.window_object setHasShadow:NO]; - CALayer *layer = [wd.window_view layer]; + CALayer *layer = [(NSView *)wd.window_view layer]; if (layer) { [layer setBackgroundColor:[NSColor clearColor].CGColor]; [layer setOpaque:NO]; @@ -246,7 +249,7 @@ void DisplayServerOSX::_set_window_per_pixel_transparency_enabled(bool p_enabled [wd.window_object setBackgroundColor:[NSColor colorWithCalibratedWhite:1 alpha:1]]; [wd.window_object setOpaque:YES]; [wd.window_object setHasShadow:YES]; - CALayer *layer = [wd.window_view layer]; + CALayer *layer = [(NSView *)wd.window_view layer]; if (layer) { [layer setBackgroundColor:[NSColor colorWithCalibratedWhite:1 alpha:1].CGColor]; [layer setOpaque:YES]; @@ -325,7 +328,7 @@ void DisplayServerOSX::_dispatch_input_event(const Ref<InputEvent> &p_event) { Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -472,6 +475,40 @@ void DisplayServerOSX::_keyboard_layout_changed(CFNotificationCenterRef center, } } +NSImage *DisplayServerOSX::_convert_to_nsimg(Ref<Image> &p_image) const { + p_image->convert(Image::FORMAT_RGBA8); + NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:p_image->get_width() + pixelsHigh:p_image->get_height() + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:int(p_image->get_width()) * 4 + bitsPerPixel:32]; + ERR_FAIL_COND_V(imgrep == nil, nil); + uint8_t *pixels = [imgrep bitmapData]; + + int len = p_image->get_width() * p_image->get_height(); + const uint8_t *r = p_image->get_data().ptr(); + + /* Premultiply the alpha channel */ + for (int i = 0; i < len; i++) { + uint8_t alpha = r[i * 4 + 3]; + pixels[i * 4 + 0] = (uint8_t)(((uint16_t)r[i * 4 + 0] * alpha) / 255); + pixels[i * 4 + 1] = (uint8_t)(((uint16_t)r[i * 4 + 1] * alpha) / 255); + pixels[i * 4 + 2] = (uint8_t)(((uint16_t)r[i * 4 + 2] * alpha) / 255); + pixels[i * 4 + 3] = alpha; + } + + NSImage *nsimg = [[NSImage alloc] initWithSize:NSMakeSize(p_image->get_width(), p_image->get_height())]; + ERR_FAIL_COND_V(nsimg == nil, nil); + [nsimg addRepresentation:imgrep]; + return nsimg; +} + NSCursor *DisplayServerOSX::_cursor_from_selector(SEL p_selector, SEL p_fallback) { if ([NSCursor respondsToSelector:p_selector]) { id object = [NSCursor performSelector:p_selector]; @@ -498,7 +535,14 @@ void DisplayServerOSX::menu_callback(id p_sender) { GodotMenuItem *value = [p_sender representedObject]; if (value) { - if (value->checkable) { + if (value->max_states > 0) { + value->state++; + if (value->state >= value->max_states) { + value->state = 0; + } + } + + if (value->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { if ([p_sender state] == NSControlStateValueOff) { [p_sender setState:NSControlStateValueOn]; } else { @@ -525,9 +569,6 @@ DisplayServerOSX::WindowData &DisplayServerOSX::get_window(WindowID p_window) { } void DisplayServerOSX::send_event(NSEvent *p_event) { - if ([p_event type] == NSEventTypeLeftMouseDown || [p_event type] == NSEventTypeRightMouseDown || [p_event type] == NSEventTypeOtherMouseDown) { - mouse_process_popups(); - } // Special case handling of command-period, which is traditionally a special // shortcut in macOS and doesn't arrive at our regular keyDown handler. if ([p_event type] == NSEventTypeKeyDown) { @@ -599,6 +640,14 @@ void DisplayServerOSX::set_last_focused_window(WindowID p_window) { last_focused_window = p_window; } +void DisplayServerOSX::set_is_resizing(bool p_is_resizing) { + is_resizing = p_is_resizing; +} + +bool DisplayServerOSX::get_is_resizing() const { + return is_resizing; +} + void DisplayServerOSX::window_update(WindowID p_window) { #if defined(GLES3_ENABLED) if (gl_manager) { @@ -652,6 +701,7 @@ bool DisplayServerOSX::has_feature(Feature p_feature) const { case FEATURE_NATIVE_ICON: //case FEATURE_KEEP_SCREEN_ON: case FEATURE_SWAP_BUFFERS: + case FEATURE_TEXT_TO_SPEECH: return true; default: { } @@ -663,35 +713,195 @@ String DisplayServerOSX::get_name() const { return "OSX"; } -void DisplayServerOSX::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag) { +void DisplayServerOSX::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - NSMenuItem *menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:@""]; + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; - obj->checkable = false; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } } -void DisplayServerOSX::global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag) { +void DisplayServerOSX::global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - NSMenuItem *menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:@""]; + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; - obj->checkable = true; + obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } } -void DisplayServerOSX::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu) { +void DisplayServerOSX::global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + if (p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); + [menu_item setImage:_convert_to_nsimg(obj->img)]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } +} + +void DisplayServerOSX::global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + obj->max_states = 0; + obj->state = 0; + if (p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); + [menu_item setImage:_convert_to_nsimg(obj->img)]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } +} + +void DisplayServerOSX::global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } +} + +void DisplayServerOSX::global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + obj->max_states = 0; + obj->state = 0; + if (p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); + [menu_item setImage:_convert_to_nsimg(obj->img)]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } +} + +void DisplayServerOSX::global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + String keycode = KeyMappingOSX::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = p_max_states; + obj->state = p_default_state; + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } +} + +void DisplayServerOSX::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); @@ -705,20 +915,60 @@ void DisplayServerOSX::global_menu_add_submenu_item(const String &p_menu_root, c ERR_PRINT("Can't set submenu to menu that is already a submenu of some other menu!"); return; } - NSMenuItem *menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@""]; + NSMenuItem *menu_item; + if (p_index != -1) { + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; + } else { + menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@""]; + } + [sub_menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; [menu setSubmenu:sub_menu forItem:menu_item]; } } -void DisplayServerOSX::global_menu_add_separator(const String &p_menu_root) { +void DisplayServerOSX::global_menu_add_separator(const String &p_menu_root, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - [menu addItem:[NSMenuItem separatorItem]]; + if (p_index != -1) { + [menu insertItem:[NSMenuItem separatorItem] atIndex:p_index]; + } else { + [menu addItem:[NSMenuItem separatorItem]]; + } } } +int DisplayServerOSX::global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + return [menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + } + + return -1; +} + +int DisplayServerOSX::global_menu_get_item_index_from_tag(const String &p_menu_root, const Variant &p_tag) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + for (NSInteger i = 0; i < [menu numberOfItems]; i++) { + const NSMenuItem *menu_item = [menu itemAtIndex:i]; + if (menu_item) { + const GodotMenuItem *obj = [menu_item representedObject]; + if (obj && obj->meta == p_tag) { + return i; + } + } + } + } + + return -1; +} + bool DisplayServerOSX::global_menu_is_item_checked(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ @@ -741,14 +991,30 @@ bool DisplayServerOSX::global_menu_is_item_checkable(const String &p_menu_root, if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; if (obj) { - return obj->checkable; + return obj->checkable_type == CHECKABLE_TYPE_CHECK_BOX; + } + } + } + return false; +} + +bool DisplayServerOSX::global_menu_is_item_radio_checkable(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checkable_type == CHECKABLE_TYPE_RADIO_BUTTON; } } } return false; } -Callable DisplayServerOSX::global_menu_get_item_callback(const String &p_menu_root, int p_idx) { +Callable DisplayServerOSX::global_menu_get_item_callback(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ const NSMenu *menu = _get_menu_root(p_menu_root); @@ -764,7 +1030,7 @@ Callable DisplayServerOSX::global_menu_get_item_callback(const String &p_menu_ro return Callable(); } -Variant DisplayServerOSX::global_menu_get_item_tag(const String &p_menu_root, int p_idx) { +Variant DisplayServerOSX::global_menu_get_item_tag(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ const NSMenu *menu = _get_menu_root(p_menu_root); @@ -780,22 +1046,20 @@ Variant DisplayServerOSX::global_menu_get_item_tag(const String &p_menu_root, in return Variant(); } -String DisplayServerOSX::global_menu_get_item_text(const String &p_menu_root, int p_idx) { +String DisplayServerOSX::global_menu_get_item_text(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { - String ret; - ret.parse_utf8([[menu_item title] UTF8String]); - return ret; + return String::utf8([[menu_item title] UTF8String]); } } return String(); } -String DisplayServerOSX::global_menu_get_item_submenu(const String &p_menu_root, int p_idx) { +String DisplayServerOSX::global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const { _THREAD_SAFE_METHOD_ const NSMenu *menu = _get_menu_root(p_menu_root); @@ -804,9 +1068,9 @@ String DisplayServerOSX::global_menu_get_item_submenu(const String &p_menu_root, if (menu_item) { const NSMenu *sub_menu = [menu_item submenu]; if (sub_menu) { - for (Map<String, NSMenu *>::Element *E = submenu.front(); E; E = E->next()) { - if (E->get() == sub_menu) { - return E->key(); + for (const KeyValue<String, NSMenu *> &E : submenu) { + if (E.value == sub_menu) { + return E.key; } } } @@ -815,6 +1079,116 @@ String DisplayServerOSX::global_menu_get_item_submenu(const String &p_menu_root, return String(); } +Key DisplayServerOSX::global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + String ret = String::utf8([[menu_item keyEquivalent] UTF8String]); + Key keycode = find_keycode(ret); + NSUInteger mask = [menu_item keyEquivalentModifierMask]; + if (mask & NSEventModifierFlagControl) { + keycode |= KeyModifierMask::CTRL; + } + if (mask & NSEventModifierFlagOption) { + keycode |= KeyModifierMask::ALT; + } + if (mask & NSEventModifierFlagShift) { + keycode |= KeyModifierMask::SHIFT; + } + if (mask & NSEventModifierFlagCommand) { + keycode |= KeyModifierMask::META; + } + if (mask & NSEventModifierFlagNumericPad) { + keycode |= KeyModifierMask::KPAD; + } + return keycode; + } + } + return Key::NONE; +} + +bool DisplayServerOSX::global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + return ![menu_item isEnabled]; + } + } + return false; +} + +String DisplayServerOSX::global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + return String::utf8([[menu_item toolTip] UTF8String]); + } + } + return String(); +} + +int DisplayServerOSX::global_menu_get_item_state(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->state; + } + } + } + return 0; +} + +int DisplayServerOSX::global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->max_states; + } + } + } + return 0; +} + +Ref<Texture2D> DisplayServerOSX::global_menu_get_item_icon(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + if (obj->img.is_valid()) { + Ref<ImageTexture> txt; + txt.instantiate(); + txt->create_from_image(obj->img); + return txt; + } + } + } + } + return Ref<Texture2D>(); +} + void DisplayServerOSX::global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) { _THREAD_SAFE_METHOD_ @@ -845,7 +1219,23 @@ void DisplayServerOSX::global_menu_set_item_checkable(const String &p_menu_root, NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; - obj->checkable = p_checkable; + obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_CHECK_BOX : CHECKABLE_TYPE_NONE; + } + } +} + +void DisplayServerOSX::global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_RADIO_BUTTON : CHECKABLE_TYPE_NONE; } } } @@ -921,6 +1311,116 @@ void DisplayServerOSX::global_menu_set_item_submenu(const String &p_menu_root, i } } +void DisplayServerOSX::global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setKeyEquivalentModifierMask:KeyMappingOSX::keycode_get_native_mask(p_keycode)]; + String keycode = KeyMappingOSX::keycode_get_native_string(p_keycode & KeyModifierMask::CODE_MASK); + [menu_item setKeyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + } +} + +void DisplayServerOSX::global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setEnabled:(!p_disabled)]; + } + } +} + +void DisplayServerOSX::global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; + } + } +} + +void DisplayServerOSX::global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + obj->state = p_state; + } + } + } +} + +void DisplayServerOSX::global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + obj->max_states = p_max_states; + } + } + } +} + +void DisplayServerOSX::global_menu_set_item_icon(const String &p_menu_root, int p_idx, const Ref<Texture2D> &p_icon) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. + return; + } + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); + [menu_item setImage:_convert_to_nsimg(obj->img)]; + } else { + obj->img = Ref<Image>(); + [menu_item setImage:nil]; + } + } + } +} + int DisplayServerOSX::global_menu_get_item_count(const String &p_menu_root) const { _THREAD_SAFE_METHOD_ @@ -958,6 +1458,41 @@ void DisplayServerOSX::global_menu_clear(const String &p_menu_root) { } } +bool DisplayServerOSX::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isSpeaking]; +} + +bool DisplayServerOSX::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return [tts isPaused]; +} + +Array DisplayServerOSX::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return [tts getVoices]; +} + +void DisplayServerOSX::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + [tts speak:p_text voice:p_voice volume:p_volume pitch:p_pitch rate:p_rate utterance_id:p_utterance_id interrupt:p_interrupt]; +} + +void DisplayServerOSX::tts_pause() { + ERR_FAIL_COND(!tts); + [tts pauseSpeaking]; +} + +void DisplayServerOSX::tts_resume() { + ERR_FAIL_COND(!tts); + [tts resumeSpeaking]; +} + +void DisplayServerOSX::tts_stop() { + ERR_FAIL_COND(!tts); + [tts stopSpeaking]; +} + Error DisplayServerOSX::dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) { _THREAD_SAFE_METHOD_ @@ -1146,7 +1681,7 @@ bool DisplayServerOSX::update_mouse_wrap(WindowData &p_wd, NSPoint &r_delta, NSP return false; } -void DisplayServerOSX::mouse_warp_to_position(const Point2i &p_to) { +void DisplayServerOSX::warp_mouse(const Point2i &p_position) { _THREAD_SAFE_METHOD_ if (mouse_mode != MOUSE_MODE_CAPTURED) { @@ -1156,7 +1691,7 @@ void DisplayServerOSX::mouse_warp_to_position(const Point2i &p_to) { // Local point in window coords. const NSRect contentRect = [wd.window_view frame]; const float scale = screen_get_max_scale(); - NSRect pointInWindowRect = NSMakeRect(p_to.x / scale, contentRect.size.height - (p_to.y / scale - 1), 0, 0); + NSRect pointInWindowRect = NSMakeRect(p_position.x / scale, contentRect.size.height - (p_position.y / scale - 1), 0, 0); NSPoint pointOnScreen = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; // Point in scren coords. @@ -1182,7 +1717,11 @@ Point2i DisplayServerOSX::mouse_get_position() const { for (NSScreen *screen in [NSScreen screens]) { NSRect frame = [screen frame]; if (NSMouseInRect(mouse_pos, frame, NO)) { - return Vector2i((int)mouse_pos.x, (int)-mouse_pos.y) * scale + _get_screens_origin(); + Vector2i pos = Vector2i((int)mouse_pos.x, (int)mouse_pos.y); + pos *= scale; + pos -= _get_screens_origin(); + pos.y *= -1; + return pos; } } return Vector2i(); @@ -1359,8 +1898,8 @@ Vector<DisplayServer::WindowID> DisplayServerOSX::get_window_list() const { _THREAD_SAFE_METHOD_ Vector<int> ret; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - ret.push_back(E->key()); + for (const KeyValue<WindowID, WindowData> &E : windows) { + ret.push_back(E.key); } return ret; } @@ -1714,7 +2253,7 @@ void DisplayServerOSX::window_set_mode(WindowMode p_mode, WindowID p_window) { } break; case WINDOW_MODE_EXCLUSIVE_FULLSCREEN: case WINDOW_MODE_FULLSCREEN: { - [wd.window_object setLevel:NSNormalWindowLevel]; + [(NSWindow *)wd.window_object setLevel:NSNormalWindowLevel]; _set_window_per_pixel_transparency_enabled(true, p_window); if (wd.resize_disabled) { // Restore resize disabled. [wd.window_object setStyleMask:[wd.window_object styleMask] & ~NSWindowStyleMaskResizable]; @@ -1838,9 +2377,9 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo return; } if (p_enabled) { - [wd.window_object setLevel:NSFloatingWindowLevel]; + [(NSWindow *)wd.window_object setLevel:NSFloatingWindowLevel]; } else { - [wd.window_object setLevel:NSNormalWindowLevel]; + [(NSWindow *)wd.window_object setLevel:NSNormalWindowLevel]; } } break; case WINDOW_FLAG_TRANSPARENT: { @@ -1856,7 +2395,7 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo } break; case WINDOW_FLAG_POPUP: { ERR_FAIL_COND_MSG(p_window == MAIN_WINDOW_ID, "Main window can't be popup."); - ERR_FAIL_COND_MSG([wd.window_object isVisible] && (wd.is_popup != p_enabled), "Pupup flag can't changed while window is opened."); + ERR_FAIL_COND_MSG([wd.window_object isVisible] && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened."); wd.is_popup = p_enabled; } break; default: { @@ -1881,7 +2420,7 @@ bool DisplayServerOSX::window_get_flag(WindowFlags p_flag, WindowID p_window) co if (wd.fullscreen) { return wd.on_top; } else { - return [wd.window_object level] == NSFloatingWindowLevel; + return [(NSWindow *)wd.window_object level] == NSFloatingWindowLevel; } } break; case WINDOW_FLAG_TRANSPARENT: { @@ -1926,8 +2465,8 @@ bool DisplayServerOSX::window_can_draw(WindowID p_window) const { bool DisplayServerOSX::can_any_window_draw() const { _THREAD_SAFE_METHOD_ - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if (window_get_mode(E->key()) != WINDOW_MODE_MINIMIZED) { + for (const KeyValue<WindowID, WindowData> &E : windows) { + if (window_get_mode(E.key) != WINDOW_MODE_MINIMIZED) { return true; } } @@ -1963,9 +2502,9 @@ DisplayServer::WindowID DisplayServerOSX::get_window_at_screen_position(const Po position /= screen_get_max_scale(); NSInteger wnum = [NSWindow windowNumberAtPoint:NSMakePoint(position.x, position.y) belowWindowWithWindowNumber:0 /*topmost*/]; - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - if ([E->get().window_object windowNumber] == wnum) { - return E->key(); + for (const KeyValue<WindowID, WindowData> &E : windows) { + if ([E.value.window_object windowNumber] == wnum) { + return E.key; } } return INVALID_WINDOW_ID; @@ -2132,14 +2671,14 @@ DisplayServerOSX::CursorShape DisplayServerOSX::cursor_get_shape() const { return cursor_shape; } -void DisplayServerOSX::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerOSX::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); + HashMap<CursorShape, Vector<Variant>>::Iterator cursor_c = cursors_cache.find(p_shape); if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { + if (cursor_c->value[0] == p_cursor && cursor_c->value[1] == p_hotspot) { cursor_set_shape(p_shape); return; } @@ -2344,8 +2883,8 @@ void DisplayServerOSX::process_events() { Input::get_singleton()->flush_buffered_events(); } - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - WindowData &wd = E->get(); + for (KeyValue<WindowID, WindowData> &E : windows) { + WindowData &wd = E.value; if (wd.mpath.size() > 0) { update_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); if (Geometry2D::is_point_in_polygon(wd.mouse_pos, wd.mpath)) { @@ -2388,14 +2927,13 @@ void DisplayServerOSX::swap_buffers() { void DisplayServerOSX::set_native_icon(const String &p_filename) { _THREAD_SAFE_METHOD_ - FileAccess *f = FileAccess::open(p_filename, FileAccess::READ); - ERR_FAIL_COND(!f); + Ref<FileAccess> f = FileAccess::open(p_filename, FileAccess::READ); + ERR_FAIL_COND(f.is_null()); Vector<uint8_t> data; uint64_t len = f->get_length(); data.resize(len); f->get_buffer((uint8_t *)&data.write[0], len); - memdelete(f); NSData *icon_data = [[NSData alloc] initWithBytes:&data.write[0] length:len]; ERR_FAIL_COND_MSG(!icon_data, "Error reading icon data."); @@ -2496,21 +3034,25 @@ Rect2i DisplayServerOSX::window_get_popup_safe_rect(WindowID p_window) const { } void DisplayServerOSX::popup_open(WindowID p_window) { + _THREAD_SAFE_METHOD_ + WindowData &wd = windows[p_window]; if (wd.is_popup) { bool was_empty = popup_list.is_empty(); - // Close all popups, up to current popup parent, or every popup if new window is not transient. + // Find current popup parent, or root popup if new window is not transient. + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); while (E) { if (wd.transient_parent != E->get() || wd.transient_parent == INVALID_WINDOW_ID) { - send_window_event(windows[E->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } else { break; } } + if (C) { + send_window_event(windows[C->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); + } if (was_empty && popup_list.is_empty()) { // Inform OS that popup was opened, to close other native popups. @@ -2522,12 +3064,16 @@ void DisplayServerOSX::popup_open(WindowID p_window) { } void DisplayServerOSX::popup_close(WindowID p_window) { + _THREAD_SAFE_METHOD_ + bool was_empty = popup_list.is_empty(); List<WindowID>::Element *E = popup_list.find(p_window); while (E) { - send_window_event(windows[E->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); List<WindowID>::Element *F = E->next(); + WindowID win_id = E->get(); popup_list.erase(E); + + send_window_event(windows[win_id], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); E = F; } if (!was_empty && popup_list.is_empty()) { @@ -2536,18 +3082,17 @@ void DisplayServerOSX::popup_close(WindowID p_window) { } } -void DisplayServerOSX::mouse_process_popups(bool p_close) { +bool DisplayServerOSX::mouse_process_popups(bool p_close) { _THREAD_SAFE_METHOD_ bool was_empty = popup_list.is_empty(); + bool closed = false; if (p_close) { // Close all popups. List<WindowID>::Element *E = popup_list.front(); - while (E) { + if (E) { send_window_event(windows[E->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->next(); - popup_list.erase(E); - E = F; + closed = true; } if (!was_empty) { // Inform OS that all popups are closed. @@ -2556,11 +3101,13 @@ void DisplayServerOSX::mouse_process_popups(bool p_close) { } else { uint64_t delta = OS::get_singleton()->get_ticks_msec() - time_since_popup; if (delta < 250) { - return; + return false; } Point2i pos = mouse_get_position(); + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); + // Find top popup to close. while (E) { // Popup window area. Rect2i win_rect = Rect2i(window_get_position(E->get()), window_get_size(E->get())); @@ -2571,17 +3118,20 @@ void DisplayServerOSX::mouse_process_popups(bool p_close) { } else if (safe_rect != Rect2i() && safe_rect.has_point(pos)) { break; } else { - send_window_event(windows[E->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } } + if (C) { + send_window_event(windows[C->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); + closed = true; + } if (!was_empty && popup_list.is_empty()) { // Inform OS that all popups are closed. [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.apple.HIToolbox.endMenuTrackingNotification" object:@"org.godotengine.godot.popup_window"]; } } + return closed; } DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { @@ -2610,6 +3160,9 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode // Register to be notified on displays arrangement changes. CGDisplayRegisterReconfigurationCallback(_displays_arrangement_changed, nullptr); + // Init TTS + tts = [[TTS_OSX alloc] init]; + NSMenuItem *menu_item; NSString *title; @@ -2620,11 +3173,13 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode // Setup Dock menu. dock_menu = [[NSMenu alloc] initWithTitle:@"_dock"]; + [dock_menu setAutoenablesItems:NO]; // Setup Apple menu. apple_menu = [[NSMenu alloc] initWithTitle:@""]; title = [NSString stringWithFormat:NSLocalizedString(@"About %@", nil), nsappname]; [apple_menu addItemWithTitle:title action:@selector(showAbout:) keyEquivalent:@""]; + [apple_menu setAutoenablesItems:NO]; [apple_menu addItem:[NSMenuItem separatorItem]]; @@ -2652,6 +3207,7 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode NSMenu *main_menu = [NSApp mainMenu]; menu_item = [main_menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; [main_menu setSubmenu:apple_menu forItem:menu_item]; + [main_menu setAutoenablesItems:NO]; //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback @@ -2711,11 +3267,11 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode DisplayServerOSX::~DisplayServerOSX() { // Destroy all windows. - 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]; + for (HashMap<WindowID, WindowData>::Iterator E = windows.begin(); E;) { + HashMap<WindowID, WindowData>::Iterator F = E; + ++E; + [F->value.window_object setContentView:nil]; + [F->value.window_object close]; } // Destroy drivers. diff --git a/platform/osx/export/codesign.cpp b/platform/osx/export/codesign.cpp index 1a2ad2bee6..fd044c00cc 100644 --- a/platform/osx/export/codesign.cpp +++ b/platform/osx/export/codesign.cpp @@ -49,8 +49,8 @@ /*************************************************************************/ String CodeSignCodeResources::hash_sha1_base64(const String &p_path) { - FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CryptoCore::SHA1Context ctx; ctx.start(); @@ -68,14 +68,13 @@ String CodeSignCodeResources::hash_sha1_base64(const String &p_path) { unsigned char hash[0x14]; ctx.finish(hash); - fa->close(); return CryptoCore::b64_encode_str(hash, 0x14); } String CodeSignCodeResources::hash_sha256_base64(const String &p_path) { - FileAccessRef fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), String(), vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CryptoCore::SHA256Context ctx; ctx.start(); @@ -93,7 +92,6 @@ String CodeSignCodeResources::hash_sha256_base64(const String &p_path) { unsigned char hash[0x20]; ctx.finish(hash); - fa->close(); return CryptoCore::b64_encode_str(hash, 0x20); } @@ -211,16 +209,14 @@ bool CodeSignCodeResources::add_nested_file(const String &p_root, const String & } \ } - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V(!da, false); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(da.is_null(), false); Vector<String> files_to_add; if (LipO::is_lipo(p_exepath)) { String tmp_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file("_lipo"); Error err = da->make_dir_recursive(tmp_path_name); - if (err != OK) { - ERR_FAIL_V_MSG(false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name)); - } + ERR_FAIL_COND_V_MSG(err != OK, false, vformat("CodeSign/CodeResources: Failed to create \"%s\" subfolder.", tmp_path_name)); LipO lip; if (lip.open_file(p_exepath)) { for (int i = 0; i < lip.get_arch_count(); i++) { @@ -287,8 +283,8 @@ bool CodeSignCodeResources::add_nested_file(const String &p_root, const String & } bool CodeSignCodeResources::add_folder_recursive(const String &p_root, const String &p_path, const String &p_main_exe_path) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V(!da, false); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V(da.is_null(), false); Error err = da->change_dir(p_root.plus_file(p_path)); ERR_FAIL_COND_V(err != OK, false); @@ -431,12 +427,11 @@ bool CodeSignCodeResources::save_to_file(const String &p_path) { String text = pl.save_text(); ERR_FAIL_COND_V_MSG(text.is_empty(), false, "CodeSign/CodeResources: Generating resources PList failed."); - FileAccessRef fa = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("CodeSign/CodeResources: Can't open file: \"%s\".", p_path)); CharString cs = text.utf8(); fa->store_buffer((const uint8_t *)cs.ptr(), cs.length()); - fa->close(); return true; } @@ -809,8 +804,8 @@ int CodeSignRequirements::get_size() const { return blob.size(); } -void CodeSignRequirements::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/Requirements: Invalid file handle."); +void CodeSignRequirements::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/Requirements: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -863,8 +858,8 @@ int CodeSignEntitlementsText::get_size() const { return blob.size(); } -void CodeSignEntitlementsText::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsText: Invalid file handle."); +void CodeSignEntitlementsText::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/EntitlementsText: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -918,8 +913,8 @@ int CodeSignEntitlementsBinary::get_size() const { return blob.size(); } -void CodeSignEntitlementsBinary::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/EntitlementsBinary: Invalid file handle."); +void CodeSignEntitlementsBinary::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/EntitlementsBinary: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -947,7 +942,7 @@ CodeSignCodeDirectory::CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash } blob.resize(cd_size); memset(blob.ptrw() + 8, 0x00, cd_size - 8); - CodeDirectoryHeader *cd = (CodeDirectoryHeader *)(blob.ptrw() + 8); + CodeDirectoryHeader *cd = reinterpret_cast<CodeDirectoryHeader *>(blob.ptrw() + 8); bool is_64_cl = (p_code_limit >= std::numeric_limits<uint32_t>::max()); @@ -1040,8 +1035,8 @@ int CodeSignCodeDirectory::get_size() const { return blob.size(); } -void CodeSignCodeDirectory::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/CodeDirectory: Invalid file handle."); +void CodeSignCodeDirectory::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/CodeDirectory: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -1086,8 +1081,8 @@ int CodeSignSignature::get_size() const { return blob.size(); } -void CodeSignSignature::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/Signature: Invalid file handle."); +void CodeSignSignature::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/Signature: Invalid file handle."); p_file->store_buffer(blob.ptr(), blob.size()); } @@ -1115,8 +1110,8 @@ int CodeSignSuperBlob::get_size() const { return size; } -void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const { - ERR_FAIL_COND_MSG(!p_file, "CodeSign/SuperBlob: Invalid file handle."); +void CodeSignSuperBlob::write_to_file(Ref<FileAccess> p_file) const { + ERR_FAIL_COND_MSG(p_file.is_null(), "CodeSign/SuperBlob: Invalid file handle."); uint32_t size = get_size(); uint32_t data_offset = 12 + blobs.size() * 8; @@ -1147,8 +1142,8 @@ void CodeSignSuperBlob::write_to_file(FileAccess *p_file) const { PackedByteArray CodeSign::file_hash_sha1(const String &p_path) { PackedByteArray file_hash; - FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); CryptoCore::SHA1Context ctx; ctx.start(); @@ -1171,8 +1166,8 @@ PackedByteArray CodeSign::file_hash_sha1(const String &p_path) { PackedByteArray CodeSign::file_hash_sha256(const String &p_path) { PackedByteArray file_hash; - FileAccessRef f = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!f, PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), PackedByteArray(), vformat("CodeSign: Can't open file: \"%s\".", p_path)); CryptoCore::SHA256Context ctx; ctx.start(); @@ -1208,8 +1203,8 @@ Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const String id; String main_exe = p_exe_path; - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (!da) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_null()) { r_error_msg = TTR("Can't get filesystem access."); ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); } @@ -1522,8 +1517,8 @@ Error CodeSign::_codesign_file(bool p_use_hardened_runtime, bool p_force, const } Error CodeSign::codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg) { - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (!da) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_null()) { r_error_msg = TTR("Can't get filesystem access."); ERR_FAIL_V_MSG(ERR_CANT_CREATE, "CodeSign: Can't get filesystem access."); } diff --git a/platform/osx/export/codesign.h b/platform/osx/export/codesign.h index e5e9be5c28..3a08c0ea86 100644 --- a/platform/osx/export/codesign.h +++ b/platform/osx/export/codesign.h @@ -132,7 +132,7 @@ public: virtual int get_size() const = 0; virtual uint32_t get_index_type() const = 0; - virtual void write_to_file(FileAccess *p_file) const = 0; + virtual void write_to_file(Ref<FileAccess> p_file) const = 0; }; /*************************************************************************/ @@ -168,7 +168,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000002; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -190,7 +190,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000005; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -212,7 +212,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000007; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -314,7 +314,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00000000; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -333,7 +333,7 @@ public: virtual int get_size() const override; virtual uint32_t get_index_type() const override { return 0x00010000; }; - virtual void write_to_file(FileAccess *p_file) const override; + virtual void write_to_file(Ref<FileAccess> p_file) const override; }; /*************************************************************************/ @@ -347,7 +347,7 @@ public: bool add_blob(const Ref<CodeSignBlob> &p_blob); int get_size() const; - void write_to_file(FileAccess *p_file) const; + void write_to_file(Ref<FileAccess> p_file) const; }; /*************************************************************************/ diff --git a/platform/osx/export/export_plugin.cpp b/platform/osx/export/export_plugin.cpp index 24b9bc02a2..7010709123 100644 --- a/platform/osx/export/export_plugin.cpp +++ b/platform/osx/export/export_plugin.cpp @@ -51,7 +51,7 @@ void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> r_features->push_back("64"); } -bool EditorExportPlatformOSX::get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const { +bool EditorExportPlatformOSX::get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const { // These options are not supported by built-in codesign, used on non macOS host. if (!OS::get_singleton()->has_feature("macos")) { if (p_option == "codesign/identity" || p_option == "codesign/timestamp" || p_option == "codesign/hardened_runtime" || p_option == "codesign/custom_options" || p_option.begins_with("notarization/")) { @@ -72,8 +72,7 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/info"), "Made with Godot Engine")); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_script", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.png,*.icns"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), "")); @@ -81,18 +80,30 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), "")); @@ -245,22 +256,24 @@ void EditorExportPlatformOSX::_make_icon(const Ref<Image> &p_icon, Vector<uint8_ String path = EditorPaths::get_singleton()->get_cache_dir().plus_file("icon.png"); ResourceSaver::save(path, it); - FileAccess *f = FileAccess::open(path, FileAccess::READ); - if (!f) { - // Clean up generated file. - DirAccess::remove_file_or_error(path); - ERR_FAIL(); - } + { + Ref<FileAccess> f = FileAccess::open(path, FileAccess::READ); + if (f.is_null()) { + // Clean up generated file. + DirAccess::remove_file_or_error(path); + add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not open icon file \"%s\"."), path)); + return; + } - int ofs = data.size(); - uint64_t len = f->get_length(); - data.resize(data.size() + len + 8); - f->get_buffer(&data.write[ofs + 8], len); - memdelete(f); - len += 8; - len = BSWAP32(len); - memcpy(&data.write[ofs], icon_infos[i].name, 4); - encode_uint32(len, &data.write[ofs + 4]); + int ofs = data.size(); + uint64_t len = f->get_length(); + data.resize(data.size() + len + 8); + f->get_buffer(&data.write[ofs + 8], len); + len += 8; + len = BSWAP32(len); + memcpy(&data.write[ofs], icon_infos[i].name, 4); + encode_uint32(len, &data.write[ofs + 4]); + } // Clean up generated file. DirAccess::remove_file_or_error(path); @@ -316,9 +329,7 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset if (lines[i].find("$binary") != -1) { strnew += lines[i].replace("$binary", p_binary) + "\n"; } else if (lines[i].find("$name") != -1) { - strnew += lines[i].replace("$name", p_binary) + "\n"; - } else if (lines[i].find("$info") != -1) { - strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n"; + strnew += lines[i].replace("$name", ProjectSettings::get_singleton()->get("application/config/name")) + "\n"; } else if (lines[i].find("$bundle_identifier") != -1) { strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n"; } else if (lines[i].find("$short_version") != -1) { @@ -431,18 +442,25 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset String str; Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable.")); + return err; + } print_verbose("altool (" + p_path + "):\n" + str); - if (str.find("RequestUUID") == -1) { - EditorNode::add_io_error("altool: " + str); + int rq_offset = str.find("RequestUUID"); + if (rq_offset == -1) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed.")); return FAILED; } else { - print_line(TTR("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.")); - print_line(" " + TTR("You can check progress manually by opening a Terminal and running the following command:")); - print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); - print_line(" " + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); - print_line(" \"xcrun stapler staple <app path>\""); + int next_nl = str.find("\n", rq_offset); + String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\""); } #endif @@ -450,7 +468,7 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset return OK; } -Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path) { +Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) { bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign"); bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); @@ -459,22 +477,22 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese #ifdef MODULE_REGEX_ENABLED #ifdef OSX_ENABLED - if (p_preset->get("codesign/timestamp")) { - WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + if (p_preset->get("codesign/timestamp") && p_warn) { + add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Timestamping is not compatible with ad-hoc signature, and was disabled!")); } - if (p_preset->get("codesign/hardened_runtime")) { - WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + if (p_preset->get("codesign/hardened_runtime") && p_warn) { + add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!")); } #endif String error_msg; Error err = CodeSign::codesign(false, p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg); if (err != OK) { - EditorNode::add_io_error("Built-in CodeSign: " + error_msg); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); return FAILED; } #else - ERR_FAIL_V_MSG(FAILED, "Built-in CodeSign require regex module"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module.")); #endif return OK; } else { @@ -482,14 +500,18 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese List<String> args; if (p_preset->get("codesign/timestamp")) { if (ad_hoc) { - WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + if (p_warn) { + add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Timestamping is not compatible with ad-hoc signature, and was disabled!")); + } } else { args.push_back("--timestamp"); } } if (p_preset->get("codesign/hardened_runtime")) { if (ad_hoc) { - WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + if (p_warn) { + add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!")); + } } else { args.push_back("--options"); args.push_back("runtime"); @@ -526,15 +548,18 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese String str; Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); + return err; + } print_verbose("codesign (" + p_path + "):\n" + str); if (str.find("no identity found") != -1) { - EditorNode::add_io_error("CodeSign: " + TTR("No identity found.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) { - EditorNode::add_io_error("CodeSign: " + TTR("Invalid entitlements file.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid entitlements file.")); return FAILED; } return OK; @@ -552,7 +577,7 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset } Error dir_access_error; - DirAccessRef dir_access{ DirAccess::open(p_path, &dir_access_error) }; + Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) }; if (dir_access_error != OK) { return dir_access_error; @@ -569,7 +594,7 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset } if (extensions_to_sign.find(current_file.get_extension()) > -1) { - Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path) }; + Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path, false) }; if (code_sign_error != OK) { return code_sign_error; } @@ -579,7 +604,7 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset return code_sign_error; } } else if (p_should_error_on_non_code) { - ERR_PRINT(vformat("Cannot sign file %s.", current_file)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file)); return Error::FAILED; } @@ -590,14 +615,14 @@ Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset return OK; } -Error EditorExportPlatformOSX::_copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, +Error EditorExportPlatformOSX::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, bool p_should_error_on_non_code_sign) { Error err{ OK }; if (dir_access->dir_exists(p_src_path)) { #ifndef UNIX_ENABLED - WARN_PRINT("Relative symlinks are not supported, exported " + p_src_path.get_file() + " might be broken!"); + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), vformat(TTR("Relative symlinks are not supported, exported \"%s\" might be broken!"), p_src_path.get_file())); #endif print_verbose("export framework: " + p_src_path + " -> " + p_in_app_path); err = dir_access->make_dir_recursive(p_in_app_path); @@ -613,14 +638,14 @@ Error EditorExportPlatformOSX::_copy_and_sign_files(DirAccessRef &dir_access, co // If it is a directory, find and sign all dynamic libraries. err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign); } else { - err = _code_sign(p_preset, p_in_app_path, p_ent_path); + err = _code_sign(p_preset, p_in_app_path, p_ent_path, false); } } return err; } Error EditorExportPlatformOSX::_export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, - const String &p_app_path_name, DirAccessRef &dir_access, + const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path) { Error error{ OK }; @@ -654,14 +679,17 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin String str; Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("Could not start hdiutil executable.")); + return err; + } print_verbose("hdiutil returned: " + str); if (str.find("create failed") != -1) { if (str.find("File exists") != -1) { - EditorNode::add_io_error("hdiutil: " + TTR("DMG creation failed, file already exists.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed - file exists.")); } else { - EditorNode::add_io_error("hdiutil: " + TTR("DMG create failed.")); + add_message(EXPORT_MESSAGE_ERROR, TTR("DMG Creation"), TTR("`hdiutil create` failed.")); } return FAILED; } @@ -669,6 +697,22 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin return OK; } +Error EditorExportPlatformOSX::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); + return ERR_CANT_CREATE; + } + + f->store_line("#!/bin/sh"); + f->store_line("echo -ne '\\033c\\033]0;" + p_app_name + "\\a'"); + f->store_line("function realpath() { python -c \"import os,sys; print(os.path.realpath(sys.argv[1]))\" \"$0\"; }"); + f->store_line("base_path=\"$(dirname \"$(realpath \"$0\")\")\""); + f->store_line("\"$base_path/" + p_pkg_name + "\" \"$@\""); + + return OK; +} + Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); @@ -686,17 +730,18 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p String err; src_pkg_name = find_export_template("osx.zip", &err); if (src_pkg_name.is_empty()) { - EditorNode::add_io_error(err); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Export template not found.")); return ERR_FILE_NOT_FOUND; } } if (!DirAccess::exists(p_path.get_base_dir())) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("The given export path doesn't exist.")); return ERR_FILE_BAD_PATH; } - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating app bundle"), 0)) { return ERR_SKIP; @@ -704,7 +749,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { - EditorNode::add_io_error(TTR("Could not find template app to export:") + "\n" + src_pkg_name); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not find template app to export: \"%s\"."), src_pkg_name)); return ERR_FILE_NOT_FOUND; } @@ -713,9 +758,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p String binary_to_use = "godot_osx_" + String(p_debug ? "debug" : "release") + ".64"; String pkg_name; - if (p_preset->get("application/name") != "") { - pkg_name = p_preset->get("application/name"); // app_name - } else if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { + if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name")); } else { pkg_name = "Unnamed"; @@ -731,30 +774,41 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } else if (p_path.ends_with("app")) { export_format = "app"; } else { - EditorNode::add_io_error("Invalid export format"); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format.")); return ERR_CANT_CREATE; } // Create our application bundle. String tmp_app_dir_name = pkg_name + ".app"; + String tmp_base_path_name; String tmp_app_path_name; + String scr_path; if (export_format == "app") { + tmp_base_path_name = p_path.get_base_dir(); tmp_app_path_name = p_path; + scr_path = p_path.get_basename() + ".command"; } else { - tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name); + tmp_base_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name); + tmp_app_path_name = tmp_base_path_name.plus_file(tmp_app_dir_name); + scr_path = tmp_base_path_name.plus_file(pkg_name + ".command"); } + print_verbose("Exporting to " + tmp_app_path_name); Error err = OK; - DirAccessRef tmp_app_dir = DirAccess::create_for_path(tmp_app_path_name); - if (!tmp_app_dir) { + Ref<DirAccess> tmp_app_dir = DirAccess::create_for_path(tmp_base_path_name); + if (tmp_app_dir.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory: \"%s\"."), tmp_base_path_name)); err = ERR_CANT_CREATE; } - if (DirAccess::exists(tmp_app_dir_name)) { + DirAccess::remove_file_or_error(scr_path); + if (DirAccess::exists(tmp_app_path_name)) { + String old_dir = tmp_app_dir->get_current_dir(); if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) { tmp_app_dir->erase_contents_recursive(); + tmp_app_dir->change_dir(old_dir); } } @@ -764,37 +818,142 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (err == OK) { print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS"); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/MacOS")); + } } if (err == OK) { print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks"); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Frameworks")); + } } if ((err == OK) && helpers.size() > 0) { print_line("Creating " + tmp_app_path_name + "/Contents/Helpers"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Helpers"); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Helpers")); + } } if (err == OK) { print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources"); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), tmp_app_path_name + "/Contents/Resources")); + } } + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); + Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized"); + Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized"); + Dictionary location_usage_descriptions = p_preset->get("privacy/location_usage_description_localized"); + Dictionary address_book_usage_descriptions = p_preset->get("privacy/address_book_usage_description_localized"); + Dictionary calendar_usage_descriptions = p_preset->get("privacy/calendar_usage_description_localized"); + Dictionary photos_library_usage_descriptions = p_preset->get("privacy/photos_library_usage_description_localized"); + Dictionary desktop_folder_usage_descriptions = p_preset->get("privacy/desktop_folder_usage_description_localized"); + Dictionary documents_folder_usage_descriptions = p_preset->get("privacy/documents_folder_usage_description_localized"); + Dictionary downloads_folder_usage_descriptions = p_preset->get("privacy/downloads_folder_usage_description_localized"); + Dictionary network_volumes_usage_descriptions = p_preset->get("privacy/network_volumes_usage_description_localized"); + Dictionary removable_volumes_usage_descriptions = p_preset->get("privacy/removable_volumes_usage_description_localized"); + Dictionary copyrights = p_preset->get("application/copyright_localized"); + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); if (translations.size() > 0) { { String fname = tmp_app_path_name + "/Contents/Resources/en.lproj"; tmp_app_dir->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";"); + if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { + f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) { + f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) { + f->store_line("NSLocationUsageDescription = \"" + p_preset->get("privacy/location_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) { + f->store_line("NSContactsUsageDescription = \"" + p_preset->get("privacy/address_book_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) { + f->store_line("NSCalendarsUsageDescription = \"" + p_preset->get("privacy/calendar_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photos_library_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) { + f->store_line("NSDesktopFolderUsageDescription = \"" + p_preset->get("privacy/desktop_folder_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) { + f->store_line("NSDocumentsFolderUsageDescription = \"" + p_preset->get("privacy/documents_folder_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) { + f->store_line("NSDownloadsFolderUsageDescription = \"" + p_preset->get("privacy/downloads_folder_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) { + f->store_line("NSNetworkVolumesUsageDescription = \"" + p_preset->get("privacy/network_volumes_usage_description").operator String() + "\";"); + } + if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) { + f->store_line("NSRemovableVolumesUsageDescription = \"" + p_preset->get("privacy/removable_volumes_usage_description").operator String() + "\";"); + } + f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";"); } for (const String &E : translations) { Ref<Translation> tr = ResourceLoader::load(E); if (tr.is_valid()) { - String fname = tmp_app_path_name + "/Contents/Resources/" + tr->get_locale() + ".lproj"; + String lang = tr->get_locale(); + String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj"; tmp_app_dir->make_dir_recursive(fname); - FileAccessRef f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + if (appnames.has(lang)) { + f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); + } + if (microphone_usage_descriptions.has(lang)) { + f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); + } + if (camera_usage_descriptions.has(lang)) { + f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); + } + if (location_usage_descriptions.has(lang)) { + f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";"); + } + if (address_book_usage_descriptions.has(lang)) { + f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";"); + } + if (calendar_usage_descriptions.has(lang)) { + f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";"); + } + if (photos_library_usage_descriptions.has(lang)) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";"); + } + if (desktop_folder_usage_descriptions.has(lang)) { + f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (documents_folder_usage_descriptions.has(lang)) { + f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (downloads_folder_usage_descriptions.has(lang)) { + f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (network_volumes_usage_descriptions.has(lang)) { + f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";"); + } + if (removable_volumes_usage_descriptions.has(lang)) { + f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";"); + } + if (copyrights.has(lang)) { + f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";"); + } } } } @@ -810,6 +969,9 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); @@ -826,16 +988,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (((info.external_fa >> 16L) & 0120000) == 0120000) { #ifndef UNIX_ENABLED - WARN_PRINT(vformat("Relative symlinks are not supported on this OS, the exported project might be broken!")); + add_message(EXPORT_MESSAGE_INFO, TTR("Export"), TTR("Relative symlinks are not supported on this OS, the exported project might be broken!")); #endif // Handle symlinks in the archive. file = tmp_app_path_name.plus_file(file); if (err == OK) { err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); + } } if (err == OK) { String lnk_data = String::utf8((const char *)data.ptr(), data.size()); err = tmp_app_dir->create_link(lnk_data, file); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not created symlink \"%s\" -> \"%s\"."), lnk_data, file)); + } print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data)); } @@ -868,12 +1036,10 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (!iconpath.is_empty()) { if (iconpath.get_extension() == "icns") { - FileAccess *icon = FileAccess::open(iconpath, FileAccess::READ); - if (icon) { + Ref<FileAccess> icon = FileAccess::open(iconpath, FileAccess::READ); + if (icon.is_valid()) { data.resize(icon->get_length()); icon->get_buffer(&data.write[0], icon->get_length()); - icon->close(); - memdelete(icon); } } else { Ref<Image> icon; @@ -912,18 +1078,20 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p file = tmp_app_path_name.plus_file(file); if (err == OK) { err = tmp_app_dir->make_dir_recursive(file.get_base_dir()); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not create directory \"%s\"."), file.get_base_dir())); + } } if (err == OK) { - FileAccess *f = FileAccess::open(file, FileAccess::WRITE); - if (f) { + Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); + if (f.is_valid()) { f->store_buffer(data.ptr(), data.size()); - f->close(); if (is_execute) { // chmod with 0755 if the file is executable. FileAccess::set_unix_permissions(file, 0755); } - memdelete(f); } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file)); err = ERR_CANT_CREATE; } } @@ -936,10 +1104,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p unzClose(src_pkg_zip); if (!found_binary) { - ERR_PRINT(vformat("Requested template binary '%s' not found. It might be missing from your template archive.", binary_to_use)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Requested template binary \"%s\" not found. It might be missing from your template archive."), binary_to_use)); err = ERR_FILE_NOT_FOUND; } + // Save console script. + if (err == OK) { + int con_scr = p_preset->get("debug/export_console_script"); + if ((con_scr == 1 && p_debug) || (con_scr == 2)) { + err = _export_debug_script(p_preset, pkg_name, tmp_app_path_name.get_file() + "/Contents/MacOS/" + pkg_name, scr_path); + FileAccess::set_unix_permissions(scr_path, 0755); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not create console script.")); + } + } + } + if (err == OK) { if (ep.step(TTR("Making PKG"), 1)) { return ERR_SKIP; @@ -947,7 +1127,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p String pack_path = tmp_app_path_name + "/Contents/Resources/" + pkg_name + ".pck"; Vector<SharedObject> shared_objects; - err = save_pack(p_preset, pack_path, &shared_objects); + err = save_pack(p_preset, p_debug, pack_path, &shared_objects); // See if we can code sign our new package. bool sign_enabled = p_preset->get("codesign/enable"); @@ -957,8 +1137,8 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (sign_enabled && (ent_path.is_empty())) { ent_path = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name + ".entitlements"); - FileAccess *ent_f = FileAccess::open(ent_path, FileAccess::WRITE); - if (ent_f) { + Ref<FileAccess> ent_f = FileAccess::open(ent_path, FileAccess::WRITE); + if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); ent_f->store_line("<plist version=\"1.0\">"); @@ -1079,16 +1259,14 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); - - ent_f->close(); - memdelete(ent_f); } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file.")); err = ERR_CANT_CREATE; } if ((err == OK) && helpers.size() > 0) { ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE); - if (ent_f) { + if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); ent_f->store_line("<plist version=\"1.0\">"); @@ -1099,22 +1277,20 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("<true/>"); ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); - - ent_f->close(); - memdelete(ent_f); } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file.")); err = ERR_CANT_CREATE; } } } if ((err == OK) && helpers.size() > 0) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < helpers.size(); i++) { String hlp_path = helpers[i]; err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); if (err == OK && sign_enabled) { - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path); + err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false); } FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); } @@ -1130,17 +1306,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ad_hoc = (sign_identity == "" || sign_identity == "-"); bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation"); if ((!dylibs_found.is_empty() || !shared_objects.is_empty()) && sign_enabled && ad_hoc && !lib_validation) { - ERR_PRINT("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries."); + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries.")); err = ERR_CANT_CREATE; } } if (err == OK) { - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); for (int i = 0; i < shared_objects.size(); i++) { String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); - String path_in_app{ tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file() }; - err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true); + if (shared_objects[i].target.is_empty()) { + String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(); + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true); + } else { + String path_in_app = tmp_app_path_name.plus_file(shared_objects[i].target).plus_file(src_path.get_file()); + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, false); + } if (err != OK) { break; } @@ -1158,7 +1339,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (sign_enabled) { for (int i = 0; i < dylibs_found.size(); i++) { if (err == OK) { - err = _code_sign(p_preset, tmp_app_path_name + "/" + dylibs_found[i], ent_path); + err = _code_sign(p_preset, tmp_app_path_name + "/" + dylibs_found[i], ent_path, false); } } } @@ -1176,14 +1357,14 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p if (ep.step(TTR("Making DMG"), 3)) { return ERR_SKIP; } - err = _create_dmg(p_path, pkg_name, tmp_app_path_name); + err = _create_dmg(p_path, pkg_name, tmp_base_path_name); } // Sign DMG. if (err == OK && sign_enabled && !ad_hoc) { if (ep.step(TTR("Code signing DMG"), 3)) { return ERR_SKIP; } - err = _code_sign(p_preset, p_path, ent_path); + err = _code_sign(p_preset, p_path, ent_path, false); } } else if (export_format == "zip") { // Create ZIP. @@ -1195,11 +1376,11 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p OS::get_singleton()->move_to_trash(p_path); } - FileAccess *dst_f = nullptr; - zlib_filefunc_def io_dst = zipio_create_io_from_file(&dst_f); + Ref<FileAccess> io_fa_dst; + zlib_filefunc_def io_dst = zipio_create_io(&io_fa_dst); zipFile zip = zipOpen2(p_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io_dst); - _zip_folder_recursive(zip, EditorPaths::get_singleton()->get_cache_dir(), pkg_name + ".app", pkg_name); + _zip_folder_recursive(zip, tmp_base_path_name, "", pkg_name); zipClose(zip, nullptr); } @@ -1209,7 +1390,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p bool noto_enabled = p_preset->get("notarization/enable"); if (err == OK && noto_enabled) { if (export_format == "app") { - WARN_PRINT("Notarization requires the app to be archived first, select the DMG or ZIP export format instead."); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead.")); } else { if (ep.step(TTR("Sending archive for notarization"), 4)) { return ERR_SKIP; @@ -1227,10 +1408,10 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p tmp_app_dir->remove(ent_path); } if (export_format != "app") { - if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) { + if (tmp_app_dir->change_dir(tmp_base_path_name) == OK) { tmp_app_dir->erase_contents_recursive(); tmp_app_dir->change_dir(".."); - tmp_app_dir->remove(tmp_app_dir_name); + tmp_app_dir->remove(pkg_name); } } } @@ -1239,9 +1420,9 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p } void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) { - String dir = p_root_path.plus_file(p_folder); + String dir = p_folder.is_empty() ? p_root_path : p_root_path.plus_file(p_folder); - DirAccessRef da = DirAccess::open(dir); + Ref<DirAccess> da = DirAccess::open(dir); da->list_dir_begin(); String f = da->get_next(); while (!f.is_empty()) { @@ -1293,7 +1474,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String } else if (da->current_is_dir()) { _zip_folder_recursive(p_zip, p_root_path, p_folder.plus_file(f), p_pkg_name); } else { - bool is_executable = (p_folder.ends_with("MacOS") && (f == p_pkg_name)) || p_folder.ends_with("Helpers"); + bool is_executable = (p_folder.ends_with("MacOS") && (f == p_pkg_name)) || p_folder.ends_with("Helpers") || f.ends_with(".command"); OS::Time time = OS::get_singleton()->get_time(); OS::Date date = OS::get_singleton()->get_date(); @@ -1332,9 +1513,10 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String 0x0314, // "version made by", 0x03 - Unix, 0x14 - ZIP specification version 2.0, required to store Unix file permissions 0); - FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); - if (!fa) { - ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f))); + Ref<FileAccess> fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); + if (fa.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("ZIP Creation"), vformat(TTR("Could not open file to read from path \"%s\"."), dir.plus_file(f))); + return; } const int bufsize = 16384; uint8_t buf[bufsize]; diff --git a/platform/osx/export/export_plugin.h b/platform/osx/export/export_plugin.h index b85e9d662c..ec97d4139f 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -56,16 +56,17 @@ class EditorExportPlatformOSX : public EditorExportPlatform { void _make_icon(const Ref<Image> &p_icon, Vector<uint8_t> &p_data); Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path); + Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true); Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); - Error _copy_and_sign_files(DirAccessRef &dir_access, const String &p_src_path, const String &p_in_app_path, + Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, bool p_should_error_on_non_code_sign); Error _export_osx_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, - DirAccessRef &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, + Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name); + Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); bool use_codesign() const { return true; } #ifdef OSX_ENABLED @@ -100,7 +101,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override; virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const override; + virtual bool get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const override; public: virtual String get_name() const override { return "macOS"; } @@ -126,7 +127,7 @@ public: r_features->push_back("macos"); } - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override { } EditorExportPlatformOSX(); diff --git a/platform/osx/export/lipo.cpp b/platform/osx/export/lipo.cpp index 66d2ecdbcf..82baf18c52 100644 --- a/platform/osx/export/lipo.cpp +++ b/platform/osx/export/lipo.cpp @@ -35,8 +35,8 @@ #ifdef MODULE_REGEX_ENABLED bool LipO::is_lipo(const String &p_path) { - FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); uint32_t magic = fb->get_32(); return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf); } @@ -45,7 +45,7 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f close(); fa = FileAccess::open(p_output_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_output_path)); uint64_t max_size = 0; for (int i = 0; i < p_files.size(); i++) { @@ -64,8 +64,8 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f archs.push_back(arch); - FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); - if (!fb) { + 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])); } @@ -101,8 +101,8 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f // Write files and padding. for (int i = 0; i < archs.size(); i++) { - FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ); - if (!fb) { + 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])); } @@ -123,7 +123,6 @@ bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_f if (br > 0) { fa->store_buffer(step, br); } - fb->close(); } return true; } @@ -132,7 +131,7 @@ bool LipO::open_file(const String &p_path) { close(); fa = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); uint32_t magic = fa->get_32(); if (magic == 0xbebafeca) { @@ -197,16 +196,16 @@ bool LipO::open_file(const String &p_path) { } int LipO::get_arch_count() const { - ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "LipO: File not opened."); return archs.size(); } bool LipO::extract_arch(int p_index, const String &p_path) { - ERR_FAIL_COND_V_MSG(!fa, false, "LipO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "LipO: File not opened."); ERR_FAIL_INDEX_V(p_index, archs.size(), false); - FileAccessRef fb = FileAccess::open(p_path, FileAccess::WRITE); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::WRITE); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("LipO: Can't open file: \"%s\".", p_path)); fa->seek(archs[p_index].offset); @@ -223,16 +222,10 @@ bool LipO::extract_arch(int p_index, const String &p_path) { if (br > 0) { fb->store_buffer(step, br); } - fb->close(); return true; } void LipO::close() { - if (fa) { - fa->close(); - memdelete(fa); - fa = nullptr; - } archs.clear(); } diff --git a/platform/osx/export/lipo.h b/platform/osx/export/lipo.h index 68bbe42dd6..0e419be17e 100644 --- a/platform/osx/export/lipo.h +++ b/platform/osx/export/lipo.h @@ -50,7 +50,7 @@ class LipO : public RefCounted { uint32_t align; }; - FileAccess *fa = nullptr; + Ref<FileAccess> fa; Vector<FatArch> archs; static inline size_t PAD(size_t s, size_t a) { diff --git a/platform/osx/export/macho.cpp b/platform/osx/export/macho.cpp index 08f2a855b0..e6e67eff06 100644 --- a/platform/osx/export/macho.cpp +++ b/platform/osx/export/macho.cpp @@ -49,7 +49,7 @@ uint32_t MachO::seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max) { } bool MachO::alloc_signature(uint64_t p_size) { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); if (signature_offset != 0) { // Nothing to do, already have signature load command. return true; @@ -103,15 +103,15 @@ bool MachO::alloc_signature(uint64_t p_size) { } bool MachO::is_macho(const String &p_path) { - FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ); - ERR_FAIL_COND_V_MSG(!fb, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + Ref<FileAccess> fb = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fb.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); uint32_t magic = fb->get_32(); return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf); } bool MachO::open_file(const String &p_path) { fa = FileAccess::open(p_path, FileAccess::READ_WRITE); - ERR_FAIL_COND_V_MSG(!fa, false, vformat("MachO: Can't open file: \"%s\".", p_path)); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); uint32_t magic = fa->get_32(); MachHeader mach_header; @@ -232,37 +232,37 @@ bool MachO::open_file(const String &p_path) { } uint64_t MachO::get_exe_base() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return exe_base; } uint64_t MachO::get_exe_limit() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return exe_limit; } int32_t MachO::get_align() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return align; } uint32_t MachO::get_cputype() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return cputype; } uint32_t MachO::get_cpusubtype() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return cpusubtype; } uint64_t MachO::get_size() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); return fa->get_length(); } uint64_t MachO::get_signature_offset() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); fa->seek(signature_offset + 8); @@ -274,7 +274,7 @@ uint64_t MachO::get_signature_offset() { } uint64_t MachO::get_code_limit() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); if (signature_offset == 0) { return fa->get_length() + PAD(fa->get_length(), 16); @@ -284,7 +284,7 @@ uint64_t MachO::get_code_limit() { } uint64_t MachO::get_signature_size() { - ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, "MachO: File not opened."); ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command."); fa->seek(signature_offset + 12); @@ -296,7 +296,7 @@ uint64_t MachO::get_signature_size() { } bool MachO::is_signed() { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); if (signature_offset == 0) { return false; } @@ -325,7 +325,7 @@ bool MachO::is_signed() { } PackedByteArray MachO::get_cdhash_sha1() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -372,7 +372,7 @@ PackedByteArray MachO::get_cdhash_sha1() { } PackedByteArray MachO::get_cdhash_sha256() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -419,7 +419,7 @@ PackedByteArray MachO::get_cdhash_sha256() { } PackedByteArray MachO::get_requirements() { - ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), PackedByteArray(), "MachO: File not opened."); if (signature_offset == 0) { return PackedByteArray(); } @@ -451,16 +451,16 @@ PackedByteArray MachO::get_requirements() { return PackedByteArray(); } -const FileAccess *MachO::get_file() const { +const Ref<FileAccess> MachO::get_file() const { return fa; } -FileAccess *MachO::get_file() { +Ref<FileAccess> MachO::get_file() { return fa; } bool MachO::set_signature_size(uint64_t p_size) { - ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened."); + ERR_FAIL_COND_V_MSG(fa.is_null(), false, "MachO: File not opened."); // Ensure signature load command exists. ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: No __LINKEDIT segment found."); @@ -545,12 +545,4 @@ bool MachO::set_signature_size(uint64_t p_size) { return true; } -MachO::~MachO() { - if (fa) { - fa->close(); - memdelete(fa); - fa = nullptr; - } -} - #endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/macho.h b/platform/osx/export/macho.h index e09906898b..6cfc3c44f5 100644 --- a/platform/osx/export/macho.h +++ b/platform/osx/export/macho.h @@ -161,7 +161,7 @@ class MachO : public RefCounted { uint32_t reserved3; }; - FileAccess *fa = nullptr; + Ref<FileAccess> fa; bool swap = false; uint64_t lc_limit = 0; @@ -203,13 +203,11 @@ public: PackedByteArray get_requirements(); - const FileAccess *get_file() const; - FileAccess *get_file(); + const Ref<FileAccess> get_file() const; + Ref<FileAccess> get_file(); uint64_t get_signature_size(); bool set_signature_size(uint64_t p_size); - - ~MachO(); }; #endif // MODULE_REGEX_ENABLED diff --git a/platform/osx/export/plist.cpp b/platform/osx/export/plist.cpp index 553b864180..36de9dd34b 100644 --- a/platform/osx/export/plist.cpp +++ b/platform/osx/export/plist.cpp @@ -140,10 +140,11 @@ size_t PListNode::get_asn1_size(uint8_t p_len_octets) const { } break; case PList::PLNodeType::PL_NODE_TYPE_DICT: { size_t size = 0; - for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { + + for (const KeyValue<String, Ref<PListNode>> &E : data_dict) { size += 1 + _asn1_size_len(p_len_octets); // Sequence. - size += 1 + _asn1_size_len(p_len_octets) + it->key().utf8().length(); //Key. - size += 1 + _asn1_size_len(p_len_octets) + it->value()->get_asn1_size(p_len_octets); // Value. + size += 1 + _asn1_size_len(p_len_octets) + E.key.utf8().length(); //Key. + size += 1 + _asn1_size_len(p_len_octets) + E.value->get_asn1_size(p_len_octets); // Value. } return size; } break; @@ -225,13 +226,13 @@ bool PListNode::store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) cons case PList::PLNodeType::PL_NODE_TYPE_DICT: { p_stream.push_back(0x31); // Set. store_asn1_size(p_stream, p_len_octets); - for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { - CharString cs = it->key().utf8(); + for (const KeyValue<String, Ref<PListNode>> &E : data_dict) { + CharString cs = E.key.utf8(); uint32_t size = cs.length(); // Sequence. p_stream.push_back(0x30); - uint32_t seq_size = 2 * (1 + _asn1_size_len(p_len_octets)) + size + it->value()->get_asn1_size(p_len_octets); + uint32_t seq_size = 2 * (1 + _asn1_size_len(p_len_octets)) + size + E.value->get_asn1_size(p_len_octets); if (p_len_octets > 1) { p_stream.push_back(0x80 + p_len_octets); } @@ -252,7 +253,7 @@ bool PListNode::store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) cons p_stream.push_back(cs[i]); } // Value. - valid = valid && it->value()->store_asn1(p_stream, p_len_octets); + valid = valid && E.value->store_asn1(p_stream, p_len_octets); } } break; } @@ -317,12 +318,12 @@ void PListNode::store_text(String &p_stream, uint8_t p_indent) const { case PList::PLNodeType::PL_NODE_TYPE_DICT: { p_stream += String("\t").repeat(p_indent); p_stream += "<dict>\n"; - for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) { + for (const KeyValue<String, Ref<PListNode>> &E : data_dict) { p_stream += String("\t").repeat(p_indent + 1); p_stream += "<key>"; - p_stream += it->key(); + p_stream += E.key; p_stream += "</key>\n"; - it->value()->store_text(p_stream, p_indent + 1); + E.value->store_text(p_stream, p_indent + 1); } p_stream += String("\t").repeat(p_indent); p_stream += "</dict>\n"; @@ -343,8 +344,8 @@ PList::PList(const String &p_string) { bool PList::load_file(const String &p_filename) { root = Ref<PListNode>(); - FileAccessRef fb = FileAccess::open(p_filename, FileAccess::READ); - if (!fb) { + Ref<FileAccess> fb = FileAccess::open(p_filename, FileAccess::READ); + if (fb.is_null()) { return false; } @@ -398,7 +399,6 @@ bool PList::load_string(const String &p_string) { } if (token == "/plist") { - in_plist = false; done_plist = true; break; } diff --git a/platform/osx/export/plist.h b/platform/osx/export/plist.h index fb4aaaa935..ba9eaec196 100644 --- a/platform/osx/export/plist.h +++ b/platform/osx/export/plist.h @@ -83,7 +83,7 @@ public: CharString data_string; Vector<Ref<PListNode>> data_array; - Map<String, Ref<PListNode>> data_dict; + HashMap<String, Ref<PListNode>> data_dict; union { int32_t data_int; bool data_bool; diff --git a/platform/osx/gl_manager_osx_legacy.h b/platform/osx/gl_manager_osx_legacy.h index b5a1b9dd98..2d4913a7a6 100644 --- a/platform/osx/gl_manager_osx_legacy.h +++ b/platform/osx/gl_manager_osx_legacy.h @@ -38,9 +38,9 @@ #include "core/templates/local_vector.h" #include "servers/display_server.h" -#include <AppKit/AppKit.h> -#include <ApplicationServices/ApplicationServices.h> -#include <CoreVideo/CoreVideo.h> +#import <AppKit/AppKit.h> +#import <ApplicationServices/ApplicationServices.h> +#import <CoreVideo/CoreVideo.h> class GLManager_OSX { public: @@ -57,7 +57,7 @@ private: NSOpenGLContext *context = nullptr; }; - Map<DisplayServer::WindowID, GLWindow> windows; + RBMap<DisplayServer::WindowID, GLWindow> windows; NSOpenGLContext *shared_context = nullptr; DisplayServer::WindowID current_window = DisplayServer::INVALID_WINDOW_ID; diff --git a/platform/osx/gl_manager_osx_legacy.mm b/platform/osx/gl_manager_osx_legacy.mm index fbe64e32a3..c769d7f5c5 100644 --- a/platform/osx/gl_manager_osx_legacy.mm +++ b/platform/osx/gl_manager_osx_legacy.mm @@ -167,8 +167,8 @@ void GLManager_OSX::make_current() { } void GLManager_OSX::swap_buffers() { - for (Map<DisplayServer::WindowID, GLWindow>::Element *E = windows.front(); E; E = E->next()) { - [E->get().context flushBuffer]; + for (const KeyValue<DisplayServer::WindowID, GLWindow> &E : windows) { + [E.value.context flushBuffer]; } } diff --git a/platform/osx/godot_application.mm b/platform/osx/godot_application.mm index 00a58700e8..13313a025a 100644 --- a/platform/osx/godot_application.mm +++ b/platform/osx/godot_application.mm @@ -37,6 +37,11 @@ - (void)sendEvent:(NSEvent *)event { DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); if (ds) { + if ([event type] == NSEventTypeLeftMouseDown || [event type] == NSEventTypeRightMouseDown || [event type] == NSEventTypeOtherMouseDown) { + if (ds->mouse_process_popups()) { + return; + } + } ds->send_event(event); } diff --git a/platform/osx/godot_main_osx.mm b/platform/osx/godot_main_osx.mm index f3db363151..053a7f4a1d 100644 --- a/platform/osx/godot_main_osx.mm +++ b/platform/osx/godot_main_osx.mm @@ -35,12 +35,22 @@ #include <string.h> #include <unistd.h> +#if defined(SANITIZERS_ENABLED) +#include <sys/resource.h> +#endif + int main(int argc, char **argv) { #if defined(VULKAN_ENABLED) // MoltenVK - enable full component swizzling support. setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); #endif +#if defined(SANITIZERS_ENABLED) + // Note: Set stack size to be at least 30 MB (vs 8 MB default) to avoid overflow, address sanitizer can increase stack usage up to 3 times. + struct rlimit stack_lim = { 0x1E00000, 0x1E00000 }; + setrlimit(RLIMIT_STACK, &stack_lim); +#endif + int first_arg = 1; const char *dbg_arg = "-NSDocumentRevisionsDebugMode"; printf("arguments\n"); diff --git a/platform/osx/godot_menu_item.h b/platform/osx/godot_menu_item.h index 50c4709c18..2c12897f10 100644 --- a/platform/osx/godot_menu_item.h +++ b/platform/osx/godot_menu_item.h @@ -36,12 +36,21 @@ #import <AppKit/AppKit.h> #import <Foundation/Foundation.h> +enum GlobalMenuCheckType { + CHECKABLE_TYPE_NONE, + CHECKABLE_TYPE_CHECK_BOX, + CHECKABLE_TYPE_RADIO_BUTTON, +}; + @interface GodotMenuItem : NSObject { @public Callable callback; Variant meta; int id; - bool checkable; + GlobalMenuCheckType checkable_type; + int max_states; + int state; + Ref<Image> img; } @end diff --git a/platform/osx/godot_window_delegate.mm b/platform/osx/godot_window_delegate.mm index dbc244650e..521127f01b 100644 --- a/platform/osx/godot_window_delegate.mm +++ b/platform/osx/godot_window_delegate.mm @@ -58,7 +58,7 @@ DisplayServerOSX::WindowData &wd = ds->get_window(window_id); while (wd.transient_children.size()) { - ds->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); + ds->window_set_transient(*wd.transient_children.begin(), DisplayServerOSX::INVALID_WINDOW_ID); } if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { @@ -149,6 +149,20 @@ } } +- (void)windowWillStartLiveResize:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->set_is_resizing(true); + } +} + +- (void)windowDidEndLiveResize:(NSNotification *)notification { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (ds) { + ds->set_is_resizing(false); + } +} + - (void)windowDidResize:(NSNotification *)notification { DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); if (!ds || !ds->has_window(window_id)) { diff --git a/platform/osx/joypad_osx.cpp b/platform/osx/joypad_osx.cpp index 7d31ede61d..be9567e17c 100644 --- a/platform/osx/joypad_osx.cpp +++ b/platform/osx/joypad_osx.cpp @@ -195,7 +195,7 @@ void joypad::add_hid_element(IOHIDElementRef p_element) { } static void hid_element_added(const void *p_value, void *p_parameter) { - joypad *joy = (joypad *)p_parameter; + joypad *joy = static_cast<joypad *>(p_parameter); joy->add_hid_element((IOHIDElementRef)p_value); } @@ -540,10 +540,10 @@ static CFDictionaryRef create_match_dictionary(const UInt32 page, const UInt32 u CFDictionaryRef retval = nullptr; CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); - const void *keys[2] = { (void *)CFSTR(kIOHIDDeviceUsagePageKey), (void *)CFSTR(kIOHIDDeviceUsageKey) }; - const void *vals[2] = { (void *)pageNumRef, (void *)usageNumRef }; if (pageNumRef && usageNumRef) { + const void *keys[2] = { (void *)CFSTR(kIOHIDDeviceUsagePageKey), (void *)CFSTR(kIOHIDDeviceUsageKey) }; + const void *vals[2] = { (void *)pageNumRef, (void *)usageNumRef }; retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); } diff --git a/platform/osx/joypad_osx.h b/platform/osx/joypad_osx.h index 4ca7fb1698..3f89048ce6 100644 --- a/platform/osx/joypad_osx.h +++ b/platform/osx/joypad_osx.h @@ -32,13 +32,13 @@ #define JOYPADOSX_H #ifdef MACOS_10_0_4 -#include <IOKit/hidsystem/IOHIDUsageTables.h> +#import <IOKit/hidsystem/IOHIDUsageTables.h> #else -#include <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> +#import <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> #endif -#include <ForceFeedback/ForceFeedback.h> -#include <ForceFeedback/ForceFeedbackConstants.h> -#include <IOKit/hid/IOHIDLib.h> +#import <ForceFeedback/ForceFeedback.h> +#import <ForceFeedback/ForceFeedbackConstants.h> +#import <IOKit/hid/IOHIDLib.h> #include "core/input/input.h" @@ -94,7 +94,7 @@ class JoypadOSX { }; private: - Input *input; + Input *input = nullptr; IOHIDManagerRef hid_manager; Vector<joypad> device_list; diff --git a/platform/osx/key_mapping_osx.mm b/platform/osx/key_mapping_osx.mm index fde9206824..bfec45de58 100644 --- a/platform/osx/key_mapping_osx.mm +++ b/platform/osx/key_mapping_osx.mm @@ -30,8 +30,8 @@ #include "key_mapping_osx.h" -#include <Carbon/Carbon.h> -#include <Cocoa/Cocoa.h> +#import <Carbon/Carbon.h> +#import <Cocoa/Cocoa.h> bool KeyMappingOSX::is_numpad_key(unsigned int key) { static const unsigned int table[] = { diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 53c5c8bd90..e4ec411c96 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -80,7 +80,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual MainLoop *get_main_loop() const override; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index 6700f8fe82..a8fa56e34b 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -47,7 +47,7 @@ _FORCE_INLINE_ String OS_OSX::get_framework_executable(const String &p_path) { // Append framework executable name, or return as is if p_path is not a framework. - DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (da->dir_exists(p_path) && da->file_exists(p_path.plus_file(p_path.get_file().get_basename()))) { return p_path.plus_file(p_path.get_file().get_basename()); } else { @@ -58,7 +58,8 @@ _FORCE_INLINE_ String OS_OSX::get_framework_executable(const String &p_path) { void OS_OSX::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { // Prevent main loop from sleeping and redraw window during resize / modal popups. - if (get_singleton()->get_main_loop()) { + DisplayServerOSX *ds = (DisplayServerOSX *)DisplayServer::get_singleton(); + if (get_singleton()->get_main_loop() && ds && (get_singleton()->get_render_thread_mode() != RENDER_SEPARATE_THREAD || !ds->get_is_resizing())) { Main::force_redraw(); if (!Main::is_iterating()) { // Avoid cyclic loop. Main::iteration(); @@ -149,7 +150,7 @@ void OS_OSX::alert(const String &p_alert, const String &p_title) { } } -Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = get_framework_executable(p_path); if (!FileAccess::exists(path)) { @@ -164,6 +165,11 @@ Error OS_OSX::open_dynamic_library(const String p_path, void *&p_library_handle, p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + dlerror() + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } @@ -312,21 +318,22 @@ String OS_OSX::get_executable_path() const { } Error OS_OSX::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { - if (@available(macOS 10.15, *)) { - // Use NSWorkspace if path is an .app bundle. - NSURL *url = [NSURL fileURLWithPath:@(p_path.utf8().get_data())]; - NSBundle *bundle = [NSBundle bundleWithURL:url]; - if (bundle) { - NSMutableArray *arguments = [[NSMutableArray alloc] init]; - for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) { - [arguments addObject:[NSString stringWithUTF8String:E->get().utf8().get_data()]]; - } + // Use NSWorkspace if path is an .app bundle. + NSURL *url = [NSURL fileURLWithPath:@(p_path.utf8().get_data())]; + NSBundle *bundle = [NSBundle bundleWithURL:url]; + if (bundle) { + NSMutableArray *arguments = [[NSMutableArray alloc] init]; + for (const String &arg : p_arguments) { + [arguments addObject:[NSString stringWithUTF8String:arg.utf8().get_data()]]; + } + if (@available(macOS 10.15, *)) { NSWorkspaceOpenConfiguration *configuration = [[NSWorkspaceOpenConfiguration alloc] init]; [configuration setArguments:arguments]; [configuration setCreatesNewApplicationInstance:YES]; __block dispatch_semaphore_t lock = dispatch_semaphore_create(0); __block Error err = ERR_TIMEOUT; __block pid_t pid = 0; + [[NSWorkspace sharedWorkspace] openApplicationAtURL:url configuration:configuration completionHandler:^(NSRunningApplication *app, NSError *error) { @@ -349,7 +356,19 @@ Error OS_OSX::create_process(const String &p_path, const List<String> &p_argumen return err; } else { - return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); + Error err = ERR_TIMEOUT; + NSError *error = nullptr; + NSRunningApplication *app = [[NSWorkspace sharedWorkspace] launchApplicationAtURL:url options:NSWorkspaceLaunchNewInstance configuration:[NSDictionary dictionaryWithObject:arguments forKey:NSWorkspaceLaunchConfigurationArguments] error:&error]; + if (error) { + err = ERR_CANT_FORK; + NSLog(@"Failed to execute: %@", error.localizedDescription); + } else { + if (r_child_id) { + *r_child_id = (ProcessID)[app processIdentifier]; + } + err = OK; + } + return err; } } else { return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); diff --git a/platform/osx/tts_osx.h b/platform/osx/tts_osx.h new file mode 100644 index 0000000000..449418e48f --- /dev/null +++ b/platform/osx/tts_osx.h @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* tts_osx.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TTS_OSX_H +#define TTS_OSX_H + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/rb_map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#import <AppKit/AppKit.h> + +#if __has_include(<AVFAudio/AVSpeechSynthesis.h>) +#import <AVFAudio/AVSpeechSynthesis.h> +#else +#import <AVFoundation/AVFoundation.h> +#endif + +@interface TTS_OSX : NSObject <AVSpeechSynthesizerDelegate> { + // AVSpeechSynthesizer + bool speaking; + HashMap<id, int> ids; + + // NSSpeechSynthesizer + bool paused; + bool have_utterance; + int last_utterance; + + id synth; // NSSpeechSynthesizer or AVSpeechSynthesizer + List<DisplayServer::TTSUtterance> queue; +} + +- (void)pauseSpeaking; +- (void)resumeSpeaking; +- (void)stopSpeaking; +- (bool)isSpeaking; +- (bool)isPaused; +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt; +- (Array)getVoices; +@end + +#endif // TTS_OSX_H diff --git a/platform/osx/tts_osx.mm b/platform/osx/tts_osx.mm new file mode 100644 index 0000000000..e6a5236cd9 --- /dev/null +++ b/platform/osx/tts_osx.mm @@ -0,0 +1,266 @@ +/*************************************************************************/ +/* tts_osx.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "tts_osx.h" + +@implementation TTS_OSX + +- (id)init { + self = [super init]; + self->speaking = false; + self->have_utterance = false; + self->last_utterance = -1; + self->paused = false; + if (@available(macOS 10.14, *)) { + self->synth = [[AVSpeechSynthesizer alloc] init]; + [self->synth setDelegate:self]; + print_verbose("Text-to-Speech: AVSpeechSynthesizer initialized."); + } else { + self->synth = [[NSSpeechSynthesizer alloc] init]; + [self->synth setDelegate:self]; + print_verbose("Text-to-Speech: NSSpeechSynthesizer initialized."); + } + return self; +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + NSString *string = [utterance speechString]; + + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, ids[utterance], pos); +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didCancelSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +// AVSpeechSynthesizer callback (macOS 10.14+) + +- (void)speechSynthesizer:(AVSpeechSynthesizer *)av_synth didFinishSpeechUtterance:(AVSpeechUtterance *)utterance API_AVAILABLE(macosx(10.14)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, ids[utterance]); + ids.erase(utterance); + speaking = false; + [self update]; +} + +// NSSpeechSynthesizer callback (macOS 10.4+) + +- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth willSpeakWord:(NSRange)characterRange ofString:(NSString *)string { + if (!paused && have_utterance) { + // Convert from UTF-16 to UTF-32 position. + int pos = 0; + for (NSUInteger i = 0; i < MIN(characterRange.location, string.length); i++) { + unichar c = [string characterAtIndex:i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, last_utterance, pos); + } +} + +- (void)speechSynthesizer:(NSSpeechSynthesizer *)ns_synth didFinishSpeaking:(BOOL)success { + if (!paused && have_utterance) { + if (success) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, last_utterance); + } else { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, last_utterance); + } + have_utterance = false; + } + speaking = false; + [self update]; +} + +- (void)update { + if (!speaking && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + AVSpeechUtterance *new_utterance = [[AVSpeechUtterance alloc] initWithString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + [new_utterance setVoice:[AVSpeechSynthesisVoice voiceWithIdentifier:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]]; + if (message.rate > 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 1.f, 10.f, AVSpeechUtteranceDefaultSpeechRate, AVSpeechUtteranceMaximumSpeechRate)]; + } else if (message.rate < 1.f) { + [new_utterance setRate:Math::range_lerp(message.rate, 0.1f, 1.f, AVSpeechUtteranceMinimumSpeechRate, AVSpeechUtteranceDefaultSpeechRate)]; + } + [new_utterance setPitchMultiplier:message.pitch]; + [new_utterance setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + + ids[new_utterance] = message.id; + [av_synth speakUtterance:new_utterance]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth setObject:nil forProperty:NSSpeechResetProperty error:nil]; + [ns_synth setVoice:[NSString stringWithUTF8String:message.voice.utf8().get_data()]]; + int base_pitch = [[ns_synth objectForProperty:NSSpeechPitchBaseProperty error:nil] intValue]; + [ns_synth setObject:[NSNumber numberWithInt:(base_pitch * (message.pitch / 2.f + 0.5f))] forProperty:NSSpeechPitchBaseProperty error:nullptr]; + [ns_synth setVolume:(Math::range_lerp(message.volume, 0.f, 100.f, 0.f, 1.f))]; + [ns_synth setRate:(message.rate * 200)]; + + last_utterance = message.id; + have_utterance = true; + [ns_synth startSpeakingString:[NSString stringWithUTF8String:message.text.utf8().get_data()]]; + } + queue.pop_front(); + + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, message.id); + speaking = true; + } +} + +- (void)pauseSpeaking { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth pauseSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth pauseSpeakingAtBoundary:NSSpeechImmediateBoundary]; + } + paused = true; +} + +- (void)resumeSpeaking { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth continueSpeaking]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + [ns_synth continueSpeaking]; + } + paused = false; +} + +- (void)stopSpeaking { + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + [av_synth stopSpeakingAtBoundary:AVSpeechBoundaryImmediate]; + } else { + NSSpeechSynthesizer *ns_synth = synth; + if (have_utterance) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, last_utterance); + } + [ns_synth stopSpeaking]; + } + have_utterance = false; + speaking = false; + paused = false; +} + +- (bool)isSpeaking { + return speaking || (queue.size() > 0); +} + +- (bool)isPaused { + if (@available(macOS 10.14, *)) { + AVSpeechSynthesizer *av_synth = synth; + return [av_synth isPaused]; + } else { + return paused; + } +} + +- (void)speak:(const String &)text voice:(const String &)voice volume:(int)volume pitch:(float)pitch rate:(float)rate utterance_id:(int)utterance_id interrupt:(bool)interrupt { + if (interrupt) { + [self stopSpeaking]; + } + + if (text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = text; + message.voice = voice; + message.volume = CLAMP(volume, 0, 100); + message.pitch = CLAMP(pitch, 0.f, 2.f); + message.rate = CLAMP(rate, 0.1f, 10.f); + message.id = utterance_id; + queue.push_back(message); + + if ([self isPaused]) { + [self resumeSpeaking]; + } else { + [self update]; + } +} + +- (Array)getVoices { + Array list; + if (@available(macOS 10.14, *)) { + for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) { + NSString *voiceIdentifierString = [voice identifier]; + NSString *voiceLocaleIdentifier = [voice language]; + NSString *voiceName = [voice name]; + Dictionary voice_d; + voice_d["name"] = String::utf8([voiceName UTF8String]); + voice_d["id"] = String::utf8([voiceIdentifierString UTF8String]); + voice_d["language"] = String::utf8([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + } else { + for (NSString *voiceIdentifierString in [NSSpeechSynthesizer availableVoices]) { + NSString *voiceLocaleIdentifier = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceLocaleIdentifier]; + NSString *voiceName = [[NSSpeechSynthesizer attributesForVoice:voiceIdentifierString] objectForKey:NSVoiceName]; + Dictionary voice_d; + voice_d["name"] = String([voiceName UTF8String]); + voice_d["id"] = String([voiceIdentifierString UTF8String]); + voice_d["language"] = String([voiceLocaleIdentifier UTF8String]); + list.push_back(voice_d); + } + } + return list; +} + +@end diff --git a/platform/osx/vulkan_context_osx.h b/platform/osx/vulkan_context_osx.h index b78b4eb141..ade0f4a4c9 100644 --- a/platform/osx/vulkan_context_osx.h +++ b/platform/osx/vulkan_context_osx.h @@ -32,7 +32,7 @@ #define VULKAN_DEVICE_OSX_H #include "drivers/vulkan/vulkan_context.h" -#include <AppKit/AppKit.h> +#import <AppKit/AppKit.h> class VulkanContextOSX : public VulkanContext { virtual const char *_get_platform_surface_extension() const; diff --git a/platform/uwp/export/app_packager.cpp b/platform/uwp/export/app_packager.cpp index e7978ff74d..09717b9d69 100644 --- a/platform/uwp/export/app_packager.cpp +++ b/platform/uwp/export/app_packager.cpp @@ -46,7 +46,7 @@ String AppxPackager::hash_block(const uint8_t *p_block_data, size_t p_block_len) } void AppxPackager::make_block_map(const String &p_path) { - FileAccess *tmp_file = FileAccess::open(p_path, FileAccess::WRITE); + Ref<FileAccess> tmp_file = FileAccess::open(p_path, FileAccess::WRITE); tmp_file->store_string("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"); tmp_file->store_string("<BlockMap xmlns=\"http://schemas.microsoft.com/appx/2010/blockmap\" HashMethod=\"http://www.w3.org/2001/04/xmlenc#sha256\">"); @@ -69,9 +69,6 @@ void AppxPackager::make_block_map(const String &p_path) { } tmp_file->store_string("</BlockMap>"); - - tmp_file->close(); - memdelete(tmp_file); } String AppxPackager::content_type(String p_extension) { @@ -89,12 +86,12 @@ String AppxPackager::content_type(String p_extension) { } void AppxPackager::make_content_types(const String &p_path) { - FileAccess *tmp_file = FileAccess::open(p_path, FileAccess::WRITE); + Ref<FileAccess> tmp_file = FileAccess::open(p_path, FileAccess::WRITE); tmp_file->store_string("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); tmp_file->store_string("<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">"); - Map<String, String> types; + HashMap<String, String> types; for (int i = 0; i < file_metadata.size(); i++) { String ext = file_metadata[i].name.get_extension().to_lower(); @@ -118,9 +115,6 @@ void AppxPackager::make_content_types(const String &p_path) { tmp_file->store_string("<Override PartName=\"/AppxMetadata/CodeIntegrity.cat\" ContentType=\"application/vnd.ms-pkiseccat\" />"); tmp_file->store_string("</Types>"); - - tmp_file->close(); - memdelete(tmp_file); } Vector<uint8_t> AppxPackager::make_file_header(FileMeta p_file_meta) { @@ -285,7 +279,7 @@ Vector<uint8_t> AppxPackager::make_end_of_central_record() { return buf; } -void AppxPackager::init(FileAccess *p_fa) { +void AppxPackager::init(Ref<FileAccess> p_fa) { package = p_fa; central_dir_offset = 0; end_of_central_dir_offset = 0; @@ -301,7 +295,6 @@ Error AppxPackager::add_file(String p_file_name, const uint8_t *p_buffer, size_t FileMeta meta; meta.name = p_file_name; meta.uncompressed_size = p_len; - meta.compressed_size = p_len; meta.compressed = p_compress; meta.zip_offset = package->get_position(); @@ -309,7 +302,6 @@ Error AppxPackager::add_file(String p_file_name, const uint8_t *p_buffer, size_t // Data for compression z_stream strm; - FileAccess *strm_f = nullptr; Vector<uint8_t> strm_in; strm_in.resize(BLOCK_SIZE); Vector<uint8_t> strm_out; @@ -317,7 +309,7 @@ Error AppxPackager::add_file(String p_file_name, const uint8_t *p_buffer, size_t if (p_compress) { strm.zalloc = zipio_alloc; strm.zfree = zipio_free; - strm.opaque = &strm_f; + strm.opaque = Z_NULL; strm_out.resize(BLOCK_SIZE + 8); @@ -419,16 +411,15 @@ void AppxPackager::finish() { const String &tmp_blockmap_file_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpblockmap.xml"); make_block_map(tmp_blockmap_file_path); - FileAccess *blockmap_file = FileAccess::open(tmp_blockmap_file_path, FileAccess::READ); - Vector<uint8_t> blockmap_buffer; - blockmap_buffer.resize(blockmap_file->get_length()); - - blockmap_file->get_buffer(blockmap_buffer.ptrw(), blockmap_buffer.size()); + { + Ref<FileAccess> blockmap_file = FileAccess::open(tmp_blockmap_file_path, FileAccess::READ); + Vector<uint8_t> blockmap_buffer; + blockmap_buffer.resize(blockmap_file->get_length()); - add_file("AppxBlockMap.xml", blockmap_buffer.ptr(), blockmap_buffer.size(), -1, -1, true); + blockmap_file->get_buffer(blockmap_buffer.ptrw(), blockmap_buffer.size()); - blockmap_file->close(); - memdelete(blockmap_file); + add_file("AppxBlockMap.xml", blockmap_buffer.ptr(), blockmap_buffer.size(), -1, -1, true); + } // Add content types @@ -437,16 +428,15 @@ void AppxPackager::finish() { const String &tmp_content_types_file_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("tmpcontenttypes.xml"); make_content_types(tmp_content_types_file_path); - FileAccess *types_file = FileAccess::open(tmp_content_types_file_path, FileAccess::READ); - Vector<uint8_t> types_buffer; - types_buffer.resize(types_file->get_length()); - - types_file->get_buffer(types_buffer.ptrw(), types_buffer.size()); + { + Ref<FileAccess> types_file = FileAccess::open(tmp_content_types_file_path, FileAccess::READ); + Vector<uint8_t> types_buffer; + types_buffer.resize(types_file->get_length()); - add_file("[Content_Types].xml", types_buffer.ptr(), types_buffer.size(), -1, -1, true); + types_file->get_buffer(types_buffer.ptrw(), types_buffer.size()); - types_file->close(); - memdelete(types_file); + add_file("[Content_Types].xml", types_buffer.ptr(), types_buffer.size(), -1, -1, true); + } // Cleanup generated files. DirAccess::remove_file_or_error(tmp_blockmap_file_path); @@ -467,9 +457,7 @@ void AppxPackager::finish() { Vector<uint8_t> end_record = make_end_of_central_record(); package->store_buffer(end_record.ptr(), end_record.size()); - package->close(); - memdelete(package); - package = nullptr; + package.unref(); } AppxPackager::AppxPackager() {} diff --git a/platform/uwp/export/app_packager.h b/platform/uwp/export/app_packager.h index da118449c7..dc5a5259ec 100644 --- a/platform/uwp/export/app_packager.h +++ b/platform/uwp/export/app_packager.h @@ -87,14 +87,14 @@ class AppxPackager { }; String progress_task; - FileAccess *package = nullptr; + Ref<FileAccess> package; - Set<String> mime_types; + HashSet<String> mime_types; Vector<FileMeta> file_metadata; - ZPOS64_T central_dir_offset; - ZPOS64_T end_of_central_dir_offset; + ZPOS64_T central_dir_offset = 0; + ZPOS64_T end_of_central_dir_offset = 0; Vector<uint8_t> central_dir_data; String hash_block(const uint8_t *p_block_data, size_t p_block_len); @@ -138,7 +138,7 @@ class AppxPackager { public: void set_progress_task(String p_task) { progress_task = p_task; } - void init(FileAccess *p_fa); + void init(Ref<FileAccess> p_fa); Error add_file(String p_file_name, const uint8_t *p_buffer, size_t p_len, int p_file_no, int p_total_files, bool p_compress = false); void finish(); diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index 594495375a..01683c656c 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -98,13 +98,13 @@ void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "orientation/portrait_flipped"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "images/background_color"), "transparent")); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/store_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square44x44_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square71x71_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square150x150_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square310x310_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/wide310x150_logo", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); - r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/splash_screen", PROPERTY_HINT_RESOURCE_TYPE, "StreamTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/store_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square44x44_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square71x71_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square150x150_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/square310x310_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/wide310x150_logo", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); + r_options->push_back(ExportOption(PropertyInfo(Variant::OBJECT, "images/splash_screen", PROPERTY_HINT_RESOURCE_TYPE, "CompressedTexture2D"), Variant())); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "tiles/show_name_on_square150x150"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "tiles/show_name_on_wide310x150"), false)); @@ -131,6 +131,14 @@ void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) } bool EditorExportPlatformUWP::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +#ifndef DEV_ENABLED + // We don't provide export templates for the UWP platform currently as it + // has not been ported for Godot 4.0. This is skipped in DEV_ENABLED so that + // contributors can still test the pipeline if/when we can build it again. + r_error = "The UWP platform is currently not supported in Godot 4.0.\n"; + return false; +#endif + String err; bool valid = false; @@ -201,37 +209,37 @@ bool EditorExportPlatformUWP::can_export(const Ref<EditorExportPreset> &p_preset err += TTR("Invalid background color.") + "\n"; } - if (!p_preset->get("images/store_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/store_logo"))), 50, 50)) { + if (!p_preset->get("images/store_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/store_logo"))), 50, 50)) { valid = false; err += TTR("Invalid Store Logo image dimensions (should be 50x50).") + "\n"; } - if (!p_preset->get("images/square44x44_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/square44x44_logo"))), 44, 44)) { + if (!p_preset->get("images/square44x44_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/square44x44_logo"))), 44, 44)) { valid = false; err += TTR("Invalid square 44x44 logo image dimensions (should be 44x44).") + "\n"; } - if (!p_preset->get("images/square71x71_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/square71x71_logo"))), 71, 71)) { + if (!p_preset->get("images/square71x71_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/square71x71_logo"))), 71, 71)) { valid = false; err += TTR("Invalid square 71x71 logo image dimensions (should be 71x71).") + "\n"; } - if (!p_preset->get("images/square150x150_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/square150x150_logo"))), 150, 150)) { + if (!p_preset->get("images/square150x150_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/square150x150_logo"))), 150, 150)) { valid = false; err += TTR("Invalid square 150x150 logo image dimensions (should be 150x150).") + "\n"; } - if (!p_preset->get("images/square310x310_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/square310x310_logo"))), 310, 310)) { + if (!p_preset->get("images/square310x310_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/square310x310_logo"))), 310, 310)) { valid = false; err += TTR("Invalid square 310x310 logo image dimensions (should be 310x310).") + "\n"; } - if (!p_preset->get("images/wide310x150_logo").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/wide310x150_logo"))), 310, 150)) { + if (!p_preset->get("images/wide310x150_logo").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/wide310x150_logo"))), 310, 150)) { valid = false; err += TTR("Invalid wide 310x150 logo image dimensions (should be 310x150).") + "\n"; } - if (!p_preset->get("images/splash_screen").is_zero() && !_valid_image((Object::cast_to<StreamTexture2D>((Object *)p_preset->get("images/splash_screen"))), 620, 300)) { + if (!p_preset->get("images/splash_screen").is_zero() && !_valid_image((Object::cast_to<CompressedTexture2D>((Object *)p_preset->get("images/splash_screen"))), 620, 300)) { valid = false; err += TTR("Invalid splash screen image dimensions (should be 620x300).") + "\n"; } @@ -287,14 +295,14 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p Error err = OK; - FileAccess *fa_pack = FileAccess::open(p_path, FileAccess::WRITE, &err); + Ref<FileAccess> fa_pack = FileAccess::open(p_path, FileAccess::WRITE, &err); ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); AppxPackager packager; packager.init(fa_pack); - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step("Creating package...", 0)) { return ERR_SKIP; @@ -324,6 +332,9 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p unz_file_info info; char fname[16834]; ret = unzGetCurrentFileInfo(pkg, &info, fname, 16834, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String path = String::utf8(fname); @@ -416,7 +427,7 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p EditorNode::progress_add_task("project_files", "Project Files", 100); packager.set_progress_task("project_files"); - err = export_project_files(p_preset, save_appx_file, &packager); + err = export_project_files(p_preset, p_debug, save_appx_file, &packager); EditorNode::progress_end_task("project_files"); @@ -488,7 +499,7 @@ void EditorExportPlatformUWP::get_platform_features(List<String> *r_features) { r_features->push_back("uwp"); } -void EditorExportPlatformUWP::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { +void EditorExportPlatformUWP::resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) { } EditorExportPlatformUWP::EditorExportPlatformUWP() { diff --git a/platform/uwp/export/export_plugin.h b/platform/uwp/export/export_plugin.h index 4c2d25e533..d92687075c 100644 --- a/platform/uwp/export/export_plugin.h +++ b/platform/uwp/export/export_plugin.h @@ -191,7 +191,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform { return false; } - bool _valid_image(const StreamTexture2D *p_image, int p_width, int p_height) const { + bool _valid_image(const CompressedTexture2D *p_image, int p_width, int p_height) const { if (!p_image) { return false; } @@ -311,22 +311,22 @@ class EditorExportPlatformUWP : public EditorExportPlatform { Vector<uint8_t> _get_image_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) { Vector<uint8_t> data; - StreamTexture2D *texture = nullptr; + CompressedTexture2D *texture = nullptr; if (p_path.find("StoreLogo") != -1) { - texture = p_preset->get("images/store_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/store_logo"))); + texture = p_preset->get("images/store_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/store_logo"))); } else if (p_path.find("Square44x44Logo") != -1) { - texture = p_preset->get("images/square44x44_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/square44x44_logo"))); + texture = p_preset->get("images/square44x44_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/square44x44_logo"))); } else if (p_path.find("Square71x71Logo") != -1) { - texture = p_preset->get("images/square71x71_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/square71x71_logo"))); + texture = p_preset->get("images/square71x71_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/square71x71_logo"))); } else if (p_path.find("Square150x150Logo") != -1) { - texture = p_preset->get("images/square150x150_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/square150x150_logo"))); + texture = p_preset->get("images/square150x150_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/square150x150_logo"))); } else if (p_path.find("Square310x310Logo") != -1) { - texture = p_preset->get("images/square310x310_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/square310x310_logo"))); + texture = p_preset->get("images/square310x310_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/square310x310_logo"))); } else if (p_path.find("Wide310x150Logo") != -1) { - texture = p_preset->get("images/wide310x150_logo").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/wide310x150_logo"))); + texture = p_preset->get("images/wide310x150_logo").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/wide310x150_logo"))); } else if (p_path.find("SplashScreen") != -1) { - texture = p_preset->get("images/splash_screen").is_zero() ? nullptr : Object::cast_to<StreamTexture2D>(((Object *)p_preset->get("images/splash_screen"))); + texture = p_preset->get("images/splash_screen").is_zero() ? nullptr : Object::cast_to<CompressedTexture2D>(((Object *)p_preset->get("images/splash_screen"))); } else { ERR_PRINT("Unable to load logo"); } @@ -346,21 +346,21 @@ class EditorExportPlatformUWP : public EditorExportPlatform { ERR_FAIL_V_MSG(data, err_string); } - FileAccess *f = FileAccess::open(tmp_path, FileAccess::READ, &err); + { + Ref<FileAccess> f = FileAccess::open(tmp_path, FileAccess::READ, &err); - if (err != OK) { - String err_string = "Couldn't open temp logo file."; - // Cleanup generated file. - DirAccess::remove_file_or_error(tmp_path); - EditorNode::add_io_error(err_string); - ERR_FAIL_V_MSG(data, err_string); - } + if (err != OK) { + String err_string = "Couldn't open temp logo file."; + // Cleanup generated file. + DirAccess::remove_file_or_error(tmp_path); + EditorNode::add_io_error(err_string); + ERR_FAIL_V_MSG(data, err_string); + } - data.resize(f->get_length()); - f->get_buffer(data.ptrw(), data.size()); + data.resize(f->get_length()); + f->get_buffer(data.ptrw(), data.size()); + } - f->close(); - memdelete(f); DirAccess::remove_file_or_error(tmp_path); return data; @@ -393,7 +393,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform { ".webp", // Same reasoning as .png ".cfb", // Don't let small config files slow-down startup ".scn", // Binary scenes are usually already compressed - ".stex", // Streamable textures are usually already compressed + ".ctex", // Streamable textures are usually already compressed // Trailer for easier processing nullptr }; @@ -417,7 +417,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform { } static Error save_appx_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { - AppxPackager *packager = (AppxPackager *)p_userdata; + AppxPackager *packager = static_cast<AppxPackager *>(p_userdata); String dst_path = p_path.replace_first("res://", "game/"); return packager->add_file(dst_path, p_data.ptr(), p_data.size(), p_file, p_total, _should_compress_asset(p_path, p_data)); @@ -441,7 +441,7 @@ public: virtual void get_platform_features(List<String> *r_features) override; - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override; + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) override; EditorExportPlatformUWP(); }; diff --git a/platform/uwp/joypad_uwp.h b/platform/uwp/joypad_uwp.h index 29f5109056..0869f1961d 100644 --- a/platform/uwp/joypad_uwp.h +++ b/platform/uwp/joypad_uwp.h @@ -68,7 +68,7 @@ private: ControllerDevice controllers[MAX_CONTROLLERS]; - InputDefault *input; + InputDefault *input = nullptr; void OnGamepadAdded(Platform::Object ^ sender, Windows::Gaming::Input::Gamepad ^ value); void OnGamepadRemoved(Platform::Object ^ sender, Windows::Gaming::Input::Gamepad ^ value); diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 22a54911f9..1614bfdcc3 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -631,7 +631,7 @@ OS::CursorShape OS_UWP::get_cursor_shape() const { return cursor_shape; } -void OS_UWP::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void OS_UWP::set_custom_mouse_cursor(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { // TODO } @@ -647,6 +647,10 @@ Error OS_UWP::kill(const ProcessID &p_pid) { return FAILED; } +bool OS_UWP::is_process_running(const ProcessID &p_pid) const { + return false; +} + Error OS_UWP::set_cwd(const String &p_cwd) { return FAILED; } @@ -733,10 +737,15 @@ static String format_error_message(DWORD id) { return msg; } -Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String full_path = "game/" + p_path; p_library_handle = (void *)LoadPackagedLibrary((LPCWSTR)(full_path.utf16().get_data()), 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + full_path + ", error: " + format_error_message(GetLastError()) + "."); + + if (r_resolved_path != nullptr) { + *r_resolved_path = full_path; + } + return OK; } diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index 573d86af7c..bddf63ff18 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -74,7 +74,7 @@ private: KEY_EVENT_BUFFER_SIZE = 512 }; - FILE *stdo; + FILE *stdo = nullptr; KeyEvent key_event_buffer[KEY_EVENT_BUFFER_SIZE]; int key_event_pos; @@ -87,16 +87,16 @@ private: bool outside; int old_x, old_y; Point2i center; - RenderingServer *rendering_server; + RenderingServer *rendering_server = nullptr; int pressrc; - ContextEGL_UWP *gl_context; + ContextEGL_UWP *gl_context = nullptr; Windows::UI::Core::CoreWindow ^ window; VideoMode video_mode; int video_driver_index; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; AudioDriverXAudio2 audio_driver; @@ -111,7 +111,7 @@ private: CursorShape cursor_shape; - InputDefault *input; + InputDefault *input = nullptr; JoypadUWP ^ joypad; @@ -198,6 +198,7 @@ public: virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false); virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false); virtual Error kill(const ProcessID &p_pid); + virtual bool is_process_running(const ProcessID &p_pid) const; virtual bool has_environment(const String &p_var) const; virtual String get_environment(const String &p_var) const; @@ -208,7 +209,7 @@ public: void set_cursor_shape(CursorShape p_shape); CursorShape get_cursor_shape() const; - virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); + virtual void set_custom_mouse_cursor(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); void set_icon(const Ref<Image> &p_icon); virtual String get_executable_path() const; @@ -233,7 +234,7 @@ public: virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); virtual void hide_virtual_keyboard(); - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr); virtual Error close_dynamic_library(void *p_library_handle); virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false); diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 76234c3065..7e412b140f 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -13,6 +13,7 @@ common_win = [ "display_server_windows.cpp", "key_mapping_windows.cpp", "joypad_windows.cpp", + "tts_windows.cpp", "windows_terminal_logger.cpp", "vulkan_context_win.cpp", "gl_manager_windows.cpp", diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index 3b2c6fe9f6..6ce10e6f0f 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/string/print_string.h" #include "core/version.h" #include "main/main.h" @@ -129,13 +130,28 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { return EXCEPTION_CONTINUE_SEARCH; } - fprintf(stderr, "\n================================================================\n"); - fprintf(stderr, "%s: Program crashed\n", __FUNCTION__); + String msg; + const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); + if (proj_settings) { + msg = proj_settings->get("debug/settings/crash_handler/message"); + } + // Tell MainLoop about the crash. This can be handled by users too in Node. if (OS::get_singleton()->get_main_loop()) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } + print_error("\n================================================================"); + print_error(vformat("%s: Program crashed", __FUNCTION__)); + + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).is_empty()) { + print_error(vformat("Engine version: %s", VERSION_FULL_NAME)); + } else { + print_error(vformat("Engine version: %s (%s)", VERSION_FULL_NAME, VERSION_HASH)); + } + print_error(vformat("Dumping the backtrace. %s", msg)); + // Load the symbols: if (!SymInitialize(process, nullptr, false)) { return EXCEPTION_CONTINUE_SEARCH; @@ -174,20 +190,6 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { IMAGE_NT_HEADERS *h = ImageNtHeader(base); DWORD image_type = h->FileHeader.Machine; - String msg; - const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); - if (proj_settings) { - msg = proj_settings->get("debug/settings/crash_handler/message"); - } - - // Print the engine version just before, so that people are reminded to include the version in backtrace reports. - if (String(VERSION_HASH).is_empty()) { - fprintf(stderr, "Engine version: %s\n", VERSION_FULL_NAME); - } else { - fprintf(stderr, "Engine version: %s (%s)\n", VERSION_FULL_NAME, VERSION_HASH); - } - fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); - int n = 0; do { if (skip_first) { @@ -197,12 +199,12 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { std::string fnName = symbol(process, frame.AddrPC.Offset).undecorated_name(); if (SymGetLineFromAddr64(process, frame.AddrPC.Offset, &offset_from_symbol, &line)) { - fprintf(stderr, "[%d] %s (%s:%d)\n", n, fnName.c_str(), line.FileName, line.LineNumber); + print_error(vformat("[%d] %s (%s:%d)", n, fnName.c_str(), (char *)line.FileName, (int)line.LineNumber)); } else { - fprintf(stderr, "[%d] %s\n", n, fnName.c_str()); + print_error(vformat("[%d] %s", n, fnName.c_str())); } } else { - fprintf(stderr, "[%d] ???\n", n); + print_error(vformat("[%d] ???", n)); } n++; @@ -213,8 +215,8 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { } } while (frame.AddrReturn.Offset != 0 && n < 256); - fprintf(stderr, "-- END OF BACKTRACE --\n"); - fprintf(stderr, "================================================================\n"); + print_error("-- END OF BACKTRACE --"); + print_error("================================================================"); SymCleanup(process); diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 249a0d2e79..b82fe5e7ad 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -252,6 +252,7 @@ def configure_msvc(env, manual_msvc_config): "kernel32", "ole32", "oleaut32", + "sapi", "user32", "gdi32", "IPHLPAPI", @@ -268,12 +269,14 @@ def configure_msvc(env, manual_msvc_config): "dwmapi", ] - env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED"]) - if not env["use_volk"]: - LIBS += ["vulkan"] + if env["vulkan"]: + env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED"]) + if not env["use_volk"]: + LIBS += ["vulkan"] - env.AppendUnique(CPPDEFINES=["GLES3_ENABLED"]) - LIBS += ["opengl32"] + if env["opengl3"]: + env.AppendUnique(CPPDEFINES=["GLES3_ENABLED"]) + LIBS += ["opengl32"] env.Append(LINKFLAGS=[p + env["LIBSUFFIX"] for p in LIBS]) @@ -426,6 +429,7 @@ def configure_mingw(env): "ws2_32", "kernel32", "oleaut32", + "sapi", "dinput8", "dxguid", "ksuser", diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 163f5c350b..998b0882b3 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -84,6 +84,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_NATIVE_ICON: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: + case FEATURE_TEXT_TO_SPEECH: return true; default: return false; @@ -133,6 +134,41 @@ void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { } } +bool DisplayServerWindows::tts_is_speaking() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_speaking(); +} + +bool DisplayServerWindows::tts_is_paused() const { + ERR_FAIL_COND_V(!tts, false); + return tts->is_paused(); +} + +Array DisplayServerWindows::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, Array()); + return tts->get_voices(); +} + +void DisplayServerWindows::tts_speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!tts); + tts->speak(p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_interrupt); +} + +void DisplayServerWindows::tts_pause() { + ERR_FAIL_COND(!tts); + tts->pause(); +} + +void DisplayServerWindows::tts_resume() { + ERR_FAIL_COND(!tts); + tts->resume(); +} + +void DisplayServerWindows::tts_stop() { + ERR_FAIL_COND(!tts); + tts->stop(); +} + void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ @@ -150,7 +186,7 @@ DisplayServer::MouseMode DisplayServerWindows::mouse_get_mode() const { return mouse_mode; } -void DisplayServerWindows::mouse_warp_to_position(const Point2i &p_to) { +void DisplayServerWindows::warp_mouse(const Point2i &p_position) { _THREAD_SAFE_METHOD_ if (!windows.has(last_focused_window)) { @@ -158,12 +194,12 @@ void DisplayServerWindows::mouse_warp_to_position(const Point2i &p_to) { } if (mouse_mode == MOUSE_MODE_CAPTURED) { - old_x = p_to.x; - old_y = p_to.y; + old_x = p_position.x; + old_y = p_position.y; } else { POINT p; - p.x = p_to.x; - p.y = p_to.y; + p.x = p_position.x; + p.y = p_position.y; ClientToScreen(windows[last_focused_window].hWnd, &p); SetCursorPos(p.x, p.y); @@ -430,9 +466,8 @@ static int QueryDpiForMonitor(HMONITOR hmon, _MonitorDpiType dpiType = MDT_Defau } UINT x = 0, y = 0; - HRESULT hr = E_FAIL; if (hmon && (Shcore != (HMODULE)INVALID_HANDLE_VALUE)) { - hr = getDPIForMonitor(hmon, dpiType /*MDT_Effective_DPI*/, &x, &y); + HRESULT hr = getDPIForMonitor(hmon, dpiType /*MDT_Effective_DPI*/, &x, &y); if (SUCCEEDED(hr) && (x > 0) && (y > 0)) { dpiX = (int)x; dpiY = (int)y; @@ -572,8 +607,11 @@ void DisplayServerWindows::show_window(WindowID p_id) { _update_window_style(p_id); } - ShowWindow(wd.hWnd, (wd.no_focus || wd.is_popup) ? SW_SHOWNOACTIVATE : SW_SHOW); // Show the window. - if (!wd.no_focus && !wd.is_popup) { + if (wd.no_focus || wd.is_popup) { + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow + ShowWindow(wd.hWnd, SW_SHOWNA); + } else { + ShowWindow(wd.hWnd, SW_SHOW); SetForegroundWindow(wd.hWnd); // Slightly higher priority. SetFocus(wd.hWnd); // Set keyboard focus. } @@ -590,7 +628,7 @@ void DisplayServerWindows::delete_sub_window(WindowID p_window) { WindowData &wd = windows[p_window]; while (wd.transient_children.size()) { - window_set_transient(wd.transient_children.front()->get(), INVALID_WINDOW_ID); + window_set_transient(*wd.transient_children.begin(), INVALID_WINDOW_ID); } if (wd.transient_parent != INVALID_WINDOW_ID) { @@ -844,8 +882,8 @@ void DisplayServerWindows::window_set_exclusive(WindowID p_window, bool p_exclus if (wd.exclusive != p_exclusive) { wd.exclusive = p_exclusive; if (wd.transient_parent != INVALID_WINDOW_ID) { - WindowData &wd_parent = windows[wd.transient_parent]; if (wd.exclusive) { + WindowData &wd_parent = windows[wd.transient_parent]; SetWindowLongPtr(wd.hWnd, GWLP_HWNDPARENT, (LONG_PTR)wd_parent.hWnd); } else { SetWindowLongPtr(wd.hWnd, GWLP_HWNDPARENT, (LONG_PTR) nullptr); @@ -1071,6 +1109,10 @@ void DisplayServerWindows::_update_window_style(WindowID p_window, bool p_repain SetWindowLongPtr(wd.hWnd, GWL_STYLE, style); SetWindowLongPtr(wd.hWnd, GWL_EXSTYLE, style_ex); + if (icon.is_valid()) { + set_icon(icon); + } + SetWindowPos(wd.hWnd, wd.always_on_top ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | ((wd.no_focus || wd.is_popup) ? SWP_NOACTIVATE : 0)); if (p_repaint) { @@ -1238,7 +1280,7 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W } break; case WINDOW_FLAG_POPUP: { ERR_FAIL_COND_MSG(p_window == MAIN_WINDOW_ID, "Main window can't be popup."); - ERR_FAIL_COND_MSG(IsWindowVisible(wd.hWnd) && (wd.is_popup != p_enabled), "Pupup flag can't changed while window is opened."); + ERR_FAIL_COND_MSG(IsWindowVisible(wd.hWnd) && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened."); wd.is_popup = p_enabled; } break; case WINDOW_FLAG_MAX: @@ -1281,7 +1323,7 @@ void DisplayServerWindows::window_request_attention(WindowID p_window) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - WindowData &wd = windows[p_window]; + const WindowData &wd = windows[p_window]; FLASHWINFO info; info.cbSize = sizeof(FLASHWINFO); @@ -1455,11 +1497,11 @@ void DisplayServerWindows::GetMaskBitmaps(HBITMAP hSourceBitmap, COLORREF clrTra DeleteDC(hMainDC); } -void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { +void DisplayServerWindows::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { _THREAD_SAFE_METHOD_ if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); + RBMap<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); if (cursor_c) { if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { @@ -1562,13 +1604,8 @@ void DisplayServerWindows::cursor_set_custom_image(const RES &p_cursor, CursorSh } } - if (hAndMask != nullptr) { - DeleteObject(hAndMask); - } - - if (hXorMask != nullptr) { - DeleteObject(hXorMask); - } + DeleteObject(hAndMask); + DeleteObject(hXorMask); memfree(buffer); DeleteObject(bitmap); @@ -1764,15 +1801,17 @@ void DisplayServerWindows::make_rendering_thread() { void DisplayServerWindows::swap_buffers() { #if defined(GLES3_ENABLED) - gl_manager->swap_buffers(); + if (gl_manager) { + gl_manager->swap_buffers(); + } #endif } void DisplayServerWindows::set_native_icon(const String &p_filename) { _THREAD_SAFE_METHOD_ - FileAccess *f = FileAccess::open(p_filename, FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Cannot open file with icon '" + p_filename + "'."); + Ref<FileAccess> f = FileAccess::open(p_filename, FileAccess::READ); + ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file with icon '" + p_filename + "'."); ICONDIR *icon_dir = (ICONDIR *)memalloc(sizeof(ICONDIR)); int pos = 0; @@ -1858,7 +1897,6 @@ void DisplayServerWindows::set_native_icon(const String &p_filename) { err = GetLastError(); ERR_FAIL_COND_MSG(err, "Error setting ICON_BIG: " + format_error_message(err) + "."); - memdelete(f); memdelete(icon_dir); } @@ -1866,9 +1904,11 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!p_icon.is_valid()); - Ref<Image> icon = p_icon->duplicate(); - if (icon->get_format() != Image::FORMAT_RGBA8) { - icon->convert(Image::FORMAT_RGBA8); + if (icon != p_icon) { + icon = p_icon->duplicate(); + if (icon->get_format() != Image::FORMAT_RGBA8) { + icon->convert(Image::FORMAT_RGBA8); + } } int w = icon->get_width(); int h = icon->get_height(); @@ -1917,16 +1957,18 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { void DisplayServerWindows::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) { _THREAD_SAFE_METHOD_ #if defined(VULKAN_ENABLED) - // TODO disabling for now - //context_vulkan->set_vsync_mode(p_window, p_vsync_mode); + if (context_vulkan) { + context_vulkan->set_vsync_mode(p_window, p_vsync_mode); + } #endif } DisplayServer::VSyncMode DisplayServerWindows::window_get_vsync_mode(WindowID p_window) const { _THREAD_SAFE_METHOD_ #if defined(VULKAN_ENABLED) - //TODO disabling for now - //return context_vulkan->get_vsync_mode(p_window); + if (context_vulkan) { + return context_vulkan->get_vsync_mode(p_window); + } #endif return DisplayServer::VSYNC_ENABLED; } @@ -1964,7 +2006,7 @@ void DisplayServerWindows::_touch_event(WindowID p_window, bool p_pressed, float } void DisplayServerWindows::_drag_event(WindowID p_window, float p_x, float p_y, int idx) { - Map<int, Vector2>::Element *curr = touch_state.find(idx); + RBMap<int, Vector2>::Element *curr = touch_state.find(idx); if (!curr) { return; } @@ -1996,7 +2038,7 @@ void DisplayServerWindows::_send_window_event(const WindowData &wd, WindowEvent } void DisplayServerWindows::_dispatch_input_events(const Ref<InputEvent> &p_event) { - ((DisplayServerWindows *)(get_singleton()))->_dispatch_input_event(p_event); + static_cast<DisplayServerWindows *>(get_singleton())->_dispatch_input_event(p_event); } void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) { @@ -2012,7 +2054,7 @@ void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) Callable::CallError ce; { - List<WindowID>::Element *E = popup_list.front(); + List<WindowID>::Element *E = popup_list.back(); if (E && Object::cast_to<InputEventKey>(*p_event)) { // Redirect keyboard input to active popup. if (windows.has(E->get())) { @@ -2083,20 +2125,24 @@ Rect2i DisplayServerWindows::window_get_popup_safe_rect(WindowID p_window) const } void DisplayServerWindows::popup_open(WindowID p_window) { - WindowData &wd = windows[p_window]; + _THREAD_SAFE_METHOD_ + + const WindowData &wd = windows[p_window]; if (wd.is_popup) { - // Close all popups, up to current popup parent, or every popup if new window is not transient. + // Find current popup parent, or root popup if new window is not transient. + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); while (E) { if (wd.transient_parent != E->get() || wd.transient_parent == INVALID_WINDOW_ID) { - _send_window_event(windows[E->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } else { break; } } + if (C) { + _send_window_event(windows[C->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + } time_since_popup = OS::get_singleton()->get_ticks_msec(); popup_list.push_back(p_window); @@ -2104,17 +2150,25 @@ void DisplayServerWindows::popup_open(WindowID p_window) { } void DisplayServerWindows::popup_close(WindowID p_window) { + _THREAD_SAFE_METHOD_ + List<WindowID>::Element *E = popup_list.find(p_window); while (E) { - _send_window_event(windows[E->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); List<WindowID>::Element *F = E->next(); + WindowID win_id = E->get(); popup_list.erase(E); + + if (win_id != p_window) { + // Only request close on related windows, not this window. We are already processing it. + _send_window_event(windows[win_id], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + } E = F; } } LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) { _THREAD_SAFE_METHOD_ + uint64_t delta = OS::get_singleton()->get_ticks_msec() - time_since_popup; if (delta > 250) { switch (wParam) { @@ -2126,7 +2180,9 @@ LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) case WM_MBUTTONDOWN: { MOUSEHOOKSTRUCT *ms = (MOUSEHOOKSTRUCT *)lParam; Point2i pos = Point2i(ms->pt.x, ms->pt.y); + List<WindowID>::Element *C = nullptr; List<WindowID>::Element *E = popup_list.back(); + // Find top popup to close. while (E) { // Popup window area. Rect2i win_rect = Rect2i(window_get_position(E->get()), window_get_size(E->get())); @@ -2137,21 +2193,53 @@ LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) } else if (safe_rect != Rect2i() && safe_rect.has_point(pos)) { break; } else { - _send_window_event(windows[E->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); - List<WindowID>::Element *F = E->prev(); - popup_list.erase(E); - E = F; + C = E; + E = E->prev(); } } - + if (C) { + _send_window_event(windows[C->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + return 1; + } } break; } } return ::CallNextHookEx(mouse_monitor, code, wParam, lParam); } -// Our default window procedure to handle processing of window-related system messages/events. -// Also known as DefProc or DefWindowProc. +// Handle a single window message received while CreateWindowEx is still on the stack and our data +// structures are not fully initialized. +LRESULT DisplayServerWindows::_handle_early_window_message(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + switch (uMsg) { + case WM_GETMINMAXINFO: { + // We receive this during CreateWindowEx and we haven't initialized the window + // struct, so let Windows figure out the maximized size. + // Silently forward to user/default. + } break; + case WM_NCCREATE: { + // We tunnel an unowned pointer to our window context (WindowData) through the + // first possible message (WM_NCCREATE) to fix up our window context collection. + CREATESTRUCTW *pCreate = (CREATESTRUCTW *)lParam; + WindowData *pWindowData = reinterpret_cast<WindowData *>(pCreate->lpCreateParams); + + // Fix this up so we can recognize the remaining messages. + pWindowData->hWnd = hWnd; + } break; + default: { + // Additional messages during window creation should happen after we fixed + // up the data structures on WM_NCCREATE, but this might change in the future, + // so report an error here and then we can implement them. + ERR_PRINT_ONCE(vformat("Unexpected window message 0x%x received for window we cannot recognize in our collection; sequence error.", uMsg)); + } break; + } + + if (user_proc) { + return CallWindowProcW(user_proc, hWnd, uMsg, wParam, lParam); + } + return DefWindowProcW(hWnd, uMsg, wParam, lParam); +} + +// The window procedure for our window class "Engine", used to handle processing of window-related system messages/events. // See: https://docs.microsoft.com/en-us/windows/win32/winmsg/window-procedures LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (drop_events) { @@ -2165,7 +2253,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA WindowID window_id = INVALID_WINDOW_ID; bool window_created = false; - // Check whether window exists. + // Check whether window exists + // FIXME this is O(n), where n is the set of currently open windows and subwindows + // we should have a secondary map from HWND to WindowID or even WindowData* alias, if we want to eliminate all the map lookups below for (const KeyValue<WindowID, WindowData> &E : windows) { if (E.value.hWnd == hWnd) { window_id = E.key; @@ -2174,10 +2264,12 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } - // Window doesn't exist or creation in progress, don't handle messages yet. + // WARNING: we get called with events before the window is registered in our collection + // specifically, even the call to CreateWindowEx already calls here while still on the stack, + // so there is no way to store the window handle in our collection before we get here if (!window_created) { - window_id = window_id_counter; - ERR_FAIL_COND_V(!windows.has(window_id), 0); + // don't let code below operate on incompletely initialized window objects or missing window_id + return _handle_early_window_message(hWnd, uMsg, wParam, lParam); } // Process window messages. @@ -2852,7 +2944,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } else { - // For reasons unknown to mankind, wheel comes in screen coordinates. + // For reasons unknown to humanity, wheel comes in screen coordinates. POINT coords; coords.x = mb->get_position().x; coords.y = mb->get_position().y; @@ -3345,11 +3437,17 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, WindowRect.top, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top, - nullptr, nullptr, hInstance, nullptr); + nullptr, + nullptr, + hInstance, + // tunnel the WindowData we need to handle creation message + // lifetime is ensured because we are still on the stack when this is + // processed in the window proc + reinterpret_cast<void *>(&wd)); if (!wd.hWnd) { MessageBoxW(nullptr, L"Window Creation Error.", L"ERROR", MB_OK | MB_ICONEXCLAMATION); windows.erase(id); - return INVALID_WINDOW_ID; + ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Failed to create Windows OS window."); } if (p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { wd.pre_fs_valid = true; @@ -3369,7 +3467,14 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, #ifdef GLES3_ENABLED if (gl_manager) { Error err = gl_manager->window_create(id, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top); - ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, "Failed to create an OpenGL window."); + + // shut down OpenGL, to mirror behavior of Vulkan code + if (err != OK) { + memdelete(gl_manager); + gl_manager = nullptr; + windows.erase(id); + ERR_FAIL_V_MSG(INVALID_WINDOW_ID, "Failed to create an OpenGL window."); + } } #endif @@ -3414,6 +3519,8 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, ImmReleaseContext(wd.hWnd, wd.im_himc); wd.im_position = Vector2(); + + // FIXME this is wrong in cases where the window coordinates were changed due to full screen mode; use WindowRect wd.last_pos = p_rect.position; wd.width = p_rect.size.width; wd.height = p_rect.size.height; @@ -3486,7 +3593,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win shift_mem = false; control_mem = false; meta_mem = false; - hInstance = ((OS_Windows *)OS::get_singleton())->get_hinstance(); + hInstance = static_cast<OS_Windows *>(OS::get_singleton())->get_hinstance(); pressrc = 0; old_invalid = true; @@ -3496,6 +3603,9 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win rendering_driver = p_rendering_driver; + // Init TTS + tts = memnew(TTS_Windows); + // Note: Wacom WinTab driver API for pen input, for devices incompatible with Windows Ink. HMODULE wintab_lib = LoadLibraryW(L"wintab32.dll"); if (wintab_lib) { @@ -3598,12 +3708,11 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win return; } - //gl_manager->set_use_vsync(current_videomode.use_vsync); RasterizerGLES3::make_current(); } #endif - HHOOK mouse_monitor = SetWindowsHookEx(WH_MOUSE, ::MouseProc, nullptr, GetCurrentThreadId()); + mouse_monitor = SetWindowsHookEx(WH_MOUSE, ::MouseProc, nullptr, GetCurrentThreadId()); Point2i window_position( (screen_get_size(0).width - p_resolution.width) / 2, @@ -3630,7 +3739,11 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } #endif - if (!OS::get_singleton()->is_in_low_processor_usage_mode()) { + if (!Engine::get_singleton()->is_editor_hint() && !OS::get_singleton()->is_in_low_processor_usage_mode()) { + // Increase priority for projects that are not in low-processor mode (typically games) + // to reduce the risk of frame stuttering. + // This is not done for the editor to prevent importers or resource bakers + // from making the system unresponsive. SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS); DWORD index = 0; HANDLE handle = AvSetMmThreadCharacteristics("Games", &index); @@ -3651,7 +3764,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win r_error = OK; - ((OS_Windows *)OS::get_singleton())->set_main_window(windows[MAIN_WINDOW_ID].hWnd); + static_cast<OS_Windows *>(OS::get_singleton())->set_main_window(windows[MAIN_WINDOW_ID].hWnd); Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); } @@ -3698,6 +3811,7 @@ DisplayServerWindows::~DisplayServerWindows() { #ifdef GLES3_ENABLED // destroy windows .. NYI? + // FIXME wglDeleteContext is never called #endif if (windows.has(MAIN_WINDOW_ID)) { @@ -3735,4 +3849,8 @@ DisplayServerWindows::~DisplayServerWindows() { gl_manager = nullptr; } #endif + if (tts) { + memdelete(tts); + } + CoUninitialize(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index a56a2b83ac..fc89517774 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -46,6 +46,7 @@ #include "servers/rendering/renderer_compositor.h" #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" #include "servers/rendering_server.h" +#include "tts_windows.h" #ifdef XAUDIO2_ENABLED #include "drivers/xaudio2/audio_driver_xaudio2.h" @@ -313,13 +314,15 @@ class DisplayServerWindows : public DisplayServer { RenderingDeviceVulkan *rendering_device_vulkan = nullptr; #endif - Map<int, Vector2> touch_state; + RBMap<int, Vector2> touch_state; int pressrc; HINSTANCE hInstance; // Holds The Instance Of The Application String rendering_driver; bool app_focused = false; + TTS_Windows *tts = nullptr; + struct WindowData { HWND hWnd; //layered window @@ -386,20 +389,21 @@ class DisplayServerWindows : public DisplayServer { Callable drop_files_callback; WindowID transient_parent = INVALID_WINDOW_ID; - Set<WindowID> transient_children; + HashSet<WindowID> transient_children; bool is_popup = false; Rect2i parent_safe_rect; }; - JoypadWindows *joypad; + JoypadWindows *joypad = nullptr; HHOOK mouse_monitor = nullptr; List<WindowID> popup_list; uint64_t time_since_popup = 0; + Ref<Image> icon; WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect); WindowID window_id_counter = MAIN_WINDOW_ID; - Map<WindowID, WindowData> windows; + RBMap<WindowID, WindowData> windows; WindowID last_focused_window = INVALID_WINDOW_ID; @@ -426,7 +430,7 @@ class DisplayServerWindows : public DisplayServer { HCURSOR cursors[CURSOR_MAX] = { nullptr }; CursorShape cursor_shape = CursorShape::CURSOR_ARROW; - Map<CursorShape, Vector<Variant>> cursors_cache; + RBMap<CursorShape, Vector<Variant>> cursors_cache; void _drag_event(WindowID p_window, float p_x, float p_y, int idx); void _touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx); @@ -444,6 +448,8 @@ class DisplayServerWindows : public DisplayServer { static void _dispatch_input_events(const Ref<InputEvent> &p_event); void _dispatch_input_event(const Ref<InputEvent> &p_event); + LRESULT _handle_early_window_message(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + public: LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT MouseProc(int code, WPARAM wParam, LPARAM lParam); @@ -454,10 +460,19 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; + virtual bool tts_is_speaking() const override; + virtual bool tts_is_paused() const override; + virtual Array tts_get_voices() const override; + + virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; + virtual void tts_pause() override; + virtual void tts_resume() override; + virtual void tts_stop() override; + virtual void mouse_set_mode(MouseMode p_mode) override; virtual MouseMode mouse_get_mode() const override; - virtual void mouse_warp_to_position(const Point2i &p_to) override; + virtual void warp_mouse(const Point2i &p_position) override; virtual Point2i mouse_get_position() const override; virtual MouseButton mouse_get_button_state() const override; @@ -549,7 +564,7 @@ public: virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; virtual bool get_swap_cancel_ok() override; diff --git a/platform/windows/export/export.cpp b/platform/windows/export/export.cpp index 17a24c08bf..0fa2913218 100644 --- a/platform/windows/export/export.cpp +++ b/platform/windows/export/export.cpp @@ -32,8 +32,6 @@ #include "export_plugin.h" -static Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size); - void register_windows_exporter() { EDITOR_DEF("export/windows/rcedit", ""); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/windows/rcedit", PROPERTY_HINT_GLOBAL_FILE, "*.exe")); @@ -57,84 +55,7 @@ void register_windows_exporter() { logo->create_from_image(img); platform->set_logo(logo); platform->set_name("Windows Desktop"); - platform->set_extension("exe"); - platform->set_release_32("windows_32_release.exe"); - platform->set_debug_32("windows_32_debug.exe"); - platform->set_release_64("windows_64_release.exe"); - platform->set_debug_64("windows_64_debug.exe"); platform->set_os_name("Windows"); - platform->set_fixup_embedded_pck_func(&fixup_embedded_pck); EditorExport::get_singleton()->add_export_platform(platform); } - -static Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { - // Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data - - FileAccess *f = FileAccess::open(p_path, FileAccess::READ_WRITE); - if (!f) { - return ERR_CANT_OPEN; - } - - // Jump to the PE header and check the magic number - { - f->seek(0x3c); - uint32_t pe_pos = f->get_32(); - - f->seek(pe_pos); - uint32_t magic = f->get_32(); - if (magic != 0x00004550) { - f->close(); - return ERR_FILE_CORRUPT; - } - } - - // Process header - - int num_sections; - { - int64_t header_pos = f->get_position(); - - f->seek(header_pos + 2); - num_sections = f->get_16(); - f->seek(header_pos + 16); - uint16_t opt_header_size = f->get_16(); - - // Skip rest of header + optional header to go to the section headers - f->seek(f->get_position() + 2 + opt_header_size); - } - - // Search for the "pck" section - - int64_t section_table_pos = f->get_position(); - - bool found = false; - for (int i = 0; i < num_sections; ++i) { - int64_t section_header_pos = section_table_pos + i * 40; - f->seek(section_header_pos); - - uint8_t section_name[9]; - f->get_buffer(section_name, 8); - section_name[8] = '\0'; - - if (strcmp((char *)section_name, "pck") == 0) { - // "pck" section found, let's patch! - - // Set virtual size to a little to avoid it taking memory (zero would give issues) - f->seek(section_header_pos + 8); - f->store_32(8); - - f->seek(section_header_pos + 16); - f->store_32(p_embedded_size); - f->seek(section_header_pos + 20); - f->store_32(p_embedded_start); - - found = true; - break; - } - } - - f->close(); - - return found ? OK : ERR_FILE_CORRUPT; -} diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index 5ebc930735..16c67345e0 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -41,23 +41,79 @@ Error EditorExportPlatformWindows::sign_shared_object(const Ref<EditorExportPres } } +Error EditorExportPlatformWindows::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path)); + return ERR_CANT_CREATE; + } + + f->store_line("@echo off"); + f->store_line("title \"" + p_app_name + "\""); + f->store_line("\"%~dp0" + p_pkg_name + "\" \"%*\""); + f->store_line("pause > nul"); + + return OK; +} + +Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { + if (p_preset->get("application/modify_resources")) { + _rcedit_add_data(p_preset, p_path); + } + return OK; +} + Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { - Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, p_path, p_flags); + String pck_path = p_path; + if (p_preset->get("binary_format/embed_pck")) { + pck_path = p_path.get_basename() + ".tmp"; + } + Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, pck_path, p_flags); + if (p_preset->get("codesign/enable") && err == OK) { + _code_sign(p_preset, pck_path); + } - if (err != OK) { - return err; + if (p_preset->get("binary_format/embed_pck") && err == OK) { + Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); + err = tmp_dir->rename(pck_path, p_path); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to rename temporary file \"%s\"."), pck_path)); + } } - _rcedit_add_data(p_preset, p_path); + String app_name; + if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { + app_name = String(ProjectSettings::get_singleton()->get("application/config/name")); + } else { + app_name = "Unnamed"; + } + app_name = OS::get_singleton()->get_safe_dir_name(app_name); - if (p_preset->get("codesign/enable") && err == OK) { - err = _code_sign(p_preset, p_path); + // Save console script. + if (err == OK) { + int con_scr = p_preset->get("debug/export_console_script"); + if ((con_scr == 1 && p_debug) || (con_scr == 2)) { + String scr_path = p_path.get_basename() + ".cmd"; + if (_export_debug_script(p_preset, app_name, p_path.get_file(), scr_path) != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), TTR("Could not create console script.")); + } + } } return err; } -bool EditorExportPlatformWindows::get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const { +String EditorExportPlatformWindows::get_template_file_name(const String &p_target, const String &p_arch) const { + return "windows_" + p_arch + "_" + p_target + ".exe"; +} + +List<String> EditorExportPlatformWindows::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { + List<String> list; + list.push_back("exe"); + return list; +} + +bool EditorExportPlatformWindows::get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const { // This option is not supported by "osslsigncode", used on non-Windows host. if (!OS::get_singleton()->has_feature("windows") && p_option == "codesign/identity_type") { return false; @@ -78,6 +134,7 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/description"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/modify_resources"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.ico"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/file_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/product_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "")); @@ -88,17 +145,16 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/trademarks"), "")); } -void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) { +Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) { String rcedit_path = EditorSettings::get_singleton()->get("export/windows/rcedit"); - if (rcedit_path.is_empty()) { - WARN_PRINT("The rcedit tool is not configured in the Editor Settings (Export > Windows > Rcedit). No custom icon or app information data will be embedded in the exported executable."); - return; + if (rcedit_path != String() && !FileAccess::exists(rcedit_path)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Could not find rcedit executable at \"%s\"."), rcedit_path)); + return ERR_FILE_NOT_FOUND; } - if (!FileAccess::exists(rcedit_path)) { - ERR_PRINT("Could not find rcedit executable at " + rcedit_path + ", no icon or app information data will be included."); - return; + if (rcedit_path == String()) { + rcedit_path = "rcedit"; // try to run rcedit from PATH } #ifndef WINDOWS_ENABLED @@ -106,8 +162,8 @@ void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> String wine_path = EditorSettings::get_singleton()->get("export/windows/wine"); if (!wine_path.is_empty() && !FileAccess::exists(wine_path)) { - ERR_PRINT("Could not find wine executable at " + wine_path + ", no icon or app information data will be included."); - return; + add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("Could not find wine executable at \"%s\"."), wine_path)); + return ERR_FILE_NOT_FOUND; } if (wine_path.is_empty()) { @@ -165,13 +221,26 @@ void EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> args.push_back(trademarks); } -#ifdef WINDOWS_ENABLED - OS::get_singleton()->execute(rcedit_path, args); -#else +#ifndef WINDOWS_ENABLED // On non-Windows we need WINE to run rcedit args.push_front(rcedit_path); - OS::get_singleton()->execute(wine_path, args); + rcedit_path = wine_path; #endif + + String str; + Error err = OS::get_singleton()->execute(rcedit_path, args, &str, nullptr, true); + if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), TTR("Could not start rcedit executable, configure rcedit path in the Editor Settings (Export > Windows > Rcedit).")); + return err; + } + print_line("rcedit (" + p_path + "): " + str); + + if (str.find("Fatal error") != -1) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), vformat(TTR("rcedit failed to modify executable:\n%s"), str)); + return FAILED; + } + + return OK; } Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path) { @@ -180,7 +249,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p #ifdef WINDOWS_ENABLED String signtool_path = EditorSettings::get_singleton()->get("export/windows/signtool"); if (!signtool_path.is_empty() && !FileAccess::exists(signtool_path)) { - ERR_PRINT("Could not find signtool executable at " + signtool_path + ", aborting."); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find signtool executable at \"%s\"."), signtool_path)); return ERR_FILE_NOT_FOUND; } if (signtool_path.is_empty()) { @@ -189,7 +258,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p #else String signtool_path = EditorSettings::get_singleton()->get("export/windows/osslsigncode"); if (!signtool_path.is_empty() && !FileAccess::exists(signtool_path)) { - ERR_PRINT("Could not find osslsigncode executable at " + signtool_path + ", aborting."); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Could not find osslsigncode executable at \"%s\"."), signtool_path)); return ERR_FILE_NOT_FOUND; } if (signtool_path.is_empty()) { @@ -209,7 +278,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p args.push_back("/f"); args.push_back(p_preset->get("codesign/identity")); } else { - EditorNode::add_io_error("codesign: no identity found"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } } else if (id_type == 2) { //Windows certificate store @@ -217,11 +286,11 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p args.push_back("/sha1"); args.push_back(p_preset->get("codesign/identity")); } else { - EditorNode::add_io_error("codesign: no identity found"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } } else { - EditorNode::add_io_error("codesign: invalid identity type"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid identity type.")); return FAILED; } #else @@ -229,7 +298,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p args.push_back("-pkcs12"); args.push_back(p_preset->get("codesign/identity")); } else { - EditorNode::add_io_error("codesign: no identity found"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } #endif @@ -261,7 +330,7 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p args.push_back(p_preset->get("codesign/timestamp_server_url")); #endif } else { - EditorNode::add_io_error("codesign: invalid timestamp server"); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid timestamp server.")); return FAILED; } } @@ -308,7 +377,10 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p String str; Error err = OS::get_singleton()->execute(signtool_path, args, &str, nullptr, true); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start signtool executable, configure signtool path in the Editor Settings (Export > Windows > Signtool).")); + return err; + } print_line("codesign (" + p_path + "): " + str); #ifndef WINDOWS_ENABLED @@ -316,17 +388,24 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p #else if (str.find("Failed") != -1) { #endif + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Signtool failed to sign executable:\n%s"), str)); return FAILED; } #ifndef WINDOWS_ENABLED - DirAccessRef tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); + Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); err = tmp_dir->remove(p_path); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to remove temporary file \"%s\"."), p_path)); + return err; + } err = tmp_dir->rename(p_path + "_signed", p_path); - ERR_FAIL_COND_V(err != OK, err); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Failed to rename temporary file \"%s\"."), p_path + "_signed")); + return err; + } #endif return OK; @@ -337,7 +416,7 @@ bool EditorExportPlatformWindows::can_export(const Ref<EditorExportPreset> &p_pr bool valid = EditorExportPlatformPC::can_export(p_preset, err, r_missing_templates); String rcedit_path = EditorSettings::get_singleton()->get("export/windows/rcedit"); - if (rcedit_path.is_empty()) { + if (p_preset->get("application/modify_resources") && rcedit_path.is_empty()) { err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > Rcedit) to change the icon or app information data.") + "\n"; } @@ -374,3 +453,82 @@ bool EditorExportPlatformWindows::can_export(const Ref<EditorExportPreset> &p_pr return valid; } + +Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { + // Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data + + if (p_embedded_size + p_embedded_start >= 0x100000000) { // Check for total executable size + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Windows executables cannot be >= 4 GiB.")); + return ERR_INVALID_DATA; + } + + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE); + if (f.is_null()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to open executable file \"%s\"."), p_path)); + return ERR_CANT_OPEN; + } + + // Jump to the PE header and check the magic number + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable file header corrupted.")); + return ERR_FILE_CORRUPT; + } + } + + // Process header + + int num_sections; + { + int64_t header_pos = f->get_position(); + + f->seek(header_pos + 2); + num_sections = f->get_16(); + f->seek(header_pos + 16); + uint16_t opt_header_size = f->get_16(); + + // Skip rest of header + optional header to go to the section headers + f->seek(f->get_position() + 2 + opt_header_size); + } + + // Search for the "pck" section + + int64_t section_table_pos = f->get_position(); + + bool found = false; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * 40; + f->seek(section_header_pos); + + uint8_t section_name[9]; + f->get_buffer(section_name, 8); + section_name[8] = '\0'; + + if (strcmp((char *)section_name, "pck") == 0) { + // "pck" section found, let's patch! + + // Set virtual size to a little to avoid it taking memory (zero would give issues) + f->seek(section_header_pos + 8); + f->store_32(8); + + f->seek(section_header_pos + 16); + f->store_32(p_embedded_size); + f->seek(section_header_pos + 20); + f->store_32(p_embedded_start); + + found = true; + break; + } + } + + if (!found) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("Executable \"pck\" section not found.")); + return ERR_FILE_CORRUPT; + } + return OK; +} diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index 86e9d49b05..51f98365a9 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -38,15 +38,20 @@ #include "platform/windows/logo.gen.h" class EditorExportPlatformWindows : public EditorExportPlatformPC { - void _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path); + Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path); Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path); + Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); public: virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) override; virtual Error sign_shared_object(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) override; + virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const override; + virtual bool get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const override; virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; + virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; }; #endif diff --git a/platform/windows/gl_manager_windows.cpp b/platform/windows/gl_manager_windows.cpp index a97fa99d7f..d509ff8c51 100644 --- a/platform/windows/gl_manager_windows.cpp +++ b/platform/windows/gl_manager_windows.cpp @@ -54,6 +54,18 @@ typedef HGLRC(APIENTRY *PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC, HGLRC, const int *); +static String format_error_message(DWORD id) { + LPWSTR messageBuffer = nullptr; + size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr); + + String msg = "Error " + itos(id) + ": " + String::utf16((const char16_t *)messageBuffer, size); + + LocalFree(messageBuffer); + + return msg; +} + int GLManager_Windows::_find_or_create_display(GLWindow &win) { // find display NYI, only 1 supported so far if (_displays.size()) { @@ -79,7 +91,7 @@ int GLManager_Windows::_find_or_create_display(GLWindow &win) { return new_display_id; } -Error GLManager_Windows::_create_context(GLWindow &win, GLDisplay &gl_display) { +static Error _configure_pixel_format(HDC hDC) { static PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor 1, @@ -101,9 +113,6 @@ Error GLManager_Windows::_create_context(GLWindow &win, GLDisplay &gl_display) { 0, 0, 0 // Layer Masks Ignored }; - // alias - HDC hDC = win.hDC; - int pixel_format = ChoosePixelFormat(hDC, &pfd); if (!pixel_format) // Did Windows Find A Matching Pixel Format? { @@ -116,13 +125,24 @@ Error GLManager_Windows::_create_context(GLWindow &win, GLDisplay &gl_display) { return ERR_CANT_CREATE; // Return FALSE } - gl_display.hRC = wglCreateContext(hDC); + return OK; +} + +Error GLManager_Windows::_create_context(GLWindow &win, GLDisplay &gl_display) { + Error err = _configure_pixel_format(win.hDC); + if (err != OK) { + return err; + } + + gl_display.hRC = wglCreateContext(win.hDC); if (!gl_display.hRC) // Are We Able To Get A Rendering Context? { return ERR_CANT_CREATE; // Return FALSE } - wglMakeCurrent(hDC, gl_display.hRC); + if (!wglMakeCurrent(win.hDC, gl_display.hRC)) { + ERR_PRINT("Could not attach OpenGL context to newly created window: " + format_error_message(GetLastError())); + } int attribs[] = { WGL_CONTEXT_MAJOR_VERSION_ARB, 3, //we want a 3.3 context @@ -143,57 +163,61 @@ Error GLManager_Windows::_create_context(GLWindow &win, GLDisplay &gl_display) { return ERR_CANT_CREATE; } - HGLRC new_hRC = wglCreateContextAttribsARB(hDC, 0, attribs); + HGLRC new_hRC = wglCreateContextAttribsARB(win.hDC, 0, attribs); if (!new_hRC) { wglDeleteContext(gl_display.hRC); gl_display.hRC = 0; - return ERR_CANT_CREATE; // Return false + return ERR_CANT_CREATE; } - wglMakeCurrent(hDC, nullptr); + + if (!wglMakeCurrent(win.hDC, nullptr)) { + ERR_PRINT("Could not detach OpenGL context from newly created window: " + format_error_message(GetLastError())); + } + wglDeleteContext(gl_display.hRC); gl_display.hRC = new_hRC; - if (!wglMakeCurrent(hDC, gl_display.hRC)) // Try To Activate The Rendering Context + if (!wglMakeCurrent(win.hDC, gl_display.hRC)) // Try To Activate The Rendering Context { + ERR_PRINT("Could not attach OpenGL context to newly created window with replaced OpenGL context: " + format_error_message(GetLastError())); wglDeleteContext(gl_display.hRC); gl_display.hRC = 0; - return ERR_CANT_CREATE; // Return FALSE + return ERR_CANT_CREATE; } return OK; } Error GLManager_Windows::window_create(DisplayServer::WindowID p_window_id, HWND p_hwnd, HINSTANCE p_hinstance, int p_width, int p_height) { - HDC hdc = GetDC(p_hwnd); - if (!hdc) { - return ERR_CANT_CREATE; // Return FALSE + HDC hDC = GetDC(p_hwnd); + if (!hDC) { + return ERR_CANT_CREATE; } - // make sure vector is big enough... - // we can mirror the external vector, it is simpler - // to keep the IDs identical for fast lookup - if (p_window_id >= (int)_windows.size()) { - _windows.resize(p_window_id + 1); + // configure the HDC to use a compatible pixel format + Error result = _configure_pixel_format(hDC); + if (result != OK) { + return result; } - GLWindow &win = _windows[p_window_id]; - win.in_use = true; - win.window_id = p_window_id; + GLWindow win; win.width = p_width; win.height = p_height; win.hwnd = p_hwnd; - win.hDC = hdc; + win.hDC = hDC; win.gldisplay_id = _find_or_create_display(win); if (win.gldisplay_id == -1) { - // release DC? - _windows.remove_at(_windows.size() - 1); return FAILED; } + // WARNING: p_window_id is an eternally growing integer since popup windows keep coming and going + // and each of them has a higher id than the previous, so it must be used in a map not a vector + _windows[p_window_id] = win; + // make current - window_make_current(_windows.size() - 1); + window_make_current(p_window_id); return OK; } @@ -217,11 +241,10 @@ int GLManager_Windows::window_get_height(DisplayServer::WindowID p_window_id) { void GLManager_Windows::window_destroy(DisplayServer::WindowID p_window_id) { GLWindow &win = get_window(p_window_id); - win.in_use = false; - if (_current_window == &win) { _current_window = nullptr; } + _windows.erase(p_window_id); } void GLManager_Windows::release_current() { @@ -229,7 +252,9 @@ void GLManager_Windows::release_current() { return; } - wglMakeCurrent(_current_window->hDC, nullptr); + if (!wglMakeCurrent(_current_window->hDC, nullptr)) { + ERR_PRINT("Could not detach OpenGL context from window marked current: " + format_error_message(GetLastError())); + } } void GLManager_Windows::window_make_current(DisplayServer::WindowID p_window_id) { @@ -237,10 +262,8 @@ void GLManager_Windows::window_make_current(DisplayServer::WindowID p_window_id) return; } + // crash if our data structures are out of sync, i.e. not found GLWindow &win = _windows[p_window_id]; - if (!win.in_use) { - return; - } // noop if (&win == _current_window) { @@ -248,7 +271,9 @@ void GLManager_Windows::window_make_current(DisplayServer::WindowID p_window_id) } const GLDisplay &disp = get_display(win.gldisplay_id); - wglMakeCurrent(win.hDC, disp.hRC); + if (!wglMakeCurrent(win.hDC, disp.hRC)) { + ERR_PRINT("Could not switch OpenGL context to other window: " + format_error_message(GetLastError())); + } _internal_set_current_window(&win); } @@ -257,34 +282,19 @@ void GLManager_Windows::make_current() { if (!_current_window) { return; } - if (!_current_window->in_use) { - WARN_PRINT("current window not in use!"); - return; - } const GLDisplay &disp = get_current_display(); - wglMakeCurrent(_current_window->hDC, disp.hRC); + if (!wglMakeCurrent(_current_window->hDC, disp.hRC)) { + ERR_PRINT("Could not switch OpenGL context to window marked current: " + format_error_message(GetLastError())); + } } void GLManager_Windows::swap_buffers() { - // NO NEED TO CALL SWAP BUFFERS for each window... - // see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glXSwapBuffers.xml - - if (!_current_window) { - return; + // on other platforms, OpenGL swaps buffers for all windows (on all displays, really?) + // Windows swaps buffers on a per-window basis + // REVISIT: this could be structurally bad, should we have "dirty" flags then? + for (KeyValue<DisplayServer::WindowID, GLWindow> &entry : _windows) { + SwapBuffers(entry.value.hDC); } - if (!_current_window->in_use) { - WARN_PRINT("current window not in use!"); - return; - } - - // print_line("\tswap_buffers"); - - // only for debugging without drawing anything - // glClearColor(Math::randf(), 0, 1, 1); - //glClear(GL_COLOR_BUFFER_BIT); - - // const GLDisplay &disp = get_current_display(); - SwapBuffers(_current_window->hDC); } Error GLManager_Windows::initialize() { diff --git a/platform/windows/gl_manager_windows.h b/platform/windows/gl_manager_windows.h index 6423c54855..5e43a3de2a 100644 --- a/platform/windows/gl_manager_windows.h +++ b/platform/windows/gl_manager_windows.h @@ -52,19 +52,14 @@ public: private: // any data specific to the window struct GLWindow { - GLWindow() { in_use = false; } - bool in_use; - - // the external ID .. should match the GL window number .. unused I think - DisplayServer::WindowID window_id; - int width; - int height; + int width = 0; + int height = 0; // windows specific HDC hDC; HWND hwnd; - int gldisplay_id; + int gldisplay_id = 0; }; struct GLDisplay { @@ -72,10 +67,10 @@ private: HGLRC hRC; }; - LocalVector<GLWindow> _windows; + RBMap<DisplayServer::WindowID, GLWindow> _windows; LocalVector<GLDisplay> _displays; - GLWindow *_current_window; + GLWindow *_current_window = nullptr; PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT; PFNWGLGETSWAPINTERVALEXTPROC wglGetSwapIntervalEXT; diff --git a/platform/windows/godot.ico b/platform/windows/godot.ico Binary files differindex dd611e07da..25830ffdc6 100644 --- a/platform/windows/godot.ico +++ b/platform/windows/godot.ico diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index ad4e3ae77c..8de3ef294a 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -39,7 +39,18 @@ #ifndef TOOLS_ENABLED #if defined _MSC_VER #pragma section("pck", read) -__declspec(allocate("pck")) static const char dummy[8] = { 0 }; +__declspec(allocate("pck")) static char dummy[8] = { 0 }; + +// Dummy function to prevent LTO from discarding "pck" section. +extern "C" char *__cdecl pck_section_dummy_call() { + return &dummy[0]; +}; +#if defined _AMD64_ +#pragma comment(linker, "/include:pck_section_dummy_call") +#elif defined _X86_ +#pragma comment(linker, "/include:_pck_section_dummy_call") +#endif + #elif defined __GNUC__ static const char dummy[8] __attribute__((section("pck"), used)) = { 0 }; #endif @@ -140,11 +151,6 @@ int widechar_main(int argc, wchar_t **argv) { setlocale(LC_CTYPE, ""); -#ifndef TOOLS_ENABLED - // Workaround to prevent LTCG (MSVC LTO) from removing "pck" section - const char *dummy_guard = dummy; -#endif - char **argv_utf8 = new char *[argc]; for (int i = 0; i < argc; ++i) { diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 494e0b9105..d039fd13a7 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -250,7 +250,7 @@ void JoypadWindows::setup_joypad_object(const DIDEVICEOBJECTINSTANCE *ob, int p_ } BOOL CALLBACK JoypadWindows::enumCallback(const DIDEVICEINSTANCE *p_instance, void *p_context) { - JoypadWindows *self = (JoypadWindows *)p_context; + JoypadWindows *self = static_cast<JoypadWindows *>(p_context); if (self->is_xinput_device(&p_instance->guidProduct)) { return DIENUM_CONTINUE; } @@ -258,9 +258,9 @@ BOOL CALLBACK JoypadWindows::enumCallback(const DIDEVICEINSTANCE *p_instance, vo return DIENUM_CONTINUE; } -BOOL CALLBACK JoypadWindows::objectsCallback(const DIDEVICEOBJECTINSTANCE *instance, void *context) { - JoypadWindows *self = (JoypadWindows *)context; - self->setup_joypad_object(instance, self->id_to_change); +BOOL CALLBACK JoypadWindows::objectsCallback(const DIDEVICEOBJECTINSTANCE *p_instance, void *p_context) { + JoypadWindows *self = static_cast<JoypadWindows *>(p_context); + self->setup_joypad_object(p_instance, self->id_to_change); return DIENUM_CONTINUE; } @@ -404,7 +404,7 @@ void JoypadWindows::process_joypads() { // on mingw, these constants are not constants int count = 8; - LONG axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, (LONG)DIJOFS_SLIDER(0), (LONG)DIJOFS_SLIDER(1) }; + const LONG axes[] = { DIJOFS_X, DIJOFS_Y, DIJOFS_Z, DIJOFS_RX, DIJOFS_RY, DIJOFS_RZ, (LONG)DIJOFS_SLIDER(0), (LONG)DIJOFS_SLIDER(1) }; int values[] = { js.lX, js.lY, js.lZ, js.lRx, js.lRy, js.lRz, js.rglSlider[0], js.rglSlider[1] }; for (int j = 0; j < joy->joy_axis.size(); j++) { diff --git a/platform/windows/joypad_windows.h b/platform/windows/joypad_windows.h index 4f15bcf080..d239471a5c 100644 --- a/platform/windows/joypad_windows.h +++ b/platform/windows/joypad_windows.h @@ -105,10 +105,10 @@ private: typedef DWORD(WINAPI *XInputGetState_t)(DWORD dwUserIndex, XINPUT_STATE *pState); typedef DWORD(WINAPI *XInputSetState_t)(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration); - HWND *hWnd; + HWND *hWnd = nullptr; HANDLE xinput_dll; LPDIRECTINPUT8 dinput; - Input *input; + Input *input = nullptr; int id_to_change; int slider_count; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 13e3aa7883..6f414c094c 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -129,9 +129,34 @@ void OS_Windows::initialize_debugging() { SetConsoleCtrlHandler(HandlerRoutine, TRUE); } +#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED +static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { + String err_str; + if (p_errorexp && p_errorexp[0]) { + err_str = String::utf8(p_errorexp); + } else { + err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error); + } + + if (p_editor_notify) { + err_str += " (User)\n"; + } else { + err_str += "\n"; + } + + OutputDebugStringW((LPCWSTR)err_str.utf16().ptr()); +} +#endif + void OS_Windows::initialize() { crash_handler.initialize(); +#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED + error_handlers.errfunc = _error_handler; + error_handlers.userdata = this; + add_error_handler(&error_handlers); +#endif + #ifndef WINDOWS_SUBSYSTEM_CONSOLE RedirectIOToConsole(); #endif @@ -153,7 +178,7 @@ void OS_Windows::initialize() { // long as the windows scheduler resolution (~16-30ms) even for calls like Sleep(1) timeBeginPeriod(1); - process_map = memnew((Map<ProcessID, ProcessInfo>)); + process_map = memnew((HashMap<ProcessID, ProcessInfo>)); // Add current Godot PID to the list of known PIDs ProcessInfo current_pi = {}; @@ -194,6 +219,10 @@ void OS_Windows::finalize_core() { memdelete(process_map); NetSocketPosix::cleanup(); + +#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED + remove_error_handler(&error_handlers); +#endif } Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) { @@ -202,7 +231,7 @@ Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) { return OK; } -Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { +Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { String path = p_path.replace("/", "\\"); if (!FileAccess::exists(path)) { @@ -230,6 +259,10 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han remove_dll_directory(cookie); } + if (r_resolved_path != nullptr) { + *r_resolved_path = path; + } + return OK; } @@ -264,12 +297,19 @@ OS::Date OS_Windows::get_date(bool p_utc) const { GetLocalTime(&systemtime); } + //Get DST information from Windows, but only if p_utc is false. + TIME_ZONE_INFORMATION info; + bool daylight = false; + if (!p_utc && GetTimeZoneInformation(&info) == TIME_ZONE_ID_DAYLIGHT) { + daylight = true; + } + Date date; date.day = systemtime.wDay; date.month = Month(systemtime.wMonth); date.weekday = Weekday(systemtime.wDayOfWeek); date.year = systemtime.wYear; - date.dst = false; + date.dst = daylight; return date; } @@ -295,16 +335,19 @@ OS::TimeZoneInfo OS_Windows::get_time_zone_info() const { daylight = true; } + // Daylight Bias needs to be added to the bias if DST is in effect, or else it will not properly update. TimeZoneInfo ret; if (daylight) { ret.name = info.DaylightName; + ret.bias = info.Bias + info.DaylightBias; } else { ret.name = info.StandardName; + ret.bias = info.Bias + info.StandardBias; } // Bias value returned by GetTimeZoneInformation is inverted of what we expect // For example, on GMT-3 GetTimeZoneInformation return a Bias of 180, so invert the value to get -180 - ret.bias = -info.Bias; + ret.bias = -ret.bias; return ret; } @@ -373,6 +416,31 @@ String OS_Windows::_quote_command_line_argument(const String &p_text) const { return p_text; } +static void _append_to_pipe(char *p_bytes, int p_size, String *r_pipe, Mutex *p_pipe_mutex) { + // Try to convert from default ANSI code page to Unicode. + LocalVector<wchar_t> wchars; + int total_wchars = MultiByteToWideChar(CP_ACP, 0, p_bytes, p_size, nullptr, 0); + if (total_wchars > 0) { + wchars.resize(total_wchars); + if (MultiByteToWideChar(CP_ACP, 0, p_bytes, p_size, wchars.ptr(), total_wchars) == 0) { + wchars.clear(); + } + } + + if (p_pipe_mutex) { + p_pipe_mutex->lock(); + } + if (wchars.is_empty()) { + // Let's hope it's compatible with UTF-8. + (*r_pipe) += String::utf8(p_bytes, p_size); + } else { + (*r_pipe) += String(wchars.ptr(), total_wchars); + } + if (p_pipe_mutex) { + p_pipe_mutex->unlock(); + } +} + Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { String path = p_path.replace("/", "\\"); String command = _quote_command_line_argument(path); @@ -421,21 +489,44 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, if (r_pipe) { CloseHandle(pipe[1]); // Close pipe write handle (only child process is writing). - char buf[4096]; + + LocalVector<char> bytes; + int bytes_in_buffer = 0; + + const int CHUNK_SIZE = 4096; DWORD read = 0; for (;;) { // Read StdOut and StdErr from pipe. - bool success = ReadFile(pipe[0], buf, 4096, &read, NULL); + bytes.resize(bytes_in_buffer + CHUNK_SIZE); + const bool success = ReadFile(pipe[0], bytes.ptr() + bytes_in_buffer, CHUNK_SIZE, &read, NULL); if (!success || read == 0) { break; } - if (p_pipe_mutex) { - p_pipe_mutex->lock(); + + // Assume that all possible encodings are ASCII-compatible. + // Break at newline to allow receiving long output in portions. + int newline_index = -1; + for (int i = read - 1; i >= 0; i--) { + if (bytes[bytes_in_buffer + i] == '\n') { + newline_index = i; + break; + } } - (*r_pipe) += String::utf8(buf, read); - if (p_pipe_mutex) { - p_pipe_mutex->unlock(); + if (newline_index == -1) { + bytes_in_buffer += read; + continue; } + + const int bytes_to_convert = bytes_in_buffer + (newline_index + 1); + _append_to_pipe(bytes.ptr(), bytes_to_convert, r_pipe, p_pipe_mutex); + + bytes_in_buffer = read - (newline_index + 1); + memmove(bytes.ptr(), bytes.ptr() + bytes_to_convert, bytes_in_buffer); + } + + if (bytes_in_buffer > 0) { + _append_to_pipe(bytes.ptr(), bytes_in_buffer, r_pipe, p_pipe_mutex); } + CloseHandle(pipe[0]); // Close pipe read handle. } else { WaitForSingleObject(pi.pi.hProcess, INFINITE); @@ -503,6 +594,25 @@ int OS_Windows::get_process_id() const { return _getpid(); } +bool OS_Windows::is_process_running(const ProcessID &p_pid) const { + if (!process_map->has(p_pid)) { + return false; + } + + const PROCESS_INFORMATION &pi = (*process_map)[p_pid].pi; + + DWORD dw_exit_code = 0; + if (!GetExitCodeProcess(pi.hProcess, &dw_exit_code)) { + return false; + } + + if (dw_exit_code != STILL_ACTIVE) { + return false; + } + + return true; +} + Error OS_Windows::set_cwd(const String &p_cwd) { if (_wchdir((LPCWSTR)(p_cwd.utf16().get_data())) != 0) { return ERR_CANT_OPEN; @@ -676,6 +786,58 @@ MainLoop *OS_Windows::get_main_loop() const { return main_loop; } +uint64_t OS_Windows::get_embedded_pck_offset() const { + Ref<FileAccess> f = FileAccess::open(get_executable_path(), FileAccess::READ); + if (f.is_null()) { + return 0; + } + + // Process header. + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + return 0; + } + } + + int num_sections; + { + int64_t header_pos = f->get_position(); + + f->seek(header_pos + 2); + num_sections = f->get_16(); + f->seek(header_pos + 16); + uint16_t opt_header_size = f->get_16(); + + // Skip rest of header + optional header to go to the section headers. + f->seek(f->get_position() + 2 + opt_header_size); + } + int64_t section_table_pos = f->get_position(); + + // Search for the "pck" section. + int64_t off = 0; + for (int i = 0; i < num_sections; ++i) { + int64_t section_header_pos = section_table_pos + i * 40; + f->seek(section_header_pos); + + uint8_t section_name[9]; + f->get_buffer(section_name, 8); + section_name[8] = '\0'; + + if (strcmp((char *)section_name, "pck") == 0) { + f->seek(section_header_pos + 20); + off = f->get_32(); + break; + } + } + + return off; +} + String OS_Windows::get_config_path() const { // The XDG Base Directory specification technically only applies on Linux/*BSD, but it doesn't hurt to support it on Windows as well. if (has_environment("XDG_CONFIG_HOME")) { diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 5bfd24327e..dc702c66e1 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -57,17 +57,22 @@ #include <windows.h> #include <windowsx.h> +#ifdef DEBUG_ENABLED +// forward error messages to OutputDebugString +#define WINDOWS_DEBUG_OUTPUT_ENABLED +#endif + class JoypadWindows; class OS_Windows : public OS { #ifdef STDOUT_FILE - FILE *stdo; + FILE *stdo = nullptr; #endif uint64_t ticks_start; uint64_t ticks_per_second; HINSTANCE hInstance; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; #ifdef WASAPI_ENABLED AudioDriverWASAPI driver_wasapi; @@ -81,6 +86,10 @@ class OS_Windows : public OS { CrashHandler crash_handler; +#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED + ErrorHandlerList error_handlers; +#endif + bool force_quit; HWND main_window; @@ -101,14 +110,14 @@ protected: STARTUPINFO si; PROCESS_INFORMATION pi; }; - Map<ProcessID, ProcessInfo> *process_map; + HashMap<ProcessID, ProcessInfo> *process_map; public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; @@ -132,6 +141,7 @@ public: virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error kill(const ProcessID &p_pid) override; virtual int get_process_id() const override; + virtual bool is_process_running(const ProcessID &p_pid) const override; virtual bool has_environment(const String &p_var) const override; virtual String get_environment(const String &p_var) const override; @@ -144,6 +154,8 @@ public: virtual int get_processor_count() const override; virtual String get_processor_name() const override; + virtual uint64_t get_embedded_pck_offset() const override; + virtual String get_config_path() const override; virtual String get_data_path() const override; virtual String get_cache_path() const override; diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp new file mode 100644 index 0000000000..e5daf602e6 --- /dev/null +++ b/platform/windows/tts_windows.cpp @@ -0,0 +1,269 @@ +/*************************************************************************/ +/* tts_windows.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "tts_windows.h" + +TTS_Windows *TTS_Windows::singleton = nullptr; + +void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) { + TTS_Windows *tts = TTS_Windows::get_singleton(); + SPEVENT event; + while (tts->synth->GetEvents(1, &event, NULL) == S_OK) { + if (tts->ids.has(event.ulStreamNum)) { + if (event.eEventId == SPEI_START_INPUT_STREAM) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, tts->ids[event.ulStreamNum].id); + } else if (event.eEventId == SPEI_END_INPUT_STREAM) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[event.ulStreamNum].id); + tts->ids.erase(event.ulStreamNum); + tts->_update_tts(); + } else if (event.eEventId == SPEI_WORD_BOUNDARY) { + const Char16String &string = tts->ids[event.ulStreamNum].string; + int pos = 0; + for (int i = 0; i < MIN(event.lParam, string.length()); i++) { + char16_t c = string[i]; + if ((c & 0xfffffc00) == 0xd800) { + i++; + } + pos++; + } + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[event.ulStreamNum].id, pos - tts->ids[event.ulStreamNum].offset); + } + } + } +} + +void TTS_Windows::_update_tts() { + if (!is_speaking() && !paused && queue.size() > 0) { + DisplayServer::TTSUtterance &message = queue.front()->get(); + + String text; + DWORD flags = SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_XML; + String pitch_tag = String("<pitch absmiddle=\"") + String::num_int64(message.pitch * 10 - 10, 10) + String("\">"); + text = pitch_tag + message.text + String("</pitch>"); + + IEnumSpObjectTokens *cpEnum; + ISpObjectToken *cpVoiceToken; + ULONG ulCount = 0; + ULONG stream_number = 0; + ISpObjectTokenCategory *cpCategory; + HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory); + if (SUCCEEDED(hr)) { + hr = cpCategory->SetId(SPCAT_VOICES, false); + if (SUCCEEDED(hr)) { + hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum); + if (SUCCEEDED(hr)) { + hr = cpEnum->GetCount(&ulCount); + while (SUCCEEDED(hr) && ulCount--) { + wchar_t *w_id = 0L; + hr = cpEnum->Next(1, &cpVoiceToken, nullptr); + cpVoiceToken->GetId(&w_id); + if (String::utf16((const char16_t *)w_id) == message.voice) { + synth->SetVoice(cpVoiceToken); + cpVoiceToken->Release(); + break; + } + cpVoiceToken->Release(); + } + cpEnum->Release(); + } + } + cpCategory->Release(); + } + + UTData ut; + ut.string = text.utf16(); + ut.offset = pitch_tag.length(); // Subtract injected <pitch> tag offset. + ut.id = message.id; + + synth->SetVolume(message.volume); + synth->SetRate(10.f * log10(message.rate) / log10(3.f)); + synth->Speak((LPCWSTR)ut.string.get_data(), flags, &stream_number); + + ids[stream_number] = ut; + + queue.pop_front(); + } +} + +bool TTS_Windows::is_speaking() const { + ERR_FAIL_COND_V(!synth, false); + + SPVOICESTATUS status; + synth->GetStatus(&status, nullptr); + return (status.dwRunningState == SPRS_IS_SPEAKING); +} + +bool TTS_Windows::is_paused() const { + ERR_FAIL_COND_V(!synth, false); + return paused; +} + +Array TTS_Windows::get_voices() const { + Array list; + IEnumSpObjectTokens *cpEnum; + ISpObjectToken *cpVoiceToken; + ISpDataKey *cpDataKeyAttribs; + ULONG ulCount = 0; + ISpObjectTokenCategory *cpCategory; + HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_INPROC_SERVER, IID_ISpObjectTokenCategory, (void **)&cpCategory); + if (SUCCEEDED(hr)) { + hr = cpCategory->SetId(SPCAT_VOICES, false); + if (SUCCEEDED(hr)) { + hr = cpCategory->EnumTokens(nullptr, nullptr, &cpEnum); + if (SUCCEEDED(hr)) { + hr = cpEnum->GetCount(&ulCount); + while (SUCCEEDED(hr) && ulCount--) { + hr = cpEnum->Next(1, &cpVoiceToken, nullptr); + HRESULT hr_attr = cpVoiceToken->OpenKey(SPTOKENKEY_ATTRIBUTES, &cpDataKeyAttribs); + if (SUCCEEDED(hr_attr)) { + wchar_t *w_id = nullptr; + wchar_t *w_lang = nullptr; + wchar_t *w_name = nullptr; + cpVoiceToken->GetId(&w_id); + cpDataKeyAttribs->GetStringValue(L"Language", &w_lang); + cpDataKeyAttribs->GetStringValue(nullptr, &w_name); + LCID locale = wcstol(w_lang, nullptr, 16); + + int locale_chars = GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, nullptr, 0); + int region_chars = GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, nullptr, 0); + wchar_t *w_lang_code = new wchar_t[locale_chars]; + wchar_t *w_reg_code = new wchar_t[region_chars]; + GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, w_lang_code, locale_chars); + GetLocaleInfoW(locale, LOCALE_SISO3166CTRYNAME, w_reg_code, region_chars); + + Dictionary voice_d; + voice_d["id"] = String::utf16((const char16_t *)w_id); + if (w_name) { + voice_d["name"] = String::utf16((const char16_t *)w_name); + } else { + voice_d["name"] = voice_d["id"].operator String().replace("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\", ""); + } + voice_d["language"] = String::utf16((const char16_t *)w_lang_code) + "_" + String::utf16((const char16_t *)w_reg_code); + list.push_back(voice_d); + + delete[] w_lang_code; + delete[] w_reg_code; + + cpDataKeyAttribs->Release(); + } + cpVoiceToken->Release(); + } + cpEnum->Release(); + } + } + cpCategory->Release(); + } + return list; +} + +void TTS_Windows::speak(const String &p_text, const String &p_voice, int p_volume, float p_pitch, float p_rate, int p_utterance_id, bool p_interrupt) { + ERR_FAIL_COND(!synth); + if (p_interrupt) { + stop(); + } + + if (p_text.is_empty()) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, p_utterance_id); + return; + } + + DisplayServer::TTSUtterance message; + message.text = p_text; + message.voice = p_voice; + message.volume = CLAMP(p_volume, 0, 100); + message.pitch = CLAMP(p_pitch, 0.f, 2.f); + message.rate = CLAMP(p_rate, 0.1f, 10.f); + message.id = p_utterance_id; + queue.push_back(message); + + if (is_paused()) { + resume(); + } else { + _update_tts(); + } +} + +void TTS_Windows::pause() { + ERR_FAIL_COND(!synth); + if (!paused) { + if (synth->Pause() == S_OK) { + paused = true; + } + } +} + +void TTS_Windows::resume() { + ERR_FAIL_COND(!synth); + synth->Resume(); + paused = false; +} + +void TTS_Windows::stop() { + ERR_FAIL_COND(!synth); + + SPVOICESTATUS status; + synth->GetStatus(&status, nullptr); + if (ids.has(status.ulCurrentStream)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[status.ulCurrentStream].id); + ids.erase(status.ulCurrentStream); + } + for (DisplayServer::TTSUtterance &message : queue) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); + } + queue.clear(); + synth->Speak(nullptr, SPF_PURGEBEFORESPEAK, nullptr); + synth->Resume(); + paused = false; +} + +TTS_Windows *TTS_Windows::get_singleton() { + return singleton; +} + +TTS_Windows::TTS_Windows() { + singleton = this; + CoInitialize(nullptr); + + if (SUCCEEDED(CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice, (void **)&synth))) { + ULONGLONG event_mask = SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_WORD_BOUNDARY); + synth->SetInterest(event_mask, event_mask); + synth->SetNotifyCallbackFunction(&speech_event_callback, (WPARAM)(this), 0); + print_verbose("Text-to-Speech: SAPI initialized."); + } else { + print_verbose("Text-to-Speech: Cannot initialize ISpVoice!"); + } +} + +TTS_Windows::~TTS_Windows() { + if (synth) { + synth->Release(); + } + singleton = nullptr; +} diff --git a/platform/windows/tts_windows.h b/platform/windows/tts_windows.h new file mode 100644 index 0000000000..d84a3d273a --- /dev/null +++ b/platform/windows/tts_windows.h @@ -0,0 +1,80 @@ +/*************************************************************************/ +/* tts_windows.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TTS_WINDOWS_H +#define TTS_WINDOWS_H + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/rb_map.h" +#include "core/variant/array.h" +#include "servers/display_server.h" + +#include <objbase.h> +#include <sapi.h> +#include <wchar.h> +#include <winnls.h> + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +class TTS_Windows { + List<DisplayServer::TTSUtterance> queue; + ISpVoice *synth = nullptr; + bool paused = false; + struct UTData { + Char16String string; + int offset; + int id; + }; + RBMap<ULONG, UTData> ids; + + static void __stdcall speech_event_callback(WPARAM wParam, LPARAM lParam); + void _update_tts(); + + static TTS_Windows *singleton; + +public: + static TTS_Windows *get_singleton(); + + bool is_speaking() const; + bool is_paused() const; + Array get_voices() const; + + void speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false); + void pause(); + void resume(); + void stop(); + + TTS_Windows(); + ~TTS_Windows(); +}; + +#endif // TTS_WINDOWS_H diff --git a/platform/windows/vulkan_context_win.cpp b/platform/windows/vulkan_context_win.cpp index 07c41395fb..e62c6c1dc8 100644 --- a/platform/windows/vulkan_context_win.cpp +++ b/platform/windows/vulkan_context_win.cpp @@ -28,6 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#if defined(WINDOWS_ENABLED) && defined(VULKAN_ENABLED) + #include "vulkan_context_win.h" #ifdef USE_VOLK #include <volk.h> @@ -57,3 +59,5 @@ VulkanContextWindows::VulkanContextWindows() { VulkanContextWindows::~VulkanContextWindows() { } + +#endif |