diff options
Diffstat (limited to 'platform')
160 files changed, 5998 insertions, 1627 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index 1a3c158d2e..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", 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/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index 96b7b48e48..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,7 +174,7 @@ 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 @@ -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 74ca10e5e2..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 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. @@ -73,7 +73,7 @@ public: 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; diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index dcaa586e3b..6b22a0ffa1 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -80,8 +80,6 @@ void AudioDriverOpenSL::_buffer_callbacks( 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 }; @@ -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 e2b1c757d6..5b9eee8117 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -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; @@ -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 3d3c9f9223..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,7 +44,7 @@ class DirAccessJAndroid : public DirAccess { static jmethodID _dir_close; static jmethodID _dir_is_dir; - int id; + int id = 0; String current_dir; String current; diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index e7de287fc6..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" @@ -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,23 +189,20 @@ 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_COND_V(!godot_io_java, 1.0f); + ERR_FAIL_NULL_V(godot_io_java, 1.0f); return godot_io_java->get_scaled_density(); } @@ -184,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); @@ -195,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(); @@ -206,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(); } @@ -417,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); @@ -480,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 1d268bbcfd..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; 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 8cfa3a67b9..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 }, @@ -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. */ @@ -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; @@ -879,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); @@ -969,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]); } @@ -1000,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; @@ -1690,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(); @@ -1700,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"), "")); @@ -1751,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++; } } @@ -1820,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; } @@ -1848,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); @@ -1898,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); } @@ -1976,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); } @@ -2031,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; @@ -2336,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; } @@ -2369,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; } @@ -2390,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; } @@ -2410,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; } @@ -2521,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) { @@ -2546,13 +2552,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("Checking build version.."); Ref<FileAccess> f = FileAccess::open("res://android/.build_version", FileAccess::READ); if (f.is_null()) { - 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.")); + 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); 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; } } @@ -2565,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); @@ -2582,7 +2588,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP user_data.debug = p_debug; 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) { @@ -2594,7 +2600,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP print_verbose("Saving apk expansion file.."); 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; } } @@ -2679,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; } @@ -2695,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; } @@ -2707,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; } @@ -2737,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; } @@ -2759,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; } } @@ -2768,7 +2774,8 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP return ERR_FILE_BAD_PATH; } - zlib_filefunc_def io = zipio_create_io(); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating APK..."), 0)) { return ERR_SKIP; @@ -2776,13 +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 = zipio_create_io(); + 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); @@ -2905,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); } @@ -2923,7 +2932,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (apk_expansion) { 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 { @@ -2936,7 +2945,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP 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); } @@ -2972,13 +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 = zipio_create_io(); + 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 @@ -3058,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 0f267cf13a..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" @@ -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 173bb8bcb7..9a470edfdd 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -232,19 +232,6 @@ 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; @@ -279,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")), @@ -293,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 d7ba31e3c9..4bb8a13bb6 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -49,11 +49,11 @@ 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; @@ -61,21 +61,21 @@ Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { } void FileAccessAndroid::_close() { - if (!a) { + 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; @@ -86,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; } @@ -110,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; } @@ -118,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; diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index 33b692da20..c16f74ac43 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -35,11 +35,10 @@ #include <android/asset_manager.h> #include <android/log.h> #include <stdio.h> -//#include <android_native_app_glue.h> class FileAccessAndroid : public FileAccess { static Ref<FileAccess> create_android(); - mutable AAsset *a = nullptr; + mutable AAsset *asset = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; @@ -49,32 +48,30 @@ class FileAccessAndroid : public FileAccess { public: static AAssetManager *asset_manager; - 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 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 b6303d1bc9..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 { @@ -79,6 +82,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + assetPacks = [":assetPacks:installTime"] defaultConfig { diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 1b2976e715..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.6.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", ] 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 e16ca65df5..da30bd3a95 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -1,7 +1,3 @@ -apply plugin: 'io.github.gradle-nexus.publish-plugin' -apply from: 'app/config.gradle' -apply from: 'scripts/publish-root.gradle' - buildscript { apply from: 'app/config.gradle' @@ -17,6 +13,13 @@ buildscript { } } +plugins { + id 'io.github.gradle-nexus.publish-plugin' +} + +apply from: 'app/config.gradle' +apply from: 'scripts/publish-root.gradle' + allprojects { repositories { google() diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 3312f61ad3..dd167c3880 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -1,10 +1,15 @@ // Gradle build config for Godot Engine's Android port. -apply plugin: 'com.android.application' +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 { @@ -29,6 +34,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + buildTypes { dev { initWith debug diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index bae075d929..659caf7ab4 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -34,6 +34,9 @@ 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" /> @@ -47,6 +50,8 @@ 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 @@ -57,6 +62,8 @@ 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> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java deleted file mode 100644 index 8a6bf88267..0000000000 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.java +++ /dev/null @@ -1,124 +0,0 @@ -/*************************************************************************/ -/* GodotEditor.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.editor; - -import org.godotengine.godot.FullScreenGodotApp; -import org.godotengine.godot.utils.PermissionsUtil; - -import android.content.Intent; -import android.os.Bundle; -import android.os.Debug; - -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * 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. - */ -public class GodotEditor extends FullScreenGodotApp { - private static final boolean WAIT_FOR_DEBUGGER = false; - private static final String COMMAND_LINE_PARAMS = "command_line_params"; - - private static final String EDITOR_ARG = "--editor"; - private static final String PROJECT_MANAGER_ARG = "--project-manager"; - - private final List<String> commandLineParams = new ArrayList<>(); - - @Override - public void onCreate(Bundle savedInstanceState) { - PermissionsUtil.requestManifestPermissions(this); - - String[] params = getIntent().getStringArrayExtra(COMMAND_LINE_PARAMS); - updateCommandLineParams(params); - - if (BuildConfig.BUILD_TYPE.equals("debug") && WAIT_FOR_DEBUGGER) { - Debug.waitForDebugger(); - } - super.onCreate(savedInstanceState); - } - - private void updateCommandLineParams(@Nullable String[] args) { - // Update the list of command line params with the new args - commandLineParams.clear(); - if (args != null && args.length > 0) { - commandLineParams.addAll(Arrays.asList(args)); - } - } - - @Override - public List<String> getCommandLine() { - return commandLineParams; - } - - @Override - public void onNewGodotInstanceRequested(String[] args) { - // Parse the arguments to figure out which activity to start. - Class<?> targetClass = GodotGame.class; - for (String arg : args) { - if (EDITOR_ARG.equals(arg)) { - targetClass = GodotEditor.class; - break; - } - - if (PROJECT_MANAGER_ARG.equals(arg)) { - targetClass = GodotProjectManager.class; - break; - } - } - - // Launch a new activity - Intent newInstance = new Intent(this, targetClass).putExtra(COMMAND_LINE_PARAMS, args); - startActivity(newInstance); - } - - @Override - public void setRequestedOrientation(int requestedOrientation) { - if (!overrideOrientationRequest()) { - super.setRequestedOrientation(requestedOrientation); - } - } - - /** - * The Godot Android Editor sets its own orientation via its AndroidManifest - */ - protected boolean overrideOrientationRequest() { - return true; - } -} 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/editor/src/main/java/org/godotengine/editor/GodotGame.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 12766775a8..783095f93a 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.java +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotGame.java */ +/* GodotGame.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,13 +28,11 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.editor; +package org.godotengine.editor /** * Drives the 'run project' window of the Godot Editor. */ -public class GodotGame extends GodotEditor { - protected boolean overrideOrientationRequest() { - return false; - } +class GodotGame : GodotEditor() { + override fun overrideOrientationRequest() = false } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt index d30f66bb8c..bcf4659603 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.java +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotProjectManager.java */ +/* GodotProjectManager.kt */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,14 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.editor; +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 - * {@link GodotEditor} activity. + * [GodotEditor] activity. */ -public class GodotProjectManager extends GodotEditor { -} +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/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 c806de1ded..6b82326a27 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -1,5 +1,7 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} ext { PUBLISH_VERSION = getGodotPublishVersion() @@ -34,6 +36,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + buildTypes { dev { initWith debug 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 8a86136daf..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; @@ -165,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); @@ -458,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); @@ -509,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")) { @@ -666,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) { @@ -699,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 | @@ -719,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); } }); } @@ -888,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; } 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 e8e292df5d..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,12 +224,30 @@ public class GodotIO { } public int getScreenDPI() { - DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); - return (int)(metrics.density * 160f); + return activity.getResources().getDisplayMetrics().densityDpi; } + /** + * Returns bucketized density values. + */ public float getScaledDensity() { - return activity.getResources().getDisplayMetrics().scaledDensity; + 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) { @@ -238,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(); @@ -260,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 253a51b83c..1f8f8c82a6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -92,6 +92,11 @@ public class GodotLib { public static native boolean step(); /** + * TTS callback. + */ + public static native void ttsCallback(int event, int id, int pos); + + /** * Forward touch events from the main thread to the GL thread. */ public static native void touch(int inputDevice, int event, int pointer, int pointerCount, float[] positions); 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..ccfb865b1a 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); } @@ -185,6 +186,9 @@ public class GodotInputHandler implements InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int godotJoyId = mJoystickIds.get(deviceId); Joystick joystick = mJoysticksDevices.get(deviceId); + if (joystick == null) { + return true; + } for (int i = 0; i < joystick.axes.size(); i++) { final int axis = joystick.axes.get(i); 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/nativeSrcsConfigs/build.gradle b/platform/android/java/nativeSrcsConfigs/build.gradle index 0cb769b539..5e810ae1ba 100644 --- a/platform/android/java/nativeSrcsConfigs/build.gradle +++ b/platform/android/java/nativeSrcsConfigs/build.gradle @@ -1,6 +1,7 @@ // 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 { @@ -18,6 +19,10 @@ android { targetCompatibility versions.javaVersion } + kotlinOptions { + jvmTarget = versions.javaVersion + } + packagingOptions { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 56e1b6fd3a..466ffebf22 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -1,4 +1,19 @@ // 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' diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 1805807f90..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; @@ -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 5b21e696c3..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,12 +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"); @@ -77,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 { @@ -88,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 { @@ -99,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 { @@ -110,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 { @@ -121,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 { @@ -132,7 +135,7 @@ 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; @@ -142,7 +145,7 @@ int GodotIOJavaWrapper::get_screen_dpi() { float GodotIOJavaWrapper::get_scaled_density() { if (_get_scaled_density) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND_V(env == nullptr, 1.0f); + ERR_FAIL_NULL_V(env, 1.0f); return env->CallFloatMethod(godot_io_instance, _get_scaled_density); } else { return 1.0f; @@ -162,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 { @@ -188,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); } @@ -203,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); } } @@ -211,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); } } @@ -219,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; @@ -229,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 08e3092afd..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,12 +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; @@ -75,7 +78,8 @@ public: 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); @@ -87,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 ea72bc0e15..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,12 +94,10 @@ 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); } @@ -142,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; @@ -213,6 +209,10 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_back(JNIEnv *env, jcl } } +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 true; @@ -430,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); @@ -441,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->callp(str_method, (const Variant **)vptr, count, err); - // something 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); @@ -474,16 +473,16 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count); for (int i = 0; i < count; i++) { - jobject obj = env->GetObjectArrayElement(params, i); - if (obj) { - args[i] = _jobject_to_variant(env, obj); + 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]; } MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count); - // something + env->PopLocalFrame(nullptr); } @@ -503,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 e686ee5c09..e1d30eea77 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -43,6 +43,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc 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 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 2c8378e685..e3456fe4e4 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -96,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 { @@ -109,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; @@ -122,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; @@ -134,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); } } @@ -154,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); } } @@ -164,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); } } @@ -175,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); } } @@ -184,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); } } @@ -193,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); @@ -203,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 { @@ -231,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 { @@ -241,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; @@ -272,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 { @@ -284,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; @@ -296,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); @@ -316,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); } } @@ -325,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; @@ -336,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; @@ -347,8 +325,7 @@ 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); } } @@ -356,8 +333,7 @@ void GodotJavaWrapper::vibrate(int p_duration_ms) { void GodotJavaWrapper::create_new_godot_instance(List<String> args) { if (_create_new_godot_instance) { JNIEnv *env = get_jni_env(); - ERR_FAIL_COND(env == nullptr); - + 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())); diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index f04fda7c3d..bbf7c0ae33 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -51,27 +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 _create_new_godot_instance = 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); @@ -108,4 +108,4 @@ public: 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 ef53415f16..6674428de8 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -76,9 +76,7 @@ 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); } @@ -106,7 +104,6 @@ void OS_Android::initialize_core() { DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_RESOURCES); } #endif - DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA); DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM); @@ -139,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() { @@ -162,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; } @@ -187,10 +189,11 @@ bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) { return false; } DisplayServerAndroid::get_singleton()->process_events(); + 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(); + *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; @@ -307,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 } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index a40e17dc2c..3f607eac48 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -90,7 +90,7 @@ 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; @@ -135,4 +135,4 @@ public: ~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 158512803a..7a39e2003d 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -123,7 +123,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS variant_params[i] = _jobject_to_variant(env, j_param); args[i] = &variant_params[i]; env->DeleteLocalRef(j_param); - }; + } singleton->emit_signalp(StringName(signal_name), args, count); } 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 57bee59523..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"), "")); @@ -99,24 +99,21 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) 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; @@ -820,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; } @@ -1181,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; @@ -1258,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; @@ -1301,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; @@ -1355,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; @@ -1397,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; } } @@ -1458,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"); @@ -1488,10 +1487,11 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE); print_line("Unzipping..."); - zlib_filefunc_def io = zipio_create_io(); + 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; } diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index 3831f3bff2..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 @@ -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(); 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 6a61f3a910..d03403bbb4 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -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 ea1bc0ef64..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) { @@ -298,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/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index 31ce71127d..1507f32375 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -64,7 +64,8 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { } String resource_path = ProjectSettings::get_singleton()->get_resource_path(); - zlib_filefunc_def io = zipio_create_io(); + 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. @@ -122,7 +123,7 @@ 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) { Ref<DirAccess> dir = DirAccess::open(p_path); - if (!dir) { + 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 a38040922d..a96c539a1f 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -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 66d93d7c49..901580c140 100644 --- a/platform/javascript/export/export_plugin.cpp +++ b/platform/javascript/export/export_plugin.cpp @@ -33,16 +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) { - zlib_filefunc_def io = zipio_create_io(); + 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; } @@ -55,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; @@ -71,7 +77,7 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template String dst = p_dir.plus_file(file.replace("godot", p_name)); Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE); if (f.is_null()) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst)); unzClose(pkg); return ERR_FILE_CANT_WRITE; } @@ -85,14 +91,14 @@ Error EditorExportPlatformJavaScript::_extract_template(const String &p_template Error EditorExportPlatformJavaScript::_write_or_error(const uint8_t *p_content, int p_size, String p_path) { Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); if (f.is_null()) { - EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path); + 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); 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"); @@ -144,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; @@ -162,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) { @@ -174,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; @@ -195,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"; @@ -209,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"); } @@ -219,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()); @@ -232,7 +238,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & { Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ); if (f.is_null()) { - EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + sw_path); + 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()); @@ -251,7 +257,7 @@ Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> & 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; } } @@ -311,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"); } } @@ -437,7 +444,7 @@ 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; } @@ -446,7 +453,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese String pck_path = base_path + ".pck"; 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; } @@ -456,7 +463,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese 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()); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file())); return error; } } @@ -484,7 +491,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese Vector<uint8_t> html; f = FileAccess::open(html_path, FileAccess::READ); if (f.is_null()) { - EditorNode::get_singleton()->show_warning(TTR("Could not read HTML shell:") + "\n" + html_path); + 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()); @@ -502,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; } @@ -512,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; } } @@ -578,10 +585,11 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese 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) { @@ -624,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; } 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 f77ac3d1ad..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; 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/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java b/platform/javascript/godot_webgl2.h index 44f0a3eb3e..7c357ff66d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotInstrumentation.java +++ b/platform/javascript/godot_webgl2.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* GodotInstrumentation.java */ +/* godot_webgl2.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,23 +28,10 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -package org.godotengine.godot; +#ifndef GODOT_WEBGL2_H +#define GODOT_WEBGL2_H -import android.app.Instrumentation; -import android.content.Intent; -import android.os.Bundle; +#include "GLES3/gl3.h" +#include "webgl/webgl2.h" -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); - } -} +#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 c9c4d6e1b9..8dc7aba5f8 100644 --- a/platform/javascript/javascript_singleton.cpp +++ b/platform/javascript/javascript_singleton.cpp @@ -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; } 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_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 f72cde955a..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": { @@ -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": { @@ -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 39f9b99108..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), @@ -319,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 @@ -366,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 7da30ac363..4aec111022 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -139,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: { } @@ -307,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_ @@ -670,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; } @@ -712,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; @@ -1202,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) { @@ -2130,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); @@ -2162,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 @@ -2188,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); @@ -2303,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; } @@ -2534,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); @@ -2557,7 +2609,7 @@ 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++) { @@ -3246,19 +3298,20 @@ void DisplayServerX11::popup_close(WindowID p_window) { } } -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; @@ -3288,6 +3341,7 @@ void DisplayServerX11::mouse_process_popups() { } if (C) { _send_window_event(windows[C->get()], DisplayServerX11::WINDOW_EVENT_CLOSE_REQUEST); + closed = true; } } } @@ -3295,6 +3349,7 @@ void DisplayServerX11::mouse_process_popups() { last_mouse_monitor_pos = pos; } } + return closed; } void DisplayServerX11::process_events() { @@ -3305,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 @@ -3355,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; @@ -3404,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]) / @@ -3418,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) { @@ -3432,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) { @@ -3456,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; @@ -3505,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 @@ -4060,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 } @@ -4479,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 **/ @@ -4531,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; @@ -4608,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); @@ -4621,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; @@ -4694,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); @@ -4873,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); @@ -4973,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 cd673d94d9..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; @@ -112,6 +116,10 @@ class DisplayServerX11 : public DisplayServer { 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 = nullptr; - int xmblen; - unsigned long last_timestamp; - ::Time last_keyrelease_time; + 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,30 +233,26 @@ 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; + xrr_get_monitors_t xrr_get_monitors = nullptr; + xrr_free_monitors_t xrr_free_monitors = nullptr; void *xrandr_handle = nullptr; Bool xrandr_ext_ok; @@ -291,13 +295,24 @@ 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; @@ -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 ec83e52f09..965b969ba8 100644 --- a/platform/linuxbsd/export/export.cpp +++ b/platform/linuxbsd/export/export.cpp @@ -44,7 +44,7 @@ void register_linuxbsd_exporter() { platform->set_name("Linux/X11"); platform->set_extension("x86_32"); platform->set_extension("x86_64", "binary_format/64_bits"); - platform->set_os_name("LinuxBSD"); + platform->set_os_name("Linux"); platform->set_chmod_flags(0755); EditorExport::get_singleton()->add_export_platform(platform); diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 9f7fab6ee8..4e14920e79 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -35,7 +35,10 @@ 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); - ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); + 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'"); @@ -67,6 +70,9 @@ Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> 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.")); + } } } @@ -98,11 +104,12 @@ List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<Edito return list; } -Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) const { +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; } @@ -110,6 +117,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int { 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; } } @@ -119,7 +127,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int int bits = f->get_8() * 32; if (bits == 32 && p_embedded_size >= 0x100000000) { - ERR_FAIL_V_MSG(ERR_INVALID_DATA, "32-bit executables cannot have embedded data >= 4 GiB."); + 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 @@ -196,5 +204,9 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int memfree(strings); - return found ? OK : ERR_FILE_CORRUPT; + 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 index f46fc68e1d..e04bcc20f9 100644 --- a/platform/linuxbsd/export/export_plugin.h +++ b/platform/linuxbsd/export/export_plugin.h @@ -38,7 +38,7 @@ #include "scene/resources/texture.h" class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { - Map<String, String> extensions; + 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: @@ -46,7 +46,7 @@ public: 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) 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/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 22a9518a25..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 = static_cast<JoypadLinux *>(p_user); - joy->run_joypad_thread(); + 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); + joypad.dpad = (HatMask)((joypad.dpad | HatMask::DOWN) & ~HatMask::UP); } - - 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; - } - 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/key_mapping_x11.cpp b/platform/linuxbsd/key_mapping_x11.cpp index afe965e038..047ee74671 100644 --- a/platform/linuxbsd/key_mapping_x11.cpp +++ b/platform/linuxbsd/key_mapping_x11.cpp @@ -135,6 +135,25 @@ static _XTranslatePair _xkeysym_to_keycode[] = { { XK_F14, Key::F14 }, { XK_F15, Key::F15 }, { XK_F16, Key::F16 }, + { XK_F17, Key::F17 }, + { XK_F18, Key::F18 }, + { XK_F19, Key::F19 }, + { XK_F20, Key::F20 }, + { XK_F21, Key::F21 }, + { XK_F22, Key::F22 }, + { XK_F23, Key::F23 }, + { XK_F24, Key::F24 }, + { XK_F25, Key::F25 }, + { XK_F26, Key::F26 }, + { XK_F27, Key::F27 }, + { XK_F28, Key::F28 }, + { XK_F29, Key::F29 }, + { XK_F30, Key::F30 }, + { XK_F31, Key::F31 }, + { XK_F32, Key::F32 }, + { XK_F33, Key::F33 }, + { XK_F34, Key::F34 }, + { XK_F35, Key::F35 }, // media keys { XF86XK_Back, Key::BACK }, @@ -294,6 +313,29 @@ static _TranslatePair _scancode_to_keycode[] = { { Key::SUPER_L, 0x85 }, { Key::SUPER_R, 0x86 }, { Key::MENU, 0x87 }, + { Key::F13, 0xBF }, + { Key::F14, 0xC0 }, + { Key::F15, 0xC1 }, + { Key::F16, 0xC2 }, + { Key::F17, 0xC3 }, + { Key::F18, 0xC4 }, + { Key::F19, 0xC5 }, + { Key::F20, 0xC6 }, + { Key::F21, 0xC7 }, + { Key::F22, 0xC8 }, + { Key::F23, 0xC9 }, + { Key::F24, 0xCA }, + { Key::F25, 0xCB }, + { Key::F26, 0xCC }, + { Key::F27, 0xCD }, + { Key::F28, 0xCE }, + { Key::F29, 0xCF }, + { Key::F30, 0xD0 }, + { Key::F31, 0xD1 }, + { Key::F32, 0xD2 }, + { Key::F33, 0xD3 }, + { Key::F34, 0xD4 }, + { Key::F35, 0xD5 }, { Key::UNKNOWN, 0 } }; diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index bf142f8804..b73d4dc626 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -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()) { diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 7b912ddee3..3f97b86eae 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -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/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 fa3091ff81..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; @@ -164,9 +167,9 @@ private: 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); @@ -205,7 +208,7 @@ 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); @@ -264,6 +267,15 @@ public: 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; @@ -361,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 d209c90d87..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" @@ -144,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; } @@ -172,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; } @@ -207,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]; } } @@ -232,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]; @@ -247,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]; @@ -567,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) { @@ -702,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: { } @@ -1068,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; } } } @@ -1458,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_ @@ -1863,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; } @@ -2218,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]; @@ -2342,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: { @@ -2385,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: { @@ -2430,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; } } @@ -2467,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; @@ -2636,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; } @@ -2848,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)) { @@ -3047,15 +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(); if (E) { send_window_event(windows[E->get()], DisplayServerOSX::WINDOW_EVENT_CLOSE_REQUEST); + closed = true; } if (!was_empty) { // Inform OS that all popups are closed. @@ -3064,7 +3101,7 @@ 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(); @@ -3087,12 +3124,14 @@ void DisplayServerOSX::mouse_process_popups(bool p_close) { } 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) { @@ -3121,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; @@ -3225,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/export_plugin.cpp b/platform/osx/export/export_plugin.cpp index 94ef875072..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/")) { @@ -261,7 +261,8 @@ void EditorExportPlatformOSX::_make_icon(const Ref<Image> &p_icon, Vector<uint8_ if (f.is_null()) { // Clean up generated file. DirAccess::remove_file_or_error(path); - ERR_FAIL(); + add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not open icon file \"%s\"."), path)); + return; } int ofs = data.size(); @@ -441,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 @@ -470,21 +478,21 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese #ifdef OSX_ENABLED if (p_preset->get("codesign/timestamp") && p_warn) { - WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + 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") && p_warn) { - WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + 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 { @@ -493,7 +501,7 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese if (p_preset->get("codesign/timestamp")) { if (ad_hoc) { if (p_warn) { - WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); + 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"); @@ -502,7 +510,7 @@ Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_prese if (p_preset->get("codesign/hardened_runtime")) { if (ad_hoc) { if (p_warn) { - WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); + 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"); @@ -540,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; @@ -593,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; } @@ -611,7 +622,7 @@ Error EditorExportPlatformOSX::_copy_and_sign_files(Ref<DirAccess> &dir_access, 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); @@ -668,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; } @@ -685,7 +699,10 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin 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); - ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); + 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'"); @@ -713,16 +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; } - zlib_filefunc_def io = zipio_create_io(); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step(TTR("Creating app bundle"), 0)) { return ERR_SKIP; @@ -730,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; } @@ -755,7 +774,7 @@ 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; } @@ -780,13 +799,16 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p 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; } 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); } } @@ -796,21 +818,33 @@ 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"); @@ -954,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)); } @@ -1038,6 +1078,9 @@ 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) { Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); @@ -1048,6 +1091,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p FileAccess::set_unix_permissions(file, 0755); } } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not open \"%s\"."), file)); err = ERR_CANT_CREATE; } } @@ -1060,7 +1104,7 @@ 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; } @@ -1070,6 +1114,9 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p 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.")); + } } } @@ -1213,6 +1260,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create entitlements file.")); err = ERR_CANT_CREATE; } @@ -1230,6 +1278,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Could not create helper entitlements file.")); err = ERR_CANT_CREATE; } } @@ -1257,7 +1306,7 @@ 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; } } @@ -1327,7 +1376,8 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p OS::get_singleton()->move_to_trash(p_path); } - zlib_filefunc_def io_dst = zipio_create_io(); + 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, tmp_base_path_name, "", pkg_name); @@ -1340,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; @@ -1465,7 +1515,8 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String Ref<FileAccess> fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); if (fa.is_null()) { - ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f))); + 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 013e5eaa71..ec97d4139f 100644 --- a/platform/osx/export/export_plugin.h +++ b/platform/osx/export/export_plugin.h @@ -101,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"; } @@ -127,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/plist.cpp b/platform/osx/export/plist.cpp index d089233b80..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"; 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_window_delegate.mm b/platform/osx/godot_window_delegate.mm index 9f49a6a4e9..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) { diff --git a/platform/osx/joypad_osx.h b/platform/osx/joypad_osx.h index b09d5ce34a..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" diff --git a/platform/osx/key_mapping_osx.mm b/platform/osx/key_mapping_osx.mm index fde9206824..0bf6bc7d1c 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[] = { @@ -130,7 +130,7 @@ static const Key _osx_to_godot_table[128] = { /* 3d */ Key::ALT, /* 3e */ Key::CTRL, /* 3f */ Key::UNKNOWN, /* Function */ - /* 40 */ Key::UNKNOWN, /* F17 */ + /* 40 */ Key::F17, /* 41 */ Key::KP_PERIOD, /* 42 */ Key::UNKNOWN, /* 43 */ Key::KP_MULTIPLY, @@ -145,8 +145,8 @@ static const Key _osx_to_godot_table[128] = { /* 4c */ Key::KP_ENTER, /* 4d */ Key::UNKNOWN, /* 4e */ Key::KP_SUBTRACT, - /* 4f */ Key::UNKNOWN, /* F18 */ - /* 50 */ Key::UNKNOWN, /* F19 */ + /* 4f */ Key::F18, + /* 50 */ Key::F19, /* 51 */ Key::EQUAL, /* KeypadEqual */ /* 52 */ Key::KP_0, /* 53 */ Key::KP_1, @@ -156,7 +156,7 @@ static const Key _osx_to_godot_table[128] = { /* 57 */ Key::KP_5, /* 58 */ Key::KP_6, /* 59 */ Key::KP_7, - /* 5a */ Key::UNKNOWN, /* F20 */ + /* 5a */ Key::F20, /* 5b */ Key::KP_8, /* 5c */ Key::KP_9, /* 5d */ Key::YEN, /* JIS Yen */ @@ -366,7 +366,26 @@ static const _KeyCodeText _native_keycodes[] = { {Key::F13 ,NSF13FunctionKey}, {Key::F14 ,NSF14FunctionKey}, {Key::F15 ,NSF15FunctionKey}, - {Key::F16 ,NSF16FunctionKey}, //* ... NSF35FunctionKey */ + {Key::F16 ,NSF16FunctionKey}, + {Key::F17 ,NSF17FunctionKey}, + {Key::F18 ,NSF18FunctionKey}, + {Key::F19 ,NSF19FunctionKey}, + {Key::F20 ,NSF20FunctionKey}, + {Key::F21 ,NSF21FunctionKey}, + {Key::F22 ,NSF22FunctionKey}, + {Key::F23 ,NSF23FunctionKey}, + {Key::F24 ,NSF24FunctionKey}, + {Key::F25 ,NSF25FunctionKey}, + {Key::F26 ,NSF26FunctionKey}, + {Key::F27 ,NSF27FunctionKey}, + {Key::F28 ,NSF28FunctionKey}, + {Key::F29 ,NSF29FunctionKey}, + {Key::F30 ,NSF30FunctionKey}, + {Key::F31 ,NSF31FunctionKey}, + {Key::F32 ,NSF32FunctionKey}, + {Key::F33 ,NSF33FunctionKey}, + {Key::F34 ,NSF34FunctionKey}, + {Key::F35 ,NSF35FunctionKey}, {Key::MENU ,NSMenuFunctionKey}, {Key::HELP ,NSHelpFunctionKey}, {Key::STOP ,NSStopFunctionKey}, 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 33fee01c08..a8fa56e34b 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -150,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)) { @@ -165,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; } 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 2f70c3e74c..09717b9d69 100644 --- a/platform/uwp/export/app_packager.cpp +++ b/platform/uwp/export/app_packager.cpp @@ -91,7 +91,7 @@ void AppxPackager::make_content_types(const String &p_path) { 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(); diff --git a/platform/uwp/export/app_packager.h b/platform/uwp/export/app_packager.h index a32b78bf04..dc5a5259ec 100644 --- a/platform/uwp/export/app_packager.h +++ b/platform/uwp/export/app_packager.h @@ -89,12 +89,12 @@ class AppxPackager { String progress_task; 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); diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index 7e06bf01e3..01683c656c 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -301,7 +301,8 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p AppxPackager packager; packager.init(fa_pack); - zlib_filefunc_def io = zipio_create_io(); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); if (ep.step("Creating package...", 0)) { return ERR_SKIP; @@ -498,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 ceb6d613b3..d92687075c 100644 --- a/platform/uwp/export/export_plugin.h +++ b/platform/uwp/export/export_plugin.h @@ -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/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 f955be1da9..bddf63ff18 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -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 31bad0f053..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_ @@ -571,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. } @@ -589,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) { @@ -1070,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) { @@ -1454,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) { @@ -1758,7 +1801,9 @@ 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 } @@ -1859,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(); @@ -1910,14 +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) - 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) - return context_vulkan->get_vsync_mode(p_window); + if (context_vulkan) { + return context_vulkan->get_vsync_mode(p_window); + } #endif return DisplayServer::VSYNC_ENABLED; } @@ -1955,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; } @@ -2107,7 +2158,10 @@ void DisplayServerWindows::popup_close(WindowID p_window) { WindowID win_id = E->get(); popup_list.erase(E); - _send_window_event(windows[win_id], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + 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; } } @@ -2122,6 +2176,7 @@ LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) case WM_NCRBUTTONDOWN: case WM_NCMBUTTONDOWN: case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: { MOUSEHOOKSTRUCT *ms = (MOUSEHOOKSTRUCT *)lParam; Point2i pos = Point2i(ms->pt.x, ms->pt.y); @@ -2144,6 +2199,7 @@ LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) } if (C) { _send_window_event(windows[C->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + return 1; } } break; } @@ -2151,8 +2207,39 @@ LRESULT DisplayServerWindows::MouseProc(int code, WPARAM wParam, LPARAM lParam) 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) { @@ -2166,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; @@ -2175,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. @@ -3346,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; @@ -3370,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 @@ -3415,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; @@ -3497,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) { @@ -3702,6 +3811,7 @@ DisplayServerWindows::~DisplayServerWindows() { #ifdef GLES3_ENABLED // destroy windows .. NYI? + // FIXME wglDeleteContext is never called #endif if (windows.has(MAIN_WINDOW_ID)) { @@ -3739,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 fcf4b5a728..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,7 +389,7 @@ 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; @@ -396,10 +399,11 @@ class DisplayServerWindows : public DisplayServer { 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,6 +460,15 @@ 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; @@ -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_plugin.cpp b/platform/windows/export/export_plugin.cpp index 917a0af90b..16c67345e0 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -43,7 +43,10 @@ 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); - ERR_FAIL_COND_V(f.is_null(), ERR_CANT_CREATE); + 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 + "\""); @@ -53,17 +56,29 @@ Error EditorExportPlatformWindows::_export_debug_script(const Ref<EditorExportPr 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); - - if (err != OK) { - return err; +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; +} - _rcedit_add_data(p_preset, p_path); - +Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int 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) { - err = _code_sign(p_preset, p_path); + _code_sign(p_preset, pck_path); + } + + 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)); + } } String app_name; @@ -79,7 +94,9 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> 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"; - err = _export_debug_script(p_preset, app_name, p_path.get_file(), scr_path); + 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.")); + } } } @@ -96,7 +113,7 @@ List<String> EditorExportPlatformWindows::get_binary_extensions(const Ref<Editor return list; } -bool EditorExportPlatformWindows::get_export_option_visibility(const String &p_option, const Map<StringName, Variant> &p_options) const { +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; @@ -117,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"), "")); @@ -127,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 @@ -145,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()) { @@ -204,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) { @@ -219,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()) { @@ -228,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()) { @@ -248,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 @@ -256,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 @@ -268,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 @@ -300,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; } } @@ -347,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 @@ -355,6 +388,7 @@ 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; } @@ -362,10 +396,16 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p 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; @@ -376,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"; } @@ -414,11 +454,17 @@ 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) const { +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; } @@ -430,6 +476,7 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 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; } } @@ -479,5 +526,9 @@ Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int6 } } - return found ? OK : ERR_FILE_CORRUPT; + 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 39d1cf4c77..51f98365a9 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -38,19 +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) 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 dc411983e8..5e43a3de2a 100644 --- a/platform/windows/gl_manager_windows.h +++ b/platform/windows/gl_manager_windows.h @@ -52,10 +52,6 @@ public: private: // any data specific to the window struct GLWindow { - bool in_use = false; - - // the external ID .. should match the GL window number .. unused I think - DisplayServer::WindowID window_id = DisplayServer::INVALID_WINDOW_ID; int width = 0; int height = 0; @@ -71,7 +67,7 @@ private: HGLRC hRC; }; - LocalVector<GLWindow> _windows; + RBMap<DisplayServer::WindowID, GLWindow> _windows; LocalVector<GLDisplay> _displays; GLWindow *_current_window = nullptr; 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/key_mapping_windows.cpp b/platform/windows/key_mapping_windows.cpp index e32dc0d1a6..2d8d68a575 100644 --- a/platform/windows/key_mapping_windows.cpp +++ b/platform/windows/key_mapping_windows.cpp @@ -179,7 +179,14 @@ static _WinTranslatePair _vk_to_keycode[] = { { Key::F14, VK_F14 }, // (0x7D) { Key::F15, VK_F15 }, // (0x7E) { Key::F16, VK_F16 }, // (0x7F) - // We have no mappings for F17-F24. (0x80-87) + { Key::F17, VK_F17 }, // (0x80) + { Key::F18, VK_F18 }, // (0x81) + { Key::F19, VK_F19 }, // (0x82) + { Key::F20, VK_F20 }, // (0x83) + { Key::F21, VK_F21 }, // (0x84) + { Key::F22, VK_F22 }, // (0x85) + { Key::F23, VK_F23 }, // (0x86) + { Key::F24, VK_F24 }, // (0x87) // 0x88-8F are reserved for UI navigation. { Key::NUMLOCK, VK_NUMLOCK }, // (0x90) { Key::SCROLLLOCK, VK_SCROLL }, // (0x91) @@ -409,6 +416,14 @@ static _WinTranslatePair _scancode_to_keycode[] = { { Key::F14, 0x65 }, { Key::F15, 0x66 }, { Key::F16, 0x67 }, + { Key::F17, 0x68 }, + { Key::F18, 0x69 }, + { Key::F19, 0x6A }, + { Key::F20, 0x6B }, + { Key::F21, 0x6C }, + { Key::F22, 0x6D }, + { Key::F23, 0x6E }, + { Key::F24, 0x76 }, { Key::UNKNOWN, 0 } }; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index b4669e452a..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; } @@ -383,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); @@ -431,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); @@ -513,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; @@ -686,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 adeecf37c5..dc702c66e1 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -57,6 +57,11 @@ #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 @@ -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 |