diff options
Diffstat (limited to 'platform/iphone')
-rw-r--r-- | platform/iphone/SCsub | 1 | ||||
-rw-r--r-- | platform/iphone/app_delegate.mm | 11 | ||||
-rw-r--r-- | platform/iphone/detect.py | 8 | ||||
-rw-r--r-- | platform/iphone/display_layer.mm | 12 | ||||
-rw-r--r-- | platform/iphone/display_server_iphone.h | 14 | ||||
-rw-r--r-- | platform/iphone/display_server_iphone.mm | 100 | ||||
-rw-r--r-- | platform/iphone/export/export_plugin.cpp | 404 | ||||
-rw-r--r-- | platform/iphone/export/export_plugin.h | 15 | ||||
-rw-r--r-- | platform/iphone/godot_iphone.mm | 12 | ||||
-rw-r--r-- | platform/iphone/godot_view.h | 5 | ||||
-rw-r--r-- | platform/iphone/godot_view.mm | 16 | ||||
-rw-r--r-- | platform/iphone/godot_view_gesture_recognizer.mm | 33 | ||||
-rw-r--r-- | platform/iphone/godot_view_renderer.mm | 3 | ||||
-rw-r--r-- | platform/iphone/ios.h | 11 | ||||
-rw-r--r-- | platform/iphone/ios.mm | 102 | ||||
-rw-r--r-- | platform/iphone/joypad_iphone.mm | 27 | ||||
-rw-r--r-- | platform/iphone/os_iphone.h | 9 | ||||
-rw-r--r-- | platform/iphone/os_iphone.mm | 43 | ||||
-rw-r--r-- | platform/iphone/tts_ios.h | 63 | ||||
-rw-r--r-- | platform/iphone/tts_ios.mm | 164 | ||||
-rw-r--r-- | platform/iphone/view_controller.mm | 2 |
21 files changed, 782 insertions, 273 deletions
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/app_delegate.mm b/platform/iphone/app_delegate.mm index 5130a26f15..c5c9b5a5f9 100644 --- a/platform/iphone/app_delegate.mm +++ b/platform/iphone/app_delegate.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #import "app_delegate.h" + #include "core/config/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #import "godot_view.h" @@ -76,7 +77,7 @@ static ViewController *mainViewController = nil; // bail, things did not go very well for us, should probably output a message on screen with our error code... exit(0); return NO; - }; + } ViewController *viewController = [[ViewController alloc] init]; viewController.godotView.useCADisplayLink = bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO; @@ -99,7 +100,7 @@ static ViewController *mainViewController = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; return YES; -}; +} - (void)onAudioInterruption:(NSNotification *)notification { if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) { @@ -111,17 +112,17 @@ static ViewController *mainViewController = nil; OSIPhone::get_singleton()->on_focus_in(); } } -}; +} - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { if (OS::get_singleton()->get_main_loop()) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_MEMORY_WARNING); } -}; +} - (void)applicationWillTerminate:(UIApplication *)application { iphone_finish(); -}; +} // When application goes to background (e.g. user switches to another app or presses Home), // then applicationWillResignActive -> applicationDidEnterBackground are called. diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index 4e4b0d81c3..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"]) @@ -120,7 +123,6 @@ def configure(env): ) ) env.Append(CPPDEFINES=["NEED_LONG_INT"]) - env.Append(CPPDEFINES=["LIBYUV_DISABLE_NEON"]) # Disable exceptions on non-tools (template) builds if not env["tools"]: diff --git a/platform/iphone/display_layer.mm b/platform/iphone/display_layer.mm index 7448dfed4a..92e81448ac 100644 --- a/platform/iphone/display_layer.mm +++ b/platform/iphone/display_layer.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #import "display_layer.h" + #include "core/config/project_settings.h" #include "core/os/keyboard.h" #include "display_server_iphone.h" @@ -153,17 +154,6 @@ return NO; } - // if (OS::get_singleton()) { - // OS::VideoMode vm; - // vm.fullscreen = true; - // vm.width = backingWidth; - // vm.height = backingHeight; - // vm.resizable = false; - // OS::get_singleton()->set_video_mode(vm); - // OSIPhone::get_singleton()->set_base_framebuffer(viewFramebuffer); - // }; - // gl_view_base_fb = viewFramebuffer; - return YES; } diff --git a/platform/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h index 6434483641..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,12 +125,24 @@ 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; virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Vector<DisplayServer::WindowID> get_window_list() const override; diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm index 48bda89fc3..573ee9b7a8 100644 --- a/platform/iphone/display_server_iphone.mm +++ b/platform/iphone/display_server_iphone.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #include "display_server_iphone.h" + #import "app_delegate.h" #include "core/config/project_settings.h" #include "core/io/file_access_pack.h" @@ -37,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> @@ -51,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, @@ -231,7 +236,7 @@ void DisplayServerIPhone::touch_press(int p_idx, int p_x, int p_y, bool p_presse ev->set_position(Vector2(p_x, p_y)); perform_event(ev); } -}; +} void DisplayServerIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y) { if (!GLOBAL_DEF("debug/disable_touch", false)) { @@ -241,16 +246,16 @@ void DisplayServerIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int ev->set_position(Vector2(p_x, p_y)); ev->set_relative(Vector2(p_x - p_prev_x, p_y - p_prev_y)); perform_event(ev); - }; -}; + } +} void DisplayServerIPhone::perform_event(const Ref<InputEvent> &p_event) { Input::get_singleton()->parse_input_event(p_event); -}; +} void DisplayServerIPhone::touches_cancelled(int p_idx) { touch_press(p_idx, -1, -1, false, false); -}; +} // MARK: Keyboard @@ -263,13 +268,13 @@ void DisplayServerIPhone::key(Key p_key, bool p_pressed) { ev->set_physical_keycode(p_key); ev->set_unicode((char32_t)p_key); perform_event(ev); -}; +} // MARK: Motion void DisplayServerIPhone::update_gravity(float p_x, float p_y, float p_z) { Input::get_singleton()->set_gravity(Vector3(p_x, p_y, p_z)); -}; +} void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, float p_z) { // Found out the Z should not be negated! Pass as is! @@ -279,15 +284,15 @@ void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, float p_z) p_z / kDisplayServerIPhoneAcceleration); Input::get_singleton()->set_accelerometer(v_accelerometer); -}; +} void DisplayServerIPhone::update_magnetometer(float p_x, float p_y, float p_z) { Input::get_singleton()->set_magnetometer(Vector3(p_x, p_y, p_z)); -}; +} void DisplayServerIPhone::update_gyroscope(float p_x, float p_y, float p_z) { Input::get_singleton()->set_gyroscope(Vector3(p_x, p_y, p_z)); -}; +} // MARK: - @@ -309,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; @@ -319,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; } @@ -338,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 { @@ -393,6 +435,10 @@ int DisplayServerIPhone::screen_get_dpi(int p_screen) const { } } +float DisplayServerIPhone::screen_get_refresh_rate(int p_screen) const { + return [UIScreen mainScreen].maximumFramesPerSecond; +} + float DisplayServerIPhone::screen_get_scale(int p_screen) const { return [UIScreen mainScreen].nativeScale; } @@ -516,7 +562,7 @@ void DisplayServerIPhone::window_move_to_foreground(WindowID p_window) { float DisplayServerIPhone::screen_get_max_scale() const { return screen_get_scale(SCREEN_OF_MAIN_WINDOW); -}; +} void DisplayServerIPhone::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) { screen_orientation = p_orientation; diff --git a/platform/iphone/export/export_plugin.cpp b/platform/iphone/export/export_plugin.cpp index ea17f1ac61..4cf1c279eb 100644 --- a/platform/iphone/export/export_plugin.cpp +++ b/platform/iphone/export/export_plugin.cpp @@ -30,6 +30,8 @@ #include "export_plugin.h" +#include "editor/editor_node.h" + void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { String driver = ProjectSettings::get_singleton()->get("rendering/driver/driver_name"); // Vulkan and OpenGL ES 3.0 both mandate ETC2 support. @@ -43,7 +45,6 @@ void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> Vector<EditorExportPlatformIOS::ExportArchitecture> EditorExportPlatformIOS::_get_supported_architectures() { Vector<ExportArchitecture> archs; - archs.push_back(ExportArchitecture("armv7", false)); // Disabled by default, not included in official templates. archs.push_back(ExportArchitecture("arm64", true)); return archs; } @@ -57,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) { @@ -77,13 +78,13 @@ 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"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_debug"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_debug", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Developer"), "iPhone Developer")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_debug", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Developer"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_debug", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_release"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_release", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Distribution"), "")); @@ -91,34 +92,28 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/targeted_device_family", PROPERTY_HINT_ENUM, "iPhone,iPad,iPhone & iPad"), 2)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/info"), "Made with Godot Engine")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); Vector<PluginConfigIOS> found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "plugins/" + found_plugins[i].name), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("%s/%s", PNAME("plugins"), found_plugins[i].name)), false)); } - Set<String> plist_keys; + HashSet<String> plist_keys; for (int i = 0; i < found_plugins.size(); i++) { // Editable plugin plist values PluginConfigIOS plugin = found_plugins[i]; - const String *K = nullptr; - while ((K = plugin.plist.next(K))) { - String key = *K; - PluginConfigIOS::PlistItem item = plugin.plist[key]; - switch (item.type) { + for (const KeyValue<String, PluginConfigIOS::PlistItem> &E : plugin.plist) { + switch (E.value.type) { case PluginConfigIOS::PlistItemType::STRING_INPUT: { - String preset_name = "plugins_plist/" + key; + String preset_name = "plugins_plist/" + E.key; if (!plist_keys.has(preset_name)) { - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), item.value)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, preset_name), E.value.value)); plist_keys.insert(preset_name); } } break; @@ -138,8 +133,11 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data/accessible_from_itunes_sharing"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photolibrary_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_120x120", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone/iPod Touch with Retina display r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_180x180", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone with Retina HD display @@ -178,6 +176,10 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ "scaleAspectFill", "scaleToFill" }; + String dbg_sign_id = p_preset->get("application/code_sign_identity_debug").operator String().is_empty() ? "iPhone Developer" : p_preset->get("application/code_sign_identity_debug"); + String rel_sign_id = p_preset->get("application/code_sign_identity_release").operator String().is_empty() ? "iPhone Distribution" : p_preset->get("application/code_sign_identity_release"); + bool dbg_manual = !p_preset->get("application/provisioning_profile_uuid_debug").operator String().is_empty() || (dbg_sign_id != "iPhone Developer"); + bool rel_manual = !p_preset->get("application/provisioning_profile_uuid_release").operator String().is_empty() || (rel_sign_id != "iPhone Distribution"); String str; String strnew; str.parse_utf8((const char *)pfile.ptr(), pfile.size()); @@ -195,8 +197,6 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$modules_buildgrp", p_config.modules_buildgrp) + "\n"; } else if (lines[i].find("$name") != -1) { strnew += lines[i].replace("$name", p_config.pkg_name) + "\n"; - } else if (lines[i].find("$info") != -1) { - strnew += lines[i].replace("$info", p_preset->get("application/info")) + "\n"; } else if (lines[i].find("$bundle_identifier") != -1) { strnew += lines[i].replace("$bundle_identifier", p_preset->get("application/bundle_identifier")) + "\n"; } else if (lines[i].find("$short_version") != -1) { @@ -205,8 +205,6 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$version", p_preset->get("application/version")) + "\n"; } else if (lines[i].find("$signature") != -1) { strnew += lines[i].replace("$signature", p_preset->get("application/signature")) + "\n"; - } else if (lines[i].find("$copyright") != -1) { - strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n"; } else if (lines[i].find("$team_id") != -1) { strnew += lines[i].replace("$team_id", p_preset->get("application/app_store_team_id")) + "\n"; } else if (lines[i].find("$default_build_config") != -1) { @@ -218,13 +216,25 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ strnew += lines[i].replace("$provisioning_profile_uuid_release", p_preset->get("application/provisioning_profile_uuid_release")) + "\n"; } else if (lines[i].find("$provisioning_profile_uuid_debug") != -1) { strnew += lines[i].replace("$provisioning_profile_uuid_debug", p_preset->get("application/provisioning_profile_uuid_debug")) + "\n"; + } else if (lines[i].find("$code_sign_style_debug") != -1) { + if (dbg_manual) { + strnew += lines[i].replace("$code_sign_style_debug", "Manual") + "\n"; + } else { + strnew += lines[i].replace("$code_sign_style_debug", "Automatic") + "\n"; + } + } else if (lines[i].find("$code_sign_style_release") != -1) { + if (rel_manual) { + strnew += lines[i].replace("$code_sign_style_release", "Manual") + "\n"; + } else { + strnew += lines[i].replace("$code_sign_style_release", "Automatic") + "\n"; + } } else if (lines[i].find("$provisioning_profile_uuid") != -1) { String uuid = p_debug ? p_preset->get("application/provisioning_profile_uuid_debug") : p_preset->get("application/provisioning_profile_uuid_release"); strnew += lines[i].replace("$provisioning_profile_uuid", uuid) + "\n"; } else if (lines[i].find("$code_sign_identity_debug") != -1) { - strnew += lines[i].replace("$code_sign_identity_debug", p_preset->get("application/code_sign_identity_debug")) + "\n"; + strnew += lines[i].replace("$code_sign_identity_debug", dbg_sign_id) + "\n"; } else if (lines[i].find("$code_sign_identity_release") != -1) { - strnew += lines[i].replace("$code_sign_identity_release", p_preset->get("application/code_sign_identity_release")) + "\n"; + strnew += lines[i].replace("$code_sign_identity_release", rel_sign_id) + "\n"; } else if (lines[i].find("$additional_plist_content") != -1) { strnew += lines[i].replace("$additional_plist_content", p_config.plist_content) + "\n"; } else if (lines[i].find("$godot_archs") != -1) { @@ -372,6 +382,36 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ String value = value_format.format(value_dictionary, "$_"); strnew += lines[i].replace("$launch_screen_background_color", value) + "\n"; + } else if (lines[i].find("$pbx_locale_file_reference") != -1) { + String locale_files; + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + int index = 0; + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = " + lang + "; path = " + lang + ".lproj/InfoPlist.strings; sourceTree = \"<group>\"; };"; + } + index++; + } + } + strnew += lines[i].replace("$pbx_locale_file_reference", locale_files); + } else if (lines[i].find("$pbx_locale_build_reference") != -1) { + String locale_files; + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + int index = 0; + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */,"; + } + index++; + } + } + strnew += lines[i].replace("$pbx_locale_build_reference", locale_files); } else { strnew += lines[i] + "\n"; } @@ -487,8 +527,8 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr String json_description = "{\"images\":["; String sizes; - DirAccess *da = DirAccess::open(p_iconset_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_iconset_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_iconset_dir + "'."); for (uint64_t i = 0; i < (sizeof(icon_infos) / sizeof(icon_infos[0])); ++i) { IconInfo info = icon_infos[i]; @@ -527,7 +567,6 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr } if (err) { - memdelete(da); String err_str = String("Failed to export icon(" + String(info.preset_key) + "): '" + icon_path + "'."); ERR_PRINT(err_str.utf8().get_data()); return err; @@ -545,19 +584,16 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr json_description += String("}"); } json_description += "]}"; - memdelete(da); - FileAccess *json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); - ERR_FAIL_COND_V(!json_file, ERR_CANT_CREATE); + Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE); + ERR_FAIL_COND_V(json_file.is_null(), ERR_CANT_CREATE); CharString json_utf8 = json_description.utf8(); json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length()); - memdelete(json_file); - FileAccess *sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); - ERR_FAIL_COND_V(!sizes_file, ERR_CANT_CREATE); + Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE); + ERR_FAIL_COND_V(sizes_file.is_null(), ERR_CANT_CREATE); CharString sizes_utf8 = sizes.utf8(); sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length()); - memdelete(sizes_file); return OK; } @@ -631,8 +667,8 @@ Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExpor } Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) { - DirAccess *da = DirAccess::open(p_dest_dir); - ERR_FAIL_COND_V_MSG(!da, ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); + Ref<DirAccess> da = DirAccess::open(p_dest_dir); + ERR_FAIL_COND_V_MSG(da.is_null(), ERR_CANT_OPEN, "Cannot open directory '" + p_dest_dir + "'."); for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) { LoadingScreenInfo info = loading_screen_infos[i]; @@ -669,7 +705,6 @@ Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExp err = da->copy(loading_screen_file, p_dest_dir + info.export_name); } if (err) { - memdelete(da); String err_str = String("Failed to export loading screen (") + info.preset_key + ") from path '" + loading_screen_file + "'."; ERR_PRINT(err_str.utf8().get_data()); return err; @@ -717,12 +752,11 @@ Error EditorExportPlatformIOS::_export_loading_screen_images(const Ref<EditorExp } } } - memdelete(da); return OK; } -Error EditorExportPlatformIOS::_walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata) { +Error EditorExportPlatformIOS::_walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata) { Vector<String> dirs; String current_dir = p_da->get_current_dir(); p_da->list_dir_begin(); @@ -768,14 +802,26 @@ struct CodesignData { Error EditorExportPlatformIOS::_codesign(String p_file, void *p_userdata) { if (p_file.ends_with(".dylib")) { - CodesignData *data = (CodesignData *)p_userdata; + CodesignData *data = static_cast<CodesignData *>(p_userdata); print_line(String("Signing ") + p_file); + + String sign_id; + if (data->debug) { + sign_id = data->preset->get("application/code_sign_identity_debug").operator String().is_empty() ? "iPhone Developer" : data->preset->get("application/code_sign_identity_debug"); + } else { + sign_id = data->preset->get("application/code_sign_identity_release").operator String().is_empty() ? "iPhone Distribution" : data->preset->get("application/code_sign_identity_release"); + } + List<String> codesign_args; codesign_args.push_back("-f"); codesign_args.push_back("-s"); - codesign_args.push_back(data->preset->get(data->debug ? "application/code_sign_identity_debug" : "application/code_sign_identity_release")); + 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; } @@ -915,21 +961,15 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String &p_asset, const String *p_custom_file_name, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { - DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - ERR_FAIL_COND_V_MSG(!filesystem_da, ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); - String binary_name = p_out_dir.get_file().get_basename(); - DirAccess *da = DirAccess::create_for_path(p_asset); - if (!da) { - memdelete(filesystem_da); + Ref<DirAccess> da = DirAccess::create_for_path(p_asset); + if (da.is_null()) { ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Can't create directory: " + p_asset + "."); } bool file_exists = da->file_exists(p_asset); bool dir_exists = da->dir_exists(p_asset); if (!file_exists && !dir_exists) { - memdelete(da); - memdelete(filesystem_da); return ERR_FILE_NOT_FOUND; } @@ -989,19 +1029,18 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String destination = p_out_dir.plus_file(asset_path); } + Ref<DirAccess> filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + ERR_FAIL_COND_V_MSG(filesystem_da.is_null(), ERR_CANT_CREATE, "Cannot create DirAccess for path '" + p_out_dir + "'."); + if (!filesystem_da->dir_exists(destination_dir)) { Error make_dir_err = filesystem_da->make_dir_recursive(destination_dir); if (make_dir_err) { - memdelete(da); - memdelete(filesystem_da); return make_dir_err; } } Error err = dir_exists ? da->copy_dir(p_asset, destination) : da->copy(p_asset, destination); - memdelete(da); if (err) { - memdelete(filesystem_da); return err; } IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; @@ -1057,17 +1096,13 @@ Error EditorExportPlatformIOS::_copy_asset(const String &p_out_dir, const String String info_plist = info_plist_format.replace("$name", file_name); - FileAccess *f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); - if (f) { + Ref<FileAccess> f = FileAccess::open(destination_dir.plus_file("Info.plist"), FileAccess::WRITE); + if (f.is_valid()) { f->store_string(info_plist); - f->close(); - memdelete(f); } } } - memdelete(filesystem_da); - return OK; } @@ -1147,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; @@ -1224,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; @@ -1267,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; @@ -1321,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; @@ -1363,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; } } @@ -1372,29 +1405,29 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return ERR_FILE_BAD_PATH; } - DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (da) { - String current_dir = da->get_current_dir(); - - // remove leftovers from last export so they don't interfere - // in case some files are no longer needed - if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) { - da->erase_contents_recursive(); - } - if (da->change_dir(dest_dir + binary_name) == OK) { - da->erase_contents_recursive(); - } + { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (da.is_valid()) { + String current_dir = da->get_current_dir(); + + // remove leftovers from last export so they don't interfere + // in case some files are no longer needed + if (da->change_dir(dest_dir + binary_name + ".xcodeproj") == OK) { + da->erase_contents_recursive(); + } + if (da->change_dir(dest_dir + binary_name) == OK) { + da->erase_contents_recursive(); + } - da->change_dir(current_dir); + da->change_dir(current_dir); - if (!da->dir_exists(dest_dir + binary_name)) { - Error err = da->make_dir(dest_dir + binary_name); - if (err) { - memdelete(da); - return err; + if (!da->dir_exists(dest_dir + binary_name)) { + Error err = da->make_dir(dest_dir + binary_name); + if (err) { + return err; + } } } - memdelete(da); } if (ep.step("Making .pck", 0)) { @@ -1402,7 +1435,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p } String pack_path = dest_dir + binary_name + ".pck"; Vector<SharedObject> libraries; - Error err = save_pack(p_preset, pack_path, &libraries); + Error err = save_pack(p_preset, p_debug, pack_path, &libraries); if (err) { return err; } @@ -1415,9 +1448,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p print_line("Static framework: " + library_to_use); String pkg_name; - if (p_preset->get("application/name") != "") { - pkg_name = p_preset->get("application/name"); // app_name - } else if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { + if (String(ProjectSettings::get_singleton()->get("application/config/name")) != "") { pkg_name = String(ProjectSettings::get_singleton()->get("application/config/name")); } else { pkg_name = "Unnamed"; @@ -1426,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"); @@ -1452,15 +1483,15 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p Vector<IOSExportAsset> assets; - DirAccess *tmp_app_path = DirAccess::create_for_path(dest_dir); - ERR_FAIL_COND_V(!tmp_app_path, ERR_CANT_CREATE); + Ref<DirAccess> tmp_app_path = DirAccess::create_for_path(dest_dir); + ERR_FAIL_COND_V(tmp_app_path.is_null(), ERR_CANT_CREATE); print_line("Unzipping..."); - FileAccess *src_f = nullptr; - zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); if (!src_pkg_zip) { - EditorNode::add_io_error("Could not open export template (not a zip file?):\n" + src_pkg_name); + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), TTR("Could not open export template (not a zip file?): \"%s\".", src_pkg_name)); return ERR_CANT_OPEN; } @@ -1479,6 +1510,9 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p unz_file_info info; char fname[16384]; ret = unzGetCurrentFileInfo(src_pkg_zip, &info, fname, 16384, nullptr, 0, nullptr, 0); + if (ret != UNZ_OK) { + break; + } String file = String::utf8(fname); @@ -1531,22 +1565,20 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (dir_err) { ERR_PRINT("Can't create '" + dir_name + "'."); unzClose(src_pkg_zip); - memdelete(tmp_app_path); return ERR_CANT_CREATE; } } /* write the file */ - FileAccess *f = FileAccess::open(file, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + file + "'."); - unzClose(src_pkg_zip); - memdelete(tmp_app_path); - return ERR_CANT_CREATE; - }; - f->store_buffer(data.ptr(), data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(file, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + file + "'."); + unzClose(src_pkg_zip); + return ERR_CANT_CREATE; + }; + f->store_buffer(data.ptr(), data.size()); + } #if defined(OSX_ENABLED) || defined(X11_ENABLED) if (is_execute) { @@ -1564,10 +1596,53 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (!found_library) { ERR_PRINT("Requested template library '" + library_to_use + "' not found. It might be missing from your template archive."); - memdelete(tmp_app_path); return ERR_FILE_NOT_FOUND; } + Dictionary appnames = ProjectSettings::get_singleton()->get("application/config/name_localized"); + Dictionary camera_usage_descriptions = p_preset->get("privacy/camera_usage_description_localized"); + Dictionary microphone_usage_descriptions = p_preset->get("privacy/microphone_usage_description_localized"); + Dictionary photolibrary_usage_descriptions = p_preset->get("privacy/photolibrary_usage_description_localized"); + + Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); + if (translations.size() > 0) { + { + String fname = dest_dir + binary_name + "/en.lproj"; + tmp_app_path->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + f->store_line("CFBundleDisplayName = \"" + ProjectSettings::get_singleton()->get("application/config/name").operator String() + "\";"); + f->store_line("NSCameraUsageDescription = \"" + p_preset->get("privacy/camera_usage_description").operator String() + "\";"); + f->store_line("NSMicrophoneUsageDescription = \"" + p_preset->get("privacy/microphone_usage_description").operator String() + "\";"); + f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";"); + } + + for (const String &E : translations) { + Ref<Translation> tr = ResourceLoader::load(E); + if (tr.is_valid()) { + String lang = tr->get_locale(); + String fname = dest_dir + binary_name + "/" + lang + ".lproj"; + tmp_app_path->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + if (appnames.has(lang)) { + f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); + } + if (camera_usage_descriptions.has(lang)) { + f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); + } + if (microphone_usage_descriptions.has(lang)) { + f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); + } + if (photolibrary_usage_descriptions.has(lang)) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + photolibrary_usage_descriptions[lang].operator String() + "\";"); + } + } + } + } + // Copy project static libs to the project Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { @@ -1578,7 +1653,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p Error lib_copy_err = tmp_app_path->copy(static_lib_path, dest_lib_file_path); if (lib_copy_err != OK) { ERR_PRINT("Can't copy '" + static_lib_path + "'."); - memdelete(tmp_app_path); return lib_copy_err; } } @@ -1589,7 +1663,6 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p if (!tmp_app_path->dir_exists(iconset_dir)) { err = tmp_app_path->make_dir_recursive(iconset_dir); } - memdelete(tmp_app_path); if (err) { return err; } @@ -1599,43 +1672,42 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return err; } - bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard"); + { + bool use_storyboard = p_preset->get("storyboard/use_launch_screen_storyboard"); - String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; - String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; + String launch_image_path = dest_dir + binary_name + "/Images.xcassets/LaunchImage.launchimage/"; + String splash_image_path = dest_dir + binary_name + "/Images.xcassets/SplashImage.imageset/"; - DirAccess *launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Ref<DirAccess> launch_screen_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (launch_screen_da.is_null()) { + return ERR_CANT_CREATE; + } - if (!launch_screen_da) { - return ERR_CANT_CREATE; - } + if (use_storyboard) { + print_line("Using Launch Storyboard"); - if (use_storyboard) { - print_line("Using Launch Storyboard"); + if (launch_screen_da->change_dir(launch_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(launch_image_path); + } - if (launch_screen_da->change_dir(launch_image_path) == OK) { - launch_screen_da->erase_contents_recursive(); - launch_screen_da->remove(launch_image_path); - } + err = _export_loading_screen_file(p_preset, splash_image_path); + } else { + print_line("Using Launch Images"); - err = _export_loading_screen_file(p_preset, splash_image_path); - } else { - print_line("Using Launch Images"); + const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard"; - const String launch_screen_path = dest_dir + binary_name + "/Launch Screen.storyboard"; + launch_screen_da->remove(launch_screen_path); - launch_screen_da->remove(launch_screen_path); + if (launch_screen_da->change_dir(splash_image_path) == OK) { + launch_screen_da->erase_contents_recursive(); + launch_screen_da->remove(splash_image_path); + } - if (launch_screen_da->change_dir(splash_image_path) == OK) { - launch_screen_da->erase_contents_recursive(); - launch_screen_da->remove(splash_image_path); + err = _export_loading_screen_images(p_preset, launch_image_path); } - - err = _export_loading_screen_images(p_preset, launch_image_path); } - memdelete(launch_screen_da); - if (err) { return err; } @@ -1644,25 +1716,26 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p _export_additional_assets(dest_dir + binary_name, libraries, assets); _add_assets_to_project(p_preset, project_file_data, assets); String project_file_name = dest_dir + binary_name + ".xcodeproj/project.pbxproj"; - FileAccess *f = FileAccess::open(project_file_name, FileAccess::WRITE); - if (!f) { - ERR_PRINT("Can't write '" + project_file_name + "'."); - return ERR_CANT_CREATE; - }; - f->store_buffer(project_file_data.ptr(), project_file_data.size()); - f->close(); - memdelete(f); + { + Ref<FileAccess> f = FileAccess::open(project_file_name, FileAccess::WRITE); + if (f.is_null()) { + ERR_PRINT("Can't write '" + project_file_name + "'."); + return ERR_CANT_CREATE; + }; + f->store_buffer(project_file_data.ptr(), project_file_data.size()); + } #ifdef OSX_ENABLED - if (ep.step("Code-signing dylibs", 2)) { - return ERR_SKIP; + { + if (ep.step("Code-signing dylibs", 2)) { + return ERR_SKIP; + } + Ref<DirAccess> dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); + ERR_FAIL_COND_V(dylibs_dir.is_null(), ERR_CANT_OPEN); + CodesignData codesign_data(p_preset, p_debug); + err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data); + ERR_FAIL_COND_V(err, err); } - DirAccess *dylibs_dir = DirAccess::open(dest_dir + binary_name + "/dylibs"); - ERR_FAIL_COND_V(!dylibs_dir, ERR_CANT_OPEN); - CodesignData codesign_data(p_preset, p_debug); - err = _walk_dir_recursive(dylibs_dir, _codesign, &codesign_data); - memdelete(dylibs_dir); - ERR_FAIL_COND_V(err, err); if (ep.step("Making .xcarchive", 3)) { return ERR_SKIP; @@ -1680,6 +1753,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p archive_args.push_back("-destination"); archive_args.push_back("generic/platform=iOS"); archive_args.push_back("archive"); + archive_args.push_back("-allowProvisioningUpdates"); archive_args.push_back("-archivePath"); archive_args.push_back(archive_path); String archive_str; diff --git a/platform/iphone/export/export_plugin.h b/platform/iphone/export/export_plugin.h index 756bca14dd..1db7418e3f 100644 --- a/platform/iphone/export/export_plugin.h +++ b/platform/iphone/export/export_plugin.h @@ -41,7 +41,6 @@ #include "core/templates/safe_refcount.h" #include "core/version.h" #include "editor/editor_export.h" -#include "editor/editor_node.h" #include "editor/editor_settings.h" #include "main/splash.gen.h" #include "platform/iphone/logo.gen.h" @@ -54,8 +53,6 @@ class EditorExportPlatformIOS : public EditorExportPlatform { GDCLASS(EditorExportPlatformIOS, EditorExportPlatform); - int version_code; - Ref<ImageTexture> logo; // Plugins @@ -66,7 +63,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<PluginConfigIOS> plugins; typedef Error (*FileHandler)(String p_file, void *p_userdata); - static Error _walk_dir_recursive(DirAccess *p_da, FileHandler p_handler, void *p_userdata); + static Error _walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata); static Error _codesign(String p_file, void *p_userdata); void _blend_and_rotate(Ref<Image> &p_dst, Ref<Image> &p_src, bool p_rot); @@ -130,7 +127,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { for (int i = 0; i < pname.length(); i++) { char32_t c = pname[i]; - if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { + if (!(is_ascii_alphanumeric_char(c) || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); } @@ -142,7 +139,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } static void _check_for_changes_poll_thread(void *ud) { - EditorExportPlatformIOS *ea = (EditorExportPlatformIOS *)ud; + EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud); while (!ea->quit_request.is_set()) { // Nothing to do if we already know the plugins have changed. @@ -207,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(); @@ -216,8 +213,8 @@ public: /// List the gdip files in the directory specified by the p_path parameter. static Vector<String> list_plugin_config_files(const String &p_path, bool p_check_directories) { Vector<String> dir_files; - DirAccessRef da = DirAccess::open(p_path); - if (da) { + Ref<DirAccess> da = DirAccess::open(p_path); + if (da.is_valid()) { da->list_dir_begin(); while (true) { String file = da->get_next(); diff --git a/platform/iphone/godot_iphone.mm b/platform/iphone/godot_iphone.mm index a76276e815..49474ef554 100644 --- a/platform/iphone/godot_iphone.mm +++ b/platform/iphone/godot_iphone.mm @@ -53,7 +53,7 @@ int add_path(int p_argc, char **p_args) { p_args[p_argc] = nullptr; return p_argc; -}; +} int add_cmdline(int p_argc, char **p_args) { NSArray *arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"]; @@ -67,12 +67,12 @@ int add_cmdline(int p_argc, char **p_args) { continue; } p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding]; - }; + } p_args[p_argc] = nullptr; return p_argc; -}; +} int iphone_main(int argc, char **argv, String data_dir, String cache_dir) { size_t len = strlen(argv[0]); @@ -103,7 +103,7 @@ int iphone_main(int argc, char **argv, String data_dir, String cache_dir) { char *fargv[64]; for (int i = 0; i < argc; i++) { fargv[i] = argv[i]; - }; + } fargv[argc] = nullptr; argc = add_path(argc, fargv); argc = add_cmdline(argc, fargv); @@ -119,10 +119,10 @@ int iphone_main(int argc, char **argv, String data_dir, String cache_dir) { os->initialize_modules(); return 0; -}; +} void iphone_finish() { printf("iphone_finish\n"); Main::cleanup(); delete os; -}; +} diff --git a/platform/iphone/godot_view.h b/platform/iphone/godot_view.h index 1c72a26b4a..fcb97fa63a 100644 --- a/platform/iphone/godot_view.h +++ b/platform/iphone/godot_view.h @@ -59,4 +59,9 @@ class String; - (void)stopRendering; - (void)startRendering; +- (void)godotTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)godotTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)godotTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)godotTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; + @end diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm index b90c10fa84..e48dd2e507 100644 --- a/platform/iphone/godot_view.mm +++ b/platform/iphone/godot_view.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #import "godot_view.h" + #include "core/os/keyboard.h" #include "core/string/ustring.h" #import "display_layer.h" @@ -153,6 +154,8 @@ static const float earth_gravity = 9.80665; [self initTouches]; + self.multipleTouchEnabled = YES; + // Configure and start accelerometer if (!self.motionManager) { self.motionManager = [[CMMotionManager alloc] init]; @@ -226,8 +229,9 @@ static const float earth_gravity = 9.80665; [self.displayLink setPaused:YES]; // Process all input events - while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) - ; + while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) { + // Continue. + } // We are good to go, resume the CADisplayLink [self.displayLink setPaused:NO]; @@ -336,7 +340,7 @@ static const float earth_gravity = 9.80665; } } -- (void)touchesBegan:(NSSet *)touchesSet withEvent:(UIEvent *)event { +- (void)godotTouchesBegan:(NSSet *)touchesSet withEvent:(UIEvent *)event { NSArray *tlist = [event.allTouches allObjects]; for (unsigned int i = 0; i < [tlist count]; i++) { if ([touchesSet containsObject:[tlist objectAtIndex:i]]) { @@ -349,7 +353,7 @@ static const float earth_gravity = 9.80665; } } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { +- (void)godotTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { NSArray *tlist = [event.allTouches allObjects]; for (unsigned int i = 0; i < [tlist count]; i++) { if ([touches containsObject:[tlist objectAtIndex:i]]) { @@ -363,7 +367,7 @@ static const float earth_gravity = 9.80665; } } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { +- (void)godotTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSArray *tlist = [event.allTouches allObjects]; for (unsigned int i = 0; i < [tlist count]; i++) { if ([touches containsObject:[tlist objectAtIndex:i]]) { @@ -377,7 +381,7 @@ static const float earth_gravity = 9.80665; } } -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { +- (void)godotTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { NSArray *tlist = [event.allTouches allObjects]; for (unsigned int i = 0; i < [tlist count]; i++) { if ([touches containsObject:[tlist objectAtIndex:i]]) { diff --git a/platform/iphone/godot_view_gesture_recognizer.mm b/platform/iphone/godot_view_gesture_recognizer.mm index b50ba5f942..49a92add5e 100644 --- a/platform/iphone/godot_view_gesture_recognizer.mm +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -30,6 +30,8 @@ #import "godot_view_gesture_recognizer.h" +#import "godot_view.h" + #include "core/config/project_settings.h" // Minimum distance for touches to move to fire @@ -58,12 +60,17 @@ const CGFloat kGLGestureMovementDistance = 0.5; @implementation GodotViewGestureRecognizer +- (GodotView *)godotView { + return (GodotView *)self.view; +} + - (instancetype)init { self = [super init]; self.cancelsTouchesInView = YES; self.delaysTouchesBegan = YES; self.delaysTouchesEnded = YES; + self.requiresExclusiveTouchType = NO; self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); @@ -104,7 +111,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; self.delayTimer = nil; if (self.delayedTouches) { - [self.view touchesBegan:self.delayedTouches withEvent:self.delayedEvent]; + [self.godotView godotTouchesBegan:self.delayedTouches withEvent:self.delayedEvent]; } self.delayedTouches = nil; @@ -114,6 +121,8 @@ const CGFloat kGLGestureMovementDistance = 0.5; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseBegan]; [self delayTouches:cleared andEvent:event]; + + [super touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { @@ -123,8 +132,8 @@ const CGFloat kGLGestureMovementDistance = 0.5; // We should check if movement was significant enough to fire an event // for dragging to work correctly. for (UITouch *touch in cleared) { - CGPoint from = [touch locationInView:self.view]; - CGPoint to = [touch previousLocationInView:self.view]; + CGPoint from = [touch locationInView:self.godotView]; + CGPoint to = [touch previousLocationInView:self.godotView]; CGFloat xDistance = from.x - to.x; CGFloat yDistance = from.y - to.y; @@ -133,7 +142,7 @@ const CGFloat kGLGestureMovementDistance = 0.5; // Early exit, since one of touches has moved enough to fire a drag event. if (distance > kGLGestureMovementDistance) { [self.delayTimer fire]; - [self.view touchesMoved:cleared withEvent:event]; + [self.godotView godotTouchesMoved:cleared withEvent:event]; return; } } @@ -141,26 +150,32 @@ const CGFloat kGLGestureMovementDistance = 0.5; return; } - [self.view touchesMoved:cleared withEvent:event]; + [self.godotView godotTouchesMoved:cleared withEvent:event]; + + [super touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self.delayTimer fire]; NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseEnded]; - [self.view touchesEnded:cleared withEvent:event]; + [self.godotView godotTouchesEnded:cleared withEvent:event]; + + [super touchesEnded:touches withEvent:event]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self.delayTimer fire]; - [self.view touchesCancelled:touches withEvent:event]; -}; + [self.godotView godotTouchesCancelled:touches withEvent:event]; + + [super touchesCancelled:touches withEvent:event]; +} - (NSSet *)copyClearedTouches:(NSSet *)touches phase:(UITouchPhase)phaseToSave { NSMutableSet *cleared = [touches mutableCopy]; for (UITouch *touch in touches) { - if (touch.phase != phaseToSave) { + if (touch.view != self.view || touch.phase != phaseToSave) { [cleared removeObject:touch]; } } diff --git a/platform/iphone/godot_view_renderer.mm b/platform/iphone/godot_view_renderer.mm index e49edf5b74..32477ae41c 100644 --- a/platform/iphone/godot_view_renderer.mm +++ b/platform/iphone/godot_view_renderer.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #import "godot_view_renderer.h" + #include "core/config/project_settings.h" #include "core/os/keyboard.h" #import "display_server_iphone.h" @@ -101,7 +102,7 @@ double dval = [n doubleValue]; ProjectSettings::get_singleton()->set("Info.plist/" + ukey, dval); - }; + } // do stuff } } 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 da21ad0ace..79baae028a 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -29,15 +29,115 @@ /*************************************************************************/ #include "ios.h" + #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) { NSString *title = [NSString stringWithUTF8String:p_title]; NSString *message = [NSString stringWithUTF8String:p_alert]; @@ -75,6 +175,6 @@ String iOS::get_rate_url(int p_app_id) const { printf("returning rate url %s\n", ret.utf8().get_data()); return ret; -}; +} iOS::iOS() {} diff --git a/platform/iphone/joypad_iphone.mm b/platform/iphone/joypad_iphone.mm index f45f4da5a8..9c2feeaaca 100644 --- a/platform/iphone/joypad_iphone.mm +++ b/platform/iphone/joypad_iphone.mm @@ -29,6 +29,7 @@ /*************************************************************************/ #import "joypad_iphone.h" + #include "core/config/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #include "main/main.h" @@ -139,10 +140,10 @@ void JoypadIPhone::start_processing() { for (NSNumber *key in keys) { int joy_id = [key intValue]; return joy_id; - }; + } return -1; -}; +} - (void)addiOSJoypad:(GCController *)controller { // get a new id for our controller @@ -156,7 +157,7 @@ void JoypadIPhone::start_processing() { // assign our player index if (controller.playerIndex == GCControllerPlayerIndexUnset) { controller.playerIndex = [self getFreePlayerIndex]; - }; + } // tell Godot about our new controller Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); @@ -202,8 +203,8 @@ void JoypadIPhone::start_processing() { // and remove it from our dictionary [self.connectedJoypads removeObjectForKey:key]; - }; -}; + } +} - (GCControllerPlayerIndex)getFreePlayerIndex { bool have_player_1 = false; @@ -223,9 +224,9 @@ void JoypadIPhone::start_processing() { have_player_3 = true; } else if (controller.playerIndex == GCControllerPlayerIndex4) { have_player_4 = true; - }; - }; - }; + } + } + } if (!have_player_1) { return GCControllerPlayerIndex1; @@ -237,7 +238,7 @@ void JoypadIPhone::start_processing() { return GCControllerPlayerIndex4; } else { return GCControllerPlayerIndexUnset; - }; + } } - (void)setControllerInputHandler:(GCController *)controller { @@ -285,7 +286,7 @@ void JoypadIPhone::start_processing() { gamepad.dpad.left.isPressed); Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, gamepad.dpad.right.isPressed); - }; + } if (element == gamepad.leftThumbstick) { float value = gamepad.leftThumbstick.xAxis.value; @@ -303,7 +304,7 @@ void JoypadIPhone::start_processing() { } else if (element == gamepad.rightTrigger) { float value = gamepad.rightTrigger.value; Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); - }; + } }; } else if (controller.microGamepad != nil) { // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad @@ -329,7 +330,7 @@ void JoypadIPhone::start_processing() { gamepad.dpad.down.isPressed); Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, gamepad.dpad.left.isPressed); Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, gamepad.dpad.right.isPressed); - }; + } }; } @@ -338,6 +339,6 @@ void JoypadIPhone::start_processing() { ///@TODO need to add support for controllerPausedHandler which should be a /// toggle -}; +} @end diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index aca6f5fe2b..d03403bbb4 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -52,11 +52,11 @@ private: AudioDriverCoreAudio audio_driver; - iOS *ios; + iOS *ios = nullptr; - JoypadIPhone *joypad_iphone; + JoypadIPhone *joypad_iphone = nullptr; - MainLoop *main_loop; + MainLoop *main_loop = nullptr; virtual void initialize_core() override; virtual void initialize() override; @@ -92,7 +92,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; @@ -109,6 +109,7 @@ public: virtual String get_locale() const override; virtual String get_unique_id() const override; + virtual String get_processor_name() const override; virtual void vibrate_handheld(int p_duration_ms = 500) override; diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index 8350365d88..95b06b728e 100644 --- a/platform/iphone/os_iphone.mm +++ b/platform/iphone/os_iphone.mm @@ -31,6 +31,7 @@ #ifdef IPHONE_ENABLED #include "os_iphone.h" + #import "app_delegate.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" @@ -45,6 +46,7 @@ #import <AudioToolbox/AudioServices.h> #import <UIKit/UIKit.h> #import <dlfcn.h> +#include <sys/sysctl.h> #if defined(VULKAN_ENABLED) #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" @@ -168,7 +170,7 @@ void OSIPhone::delete_main_loop() { if (main_loop) { main_loop->finalize(); memdelete(main_loop); - }; + } main_loop = nullptr; } @@ -197,17 +199,22 @@ void OSIPhone::finalize() { deinitialize_modules(); // Already gets called - // delete_main_loop(); + //delete_main_loop(); } // 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) { @@ -230,12 +237,13 @@ Error OSIPhone::get_dynamic_library_symbol_handle(void *p_library_handle, const String OSIPhone::get_name() const { return "iOS"; -}; +} String OSIPhone::get_model_name() const { String model = ios->get_model(); - if (model != "") + if (model != "") { return model; + } return OS_Unix::get_model_name(); } @@ -253,14 +261,12 @@ Error OSIPhone::shell_open(String p_uri) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; return OK; -}; +} void OSIPhone::set_user_data_dir(String p_dir) { - DirAccess *da = DirAccess::open(p_dir); - + Ref<DirAccess> da = DirAccess::open(p_dir); user_data_dir = da->get_current_dir(); printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); - memdelete(da); } String OSIPhone::get_user_data_dir() const { @@ -287,9 +293,22 @@ String OSIPhone::get_unique_id() const { return String::utf8([uuid UTF8String]); } +String OSIPhone::get_processor_name() const { + char buffer[256]; + size_t buffer_len = 256; + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &buffer_len, NULL, 0) == 0) { + return String::utf8(buffer, buffer_len); + } + ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name. Returning an empty string.")); +} + 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/iphone/view_controller.mm b/platform/iphone/view_controller.mm index e1fc645c13..4f4ef4f046 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -203,7 +203,7 @@ case DisplayServer::SCREEN_LANDSCAPE: return UIInterfaceOrientationMaskLandscapeLeft; } -}; +} - (BOOL)prefersStatusBarHidden { return YES; |