diff options
Diffstat (limited to 'platform')
128 files changed, 4226 insertions, 1115 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index e4d04f1df9..1f3bbc2350 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -56,7 +56,10 @@ if lib_arch_dir != "": if env.dev_build: lib_type_dir = "dev" elif env.debug_features: - lib_type_dir = "debug" + if env.editor_build and env["store_release"]: + lib_type_dir = "release" + else: + lib_type_dir = "debug" else: # Release lib_type_dir = "release" diff --git a/platform/android/android_input_handler.cpp b/platform/android/android_input_handler.cpp index 63045237e9..37a019eaa4 100644 --- a/platform/android/android_input_handler.cpp +++ b/platform/android/android_input_handler.cpp @@ -138,22 +138,19 @@ void AndroidInputHandler::process_key_event(int p_physical_keycode, int p_unicod } void AndroidInputHandler::_cancel_all_touch() { - _parse_all_touch(false, false, true); + _parse_all_touch(false, true); touch.clear(); } -void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index) { +void AndroidInputHandler::_parse_all_touch(bool p_pressed, bool p_canceled, bool p_double_tap) { if (touch.size()) { //end all if exist for (int i = 0; i < touch.size(); i++) { Ref<InputEventScreenTouch> ev; ev.instantiate(); - if (reset_index) { - ev->set_index(-1); - } else { - ev->set_index(touch[i].id); - } + ev->set_index(touch[i].id); ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); ev->set_position(touch[i].pos); ev->set_double_tap(p_double_tap); Input::get_singleton()->parse_input_event(ev); @@ -180,7 +177,7 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const } //send touch - _parse_all_touch(true, p_double_tap); + _parse_all_touch(true, false, p_double_tap); } break; case AMOTION_EVENT_ACTION_MOVE: { //motion @@ -257,11 +254,11 @@ void AndroidInputHandler::process_touch_event(int p_event, int p_pointer, const void AndroidInputHandler::_cancel_mouse_event_info(bool p_source_mouse_relative) { buttons_state = BitField<MouseButtonMask>(); - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, true, false, p_source_mouse_relative); mouse_event_info.valid = false; } -void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative) { +void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative) { if (!mouse_event_info.valid) { return; } @@ -278,6 +275,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even hover_prev_pos = mouse_event_info.pos; } ev->set_pressed(p_pressed); + ev->set_canceled(p_canceled); BitField<MouseButtonMask> changed_button_mask = BitField<MouseButtonMask>(buttons_state.operator int64_t() ^ event_buttons_mask.operator int64_t()); buttons_state = event_buttons_mask; @@ -289,7 +287,7 @@ void AndroidInputHandler::_parse_mouse_event_info(BitField<MouseButtonMask> even } void AndroidInputHandler::_release_mouse_event_info(bool p_source_mouse_relative) { - _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, p_source_mouse_relative); + _parse_mouse_event_info(BitField<MouseButtonMask>(), false, false, false, p_source_mouse_relative); mouse_event_info.valid = false; } @@ -318,7 +316,7 @@ void AndroidInputHandler::process_mouse_event(int p_event_action, int p_event_an mouse_event_info.valid = true; mouse_event_info.pos = p_event_pos; - _parse_mouse_event_info(event_buttons_mask, true, p_double_click, p_source_mouse_relative); + _parse_mouse_event_info(event_buttons_mask, true, false, p_double_click, p_source_mouse_relative); } break; case AMOTION_EVENT_ACTION_CANCEL: { diff --git a/platform/android/android_input_handler.h b/platform/android/android_input_handler.h index 2badd32636..42d1c228a8 100644 --- a/platform/android/android_input_handler.h +++ b/platform/android/android_input_handler.h @@ -83,13 +83,13 @@ private: void _wheel_button_click(BitField<MouseButtonMask> event_buttons_mask, const Ref<InputEventMouseButton> &ev, MouseButton wheel_button, float factor); - void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_double_click, bool p_source_mouse_relative); + void _parse_mouse_event_info(BitField<MouseButtonMask> event_buttons_mask, bool p_pressed, bool p_canceled, bool p_double_click, bool p_source_mouse_relative); void _release_mouse_event_info(bool p_source_mouse_relative = false); void _cancel_mouse_event_info(bool p_source_mouse_relative = false); - void _parse_all_touch(bool p_pressed, bool p_double_tap, bool reset_index = false); + void _parse_all_touch(bool p_pressed, bool p_canceled = false, bool p_double_tap = false); void _release_all_touch(); diff --git a/platform/android/android_keys_utils.h b/platform/android/android_keys_utils.h index 3a587dd680..bdfaaa3215 100644 --- a/platform/android/android_keys_utils.h +++ b/platform/android/android_keys_utils.h @@ -60,6 +60,7 @@ static AndroidGodotCodePair android_godot_code_pairs[] = { { AKEYCODE_DPAD_DOWN, Key::DOWN }, // (20) Directional Pad Down key. { AKEYCODE_DPAD_LEFT, Key::LEFT }, // (21) Directional Pad Left key. { AKEYCODE_DPAD_RIGHT, Key::RIGHT }, // (22) Directional Pad Right key. + { AKEYCODE_DPAD_CENTER, Key::ENTER }, // (23) Directional Pad Center key. { AKEYCODE_VOLUME_UP, Key::VOLUMEUP }, // (24) Volume Up key. { AKEYCODE_VOLUME_DOWN, Key::VOLUMEDOWN }, // (25) Volume Down key. { AKEYCODE_POWER, Key::STANDBY }, // (26) Power key. diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 5fc32132e3..51e89c720d 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -272,7 +272,8 @@ Error AudioDriverOpenSL::input_start() { return init_input_device(); } - return OK; + WARN_PRINT("Unable to start audio capture - No RECORD_AUDIO permission"); + return ERR_UNAUTHORIZED; } Error AudioDriverOpenSL::input_stop() { diff --git a/platform/android/detect.py b/platform/android/detect.py index ec36a40941..20aced3524 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -22,6 +22,8 @@ def can_build(): def get_opts(): + from SCons.Variables import BoolVariable + return [ ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), ( @@ -29,9 +31,20 @@ def get_opts(): 'Target platform (android-<api>, e.g. "android-' + str(get_min_target_api()) + '")', "android-" + str(get_min_target_api()), ), + BoolVariable("store_release", "Editor build for Google Play Store (for official builds only)", False), ] +def get_doc_classes(): + return [ + "EditorExportPlatformAndroid", + ] + + +def get_doc_path(): + return "doc_classes" + + # Return the ANDROID_SDK_ROOT environment variable. def get_env_android_sdk_root(): return os.environ.get("ANDROID_SDK_ROOT", -1) diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index b2400d81dc..ad1cbddb08 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -39,6 +39,8 @@ class RenderingDeviceVulkan; #endif class DisplayServerAndroid : public DisplayServer { + // No need to register with GDCLASS, it's platform-specific and nothing is added. + String rendering_driver; // https://developer.android.com/reference/android/view/PointerIcon diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml new file mode 100644 index 0000000000..9d68cb78f6 --- /dev/null +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -0,0 +1,590 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformAndroid" inherits="EditorExportPlatform" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for Android. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for Android">$DOCS_URL/tutorials/export/exporting_for_android.html</link> + <link title="Custom builds for Android">$DOCS_URL/tutorials/export/android_custom_build.html</link> + </tutorials> + <members> + <member name="apk_expansion/SALT" type="String" setter="" getter=""> + Array of random bytes that the licensing Policy uses to create an [url=https://developer.android.com/google/play/licensing/adding-licensing#impl-Obfuscator]Obfuscator[/url]. + </member> + <member name="apk_expansion/enable" type="bool" setter="" getter=""> + If [code]true[/code], project resources are stored in the separate APK expansion file, instead APK. + [b]Note:[/b] APK expansion should be enabled to use PCK encryption. + </member> + <member name="apk_expansion/public_key" type="String" setter="" getter=""> + Base64 encoded RSA public key for your publisher account, available from the profile page on the "Play Console". + </member> + <member name="architectures/arm64-v8a" type="bool" setter="" getter=""> + If [code]true[/code], [code]arm64[/code] binaries are included into exported project. + </member> + <member name="architectures/armeabi-v7a" type="bool" setter="" getter=""> + If [code]true[/code], [code]arm32[/code] binaries are included into exported project. + </member> + <member name="architectures/x86" type="bool" setter="" getter=""> + If [code]true[/code], [code]x86_32[/code] binaries are included into exported project. + </member> + <member name="architectures/x86_64" type="bool" setter="" getter=""> + If [code]true[/code], [code]x86_64[/code] binaries are included into exported project. + </member> + <member name="command_line/extra_args" type="String" setter="" getter=""> + A list of additional command line arguments, exported project will receive when started. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="gradle_build/export_format" type="int" setter="" getter=""> + Export format for Gradle build. + </member> + <member name="gradle_build/min_sdk" type="String" setter="" getter=""> + Minimal Android SDK version for Gradle build. + </member> + <member name="gradle_build/target_sdk" type="String" setter="" getter=""> + Target Android SDK version for Gradle build. + </member> + <member name="gradle_build/use_gradle_build" type="bool" setter="" getter=""> + If [code]true[/code], Gradle build is used instead of pre-built APK. + </member> + <member name="graphics/opengl_debug" type="bool" setter="" getter=""> + If [code]true[/code], OpenGL ES debug context will be created (additional runtime checking, validation, and logging). + </member> + <member name="keystore/debug" type="String" setter="" getter=""> + Path of the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PATH[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore[/code] if empty. + </member> + <member name="keystore/debug_password" type="String" setter="" getter=""> + Password for the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_pass[/code] if both it and [member keystore/debug] are empty. + </member> + <member name="keystore/debug_user" type="String" setter="" getter=""> + User name for the debug keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_DEBUG_USER[/code]. + Fallbacks to [code]EditorSettings.export/android/debug_keystore_user[/code] if both it and [member keystore/debug] are empty. + </member> + <member name="keystore/release" type="String" setter="" getter=""> + Path of the release keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_PATH[/code]. + </member> + <member name="keystore/release_password" type="String" setter="" getter=""> + Password for the release keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD[/code]. + </member> + <member name="keystore/release_user" type="String" setter="" getter=""> + User name for the release keystore file. + Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_USER[/code]. + </member> + <member name="launcher_icons/adaptive_background_432x432" type="String" setter="" getter=""> + Background layer of the application adaptive icon file. + </member> + <member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter=""> + Foreground layer of the application adaptive icon file. + </member> + <member name="launcher_icons/main_192x192" type="String" setter="" getter=""> + Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon]. + </member> + <member name="package/app_category" type="int" setter="" getter=""> + Application category for the Play Store. + </member> + <member name="package/exclude_from_recents" type="bool" setter="" getter=""> + If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications. + </member> + <member name="package/name" type="String" setter="" getter=""> + Name of the application. + </member> + <member name="package/retain_data_on_uninstall" type="bool" setter="" getter=""> + If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. + </member> + <member name="package/signed" type="bool" setter="" getter=""> + If [code]true[/code], package signing is enabled. + </member> + <member name="package/unique_name" type="String" setter="" getter=""> + Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]). + </member> + <member name="permissions/access_checkin_properties" type="bool" setter="" getter=""> + Allows read/write access to the "properties" table in the checkin database. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_CHECKIN_PROPERTIES]ACCESS_CHECKIN_PROPERTIES[/url]. + </member> + <member name="permissions/access_coarse_location" type="bool" setter="" getter=""> + Allows access to the approximate location information. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_COARSE_LOCATION]ACCESS_COARSE_LOCATION[/url]. + </member> + <member name="permissions/access_fine_location" type="bool" setter="" getter=""> + Allows access to the precise location information. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_FINE_LOCATION]ACCESS_FINE_LOCATION[/url]. + </member> + <member name="permissions/access_location_extra_commands" type="bool" setter="" getter=""> + Allows access to the extra location provider commands. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_LOCATION_EXTRA_COMMANDS]ACCESS_LOCATION_EXTRA_COMMANDS[/url]. + </member> + <member name="permissions/access_mock_location" type="bool" setter="" getter=""> + Allows an application to create mock location providers for testing. + </member> + <member name="permissions/access_network_state" type="bool" setter="" getter=""> + Allows access to the information about networks. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_NETWORK_STATE]ACCESS_NETWORK_STATE[/url]. + </member> + <member name="permissions/access_surface_flinger" type="bool" setter="" getter=""> + Allows an application to use SurfaceFlinger's low level features. + </member> + <member name="permissions/access_wifi_state" type="bool" setter="" getter=""> + Allows access to the information about Wi-Fi networks. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCESS_WIFI_STATE]ACCESS_WIFI_STATE[/url]. + </member> + <member name="permissions/account_manager" type="bool" setter="" getter=""> + Allows applications to call into AccountAuthenticators. See [url=https://developer.android.com/reference/android/Manifest.permission#ACCOUNT_MANAGER]ACCOUNT_MANAGER[/url]. + </member> + <member name="permissions/add_voicemail" type="bool" setter="" getter=""> + Allows an application to add voicemails into the system. See [url=https://developer.android.com/reference/android/Manifest.permission#ADD_VOICEMAIL]ADD_VOICEMAIL[/url]. + </member> + <member name="permissions/authenticate_accounts" type="bool" setter="" getter=""> + Allows an application to act as an AccountAuthenticator for the AccountManager. + </member> + <member name="permissions/battery_stats" type="bool" setter="" getter=""> + Allows an application to collect battery statistics. Sett [url=https://developer.android.com/reference/android/Manifest.permission#BATTERY_STATS]BATTERY_STATS[/url]. + </member> + <member name="permissions/bind_accessibility_service" type="bool" setter="" getter=""> + Must be required by an AccessibilityService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_ACCESSIBILITY_SERVICE]BIND_ACCESSIBILITY_SERVICE[/url]. + </member> + <member name="permissions/bind_appwidget" type="bool" setter="" getter=""> + Allows an application to tell the AppWidget service which application can access AppWidget's data. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_APPWIDGET]BIND_APPWIDGET[/url]. + </member> + <member name="permissions/bind_device_admin" type="bool" setter="" getter=""> + Must be required by device administration receiver, to ensure that only the system can interact with it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_DEVICE_ADMIN]BIND_DEVICE_ADMIN[/url]. + </member> + <member name="permissions/bind_input_method" type="bool" setter="" getter=""> + Must be required by an InputMethodService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_INPUT_METHOD]BIND_INPUT_METHOD[/url]. + </member> + <member name="permissions/bind_nfc_service" type="bool" setter="" getter=""> + Must be required by a HostApduService or OffHostApduService to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_NFC_SERVICE]BIND_NFC_SERVICE[/url]. + </member> + <member name="permissions/bind_notification_listener_service" type="bool" setter="" getter=""> + Must be required by a NotificationListenerService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE]BIND_NOTIFICATION_LISTENER_SERVICE[/url]. + </member> + <member name="permissions/bind_print_service" type="bool" setter="" getter=""> + Must be required by a PrintService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_PRINT_SERVICE]BIND_PRINT_SERVICE[/url]. + </member> + <member name="permissions/bind_remoteviews" type="bool" setter="" getter=""> + Must be required by a RemoteViewsService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_REMOTEVIEWS]BIND_REMOTEVIEWS[/url]. + </member> + <member name="permissions/bind_text_service" type="bool" setter="" getter=""> + Must be required by a TextService (e.g. SpellCheckerService) to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_TEXT_SERVICE]BIND_TEXT_SERVICE[/url]. + </member> + <member name="permissions/bind_vpn_service" type="bool" setter="" getter=""> + Must be required by a VpnService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_VPN_SERVICE]BIND_VPN_SERVICE[/url]. + </member> + <member name="permissions/bind_wallpaper" type="bool" setter="" getter=""> + Must be required by a WallpaperService, to ensure that only the system can bind to it. See [url=https://developer.android.com/reference/android/Manifest.permission#BIND_WALLPAPER]BIND_WALLPAPER[/url]. + </member> + <member name="permissions/bluetooth" type="bool" setter="" getter=""> + Allows applications to connect to paired bluetooth devices. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH]BLUETOOTH[/url]. + </member> + <member name="permissions/bluetooth_admin" type="bool" setter="" getter=""> + Allows applications to discover and pair bluetooth devices. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_ADMIN]BLUETOOTH_ADMIN[/url]. + </member> + <member name="permissions/bluetooth_privileged" type="bool" setter="" getter=""> + Allows applications to pair bluetooth devices without user interaction, and to allow or disallow phonebook access or message access. See [url=https://developer.android.com/reference/android/Manifest.permission#BLUETOOTH_PRIVILEGED]BLUETOOTH_PRIVILEGED[/url]. + </member> + <member name="permissions/brick" type="bool" setter="" getter=""> + Required to be able to disable the device (very dangerous!). + </member> + <member name="permissions/broadcast_package_removed" type="bool" setter="" getter=""> + Allows an application to broadcast a notification that an application package has been removed. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_PACKAGE_REMOVED]BROADCAST_PACKAGE_REMOVED[/url]. + </member> + <member name="permissions/broadcast_sms" type="bool" setter="" getter=""> + Allows an application to broadcast an SMS receipt notification. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_SMS]BROADCAST_SMS[/url]. + </member> + <member name="permissions/broadcast_sticky" type="bool" setter="" getter=""> + Allows an application to broadcast sticky intents. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_STICKY]BROADCAST_STICKY[/url]. + </member> + <member name="permissions/broadcast_wap_push" type="bool" setter="" getter=""> + Allows an application to broadcast a WAP PUSH receipt notification. See [url=https://developer.android.com/reference/android/Manifest.permission#BROADCAST_WAP_PUSH]BROADCAST_WAP_PUSH[/url]. + </member> + <member name="permissions/call_phone" type="bool" setter="" getter=""> + Allows an application to initiate a phone call without going through the Dialer user interface. See [url=https://developer.android.com/reference/android/Manifest.permission#CALL_PHONE]CALL_PHONE[/url]. + </member> + <member name="permissions/call_privileged" type="bool" setter="" getter=""> + Allows an application to call any phone number, including emergency numbers, without going through the Dialer user interface. See [url=https://developer.android.com/reference/android/Manifest.permission#CALL_PRIVILEGED]CALL_PRIVILEGED[/url]. + </member> + <member name="permissions/camera" type="bool" setter="" getter=""> + Required to be able to access the camera device. See [url=https://developer.android.com/reference/android/Manifest.permission#CAMERA]CAMERA[/url]. + </member> + <member name="permissions/capture_audio_output" type="bool" setter="" getter=""> + Allows an application to capture audio output. See [url=https://developer.android.com/reference/android/Manifest.permission#CAPTURE_AUDIO_OUTPUT]CAPTURE_AUDIO_OUTPUT[/url]. + </member> + <member name="permissions/capture_secure_video_output" type="bool" setter="" getter=""> + Allows an application to capture secure video output. + </member> + <member name="permissions/capture_video_output" type="bool" setter="" getter=""> + Allows an application to capture video output. + </member> + <member name="permissions/change_component_enabled_state" type="bool" setter="" getter=""> + Allows an application to change whether an application component (other than its own) is enabled or not. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_COMPONENT_ENABLED_STATE]CHANGE_COMPONENT_ENABLED_STATE[/url]. + </member> + <member name="permissions/change_configuration" type="bool" setter="" getter=""> + Allows an application to modify the current configuration, such as locale. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_CONFIGURATION]CHANGE_CONFIGURATION[/url]. + </member> + <member name="permissions/change_network_state" type="bool" setter="" getter=""> + Allows applications to change network connectivity state. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_NETWORK_STATE]CHANGE_NETWORK_STATE[/url]. + </member> + <member name="permissions/change_wifi_multicast_state" type="bool" setter="" getter=""> + Allows applications to enter Wi-Fi Multicast mode. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_MULTICAST_STATE]CHANGE_WIFI_MULTICAST_STATE[/url]. + </member> + <member name="permissions/change_wifi_state" type="bool" setter="" getter=""> + Allows applications to change Wi-Fi connectivity state. See [url=https://developer.android.com/reference/android/Manifest.permission#CHANGE_WIFI_STATE]CHANGE_WIFI_STATE[/url]. + </member> + <member name="permissions/clear_app_cache" type="bool" setter="" getter=""> + Allows an application to clear the caches of all installed applications on the device. See [url=https://developer.android.com/reference/android/Manifest.permission#CLEAR_APP_CACHE]CLEAR_APP_CACHE[/url]. + </member> + <member name="permissions/clear_app_user_data" type="bool" setter="" getter=""> + Allows an application to clear user data. + </member> + <member name="permissions/control_location_updates" type="bool" setter="" getter=""> + Allows enabling/disabling location update notifications from the radio. See [url=https://developer.android.com/reference/android/Manifest.permission#CONTROL_LOCATION_UPDATES]CONTROL_LOCATION_UPDATES[/url]. + </member> + <member name="permissions/custom_permissions" type="PackedStringArray" setter="" getter=""> + Array of custom permission strings. + </member> + <member name="permissions/delete_cache_files" type="bool" setter="" getter=""> + Deprecated. + </member> + <member name="permissions/delete_packages" type="bool" setter="" getter=""> + Allows an application to delete packages. See [url=https://developer.android.com/reference/android/Manifest.permission#DELETE_PACKAGES]DELETE_PACKAGES[/url]. + </member> + <member name="permissions/device_power" type="bool" setter="" getter=""> + Allows low-level access to power management. + </member> + <member name="permissions/diagnostic" type="bool" setter="" getter=""> + Allows applications to RW to diagnostic resources. See [url=https://developer.android.com/reference/android/Manifest.permission#DIAGNOSTIC]DIAGNOSTIC[/url]. + </member> + <member name="permissions/disable_keyguard" type="bool" setter="" getter=""> + Allows applications to disable the keyguard if it is not secure. See [url=https://developer.android.com/reference/android/Manifest.permission#DISABLE_KEYGUARD]DISABLE_KEYGUARD[/url]. + </member> + <member name="permissions/dump" type="bool" setter="" getter=""> + Allows an application to retrieve state dump information from system services. See [url=https://developer.android.com/reference/android/Manifest.permission#DUMP]DUMP[/url]. + </member> + <member name="permissions/expand_status_bar" type="bool" setter="" getter=""> + Allows an application to expand or collapse the status bar. See [url=https://developer.android.com/reference/android/Manifest.permission#EXPAND_STATUS_BAR]EXPAND_STATUS_BAR[/url]. + </member> + <member name="permissions/factory_test" type="bool" setter="" getter=""> + Run as a manufacturer test application, running as the root user. See [url=https://developer.android.com/reference/android/Manifest.permission#FACTORY_TEST]FACTORY_TEST[/url]. + </member> + <member name="permissions/flashlight" type="bool" setter="" getter=""> + Allows access to the flashlight. + </member> + <member name="permissions/force_back" type="bool" setter="" getter=""> + Allows an application to force a BACK operation on whatever is the top activity. + </member> + <member name="permissions/get_accounts" type="bool" setter="" getter=""> + Allows access to the list of accounts in the Accounts Service. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_ACCOUNTS]GET_ACCOUNTS[/url]. + </member> + <member name="permissions/get_package_size" type="bool" setter="" getter=""> + Allows an application to find out the space used by any package. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_PACKAGE_SIZE]GET_PACKAGE_SIZE[/url]. + </member> + <member name="permissions/get_tasks" type="bool" setter="" getter=""> + Deprecated in API level 21. + </member> + <member name="permissions/get_top_activity_info" type="bool" setter="" getter=""> + Allows an application to retrieve private information about the current top activity. + </member> + <member name="permissions/global_search" type="bool" setter="" getter=""> + Used on content providers to allow the global search system to access their data. See [url=https://developer.android.com/reference/android/Manifest.permission#GLOBAL_SEARCH]GLOBAL_SEARCH[/url]. + </member> + <member name="permissions/hardware_test" type="bool" setter="" getter=""> + Allows access to hardware peripherals. + </member> + <member name="permissions/inject_events" type="bool" setter="" getter=""> + Allows an application to inject user events (keys, touch, trackball) into the event stream and deliver them to ANY window. + </member> + <member name="permissions/install_location_provider" type="bool" setter="" getter=""> + Allows an application to install a location provider into the Location Manager. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_LOCATION_PROVIDER]INSTALL_LOCATION_PROVIDER[/url]. + </member> + <member name="permissions/install_packages" type="bool" setter="" getter=""> + Allows an application to install packages. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_PACKAGES]INSTALL_PACKAGES[/url]. + </member> + <member name="permissions/install_shortcut" type="bool" setter="" getter=""> + Allows an application to install a shortcut in Launcher. See [url=https://developer.android.com/reference/android/Manifest.permission#INSTALL_SHORTCUT]INSTALL_SHORTCUT[/url]. + </member> + <member name="permissions/internal_system_window" type="bool" setter="" getter=""> + Allows an application to open windows that are for use by parts of the system user interface. + </member> + <member name="permissions/internet" type="bool" setter="" getter=""> + Allows applications to open network sockets. See [url=https://developer.android.com/reference/android/Manifest.permission#INTERNET]INTERNET[/url]. + </member> + <member name="permissions/kill_background_processes" type="bool" setter="" getter=""> + Allows an application to call ActivityManager.killBackgroundProcesses(String). See [url=https://developer.android.com/reference/android/Manifest.permission#KILL_BACKGROUND_PROCESSES]KILL_BACKGROUND_PROCESSES[/url]. + </member> + <member name="permissions/location_hardware" type="bool" setter="" getter=""> + Allows an application to use location features in hardware, such as the geofencing api. See [url=https://developer.android.com/reference/android/Manifest.permission#LOCATION_HARDWARE]LOCATION_HARDWARE[/url]. + </member> + <member name="permissions/manage_accounts" type="bool" setter="" getter=""> + Allows an application to manage the list of accounts in the AccountManager. + </member> + <member name="permissions/manage_app_tokens" type="bool" setter="" getter=""> + Allows an application to manage (create, destroy, Z-order) application tokens in the window manager. + </member> + <member name="permissions/manage_documents" type="bool" setter="" getter=""> + Allows an application to manage access to documents, usually as part of a document picker. See [url=https://developer.android.com/reference/android/Manifest.permission#MANAGE_DOCUMENTS]MANAGE_DOCUMENTS[/url]. + </member> + <member name="permissions/manage_external_storage" type="bool" setter="" getter=""> + Allows an application a broad access to external storage in scoped storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MANAGE_EXTERNAL_STORAGE]MANAGE_EXTERNAL_STORAGE[/url]. + </member> + <member name="permissions/master_clear" type="bool" setter="" getter=""> + See [url=https://developer.android.com/reference/android/Manifest.permission#MASTER_CLEAR]MASTER_CLEAR[/url]. + </member> + <member name="permissions/media_content_control" type="bool" setter="" getter=""> + Allows an application to know what content is playing and control its playback. See [url=https://developer.android.com/reference/android/Manifest.permission#MEDIA_CONTENT_CONTROL]MEDIA_CONTENT_CONTROL[/url]. + </member> + <member name="permissions/modify_audio_settings" type="bool" setter="" getter=""> + Allows an application to modify global audio settings. See [url=https://developer.android.com/reference/android/Manifest.permission#MODIFY_AUDIO_SETTINGS]MODIFY_AUDIO_SETTINGS[/url]. + </member> + <member name="permissions/modify_phone_state" type="bool" setter="" getter=""> + Allows modification of the telephony state - power on, mmi, etc. Does not include placing calls. See [url=https://developer.android.com/reference/android/Manifest.permission#MODIFY_PHONE_STATE]MODIFY_PHONE_STATE[/url]. + </member> + <member name="permissions/mount_format_filesystems" type="bool" setter="" getter=""> + Allows formatting file systems for removable storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MOUNT_FORMAT_FILESYSTEMS]MOUNT_FORMAT_FILESYSTEMS[/url]. + </member> + <member name="permissions/mount_unmount_filesystems" type="bool" setter="" getter=""> + Allows mounting and unmounting file systems for removable storage. See [url=https://developer.android.com/reference/android/Manifest.permission#MOUNT_UNMOUNT_FILESYSTEMS]MOUNT_UNMOUNT_FILESYSTEMS[/url]. + </member> + <member name="permissions/nfc" type="bool" setter="" getter=""> + Allows applications to perform I/O operations over NFC. See [url=https://developer.android.com/reference/android/Manifest.permission#NFC]NFC[/url]. + </member> + <member name="permissions/persistent_activity" type="bool" setter="" getter=""> + Allow an application to make its activities persistent. + Deprecated in API level 15. + </member> + <member name="permissions/process_outgoing_calls" type="bool" setter="" getter=""> + Allows an application to see the number being dialed during an outgoing call with the option to redirect the call to a different number or abort the call altogether. See [url=https://developer.android.com/reference/android/Manifest.permission#PROCESS_OUTGOING_CALLS]PROCESS_OUTGOING_CALLS[/url]. + Deprecated in API level 29. + </member> + <member name="permissions/read_calendar" type="bool" setter="" getter=""> + Allows an application to read the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALENDAR]READ_CALENDAR[/url]. + </member> + <member name="permissions/read_call_log" type="bool" setter="" getter=""> + Allows an application to read the user's call log. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALL_LOG]READ_CALL_LOG[/url]. + </member> + <member name="permissions/read_contacts" type="bool" setter="" getter=""> + Allows an application to read the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CONTACTS]READ_CONTACTS[/url]. + </member> + <member name="permissions/read_external_storage" type="bool" setter="" getter=""> + Allows an application to read from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE]READ_EXTERNAL_STORAGE[/url]. + Deprecated in API level 33. + </member> + <member name="permissions/read_frame_buffer" type="bool" setter="" getter=""> + Allows an application to take screen shots and more generally get access to the frame buffer data. + </member> + <member name="permissions/read_history_bookmarks" type="bool" setter="" getter=""> + Allows an application to read (but not write) the user's browsing history and bookmarks. + </member> + <member name="permissions/read_input_state" type="bool" setter="" getter=""> + Deprecated in API level 16. + </member> + <member name="permissions/read_logs" type="bool" setter="" getter=""> + Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url]. + </member> + <member name="permissions/read_phone_state" type="bool" setter="" getter=""> + Allows read only access to phone state. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE]READ_PHONE_STATE[/url]. + </member> + <member name="permissions/read_profile" type="bool" setter="" getter=""> + Allows an application to read the user's personal profile data. + </member> + <member name="permissions/read_sms" type="bool" setter="" getter=""> + Allows an application to read SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SMS]READ_SMS[/url]. + </member> + <member name="permissions/read_social_stream" type="bool" setter="" getter=""> + Allows an application to read from the user's social stream. + </member> + <member name="permissions/read_sync_settings" type="bool" setter="" getter=""> + Allows applications to read the sync settings. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SYNC_SETTINGS]READ_SYNC_SETTINGS[/url]. + </member> + <member name="permissions/read_sync_stats" type="bool" setter="" getter=""> + Allows applications to read the sync stats. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_SYNC_STATS]READ_SYNC_STATS[/url]. + </member> + <member name="permissions/read_user_dictionary" type="bool" setter="" getter=""> + Allows an application to read the user dictionary. + </member> + <member name="permissions/reboot" type="bool" setter="" getter=""> + Required to be able to reboot the device. See [url=https://developer.android.com/reference/android/Manifest.permission#REBOOT]REBOOT[/url]. + </member> + <member name="permissions/receive_boot_completed" type="bool" setter="" getter=""> + Allows an application to receive the Intent.ACTION_BOOT_COMPLETED that is broadcast after the system finishes booting. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_BOOT_COMPLETED]RECEIVE_BOOT_COMPLETED[/url]. + </member> + <member name="permissions/receive_mms" type="bool" setter="" getter=""> + Allows an application to monitor incoming MMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_MMS]RECEIVE_MMS[/url]. + </member> + <member name="permissions/receive_sms" type="bool" setter="" getter=""> + Allows an application to receive SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_SMS]RECEIVE_SMS[/url]. + </member> + <member name="permissions/receive_wap_push" type="bool" setter="" getter=""> + Allows an application to receive WAP push messages. See [url=https://developer.android.com/reference/android/Manifest.permission#RECEIVE_WAP_PUSH]RECEIVE_WAP_PUSH[/url]. + </member> + <member name="permissions/record_audio" type="bool" setter="" getter=""> + Allows an application to record audio. See [url=https://developer.android.com/reference/android/Manifest.permission#RECORD_AUDIO]RECORD_AUDIO[/url]. + </member> + <member name="permissions/reorder_tasks" type="bool" setter="" getter=""> + Allows an application to change the Z-order of tasks. See [url=https://developer.android.com/reference/android/Manifest.permission#REORDER_TASKS]REORDER_TASKS[/url]. + </member> + <member name="permissions/restart_packages" type="bool" setter="" getter=""> + Deprecated in API level 15. + </member> + <member name="permissions/send_respond_via_message" type="bool" setter="" getter=""> + Allows an application (Phone) to send a request to other applications to handle the respond-via-message action during incoming calls. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_RESPOND_VIA_MESSAGE]SEND_RESPOND_VIA_MESSAGE[/url]. + </member> + <member name="permissions/send_sms" type="bool" setter="" getter=""> + Allows an application to send SMS messages. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_SMS]SEND_SMS[/url]. + </member> + <member name="permissions/set_activity_watcher" type="bool" setter="" getter=""> + Allows an application to watch and control how activities are started globally in the system. + </member> + <member name="permissions/set_alarm" type="bool" setter="" getter=""> + Allows an application to broadcast an Intent to set an alarm for the user. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ALARM]SET_ALARM[/url]. + </member> + <member name="permissions/set_always_finish" type="bool" setter="" getter=""> + Allows an application to control whether activities are immediately finished when put in the background. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ALWAYS_FINISH]SET_ALWAYS_FINISH[/url]. + </member> + <member name="permissions/set_animation_scale" type="bool" setter="" getter=""> + Allows to modify the global animation scaling factor. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_ANIMATION_SCALE]SET_ANIMATION_SCALE[/url]. + </member> + <member name="permissions/set_debug_app" type="bool" setter="" getter=""> + Configure an application for debugging. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_DEBUG_APP]SET_DEBUG_APP[/url]. + </member> + <member name="permissions/set_orientation" type="bool" setter="" getter=""> + Allows low-level access to setting the orientation (actually rotation) of the screen. + </member> + <member name="permissions/set_pointer_speed" type="bool" setter="" getter=""> + Allows low-level access to setting the pointer speed. + </member> + <member name="permissions/set_preferred_applications" type="bool" setter="" getter=""> + Deprecated in API level 15. + </member> + <member name="permissions/set_process_limit" type="bool" setter="" getter=""> + Allows an application to set the maximum number of (not needed) application processes that can be running. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_PROCESS_LIMIT]SET_PROCESS_LIMIT[/url]. + </member> + <member name="permissions/set_time" type="bool" setter="" getter=""> + Allows applications to set the system time directly. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_TIME]SET_TIME[/url]. + </member> + <member name="permissions/set_time_zone" type="bool" setter="" getter=""> + Allows applications to set the system time zone directly. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_TIME_ZONE]SET_TIME_ZONE[/url]. + </member> + <member name="permissions/set_wallpaper" type="bool" setter="" getter=""> + Allows applications to set the wallpaper. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_WALLPAPER]SET_WALLPAPER[/url]. + </member> + <member name="permissions/set_wallpaper_hints" type="bool" setter="" getter=""> + Allows applications to set the wallpaper hints. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_WALLPAPER_HINTS]SET_WALLPAPER_HINTS[/url]. + </member> + <member name="permissions/signal_persistent_processes" type="bool" setter="" getter=""> + Allow an application to request that a signal be sent to all persistent processes. See [url=https://developer.android.com/reference/android/Manifest.permission#SIGNAL_PERSISTENT_PROCESSES]SIGNAL_PERSISTENT_PROCESSES[/url]. + </member> + <member name="permissions/status_bar" type="bool" setter="" getter=""> + Allows an application to open, close, or disable the status bar and its icons. See [url=https://developer.android.com/reference/android/Manifest.permission#STATUS_BAR]STATUS_BAR[/url]. + </member> + <member name="permissions/subscribed_feeds_read" type="bool" setter="" getter=""> + Allows an application to allow access the subscribed feeds ContentProvider. + </member> + <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter=""> + Deprecated. + </member> + <member name="permissions/system_alert_window" type="bool" setter="" getter=""> + Allows an app to create windows using the type WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, shown on top of all other apps. See [url=https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW]SYSTEM_ALERT_WINDOW[/url]. + </member> + <member name="permissions/transmit_ir" type="bool" setter="" getter=""> + Allows using the device's IR transmitter, if available. See [url=https://developer.android.com/reference/android/Manifest.permission#TRANSMIT_IR]TRANSMIT_IR[/url]. + </member> + <member name="permissions/uninstall_shortcut" type="bool" setter="" getter=""> + Deprecated. + </member> + <member name="permissions/update_device_stats" type="bool" setter="" getter=""> + Allows an application to update device statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#UPDATE_DEVICE_STATS]UPDATE_DEVICE_STATS[/url]. + </member> + <member name="permissions/use_credentials" type="bool" setter="" getter=""> + Allows an application to request authtokens from the AccountManager. + </member> + <member name="permissions/use_sip" type="bool" setter="" getter=""> + Allows an application to use SIP service. See [url=https://developer.android.com/reference/android/Manifest.permission#USE_SIP]USE_SIP[/url]. + </member> + <member name="permissions/vibrate" type="bool" setter="" getter=""> + Allows access to the vibrator. See [url=https://developer.android.com/reference/android/Manifest.permission#VIBRATE]VIBRATE[/url]. + </member> + <member name="permissions/wake_lock" type="bool" setter="" getter=""> + Allows using PowerManager WakeLocks to keep processor from sleeping or screen from dimming. See [url=https://developer.android.com/reference/android/Manifest.permission#WAKE_LOCK]WAKE_LOCK[/url]. + </member> + <member name="permissions/write_apn_settings" type="bool" setter="" getter=""> + Allows applications to write the apn settings and read sensitive fields of an existing apn settings like user and password. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_APN_SETTINGS]WRITE_APN_SETTINGS[/url]. + </member> + <member name="permissions/write_calendar" type="bool" setter="" getter=""> + Allows an application to write the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CALENDAR]WRITE_CALENDAR[/url]. + </member> + <member name="permissions/write_call_log" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's call log data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CALL_LOG]WRITE_CALL_LOG[/url]. + </member> + <member name="permissions/write_contacts" type="bool" setter="" getter=""> + Allows an application to write the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_CONTACTS]WRITE_CONTACTS[/url]. + </member> + <member name="permissions/write_external_storage" type="bool" setter="" getter=""> + Allows an application to write to external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE]WRITE_EXTERNAL_STORAGE[/url]. + </member> + <member name="permissions/write_gservices" type="bool" setter="" getter=""> + Allows an application to modify the Google service map. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_GSERVICES]WRITE_GSERVICES[/url]. + </member> + <member name="permissions/write_history_bookmarks" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's browsing history and bookmarks. + </member> + <member name="permissions/write_profile" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's personal profile data. + </member> + <member name="permissions/write_secure_settings" type="bool" setter="" getter=""> + Allows an application to read or write the secure system settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SECURE_SETTINGS]WRITE_SECURE_SETTINGS[/url]. + </member> + <member name="permissions/write_settings" type="bool" setter="" getter=""> + Allows an application to read or write the system settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SETTINGS]WRITE_SETTINGS[/url]. + </member> + <member name="permissions/write_sms" type="bool" setter="" getter=""> + Allows an application to write SMS messages. + </member> + <member name="permissions/write_social_stream" type="bool" setter="" getter=""> + Allows an application to write (but not read) the user's social stream data. + </member> + <member name="permissions/write_sync_settings" type="bool" setter="" getter=""> + Allows applications to write the sync settings. See [url=https://developer.android.com/reference/android/Manifest.permission#WRITE_SYNC_SETTINGS]WRITE_SYNC_SETTINGS[/url]. + </member> + <member name="permissions/write_user_dictionary" type="bool" setter="" getter=""> + Allows an application to write to the user dictionary. + </member> + <member name="screen/immersive_mode" type="bool" setter="" getter=""> + If [code]true[/code], hides navigation and status bar. + </member> + <member name="screen/support_large" type="bool" setter="" getter=""> + Indicates whether the application supports larger screen form-factors. + </member> + <member name="screen/support_normal" type="bool" setter="" getter=""> + Indicates whether an application supports the "normal" screen form-factors. + </member> + <member name="screen/support_small" type="bool" setter="" getter=""> + Indicates whether the application supports smaller screen form-factors. + </member> + <member name="screen/support_xlarge" type="bool" setter="" getter=""> + Indicates whether the application supports extra large screen form-factors. + </member> + <member name="user_data_backup/allow" type="bool" setter="" getter=""> + If [code]true[/code], allows the application to participate in the backup and restore infrastructure. + </member> + <member name="version/code" type="int" setter="" getter=""> + Machine-readable application version. + </member> + <member name="version/name" type="String" setter="" getter=""> + Application version visible to the user. + </member> + <member name="xr_features/hand_tracking" type="int" setter="" getter=""> + </member> + <member name="xr_features/hand_tracking_frequency" type="int" setter="" getter=""> + </member> + <member name="xr_features/passthrough" type="int" setter="" getter=""> + </member> + <member name="xr_features/xr_mode" type="int" setter="" getter=""> + </member> + </members> +</class> diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index d7fe89d97d..1e048100d1 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -35,6 +35,10 @@ #include "editor/export/editor_export.h" #include "export_plugin.h" +void register_android_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformAndroid); +} + void register_android_exporter() { #ifndef ANDROID_ENABLED EDITOR_DEF("export/android/android_sdk_path", ""); diff --git a/platform/android/export/export.h b/platform/android/export/export.h index 7ada91aec9..bd689a400d 100644 --- a/platform/android/export/export.h +++ b/platform/android/export/export.h @@ -31,6 +31,7 @@ #ifndef ANDROID_EXPORT_H #define ANDROID_EXPORT_H +void register_android_exporter_types(); void register_android_exporter(); #endif // ANDROID_EXPORT_H diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 641258a26c..71b8a2b09b 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -253,7 +253,7 @@ static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/instal static const int OPENGL_MIN_SDK_VERSION = 21; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' static const int VULKAN_MIN_SDK_VERSION = 24; -static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' +static const int DEFAULT_TARGET_SDK_VERSION = 33; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' #ifndef ANDROID_ENABLED void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { @@ -1066,8 +1066,13 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p if (_uses_vulkan()) { // Require vulkan hardware level 1 support feature_names.push_back("android.hardware.vulkan.level"); - feature_required_list.push_back(true); + feature_required_list.push_back(false); feature_versions.push_back(1); + + // Require vulkan version 1.0 + feature_names.push_back("android.hardware.vulkan.version"); + feature_required_list.push_back(true); + feature_versions.push_back(0x400003); // Encoded value for api version 1.0 } if (feature_names.size() > 0) { @@ -1698,16 +1703,109 @@ void EditorExportPlatformAndroid::get_preset_features(const Ref<EditorExportPres } } -void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) { +String EditorExportPlatformAndroid::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + if (p_preset) { + if (p_name == ("apk_expansion/public_key")) { + bool apk_expansion = p_preset->get("apk_expansion/enable"); + String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); + if (apk_expansion && apk_expansion_pkey.is_empty()) { + return TTR("Invalid public key for APK expansion."); + } + } else if (p_name == "package/unique_name") { + String pn = p_preset->get("package/unique_name"); + String pn_err; + + if (!is_package_name_valid(pn, &pn_err)) { + return TTR("Invalid package name:") + " " + pn_err; + } + } else if (p_name == "gradle_build/use_gradle_build") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(Ref<EditorExportPreset>(p_preset))); + if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { + return TTR("\"Use Gradle Build\" must be enabled to use the plugins."); + } + } else if (p_name == "xr_features/xr_mode") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { + return TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); + } + } else if (p_name == "xr_features/hand_tracking") { + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + int hand_tracking = p_preset->get("xr_features/hand_tracking"); + if (xr_mode_index != XR_MODE_OPENXR) { + if (hand_tracking > XR_HAND_TRACKING_NONE) { + return TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); + } + } + } else if (p_name == "xr_features/passthrough") { + int xr_mode_index = p_preset->get("xr_features/xr_mode"); + int passthrough_mode = p_preset->get("xr_features/passthrough"); + if (xr_mode_index != XR_MODE_OPENXR) { + if (passthrough_mode > XR_PASSTHROUGH_NONE) { + return TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); + } + } + } else if (p_name == "gradle_build/export_format") { + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && !gradle_build_enabled) { + return TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled."); + } + } else if (p_name == "gradle_build/min_sdk") { + String min_sdk_str = p_preset->get("gradle_build/min_sdk"); + int min_sdk_int = VULKAN_MIN_SDK_VERSION; + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!gradle_build_enabled) { + return TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); + } + if (!min_sdk_str.is_valid_int()) { + return vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str); + } else { + min_sdk_int = min_sdk_str.to_int(); + if (min_sdk_int < OPENGL_MIN_SDK_VERSION) { + return vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION); + } + } + } + } else if (p_name == "gradle_build/target_sdk") { + String target_sdk_str = p_preset->get("gradle_build/target_sdk"); + int target_sdk_int = DEFAULT_TARGET_SDK_VERSION; + + String min_sdk_str = p_preset->get("gradle_build/min_sdk"); + int min_sdk_int = VULKAN_MIN_SDK_VERSION; + if (min_sdk_str.is_valid_int()) { + min_sdk_int = min_sdk_str.to_int(); + } + bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); + if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do. + if (!gradle_build_enabled) { + return TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); + } + if (!target_sdk_str.is_valid_int()) { + return vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str); + } else { + target_sdk_int = target_sdk_str.to_int(); + if (target_sdk_int < min_sdk_int) { + return TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version."); + } + } + } + } + } + return String(); +} + +void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_options) const { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "gradle_build/use_gradle_build"), false, false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "gradle_build/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), EXPORT_FORMAT_APK, false, true)); // Using String instead of int to default to an empty string (no override) with placeholder for instructions (see GH-62465). // This implies doing validation that the string is a proper int. - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", VULKAN_MIN_SDK_VERSION)), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true)); Vector<PluginConfigAndroid> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { @@ -1727,17 +1825,17 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio 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"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/debug_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_user", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "keystore/release_password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/code", PROPERTY_HINT_RANGE, "1,4096,1,or_greater"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "version/name"), "1.0")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/unique_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "ext.domain.name"), "org.godotengine.$genname", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "package/name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name [default if blank]"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "package/signed"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "package/app_category", PROPERTY_HINT_ENUM, "Accessibility,Audio,Game,Image,Maps,News,Productivity,Social,Video"), APP_CATEGORY_GAME)); @@ -1750,10 +1848,10 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "graphics/opengl_debug"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/xr_mode", PROPERTY_HINT_ENUM, "Regular,OpenXR"), XR_MODE_REGULAR, false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_HAND_TRACKING_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/hand_tracking_frequency", PROPERTY_HINT_ENUM, "Low,High"), XR_HAND_TRACKING_FREQUENCY_LOW)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "xr_features/passthrough", PROPERTY_HINT_ENUM, "None,Optional,Required"), XR_PASSTHROUGH_NONE, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/immersive_mode"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_small"), true)); @@ -1765,9 +1863,9 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "command_line/extra_args"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "apk_expansion/enable"), false, false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/SALT"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "apk_expansion/public_key", PROPERTY_HINT_MULTILINE_TEXT), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "permissions/custom_permissions"), PackedStringArray())); @@ -2179,9 +2277,9 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito // Validate the rest of the export configuration. - String dk = p_preset->get("keystore/debug"); - String dk_user = p_preset->get("keystore/debug_user"); - String dk_password = p_preset->get("keystore/debug_password"); + String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); + String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); if ((dk.is_empty() || dk_user.is_empty() || dk_password.is_empty()) && (!dk.is_empty() || !dk_user.is_empty() || !dk_password.is_empty())) { valid = false; @@ -2196,9 +2294,9 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito } } - String rk = p_preset->get("keystore/release"); - String rk_user = p_preset->get("keystore/release_user"); - String rk_password = p_preset->get("keystore/release_password"); + String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); + String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); if ((rk.is_empty() || rk_user.is_empty() || rk_password.is_empty()) && (!rk.is_empty() || !rk_user.is_empty() || !rk_password.is_empty())) { valid = false; @@ -2267,111 +2365,39 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { String err; bool valid = true; - const bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); - - // Validate the project configuration. - bool apk_expansion = p_preset->get("apk_expansion/enable"); - - if (apk_expansion) { - String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); - if (apk_expansion_pkey.is_empty()) { - valid = false; - - err += TTR("Invalid public key for APK expansion.") + "\n"; + List<ExportOption> options; + get_export_options(&options); + for (const EditorExportPlatform::ExportOption &E : options) { + if (get_export_option_visibility(p_preset.ptr(), E.option.name)) { + String warn = get_export_option_warning(p_preset.ptr(), E.option.name); + if (!warn.is_empty()) { + err += warn + "\n"; + if (E.required) { + valid = false; + } + } } } - String pn = p_preset->get("package/unique_name"); - String pn_err; - - if (!is_package_name_valid(pn, &pn_err)) { - valid = false; - err += TTR("Invalid package name:") + " " + pn_err + "\n"; - } - String etc_error = test_etc2(); if (!etc_error.is_empty()) { valid = false; err += etc_error; } - // Ensure that `Use Gradle Build` is enabled if a plugin is selected. - String enabled_plugins_names = PluginConfigAndroid::get_plugins_names(get_enabled_plugins(p_preset)); - if (!enabled_plugins_names.is_empty() && !gradle_build_enabled) { - valid = false; - err += TTR("\"Use Gradle Build\" must be enabled to use the plugins."); - err += "\n"; - } - - // Validate the Xr features are properly populated - int xr_mode_index = p_preset->get("xr_features/xr_mode"); - int hand_tracking = p_preset->get("xr_features/hand_tracking"); - int passthrough_mode = p_preset->get("xr_features/passthrough"); - if (xr_mode_index == XR_MODE_OPENXR && !gradle_build_enabled) { - valid = false; - err += TTR("OpenXR requires \"Use Gradle Build\" to be enabled"); - err += "\n"; - } - - if (xr_mode_index != XR_MODE_OPENXR) { - if (hand_tracking > XR_HAND_TRACKING_NONE) { - valid = false; - err += TTR("\"Hand Tracking\" is only valid when \"XR Mode\" is \"OpenXR\"."); - err += "\n"; - } - - if (passthrough_mode > XR_PASSTHROUGH_NONE) { - valid = false; - err += TTR("\"Passthrough\" is only valid when \"XR Mode\" is \"OpenXR\"."); - err += "\n"; - } - } - - if (int(p_preset->get("gradle_build/export_format")) == EXPORT_FORMAT_AAB && - !gradle_build_enabled) { - valid = false; - err += TTR("\"Export AAB\" is only valid when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - - // Check the min sdk version. String min_sdk_str = p_preset->get("gradle_build/min_sdk"); int min_sdk_int = VULKAN_MIN_SDK_VERSION; if (!min_sdk_str.is_empty()) { // Empty means no override, nothing to do. - if (!gradle_build_enabled) { - valid = false; - err += TTR("\"Min SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - if (!min_sdk_str.is_valid_int()) { - valid = false; - err += vformat(TTR("\"Min SDK\" should be a valid integer, but got \"%s\" which is invalid."), min_sdk_str); - err += "\n"; - } else { + if (min_sdk_str.is_valid_int()) { min_sdk_int = min_sdk_str.to_int(); - if (min_sdk_int < OPENGL_MIN_SDK_VERSION) { - valid = false; - err += vformat(TTR("\"Min SDK\" cannot be lower than %d, which is the version needed by the Godot library."), OPENGL_MIN_SDK_VERSION); - err += "\n"; - } } } - // Check the target sdk version. String target_sdk_str = p_preset->get("gradle_build/target_sdk"); int target_sdk_int = DEFAULT_TARGET_SDK_VERSION; if (!target_sdk_str.is_empty()) { // Empty means no override, nothing to do. - if (!gradle_build_enabled) { - valid = false; - err += TTR("\"Target SDK\" can only be overridden when \"Use Gradle Build\" is enabled."); - err += "\n"; - } - if (!target_sdk_str.is_valid_int()) { - valid = false; - err += vformat(TTR("\"Target SDK\" should be a valid integer, but got \"%s\" which is invalid."), target_sdk_str); - err += "\n"; - } else { + if (target_sdk_str.is_valid_int()) { target_sdk_int = target_sdk_str.to_int(); if (target_sdk_int > DEFAULT_TARGET_SDK_VERSION) { // Warning only, so don't override `valid`. @@ -2381,18 +2407,13 @@ bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<Edit } } - if (target_sdk_int < min_sdk_int) { - valid = false; - err += TTR("\"Target SDK\" version must be greater or equal to \"Min SDK\" version."); - err += "\n"; - } - String current_renderer = GLOBAL_GET("rendering/renderer/rendering_method.mobile"); if (current_renderer == "forward_plus") { // Warning only, so don't override `valid`. err += vformat(TTR("The \"%s\" renderer is designed for Desktop devices, and is not suitable for Android devices."), current_renderer); err += "\n"; } + if (_uses_vulkan() && min_sdk_int < VULKAN_MIN_SDK_VERSION) { // Warning only, so don't override `valid`. err += vformat(TTR("\"Min SDK\" should be greater or equal to %d for the \"%s\" renderer."), VULKAN_MIN_SDK_VERSION, current_renderer); @@ -2486,9 +2507,9 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) { int export_format = int(p_preset->get("gradle_build/export_format")); String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; - String release_keystore = p_preset->get("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); + String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); + String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); String target_sdk_version = p_preset->get("gradle_build/target_sdk"); if (!target_sdk_version.is_valid_int()) { target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); @@ -2508,9 +2529,9 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre String password; String user; if (p_debug) { - keystore = p_preset->get("keystore/debug"); - password = p_preset->get("keystore/debug_password"); - user = p_preset->get("keystore/debug_user"); + keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); + user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); if (keystore.is_empty()) { keystore = EDITOR_GET("export/android/debug_keystore"); @@ -2521,7 +2542,6 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) { return ERR_SKIP; } - } else { keystore = release_keystore; password = release_password; @@ -2770,7 +2790,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP CustomExportData user_data; user_data.assets_directory = assets_directory; user_data.debug = p_debug; - err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { + err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); + } else { + err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + } if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); return err; @@ -2861,9 +2885,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (should_sign) { if (p_debug) { - String debug_keystore = p_preset->get("keystore/debug"); - String debug_password = p_preset->get("keystore/debug_password"); - String debug_user = p_preset->get("keystore/debug_user"); + String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); + String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); if (debug_keystore.is_empty()) { debug_keystore = EDITOR_GET("export/android/debug_keystore"); @@ -2883,9 +2907,9 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password. } else { // Pass the release keystore info as well - String release_keystore = p_preset->get("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); + String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); + String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); if (release_keystore.is_relative_path()) { release_keystore = OS::get_singleton()->get_resource_dir().path_join(release_keystore).simplify_path(); } @@ -3261,28 +3285,32 @@ void EditorExportPlatformAndroid::resolve_platform_feature_priorities(const Ref< } EditorExportPlatformAndroid::EditorExportPlatformAndroid() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _android_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); - img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _android_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - devices_changed.set(); - plugins_changed.set(); + devices_changed.set(); + plugins_changed.set(); #ifndef ANDROID_ENABLED - check_for_changes_thread.start(_check_for_changes_poll_thread, this); + check_for_changes_thread.start(_check_for_changes_poll_thread, this); #endif + } } EditorExportPlatformAndroid::~EditorExportPlatformAndroid() { #ifndef ANDROID_ENABLED quit_request.set(); - check_for_changes_thread.wait_to_finish(); + if (check_for_changes_thread.is_started()) { + check_for_changes_thread.wait_to_finish(); + } #endif } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 337a0228d0..390b8c6465 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -49,6 +49,15 @@ const String SPLASH_CONFIG_XML_CONTENT = R"SPLASH(<?xml version="1.0" encoding=" </layer-list> )SPLASH"; +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_ANDROID_KEYSTORE_DEBUG_PATH = "GODOT_ANDROID_KEYSTORE_DEBUG_PATH"; +const String ENV_ANDROID_KEYSTORE_DEBUG_USER = "GODOT_ANDROID_KEYSTORE_DEBUG_USER"; +const String ENV_ANDROID_KEYSTORE_DEBUG_PASS = "GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD"; +const String ENV_ANDROID_KEYSTORE_RELEASE_PATH = "GODOT_ANDROID_KEYSTORE_RELEASE_PATH"; +const String ENV_ANDROID_KEYSTORE_RELEASE_USER = "GODOT_ANDROID_KEYSTORE_RELEASE_USER"; +const String ENV_ANDROID_KEYSTORE_RELEASE_PASS = "GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD"; + struct LauncherIcon { const char *export_path; int dimensions = 0; @@ -72,10 +81,10 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { EditorProgress *ep = nullptr; }; - Vector<PluginConfigAndroid> plugins; + mutable Vector<PluginConfigAndroid> plugins; String last_plugin_names; uint64_t last_gradle_build_time = 0; - SafeFlag plugins_changed; + mutable SafeFlag plugins_changed; Mutex plugins_lock; Vector<Device> devices; SafeFlag devices_changed; @@ -180,7 +189,9 @@ public: public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; + + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; virtual String get_name() const override; diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 61f8c5574b..ba4487cc4d 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -275,7 +275,8 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset, bool p_uses } if (p_uses_vulkan) { - manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"true\" android:version=\"1\" />\n"; + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.level\" android:required=\"false\" android:version=\"1\" />\n"; + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vulkan.version\" android:required=\"true\" android:version=\"0x400003\" />\n"; } return manifest_xr_features; } diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 95f46bbb7b..e7c06628c8 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,11 +1,11 @@ ext.versions = [ androidGradlePlugin: '7.2.1', - compileSdk : 32, - // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION' + compileSdk : 33, + // Also update 'platform/android/export/export_plugin.cpp#OPENGL_MIN_SDK_VERSION' minSdk : 21, // Also update 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' - targetSdk : 32, - buildTools : '32.0.0', + targetSdk : 33, + buildTools : '33.0.2', kotlinVersion : '1.7.0', fragmentVersion : '1.3.6', nexusPublishVersion: '1.1.0', @@ -135,14 +135,16 @@ ext.generateGodotLibraryVersion = { List<String> requiredKeys -> String statusValue = map["status"] if (statusValue == null) { statusCode = 0 - } else if (statusValue.startsWith("alpha")) { + } else if (statusValue.startsWith("dev")) { statusCode = 1 - } else if (statusValue.startsWith("beta")) { + } else if (statusValue.startsWith("alpha")) { statusCode = 2 - } else if (statusValue.startsWith("rc")) { + } else if (statusValue.startsWith("beta")) { statusCode = 3 - } else if (statusValue.startsWith("stable")) { + } else if (statusValue.startsWith("rc")) { statusCode = 4 + } else if (statusValue.startsWith("stable")) { + statusCode = 5 } else { statusCode = 0 } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 10c28a00b2..f94454e2a7 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath libraries.androidGradlePlugin classpath libraries.kotlinGradlePlugin - classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' } } @@ -38,9 +38,7 @@ ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedFlavors = ["editor", "template"] supportedFlavorsBuildTypes = [ - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - "editor": ["dev", "debug"], + "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] ] @@ -54,6 +52,7 @@ ext { def rootDir = "../../.." def binDir = "$rootDir/bin/" +def androidEditorBuildsDir = "$binDir/android_editor_builds/" def getSconsTaskName(String flavor, String buildType, String abi) { return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() @@ -221,18 +220,46 @@ def isAndroidStudio() { return sysProps != null && sysProps['idea.platform.prefix'] != null } -task copyEditorDebugBinaryToBin(type: Copy) { +task copyEditorReleaseApkToBin(type: Copy) { + dependsOn ':editor:assembleRelease' + from('editor/build/outputs/apk/release') + into(androidEditorBuildsDir) + include('android_editor-release*.apk') +} + +task copyEditorReleaseAabToBin(type: Copy) { + dependsOn ':editor:bundleRelease' + from('editor/build/outputs/bundle/release') + into(androidEditorBuildsDir) + include('android_editor-release*.aab') +} + +task copyEditorDebugApkToBin(type: Copy) { dependsOn ':editor:assembleDebug' from('editor/build/outputs/apk/debug') - into(binDir) - include('android_editor.apk') + into(androidEditorBuildsDir) + include('android_editor-debug.apk') } -task copyEditorDevBinaryToBin(type: Copy) { +task copyEditorDebugAabToBin(type: Copy) { + dependsOn ':editor:bundleDebug' + from('editor/build/outputs/bundle/debug') + into(androidEditorBuildsDir) + include('android_editor-debug.aab') +} + +task copyEditorDevApkToBin(type: Copy) { dependsOn ':editor:assembleDev' from('editor/build/outputs/apk/dev') - into(binDir) - include('android_editor_dev.apk') + into(androidEditorBuildsDir) + include('android_editor-dev.apk') +} + +task copyEditorDevAabToBin(type: Copy) { + dependsOn ':editor:bundleDev' + from('editor/build/outputs/bundle/dev') + into(androidEditorBuildsDir) + include('android_editor-dev.aab') } /** @@ -253,7 +280,8 @@ task generateGodotEditor { && targetLibs.isDirectory() && targetLibs.listFiles() != null && targetLibs.listFiles().length > 0) { - tasks += "copyEditor${target.capitalize()}BinaryToBin" + tasks += "copyEditor${target.capitalize()}ApkToBin" + tasks += "copyEditor${target.capitalize()}AabToBin" } } @@ -301,9 +329,11 @@ task cleanGodotEditor(type: Delete) { // Delete the generated binary apks delete("editor/build/outputs/apk") - // Delete the Godot editor apks in the Godot bin directory - delete("$binDir/android_editor.apk") - delete("$binDir/android_editor_dev.apk") + // Delete the generated aab binaries + delete("editor/build/outputs/bundle") + + // Delete the Godot editor apks & aabs in the Godot bin directory + delete(androidEditorBuildsDir) } /** diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 9152492e9d..38034aa47c 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -13,22 +13,67 @@ dependencies { } ext { - // Build number added as a suffix to the version code, and incremented for each build/upload to - // the Google Play store. - // This should be reset on each stable release of Godot. - editorBuildNumber = 0 + // Retrieve the build number from the environment variable; default to 0 if none is specified. + // The build number is added as a suffix to the version code for upload to the Google Play store. + getEditorBuildNumber = { -> + int buildNumber = 0 + String versionStatus = System.getenv("GODOT_VERSION_STATUS") + if (versionStatus != null && !versionStatus.isEmpty()) { + try { + buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")); + } catch (NumberFormatException ignored) { + buildNumber = 0 + } + } + + return buildNumber + } // Value by which the Godot version code should be offset by to make room for the build number editorBuildNumberOffset = 100 + + // Return the keystore file used for signing the release build. + getGodotKeystoreFile = { -> + def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE") + if (keyStore == null) { + return null + } + return file(keyStore) + } + + // Return the key alias used for signing the release build. + getGodotKeyAlias = { -> + def kAlias = System.getenv("GODOT_ANDROID_KEYSTORE_ALIAS") + return kAlias + } + + // Return the password for the key used for signing the release build. + getGodotSigningPassword = { -> + def signingPassword = System.getenv("GODOT_ANDROID_SIGN_PASSWORD") + return signingPassword + } + + // Returns true if the environment variables contains the configuration for signing the release + // build. + hasReleaseSigningConfigs = { -> + def keystoreFile = getGodotKeystoreFile() + def keyAlias = getGodotKeyAlias() + def signingPassword = getGodotSigningPassword() + + return keystoreFile != null && keystoreFile.isFile() + && keyAlias != null && !keyAlias.isEmpty() + && signingPassword != null && !signingPassword.isEmpty() + } } def generateVersionCode() { int libraryVersionCode = getGodotLibraryVersionCode() - return (libraryVersionCode * editorBuildNumberOffset) + editorBuildNumber + return (libraryVersionCode * editorBuildNumberOffset) + getEditorBuildNumber() } def generateVersionName() { String libraryVersionName = getGodotLibraryVersionName() - return libraryVersionName + ".$editorBuildNumber" + int buildNumber = getEditorBuildNumber() + return buildNumber == 0 ? libraryVersionName : libraryVersionName + ".$buildNumber" } android { @@ -45,6 +90,7 @@ android { targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' + setProperty("archivesBaseName", "android_editor") } compileOptions { @@ -56,6 +102,15 @@ android { jvmTarget = versions.javaVersion } + signingConfigs { + release { + storeFile getGodotKeystoreFile() + storePassword getGodotSigningPassword() + keyAlias getGodotKeyAlias() + keyPassword getGodotSigningPassword() + } + } + buildTypes { dev { initWith debug @@ -64,15 +119,14 @@ android { debug { initWith release - - // Need to swap with the release signing config when this is ready for public release. + applicationIdSuffix ".debug" signingConfig signingConfigs.debug } release { - // This buildtype is disabled below. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. + if (hasReleaseSigningConfigs()) { + signingConfig signingConfigs.release + } } } @@ -82,20 +136,4 @@ android { doNotStrip '**/*.so' } } - - // Disable 'release' buildtype. - // The editor can't be used with target=release only, as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.buildType.name == "release") { - setIgnore(true) - } - } - - applicationVariants.all { variant -> - variant.outputs.all { output -> - def suffix = variant.name == "dev" ? "_dev" : "" - output.outputFileName = "android_editor${suffix}.apk" - } - } } diff --git a/platform/android/java/editor/src/debug/res/values/strings.xml b/platform/android/java/editor/src/debug/res/values/strings.xml new file mode 100644 index 0000000000..09ee2d77e1 --- /dev/null +++ b/platform/android/java/editor/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="godot_editor_name_string">Godot Editor 4 (debug)</string> +</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 80ef10b6a4..2bb6b43503 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -21,6 +21,8 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.VIBRATE" /> <application android:allowBackup="false" 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 index 42ef1436f3..ce485afd91 100644 --- 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 @@ -81,7 +81,9 @@ open class GodotEditor : FullScreenGodotApp() { private val commandLineParams = ArrayList<String>() override fun onCreate(savedInstanceState: Bundle?) { - PermissionsUtil.requestManifestPermissions(this) + // We exclude certain permissions from the set we request at startup, as they'll be + // requested on demand based on use-cases. + PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS) updateCommandLineParams(params) @@ -98,6 +100,8 @@ open class GodotEditor : FullScreenGodotApp() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() + checkForProjectPermissionsToEnable() + runOnUiThread { // Enable long press, panning and scaling gestures godotFragment?.renderView?.inputHandler?.apply { @@ -107,12 +111,26 @@ open class GodotEditor : FullScreenGodotApp() { } } + /** + * Check for project permissions to enable + */ + protected open fun checkForProjectPermissionsToEnable() { + // Check for RECORD_AUDIO permission + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")); + if (audioInputEnabled) { + PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) + } + } + 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)) } + if (BuildConfig.BUILD_TYPE == "dev") { + commandLineParams.add("--benchmark") + } } override fun getCommandLine() = commandLineParams diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 104af0fcff..aa4d02b5b2 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -39,4 +39,9 @@ class GodotGame : GodotEditor() { override fun enableLongPressGestures() = false override fun enablePanAndScaleGestures() = false + + override fun checkForProjectPermissionsToEnable() { + // Nothing to do.. by the time we get here, the project permissions will have already + // been requested by the Editor window. + } } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt index 62a9384f2e..d0e4279eeb 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt @@ -37,4 +37,9 @@ package org.godotengine.editor * Upon selection of a project, this activity (via its parent logic) starts the * [GodotEditor] activity. */ -class GodotProjectManager : GodotEditor() +class GodotProjectManager : GodotEditor() { + override fun checkForProjectPermissionsToEnable() { + // Nothing to do here.. we have yet to select a project to load. + } +} + diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties index 41dfb87909..aa991fceae 100644 --- a/platform/android/java/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 38133ddd51..4340250ad3 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -80,19 +80,11 @@ android { release.jniLibs.srcDirs = ['libs/release'] // Editor jni library + editorRelease.jniLibs.srcDirs = ['libs/tools/release'] editorDebug.jniLibs.srcDirs = ['libs/tools/debug'] editorDev.jniLibs.srcDirs = ['libs/tools/dev'] } - // Disable 'editorRelease'. - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - variantFilter { variant -> - if (variant.name == "editorRelease") { - setIgnore(true) - } - } - libraryVariants.all { variant -> def flavorName = variant.getFlavorName() if (flavorName == null || flavorName == "") { @@ -105,9 +97,14 @@ android { } boolean devBuild = buildType == "dev" + boolean runTests = devBuild + boolean productionBuild = !devBuild + boolean storeRelease = buildType == "release" def sconsTarget = flavorName if (sconsTarget == "template") { + // Tests are not supported on template builds + runTests = false switch (buildType) { case "release": sconsTarget += "_release" @@ -135,10 +132,10 @@ android { def sconsExts = (org.gradle.internal.os.OperatingSystem.current().isWindows() ? [".bat", ".cmd", ".ps1", ".exe"] : [""]) - logger.lifecycle("Looking for $sconsName executable path") + logger.debug("Looking for $sconsName executable path") for (ext in sconsExts) { String sconsNameExt = sconsName + ext - logger.lifecycle("Checking $sconsNameExt") + logger.debug("Checking $sconsNameExt") sconsExecutableFile = org.gradle.internal.os.OperatingSystem.current().findInPath(sconsNameExt) if (sconsExecutableFile != null) { // We're done! @@ -155,7 +152,7 @@ android { if (sconsExecutableFile == null) { throw new GradleException("Unable to find executable path for the '$sconsName' command.") } else { - logger.lifecycle("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") + logger.debug("Found executable path for $sconsName: ${sconsExecutableFile.absolutePath}") } for (String selectedAbi : selectedAbis) { @@ -167,7 +164,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + args "--directory=${pathToRootDir}", "platform=android", "store_release=${storeRelease}", "production=${productionBuild}", "dev_mode=${devBuild}", "dev_build=${devBuild}", "tests=${runTests}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() } // Schedule the tasks so the generated libs are present before the aar file is packaged. 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 9b65a52b70..748a1c41fd 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -39,6 +39,7 @@ import org.godotengine.godot.io.file.FileAccessHandler; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.tts.GodotTTS; +import org.godotengine.godot.utils.BenchmarkUtils; import org.godotengine.godot.utils.GodotNetUtils; import org.godotengine.godot.utils.PermissionsUtil; import org.godotengine.godot.xr.XRMode; @@ -180,7 +181,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC public GodotIO io; public GodotNetUtils netUtils; public GodotTTS tts; - DirectoryAccessHandler directoryAccessHandler; + private DirectoryAccessHandler directoryAccessHandler; + private FileAccessHandler fileAccessHandler; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -274,7 +276,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // ...add to FrameLayout containerLayout.addView(editText); - if (!GodotLib.setup(command_line)) { + tts = new GodotTTS(activity); + + if (!GodotLib.setup(command_line, tts)) { Log.e(TAG, "Unable to setup the Godot engine! Aborting..."); alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit); return false; @@ -282,7 +286,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC if (usesVulkan()) { if (!meetsVulkanRequirements(activity.getPackageManager())) { - Log.w(TAG, "Missing requirements for vulkan support!"); + alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit); + return false; } mRenderView = new GodotVulkanRenderView(activity, this); } else { @@ -392,7 +397,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return false; } - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL, 1)) { + // Optional requirements.. log as warning if missing + Log.w(TAG, "The vulkan hardware level does not meet the minimum requirement: 1"); + } + + // Check for api version 1.0 + return packageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN_HARDWARE_VERSION, 0x400003); + } + + return false; } public void setKeepScreenOn(final boolean p_enabled) { @@ -509,7 +524,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } return cmdline; } catch (Exception e) { - e.printStackTrace(); + // The _cl_ file can be missing with no adverse effect return new String[0]; } } @@ -563,10 +578,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); io = new GodotIO(activity); netUtils = new GodotNetUtils(activity); - tts = new GodotTTS(activity); Context context = getContext(); directoryAccessHandler = new DirectoryAccessHandler(context); - FileAccessHandler fileAccessHandler = new FileAccessHandler(context); + fileAccessHandler = new FileAccessHandler(context); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); @@ -580,8 +594,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC netUtils, directoryAccessHandler, fileAccessHandler, - use_apk_expansion, - tts); + use_apk_expansion); result_callback = null; } @@ -594,6 +607,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC @Override public void onCreate(Bundle icicle) { + BenchmarkUtils.beginBenchmarkMeasure("Godot::onCreate"); super.onCreate(icicle); final Activity activity = getActivity(); @@ -642,6 +656,18 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC editor.apply(); i++; + } else if (command_line[i].equals("--benchmark")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + } else if (has_extra && command_line[i].equals("--benchmark-file")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + + // Retrieve the filepath + BenchmarkUtils.setBenchmarkFile(command_line[i + 1]); + new_args.add(command_line[i + 1]); + + i++; } else if (command_line[i].trim().length() != 0) { new_args.add(command_line[i]); } @@ -712,6 +738,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC mCurrentIntent = activity.getIntent(); initializeGodot(); + BenchmarkUtils.endBenchmarkMeasure("Godot::onCreate"); } @Override @@ -917,20 +944,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // Do something here if sensor accuracy changes. } - /* - @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) { - System.out.printf("** BACK REQUEST!\n"); - - GodotLib.quit(); - return true; - } - System.out.printf("** OTHER KEY!\n"); - - return false; - } - */ - public void onBackPressed() { boolean shouldQuit = true; @@ -1033,6 +1046,11 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC return PermissionsUtil.getGrantedPermissions(getActivity()); } + @Keep + private String getCACertificates() { + return GodotNetUtils.getCACertificates(); + } + /** * The download state should trigger changes in the UI --- it may be useful * to show the state as being indeterminate at times. This sample can be @@ -1137,10 +1155,35 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC } @Keep + public DirectoryAccessHandler getDirectoryAccessHandler() { + return directoryAccessHandler; + } + + @Keep + public FileAccessHandler getFileAccessHandler() { + return fileAccessHandler; + } + + @Keep private int createNewGodotInstance(String[] args) { if (godotHost != null) { return godotHost.onNewGodotInstanceRequested(args); } return 0; } + + @Keep + private void beginBenchmarkMeasure(String label) { + BenchmarkUtils.beginBenchmarkMeasure(label); + } + + @Keep + private void endBenchmarkMeasure(String label) { + BenchmarkUtils.endBenchmarkMeasure(label); + } + + @Keep + private void dumpBenchmark(String benchmarkFile) { + BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 330e2ede76..b465377743 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -166,8 +166,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Override public void requestPointerCapture() { - super.requestPointerCapture(); - inputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + inputHandler.onPointerCaptureChange(true); + } } @Override @@ -188,10 +190,10 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); 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 d9aab950df..c725b1a7c9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -61,8 +61,7 @@ public class GodotLib { GodotNetUtils netUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, - boolean use_apk_expansion, - GodotTTS tts); + boolean use_apk_expansion); /** * Invoked on the main thread to clean up Godot native layer. @@ -74,7 +73,7 @@ public class GodotLib { * Invoked on the GL thread to complete setup for the Godot native layer logic. * @param p_cmdline Command line arguments used to configure Godot native layer components. */ - public static native boolean setup(String[] p_cmdline); + public static native boolean setup(String[] p_cmdline, GodotTTS tts); /** * Invoked on the GL thread when the underlying Android surface has changed size. diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 02c0d67fff..00243dab2a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -51,4 +51,8 @@ public interface GodotRenderView { void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY); void setPointerIcon(int pointerType); + + default boolean canCapturePointer() { + return getInputHandler().canCapturePointer(); + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index 34490d4625..681e182adb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -134,8 +134,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Override public void requestPointerCapture() { - super.requestPointerCapture(); - mInputHandler.onPointerCaptureChange(true); + if (canCapturePointer()) { + super.requestPointerCapture(); + mInputHandler.onPointerCaptureChange(true); + } } @Override @@ -162,10 +164,10 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV try { Bitmap bitmap = null; if (!TextUtils.isEmpty(imagePath)) { - if (godot.directoryAccessHandler.filesystemFileExists(imagePath)) { + if (godot.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { // Try to load the bitmap from the file system bitmap = BitmapFactory.decodeFile(imagePath); - } else if (godot.directoryAccessHandler.assetsFileExists(imagePath)) { + } else if (godot.getDirectoryAccessHandler().assetsFileExists(imagePath)) { // Try to load the bitmap from the assets directory AssetManager am = getContext().getAssets(); InputStream imageInputStream = am.open(imagePath); diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 1be009b6dc..e9bc435689 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -197,7 +197,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false GodotInputHandler.handleMotionEvent(event) + } else if (event.actionMasked == MotionEvent.ACTION_MOVE && panningAndScalingEnabled == false) { + GodotInputHandler.handleMotionEvent(event) } + return true } @@ -224,42 +227,43 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi ) dragInProgress = false } - return true } - dragInProgress = true - val x = terminusEvent.x val y = terminusEvent.y if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress) { GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) - } else { + } else if (!scaleInProgress){ + dragInProgress = true GodotInputHandler.handleMotionEvent(terminusEvent) } return true } - override fun onScale(detector: ScaleGestureDetector?): Boolean { - if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { + override fun onScale(detector: ScaleGestureDetector): Boolean { + if (!panningAndScalingEnabled || pointerCaptureInProgress) { return false } - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + + if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { + GodotLib.magnify( + detector.focusX, + detector.focusY, + detector.scaleFactor + ) + } return true } - override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { - if (detector == null || !panningAndScalingEnabled || pointerCaptureInProgress) { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + if (!panningAndScalingEnabled || pointerCaptureInProgress) { return false } scaleInProgress = true return true } - override fun onScaleEnd(detector: ScaleGestureDetector?) { + override fun onScaleEnd(detector: ScaleGestureDetector) { scaleInProgress = false } } 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 cedbbfb7c3..317344f2a5 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 @@ -66,6 +66,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; + /** + * Used to decide whether mouse capture can be enabled. + */ + private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; + public GodotInputHandler(GodotRenderView godotView) { final Context context = godotView.getView().getContext(); mRenderView = godotView; @@ -105,6 +110,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; } + public boolean canCapturePointer() { + return lastSeenToolType == MotionEvent.TOOL_TYPE_MOUSE; + } + public void onPointerCaptureChange(boolean hasCapture) { godotGestureHandler.onPointerCaptureChange(hasCapture); } @@ -174,6 +183,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onTouchEvent(final MotionEvent event) { + lastSeenToolType = event.getToolType(0); + this.scaleGestureDetector.onTouchEvent(event); if (this.gestureDetector.onTouchEvent(event)) { // The gesture detector has handled the event. @@ -198,6 +209,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onGenericMotionEvent(MotionEvent event) { + lastSeenToolType = event.getToolType(0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { // The gesture detector has handled the event. return true; @@ -471,15 +484,27 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { + // Fix the buttonsMask + switch (eventAction) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // Zero-up the button state + buttonsMask = 0; + break; + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + if (buttonsMask == 0) { + buttonsMask = MotionEvent.BUTTON_PRIMARY; + } + break; + } + // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. switch (eventAction) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - // Zero-up the button state - buttonsMask = 0; - // FALL THROUGH case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_EXIT: diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 7b628e25ed..f48dba56df 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -124,11 +124,12 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) { final String characters = pKeyEvent.getCharacters(); - - for (int i = 0; i < characters.length(); i++) { - final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true); - GodotLib.key(0, character, 0, false); + if (characters != null) { + for (int i = 0; i < characters.length(); i++) { + final int character = characters.codePointAt(i); + GodotLib.key(0, character, 0, true); + GodotLib.key(0, character, 0, false); + } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index 833ab40af0..8ee3d5f48f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -76,6 +76,13 @@ internal enum class StorageScope { return UNKNOWN } + // If we have 'All Files Access' permission, we can access all directories without + // restriction. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + && Environment.isExternalStorageManager()) { + return APP + } + val canonicalPathFile = pathFile.canonicalPath if (internalAppDir != null && canonicalPathFile.startsWith(internalAppDir)) { @@ -90,7 +97,7 @@ internal enum class StorageScope { return APP } - var rootDir: String? = System.getenv("ANDROID_ROOT") + val rootDir: String? = System.getenv("ANDROID_ROOT") if (rootDir != null && canonicalPathFile.startsWith(rootDir)) { return APP } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 357008ca66..984bf607d0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -46,7 +46,7 @@ class FileAccessHandler(val context: Context) { private val TAG = FileAccessHandler::class.java.simpleName private const val FILE_NOT_FOUND_ERROR_ID = -1 - private const val INVALID_FILE_ID = 0 + internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { @@ -96,13 +96,17 @@ class FileAccessHandler(val context: Context) { private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 fun fileOpen(path: String?, modeFlags: Int): Int { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + return fileOpen(path, accessFlag) + } + + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { return INVALID_FILE_ID } try { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID files.put(++lastFileId, dataAccess) 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 index ebab8398de..edace53e7f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java +++ b/platform/android/java/lib/src/org/godotengine/godot/tts/GodotTTS.java @@ -62,8 +62,9 @@ public class GodotTTS extends UtteranceProgressListener { 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 Activity activity; + private TextToSpeech synth; + private LinkedList<GodotUtterance> queue; final private Object lock = new Object(); private GodotUtterance lastUtterance; @@ -71,10 +72,7 @@ public class GodotTTS extends UtteranceProgressListener { private boolean paused; public GodotTTS(Activity p_activity) { - synth = new TextToSpeech(p_activity, null); - queue = new LinkedList<GodotUtterance>(); - - synth.setOnUtteranceProgressListener(this); + activity = p_activity; } private void updateTTS() { @@ -187,6 +185,16 @@ public class GodotTTS extends UtteranceProgressListener { } /** + * Initialize synth and query. + */ + public void init() { + synth = new TextToSpeech(activity, null); + queue = new LinkedList<GodotUtterance>(); + + synth.setOnUtteranceProgressListener(this); + } + + /** * Adds an utterance to the queue. */ public void speak(String text, String voice, int volume, float pitch, float rate, int utterance_id, boolean interrupt) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt new file mode 100644 index 0000000000..1552c8f082 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -0,0 +1,122 @@ +/**************************************************************************/ +/* BenchmarkUtils.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +@file:JvmName("BenchmarkUtils") + +package org.godotengine.godot.utils + +import android.os.Build +import android.os.SystemClock +import android.os.Trace +import android.util.Log +import org.godotengine.godot.BuildConfig +import org.godotengine.godot.io.file.FileAccessFlags +import org.godotengine.godot.io.file.FileAccessHandler +import org.json.JSONObject +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentSkipListMap + +/** + * Contains benchmark related utilities methods + */ +private const val TAG = "GodotBenchmark" + +var useBenchmark = false +var benchmarkFile = "" + +private val startBenchmarkFrom = ConcurrentSkipListMap<String, Long>() +private val benchmarkTracker = ConcurrentSkipListMap<String, Double>() + +/** + * Start measuring and tracing the execution of a given section of code using the given label. + * + * Must be followed by a call to [endBenchmarkMeasure]. + * + * Note: Only enabled on 'editorDev' build variant. + */ +fun beginBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + startBenchmarkFrom[label] = SystemClock.elapsedRealtime() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.beginAsyncSection(label, 0) + } +} + +/** + * End measuring and tracing of the section of code with the given label. + * + * Must be preceded by a call [beginBenchmarkMeasure] + * + * * Note: Only enabled on 'editorDev' build variant. + */ +fun endBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + val startTime = startBenchmarkFrom[label] ?: return + val total = SystemClock.elapsedRealtime() - startTime + benchmarkTracker[label] = total / 1000.0 + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.endAsyncSection(label, 0) + } +} + +/** + * Dump the benchmark measurements. + * If [filepath] is valid, the data is also written in json format to the specified file. + * + * * Note: Only enabled on 'editorDev' build variant. + */ +@JvmOverloads +fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + if (!useBenchmark) { + return + } + + val printOut = + benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n") + Log.i(TAG, "BENCHMARK:\n$printOut") + + if (fileAccessHandler != null && !filepath.isNullOrBlank()) { + val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) + fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) + fileAccessHandler.fileClose(fileId) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java index 401c105cd7..c31d56a3e1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GodotNetUtils.java @@ -33,11 +33,17 @@ package org.godotengine.godot.utils; import android.app.Activity; import android.content.Context; import android.net.wifi.WifiManager; +import android.util.Base64; import android.util.Log; +import java.io.StringWriter; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + /** * This class handles Android-specific networking functions. - * For now, it only provides access to WifiManager.MulticastLock, which is needed on some devices + * It provides access to the CA certificates KeyStore, and the WifiManager.MulticastLock, which is needed on some devices * to receive broadcast and multicast packets. */ public class GodotNetUtils { @@ -79,4 +85,34 @@ public class GodotNetUtils { Log.e("Godot", "Exception during multicast lock release: " + e); } } + + /** + * Retrieves the list of trusted CA certificates from the "AndroidCAStore" and returns them in PRM format. + * @see https://developer.android.com/reference/java/security/KeyStore . + * @return A string of concatenated X509 certificates in PEM format. + */ + public static String getCACertificates() { + try { + KeyStore ks = KeyStore.getInstance("AndroidCAStore"); + StringBuilder writer = new StringBuilder(); + + if (ks != null) { + ks.load(null, null); + Enumeration<String> aliases = ks.aliases(); + + while (aliases.hasMoreElements()) { + String alias = (String)aliases.nextElement(); + + X509Certificate cert = (X509Certificate)ks.getCertificate(alias); + writer.append("-----BEGIN CERTIFICATE-----\n"); + writer.append(Base64.encodeToString(cert.getEncoded(), Base64.DEFAULT)); + writer.append("-----END CERTIFICATE-----\n"); + } + } + return writer.toString(); + } catch (Exception e) { + Log.e("Godot", "Exception while reading CA certificates: " + e); + return ""; + } + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index e34c94975b..a94188c405 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -42,10 +42,12 @@ import android.os.Environment; import android.provider.Settings; import android.util.Log; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * This class includes utility functions for Android permissions related operations. @@ -58,6 +60,7 @@ public final class PermissionsUtil { static final int REQUEST_CAMERA_PERMISSION = 2; static final int REQUEST_VIBRATE_PERMISSION = 3; public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002; public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; private PermissionsUtil() { @@ -65,31 +68,57 @@ public final class PermissionsUtil { /** * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a> - * @param name the name of the requested permission. + * @param permissionName the name of the requested permission. * @param activity the caller activity for this method. * @return true/false. "true" if permission was granted otherwise returns "false". */ - public static boolean requestPermission(String name, Activity activity) { + public static boolean requestPermission(String permissionName, Activity activity) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already return true; } - if (name.equals("RECORD_AUDIO") && ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); - return false; - } + switch (permissionName) { + case "RECORD_AUDIO": + case Manifest.permission.RECORD_AUDIO: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, REQUEST_RECORD_AUDIO_PERMISSION); + return false; + } + return true; - if (name.equals("CAMERA") && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); - return false; - } + case "CAMERA": + case Manifest.permission.CAMERA: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.CAMERA }, REQUEST_CAMERA_PERMISSION); + return false; + } + return true; - if (name.equals("VIBRATE") && ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { - activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); - return false; + case "VIBRATE": + case Manifest.permission.VIBRATE: + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { Manifest.permission.VIBRATE }, REQUEST_VIBRATE_PERMISSION); + return false; + } + return true; + + default: + // Check if the given permission is a dangerous permission + try { + PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) { + activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + // Unknown permission - return false as it can't be granted. + Log.w(TAG, "Unable to identify permission " + permissionName, e); + return false; + } + return true; } - return true; } /** @@ -98,6 +127,16 @@ public final class PermissionsUtil { * @return true/false. "true" if all permissions were granted otherwise returns "false". */ public static boolean requestManifestPermissions(Activity activity) { + return requestManifestPermissions(activity, null); + } + + /** + * Request dangerous permissions which are defined in the Android manifest file from the user. + * @param activity the caller activity for this method. + * @param excludes Set of permissions to exclude from the request + * @return true/false. "true" if all permissions were granted otherwise returns "false". + */ + public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } @@ -115,6 +154,9 @@ public final class PermissionsUtil { List<String> requestedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { + if (excludes != null && excludes.contains(manifestPermission)) { + continue; + } try { if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 1a0087e18d..63435853e9 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -116,7 +116,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) { JavaVM *jvm; env->GetJavaVM(&jvm); @@ -133,7 +133,6 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv DirAccessJAndroid::setup(p_directory_access_handler); FileAccessFilesystemJAndroid::setup(p_file_access_handler); NetSocketAndroid::setup(p_net_utils); - TTS_Android::setup(p_godot_tts); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); @@ -144,7 +143,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env _terminate(env, false); } -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline) { +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts) { setup_android_thread(); const char **cmdline = nullptr; @@ -185,6 +184,8 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env return false; } + TTS_Android::setup(p_godot_tts); + java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); GDREGISTER_CLASS(JNISingleton); return true; @@ -199,10 +200,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, j if (p_surface) { ANativeWindow *native_window = ANativeWindow_fromSurface(env, p_surface); os_android->set_native_window(native_window); - - DisplayServerAndroid::get_singleton()->reset_window(); - DisplayServerAndroid::get_singleton()->notify_surface_changed(p_width, p_height); } + DisplayServerAndroid::get_singleton()->reset_window(); + DisplayServerAndroid::get_singleton()->notify_surface_changed(p_width, p_height); } } } @@ -244,7 +244,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, if (step.get() == 0) { // Since Godot is initialized on the UI thread, main_thread_id was set to that thread's id, // but for Godot purposes, the main thread is the one running the game loop - Main::setup2(Thread::get_caller_id()); + Main::setup2(); input_handler = new AndroidInputHandler(); step.increment(); return true; @@ -447,39 +447,29 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en Object *obj = ObjectDB::get_instance(ObjectID(ID)); ERR_FAIL_NULL(obj); - int res = env->PushLocalFrame(16); - ERR_FAIL_COND(res != 0); - String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); + Variant *vlist = (Variant *)alloca(sizeof(Variant) * count); - Variant **vptr = (Variant **)alloca(sizeof(Variant *) * count); + const Variant **vptr = (const Variant **)alloca(sizeof(Variant *) * count); + for (int i = 0; i < count; i++) { jobject jobj = env->GetObjectArrayElement(params, i); - Variant v; - if (jobj) { - v = _jobject_to_variant(env, jobj); - } - memnew_placement(&vlist[i], Variant); - vlist[i] = v; + ERR_FAIL_NULL(jobj); + memnew_placement(&vlist[i], Variant(_jobject_to_variant(env, jobj))); vptr[i] = &vlist[i]; env->DeleteLocalRef(jobj); } Callable::CallError err; - obj->callp(str_method, (const Variant **)vptr, count, err); - - env->PopLocalFrame(nullptr); + obj->callp(str_method, vptr, count, err); } 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_NULL(obj); - int res = env->PushLocalFrame(16); - ERR_FAIL_COND(res != 0); - String str_method = jstring_to_string(method, env); int count = env->GetArrayLength(params); @@ -489,16 +479,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * for (int i = 0; i < count; i++) { jobject jobj = env->GetObjectArrayElement(params, i); - if (jobj) { - args[i] = _jobject_to_variant(env, jobj); - } - env->DeleteLocalRef(jobj); + ERR_FAIL_NULL(jobj); + memnew_placement(&args[i], Variant(_jobject_to_variant(env, jobj))); argptrs[i] = &args[i]; + env->DeleteLocalRef(jobj); } - MessageQueue::get_singleton()->push_callp(obj, str_method, (const Variant **)argptrs, count); - - env->PopLocalFrame(nullptr); + MessageQueue::get_singleton()->push_callp(obj, str_method, argptrs, count); } JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 59ab2448d7..9158e89c13 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,9 +37,9 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion, jobject p_godot_tts); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz); -JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline, jobject p_godot_tts); 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); diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index b50d4870bd..a95f762e01 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -49,6 +49,8 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { _request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V"); _release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V"); } + + _can_capture_pointer = env->GetMethodID(_cls, "canCapturePointer", "()Z"); } bool GodotJavaViewWrapper::can_update_pointer_icon() const { @@ -56,7 +58,16 @@ bool GodotJavaViewWrapper::can_update_pointer_icon() const { } bool GodotJavaViewWrapper::can_capture_pointer() const { - return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr; + // We can capture the pointer if the other jni capture method ids are initialized, + // and GodotView#canCapturePointer() returns true. + if (_request_pointer_capture != nullptr && _release_pointer_capture != nullptr && _can_capture_pointer != nullptr) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + + return env->CallBooleanMethod(_godot_view, _can_capture_pointer); + } + + return false; } void GodotJavaViewWrapper::request_pointer_capture() { diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index 9b64ded29c..07742d6bb0 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -44,6 +44,7 @@ private: jobject _godot_view; + jmethodID _can_capture_pointer = 0; jmethodID _request_pointer_capture = 0; jmethodID _release_pointer_capture = 0; diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 9d9d087896..862d9f0436 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -70,6 +70,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z"); _request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z"); _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); + _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); _get_surface = p_env->GetMethodID(godot_class, "getSurface", "()Landroid/view/Surface;"); _is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z"); @@ -79,6 +80,9 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); + _begin_benchmark_measure = p_env->GetMethodID(godot_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V"); + _end_benchmark_measure = p_env->GetMethodID(godot_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V"); + _dump_benchmark = p_env->GetMethodID(godot_class, "dumpBenchmark", "(Ljava/lang/String;)V"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); @@ -310,6 +314,17 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const { return permissions_list; } +String GodotJavaWrapper::get_ca_certificates() const { + if (_get_ca_certificates) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, String()); + jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates); + return jstring_to_string(s, env); + } else { + return String(); + } +} + void GodotJavaWrapper::init_input_devices() { if (_init_input_devices) { JNIEnv *env = get_jni_env(); @@ -359,3 +374,30 @@ int GodotJavaWrapper::create_new_godot_instance(List<String> args) { return 0; } } + +void GodotJavaWrapper::begin_benchmark_measure(const String &p_label) { + if (_begin_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::end_benchmark_measure(const String &p_label) { + if (_end_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_label); + } +} + +void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) { + if (_dump_benchmark) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data()); + env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file); + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 1bd79584d8..245ab33dcf 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -60,6 +60,7 @@ private: jmethodID _request_permission = nullptr; jmethodID _request_permissions = nullptr; jmethodID _get_granted_permissions = nullptr; + jmethodID _get_ca_certificates = nullptr; jmethodID _init_input_devices = nullptr; jmethodID _get_surface = nullptr; jmethodID _is_activity_resumed = nullptr; @@ -70,6 +71,9 @@ private: jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; jmethodID _get_render_view = nullptr; + jmethodID _begin_benchmark_measure = nullptr; + jmethodID _end_benchmark_measure = nullptr; + jmethodID _dump_benchmark = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -98,12 +102,16 @@ public: bool request_permission(const String &p_name); bool request_permissions(); Vector<String> get_granted_permissions() const; + String get_ca_certificates() const; void init_input_devices(); jobject get_surface(); bool is_activity_resumed(); void vibrate(int p_duration_ms); String get_input_fallback_mapping(); int create_new_godot_instance(List<String> args); + void begin_benchmark_measure(const String &p_label); + void end_benchmark_measure(const String &p_label); + void dump_benchmark(const String &benchmark_file); }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 725fea8d54..a96dcca3b3 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -182,7 +182,7 @@ String OS_Android::get_name() const { } String OS_Android::get_system_property(const char *key) const { - static String value; + String value; char value_str[PROP_VALUE_MAX]; if (__system_property_get(key, value_str)) { value = String(value_str); @@ -230,20 +230,20 @@ String OS_Android::get_version() const { "ro.potato.version", "ro.xtended.version", "org.evolution.version", "ro.corvus.version", "ro.pa.version", "ro.crdroid.version", "ro.syberia.version", "ro.arrow.version", "ro.lineage.version" }; for (int i = 0; i < roms.size(); i++) { - static String rom_version = get_system_property(roms[i]); + String rom_version = get_system_property(roms[i]); if (!rom_version.is_empty()) { return rom_version; } } - static String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. + String mod_version = get_system_property("ro.modversion"); // Handles other Android custom ROMs. if (!mod_version.is_empty()) { return mod_version; } // Handles stock Android. - static String sdk_version = get_system_property("ro.build.version.sdk_int"); - static String build = get_system_property("ro.build.version.incremental"); + String sdk_version = get_system_property("ro.build.version.sdk_int"); + String build = get_system_property("ro.build.version.incremental"); if (!sdk_version.is_empty()) { if (!build.is_empty()) { return vformat("%s.%s", sdk_version, build); @@ -311,7 +311,11 @@ String OS_Android::get_resource_dir() const { #ifdef TOOLS_ENABLED return OS_Unix::get_resource_dir(); #else - return "/"; //android has its own filesystem for resources inside the APK + if (remote_fs_dir.is_empty()) { + return "/"; // Android has its own filesystem for resources inside the APK + } else { + return remote_fs_dir; + } #endif } @@ -671,6 +675,27 @@ String OS_Android::get_config_path() const { return get_user_data_dir().path_join("config"); } +void OS_Android::benchmark_begin_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + godot_java->begin_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_end_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + godot_java->end_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_dump() { +#ifdef TOOLS_ENABLED + if (!is_use_benchmark_set()) { + return; + } + godot_java->dump_benchmark(get_benchmark_file()); +#endif +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "system_fonts") { return true; @@ -753,5 +778,19 @@ Error OS_Android::kill(const ProcessID &p_pid) { return OS_Unix::kill(p_pid); } +String OS_Android::get_system_ca_certificates() { + return godot_java->get_ca_certificates(); +} + +Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) { + r_project_path = get_user_data_dir(); + Error err = OS_Unix::setup_remote_filesystem(p_server_host, p_port, p_password, r_project_path); + if (err == OK) { + remote_fs_dir = r_project_path; + FileAccess::make_default<FileAccessFilesystemJAndroid>(FileAccess::ACCESS_RESOURCES); + } + return err; +} + OS_Android::~OS_Android() { } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 53910b1498..99fe501975 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -57,6 +57,7 @@ private: mutable String data_dir_cache; mutable String cache_dir_cache; + mutable String remote_fs_dir; AudioDriverOpenSL audio_driver_android; @@ -159,6 +160,13 @@ 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 create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; virtual Error kill(const ProcessID &p_pid) override; + virtual String get_system_ca_certificates() override; + + virtual Error setup_remote_filesystem(const String &p_server_host, int p_port, const String &p_password, String &r_project_path) override; + + virtual void benchmark_begin_measure(const String &p_what) override; + virtual void benchmark_end_measure(const String &p_what) override; + virtual void benchmark_dump() override; virtual bool _check_internal_feature_support(const String &p_feature) override; OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion); diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 4bb90cb971..843c015d49 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -120,7 +120,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS for (int i = 0; i < count; i++) { jobject j_param = env->GetObjectArrayElement(j_signal_params, i); - variant_params[i] = _jobject_to_variant(env, j_param); + ERR_FAIL_NULL(j_param); + memnew_placement(&variant_params[i], Variant(_jobject_to_variant(env, j_param))); args[i] = &variant_params[i]; env->DeleteLocalRef(j_param); } diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp index c08c1d4941..aef59c2584 100644 --- a/platform/android/tts_android.cpp +++ b/platform/android/tts_android.cpp @@ -35,9 +35,11 @@ #include "string_android.h" #include "thread_jandroid.h" +bool TTS_Android::initialized = false; jobject TTS_Android::tts = nullptr; jclass TTS_Android::cls = nullptr; +jmethodID TTS_Android::_init = nullptr; jmethodID TTS_Android::_is_speaking = nullptr; jmethodID TTS_Android::_is_paused = nullptr; jmethodID TTS_Android::_get_voices = nullptr; @@ -49,23 +51,34 @@ jmethodID TTS_Android::_stop_speaking = nullptr; HashMap<int, Char16String> TTS_Android::ids; void TTS_Android::setup(jobject p_tts) { - JNIEnv *env = get_jni_env(); + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + tts = env->NewGlobalRef(p_tts); - tts = env->NewGlobalRef(p_tts); + jclass c = env->GetObjectClass(tts); + cls = (jclass)env->NewGlobalRef(c); - jclass c = env->GetObjectClass(tts); - cls = (jclass)env->NewGlobalRef(c); + _init = env->GetMethodID(cls, "init", "()V"); + _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"); - _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"); + if (_init) { + env->CallVoidMethod(tts, _init); + initialized = true; + } + } } void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (ids.has(p_id)) { int pos = 0; if ((DisplayServer::TTSUtteranceEvent)p_event == DisplayServer::TTS_UTTERANCE_BOUNDARY) { @@ -86,6 +99,7 @@ void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { } bool TTS_Android::is_speaking() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_speaking) { JNIEnv *env = get_jni_env(); @@ -97,6 +111,7 @@ bool TTS_Android::is_speaking() { } bool TTS_Android::is_paused() { + ERR_FAIL_COND_V_MSG(!initialized, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_is_paused) { JNIEnv *env = get_jni_env(); @@ -108,6 +123,7 @@ bool TTS_Android::is_paused() { } Array TTS_Android::get_voices() { + ERR_FAIL_COND_V_MSG(!initialized, Array(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); Array list; if (_get_voices) { JNIEnv *env = get_jni_env(); @@ -135,6 +151,7 @@ Array TTS_Android::get_voices() { } 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) { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (p_interrupt) { stop(); } @@ -157,6 +174,7 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum } void TTS_Android::pause() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_pause_speaking) { JNIEnv *env = get_jni_env(); @@ -166,6 +184,7 @@ void TTS_Android::pause() { } void TTS_Android::resume() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (_resume_speaking) { JNIEnv *env = get_jni_env(); @@ -175,6 +194,7 @@ void TTS_Android::resume() { } void TTS_Android::stop() { + ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); for (const KeyValue<int, Char16String> &E : ids) { DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); } diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h index 8e3bfccbdb..39efef6ed1 100644 --- a/platform/android/tts_android.h +++ b/platform/android/tts_android.h @@ -31,16 +31,20 @@ #ifndef TTS_ANDROID_H #define TTS_ANDROID_H +#include "core/config/project_settings.h" #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/variant/array.h" #include "servers/display_server.h" #include <jni.h> class TTS_Android { + static bool initialized; static jobject tts; static jclass cls; + static jmethodID _init; static jmethodID _is_speaking; static jmethodID _is_paused; static jmethodID _get_voices; diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 71612cf1fa..bab055dbd5 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -39,6 +39,16 @@ def get_opts(): ] +def get_doc_classes(): + return [ + "EditorExportPlatformIOS", + ] + + +def get_doc_path(): + return "doc_classes" + + def get_flags(): return [ ("arch", "arm64"), # Default for convenience. diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index 6eaa7c8edc..57f601a858 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -55,7 +55,7 @@ #import <QuartzCore/CAMetalLayer.h> class DisplayServerIOS : public DisplayServer { - GDCLASS(DisplayServerIOS, DisplayServer) + // No need to register with GDCLASS, it's platform-specific and nothing is added. _THREAD_SAFE_CLASS_ diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index 469bc9be17..a52d2b185d 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -56,7 +56,10 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode rendering_driver = p_rendering_driver; // Init TTS - tts = [[TTS_IOS alloc] init]; + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + tts = [[TTS_IOS alloc] init]; + } #if defined(VULKAN_ENABLED) context_vulkan = nullptr; @@ -316,37 +319,37 @@ String DisplayServerIOS::get_name() const { } bool DisplayServerIOS::tts_is_speaking() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts isSpeaking]; } bool DisplayServerIOS::tts_is_paused() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts isPaused]; } TypedArray<Dictionary> DisplayServerIOS::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); + ERR_FAIL_COND_V_MSG(!tts, TypedArray<Dictionary>(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts getVoices]; } void DisplayServerIOS::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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [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 DisplayServerIOS::tts_pause() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts pauseSpeaking]; } void DisplayServerIOS::tts_resume() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts resumeSpeaking]; } void DisplayServerIOS::tts_stop() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts stopSpeaking]; } diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml new file mode 100644 index 0000000000..b37868e543 --- /dev/null +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformIOS" inherits="EditorExportPlatform" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for iOS. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for iOS">$DOCS_URL/tutorials/export/exporting_for_ios.html</link> + </tutorials> + <members> + <member name="application/app_store_team_id" type="String" setter="" getter=""> + Apple Team ID, unique 10-character string. To locate your Team ID check "Membership details" section in your Apple developer account dashboard, or "Organizational Unit" of your code signing certificate. See [url=https://developer.apple.com/help/account/manage-your-team/locate-your-team-id]Locate your Team ID[/url]. + </member> + <member name="application/bundle_identifier" type="String" setter="" getter=""> + Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]). + </member> + <member name="application/code_sign_identity_debug" type="String" setter="" getter=""> + The "Full Name", "Common Name" or SHA-1 hash of the signing identity used for debug export. + </member> + <member name="application/code_sign_identity_release" type="String" setter="" getter=""> + The "Full Name", "Common Name" or SHA-1 hash of the signing identity used for release export. + </member> + <member name="application/export_method_debug" type="int" setter="" getter=""> + Application distribution target (debug export). + </member> + <member name="application/export_method_release" type="int" setter="" getter=""> + Application distribution target (release export). + </member> + <member name="application/icon_interpolation" type="int" setter="" getter=""> + Interpolation method used to resize application icon. + </member> + <member name="application/launch_screens_interpolation" type="int" setter="" getter=""> + Interpolation method used to resize launch screen images. + </member> + <member name="application/provisioning_profile_uuid_debug" type="String" setter="" getter=""> + UUID of the provisioning profile. If left empty, Xcode will download or create a provisioning profile automatically. See [url=https://developer.apple.com/help/account/manage-profiles/edit-download-or-delete-profiles]Edit, download, or delete provisioning profiles[/url]. + Can be overridden with the environment variable [code]GODOT_IOS_PROVISIONING_PROFILE_UUID_DEBUG[/code]. + </member> + <member name="application/provisioning_profile_uuid_release" type="String" setter="" getter=""> + UUID of the provisioning profile. If left empty, Xcode will download or create a provisioning profile automatically. See [url=https://developer.apple.com/help/account/manage-profiles/edit-download-or-delete-profiles]Edit, download, or delete provisioning profiles[/url]. + Can be overridden with the environment variable [code]GODOT_IOS_PROVISIONING_PROFILE_UUID_RELEASE[/code]. + </member> + <member name="application/short_version" type="String" setter="" getter=""> + Application version visible to the user, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). + </member> + <member name="application/signature" type="String" setter="" getter=""> + A four-character creator code that is specific to the bundle. Optional. + </member> + <member name="application/targeted_device_family" type="int" setter="" getter=""> + Supported device family. + </member> + <member name="application/version" type="String" setter="" getter=""> + Machine-readable application version, in the [code]major.minor.patch[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). + </member> + <member name="architectures/arm64" type="bool" setter="" getter=""> + If [code]true[/code], [code]arm64[/code] binaries are included into exported project. + </member> + <member name="capabilities/access_wifi" type="bool" setter="" getter=""> + If [code]true[/code], networking features related to Wi-Fi access are enabled. See [url=https://developer.apple.com/support/required-device-capabilities/]Required Device Capabilities[/url]. + </member> + <member name="capabilities/push_notifications" type="bool" setter="" getter=""> + If [code]true[/code], push notifications are enabled. See [url=https://developer.apple.com/support/required-device-capabilities/]Required Device Capabilities[/url]. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="icons/app_store_1024x1024" type="String" setter="" getter=""> + App Store application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/ipad_76x76" type="String" setter="" getter=""> + Home screen application icon file on iPad (1x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/ipad_152x152" type="String" setter="" getter=""> + Home screen application icon file on iPad (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/ipad_167x167" type="String" setter="" getter=""> + Home screen application icon file on iPad (3x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/iphone_120x120" type="String" setter="" getter=""> + Home screen application icon file on iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/iphone_180x180" type="String" setter="" getter=""> + Home screen application icon file on iPhone (3x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/notification_40x40" type="String" setter="" getter=""> + Notification icon file on iPad and iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/notification_60x60" type="String" setter="" getter=""> + Notification icon file on iPhone (3x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/settings_58x58" type="String" setter="" getter=""> + Application settings icon file on iPad and iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/settings_87x87" type="String" setter="" getter=""> + Application settings icon file on iPhone (3x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/spotlight_40x40" type="String" setter="" getter=""> + Spotlight icon file on iPad (1x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="icons/spotlight_80x80" type="String" setter="" getter=""> + Spotlight icon file on iPad and iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. + </member> + <member name="landscape_launch_screens/ipad_1024x768" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="landscape_launch_screens/ipad_2048x1536" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="landscape_launch_screens/iphone_2208x1242" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="landscape_launch_screens/iphone_2436x1125" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/ipad_768x1024" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/ipad_1536x2048" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/iphone_640x960" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/iphone_640x1136" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/iphone_750x1334" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/iphone_1125x2436" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="portrait_launch_screens/iphone_1242x2208" type="String" setter="" getter=""> + Application launch screen image file. If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="privacy/camera_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the device's camera (in English). + </member> + <member name="privacy/camera_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the device's camera (localized). + </member> + <member name="privacy/microphone_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the device's microphone (in English). + </member> + <member name="privacy/microphone_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the device's microphone (localized). + </member> + <member name="privacy/photolibrary_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's photo library (in English). + </member> + <member name="privacy/photolibrary_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's photo library (localized). + </member> + <member name="storyboard/custom_bg_color" type="Color" setter="" getter=""> + A custom background color of the storyboard launch screen. + </member> + <member name="storyboard/custom_image@2x" type="String" setter="" getter=""> + Application launch screen image file (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="storyboard/custom_image@3x" type="String" setter="" getter=""> + Application launch screen image file (3x DPI). If left empty, it will fallback to [member ProjectSettings.application/boot_splash/image]. + </member> + <member name="storyboard/image_scale_mode" type="int" setter="" getter=""> + Launch screen image scaling mode. + </member> + <member name="storyboard/use_custom_bg_color" type="bool" setter="" getter=""> + If [code]true[/code], [member storyboard/custom_bg_color] is used as a launch screen background color, otherwise [code]application/boot_splash/bg_color[/code] project setting is used. + </member> + <member name="storyboard/use_launch_screen_storyboard" type="bool" setter="" getter=""> + If [code]true[/code], storyboard launch screen is used instead of launch screen images. + </member> + <member name="user_data/accessible_from_files_app" type="bool" setter="" getter=""> + If [code]true[/code], the app "Documents" folder can be accessed via "Files" app. See [url=https://developer.apple.com/documentation/bundleresources/information_property_list/lssupportsopeningdocumentsinplace]LSSupportsOpeningDocumentsInPlace[/url]. + </member> + <member name="user_data/accessible_from_itunes_sharing" type="bool" setter="" getter=""> + If [code]true[/code], the app "Documents" folder can be accessed via iTunes file sharing. See [url=https://developer.apple.com/documentation/bundleresources/information_property_list/uifilesharingenabled]UIFileSharingEnabled[/url]. + </member> + </members> +</class> diff --git a/platform/ios/export/export.cpp b/platform/ios/export/export.cpp index e35383206f..f4b90d8883 100644 --- a/platform/ios/export/export.cpp +++ b/platform/ios/export/export.cpp @@ -33,6 +33,10 @@ #include "editor/export/editor_export.h" #include "export_plugin.h" +void register_ios_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformIOS); +} + void register_ios_exporter() { Ref<EditorExportPlatformIOS> platform; platform.instantiate(); diff --git a/platform/ios/export/export.h b/platform/ios/export/export.h index 03e92bfccd..355811054a 100644 --- a/platform/ios/export/export.h +++ b/platform/ios/export/export.h @@ -31,6 +31,7 @@ #ifndef IOS_EXPORT_H #define IOS_EXPORT_H +void register_ios_exporter_types(); void register_ios_exporter(); #endif // IOS_EXPORT_H diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index c6f7ec09b1..06741a12e4 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -33,6 +33,7 @@ #include "core/string/translation.h" #include "editor/editor_node.h" #include "editor/editor_scale.h" +#include "editor/export/editor_export.h" #include "platform/ios/logo_svg.gen.h" #include "modules/modules_enabled.gen.h" // For svg. @@ -111,16 +112,44 @@ static const LoadingScreenInfo loading_screen_infos[] = { { 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 } + { PNAME("portrait_launch_screens/iphone_640x960"), "Default-480h@2x.png", 640, 960, false }, + { PNAME("portrait_launch_screens/iphone_640x1136"), "Default-568h@2x.png", 640, 1136, false }, + { PNAME("portrait_launch_screens/iphone_750x1334"), "Default-667h@2x.png", 750, 1334, false }, + { PNAME("portrait_launch_screens/iphone_1125x2436"), "Default-Portrait-X.png", 1125, 2436, false }, + { PNAME("portrait_launch_screens/ipad_768x1024"), "Default-Portrait.png", 768, 1024, false }, + { PNAME("portrait_launch_screens/ipad_1536x2048"), "Default-Portrait@2x.png", 1536, 2048, false }, + { PNAME("portrait_launch_screens/iphone_1242x2208"), "Default-Portrait-736h@3x.png", 1242, 2208, false } }; -void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) { +String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + if (p_preset) { + if (p_name == "application/app_store_team_id") { + String team_id = p_preset->get("application/app_store_team_id"); + if (team_id.is_empty()) { + return TTR("App Store Team ID not specified.") + "\n"; + } + } else if (p_name == "application/bundle_identifier") { + String identifier = p_preset->get("application/bundle_identifier"); + String pn_err; + if (!is_package_name_valid(identifier, &pn_err)) { + return TTR("Invalid Identifier:") + " " + pn_err; + } + } + } + return String(); +} + +bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + if (p_preset) { + bool sb = p_preset->get("storyboard/use_launch_screen_storyboard"); + if (!sb && p_option != "storyboard/use_launch_screen_storyboard" && p_option.begins_with("storyboard/")) { + return false; + } + } + return true; +} + +void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) const { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); @@ -129,18 +158,18 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) 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/app_store_team_id"), "", false, true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_debug"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/provisioning_profile_uuid_debug", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); 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/provisioning_profile_uuid_release", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/code_sign_identity_release", PROPERTY_HINT_PLACEHOLDER_TEXT, "iPhone Distribution"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/export_method_release", PROPERTY_HINT_ENUM, "App Store,Development,Ad-Hoc,Enterprise"), 0)); 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/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true)); 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")); @@ -197,7 +226,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, icon_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); } } - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_launch_screen_storyboard"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_launch_screen_storyboard"), false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "storyboard/image_scale_mode", PROPERTY_HINT_ENUM, "Same as Logo,Center,Scale to Fit,Scale to Fill,Scale"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); @@ -224,8 +253,8 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ }; 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"); + bool dbg_manual = !p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_PROFILE_UUID_DEBUG).operator String().is_empty() || (dbg_sign_id != "iPhone Developer"); + bool rel_manual = !p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_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()); @@ -259,9 +288,9 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ int export_method = p_preset->get(p_debug ? "application/export_method_debug" : "application/export_method_release"); strnew += lines[i].replace("$export_method", export_method_string[export_method]) + "\n"; } else if (lines[i].find("$provisioning_profile_uuid_release") != -1) { - strnew += lines[i].replace("$provisioning_profile_uuid_release", p_preset->get("application/provisioning_profile_uuid_release")) + "\n"; + strnew += lines[i].replace("$provisioning_profile_uuid_release", p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_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"; + strnew += lines[i].replace("$provisioning_profile_uuid_debug", p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_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"; @@ -275,7 +304,7 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ 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"); + String uuid = p_debug ? p_preset->get_or_env("application/provisioning_profile_uuid_debug", ENV_IOS_PROFILE_UUID_DEBUG) : p_preset->get_or_env("application/provisioning_profile_uuid_release", ENV_IOS_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", dbg_sign_id) + "\n"; @@ -1907,17 +1936,18 @@ bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorEx // Validate the project configuration. - String team_id = p_preset->get("application/app_store_team_id"); - if (team_id.length() == 0) { - err += TTR("App Store Team ID not specified - cannot configure the project.") + "\n"; - valid = false; - } - - String identifier = p_preset->get("application/bundle_identifier"); - String pn_err; - if (!is_package_name_valid(identifier, &pn_err)) { - err += TTR("Invalid Identifier:") + " " + pn_err + "\n"; - valid = false; + List<ExportOption> options; + get_export_options(&options); + for (const EditorExportPlatform::ExportOption &E : options) { + if (get_export_option_visibility(p_preset.ptr(), E.option.name)) { + String warn = get_export_option_warning(p_preset.ptr(), E.option.name); + if (!warn.is_empty()) { + err += warn + "\n"; + if (E.required) { + valid = false; + } + } + } } const String etc_error = test_etc2(); @@ -1934,24 +1964,28 @@ bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorEx } EditorExportPlatformIOS::EditorExportPlatformIOS() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _ios_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _ios_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); #endif - plugins_changed.set(); + plugins_changed.set(); #ifndef ANDROID_ENABLED - check_for_changes_thread.start(_check_for_changes_poll_thread, this); + check_for_changes_thread.start(_check_for_changes_poll_thread, this); #endif + } } EditorExportPlatformIOS::~EditorExportPlatformIOS() { #ifndef ANDROID_ENABLED quit_request.set(); - check_for_changes_thread.wait_to_finish(); + if (check_for_changes_thread.is_started()) { + check_for_changes_thread.wait_to_finish(); + } #endif } diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h index 628dae2e6f..9afefef121 100644 --- a/platform/ios/export/export_plugin.h +++ b/platform/ios/export/export_plugin.h @@ -49,19 +49,24 @@ #include <sys/stat.h> +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_IOS_PROFILE_UUID_DEBUG = "GODOT_IOS_PROVISIONING_PROFILE_UUID_DEBUG"; +const String ENV_IOS_PROFILE_UUID_RELEASE = "GODOT_IOS_PROVISIONING_PROFILE_UUID_RELEASE"; + class EditorExportPlatformIOS : public EditorExportPlatform { GDCLASS(EditorExportPlatformIOS, EditorExportPlatform); Ref<ImageTexture> logo; // Plugins - SafeFlag plugins_changed; + mutable SafeFlag plugins_changed; #ifndef ANDROID_ENABLED Thread check_for_changes_thread; SafeFlag quit_request; #endif Mutex plugins_lock; - Vector<PluginConfigIOS> plugins; + mutable Vector<PluginConfigIOS> plugins; typedef Error (*FileHandler)(String p_file, void *p_userdata); static Error _walk_dir_recursive(Ref<DirAccess> &p_da, FileHandler p_handler, void *p_userdata); @@ -178,7 +183,9 @@ class EditorExportPlatformIOS : public EditorExportPlatform { protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; public: virtual String get_name() const override { return "iOS"; } diff --git a/platform/ios/godot_ios.mm b/platform/ios/godot_ios.mm index dc6f7c82bd..b01e339211 100644 --- a/platform/ios/godot_ios.mm +++ b/platform/ios/godot_ios.mm @@ -83,14 +83,9 @@ int ios_main(int argc, char **argv) { char path[512]; memcpy(path, argv[0], len > sizeof(path) ? sizeof(path) : len); path[len] = 0; - printf("Path: %s\n", path); chdir(path); } - printf("godot_ios %s\n", argv[0]); - char cwd[512]; - getcwd(cwd, sizeof(cwd)); - printf("cwd %s\n", cwd); os = new OS_IOS(); // We must override main when testing is enabled @@ -104,10 +99,7 @@ int ios_main(int argc, char **argv) { argc = add_path(argc, fargv); argc = add_cmdline(argc, fargv); - printf("os created\n"); - Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); - printf("setup %i\n", err); if (err == ERR_HELP) { // Returned by --help and --version, so success. return 0; @@ -121,7 +113,6 @@ int ios_main(int argc, char **argv) { } void ios_finish() { - printf("ios_finish\n"); Main::cleanup(); delete os; } diff --git a/platform/ios/godot_view.mm b/platform/ios/godot_view.mm index 67e47092d8..ff04ea9838 100644 --- a/platform/ios/godot_view.mm +++ b/platform/ios/godot_view.mm @@ -173,7 +173,7 @@ static const float earth_gravity = 9.80665; self.isActive = NO; - printf("******** stop animation!\n"); + print_verbose("Stop animation!"); if (self.useCADisplayLink) { [self.displayLink invalidate]; @@ -193,7 +193,7 @@ static const float earth_gravity = 9.80665; self.isActive = YES; - printf("start animation!\n"); + print_verbose("Start animation!"); if (self.useCADisplayLink) { self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; @@ -213,7 +213,7 @@ static const float earth_gravity = 9.80665; - (void)drawView { if (!self.isActive) { - printf("draw view not active!\n"); + print_verbose("Draw view not active!"); return; } diff --git a/platform/ios/ios.mm b/platform/ios/ios.mm index d240891a5c..c911a512a5 100644 --- a/platform/ios/ios.mm +++ b/platform/ios/ios.mm @@ -72,8 +72,8 @@ CHHapticEngine *iOS::get_haptic_engine_instance() API_AVAILABLE(ios(13)) { 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) { + CHHapticEngine *cur_haptic_engine = get_haptic_engine_instance(); + if (cur_haptic_engine) { NSDictionary *hapticDict = @{ CHHapticPatternKeyPattern : @[ @{CHHapticPatternKeyEvent : @{ @@ -88,7 +88,7 @@ void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) NSError *error; CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; - [[haptic_engine createPlayerWithPattern:pattern error:&error] startAtTime:0 error:&error]; + [[cur_haptic_engine createPlayerWithPattern:pattern error:&error] startAtTime:0 error:&error]; NSLog(@"Could not vibrate using haptic engine: %@", error); } @@ -103,9 +103,9 @@ void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) 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) { + CHHapticEngine *cur_haptic_engine = get_haptic_engine_instance(); + if (cur_haptic_engine) { + [cur_haptic_engine startWithCompletionHandler:^(NSError *returnedError) { if (returnedError) { NSLog(@"Could not start haptic engine: %@", returnedError); } @@ -122,9 +122,9 @@ void iOS::start_haptic_engine() { 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) { + CHHapticEngine *cur_haptic_engine = get_haptic_engine_instance(); + if (cur_haptic_engine) { + [cur_haptic_engine stopWithCompletionHandler:^(NSError *returnedError) { if (returnedError) { NSLog(@"Could not stop haptic engine: %@", returnedError); } @@ -173,7 +173,7 @@ String iOS::get_rate_url(int p_app_id) const { String ret = app_url_path.replace("APP_ID", String::num(p_app_id)); - printf("returning rate url %s\n", ret.utf8().get_data()); + print_verbose(vformat("Returning rate url %s", ret)); return ret; } diff --git a/platform/ios/joypad_ios.mm b/platform/ios/joypad_ios.mm index 9194b09ef6..421c82dfc4 100644 --- a/platform/ios/joypad_ios.mm +++ b/platform/ios/joypad_ios.mm @@ -150,7 +150,7 @@ void JoypadIOS::start_processing() { int joy_id = Input::get_singleton()->get_unused_joy_id(); if (joy_id == -1) { - printf("Couldn't retrieve new joy id\n"); + print_verbose("Couldn't retrieve new joy ID."); return; } @@ -174,12 +174,12 @@ void JoypadIOS::start_processing() { GCController *controller = (GCController *)notification.object; if (!controller) { - printf("Couldn't retrieve new controller\n"); + print_verbose("Couldn't retrieve new controller."); return; } if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) { - printf("Controller is already registered\n"); + print_verbose("Controller is already registered."); } else if (!self.isProcessing) { [self.joypadsQueue addObject:controller]; } else { diff --git a/platform/ios/main.m b/platform/ios/main.m index 9ccc420c73..33b1034d98 100644 --- a/platform/ios/main.m +++ b/platform/ios/main.m @@ -42,15 +42,12 @@ int main(int argc, char *argv[]) { setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); #endif - printf("*********** main.m\n"); gargc = argc; gargv = argv; - printf("running app main\n"); @autoreleasepool { NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]); UIApplicationMain(argc, argv, nil, className); } - printf("main done\n"); return 0; } diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 4d2dd3c52c..739db419a3 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -240,10 +240,20 @@ Error OS_IOS::open_dynamic_library(const String p_path, void *&p_library_handle, } if (!FileAccess::exists(path)) { + // Load .dylib converted to framework from within the executable path. + path = get_framework_executable(get_executable_path().get_base_dir().path_join(p_path.get_file().get_basename() + ".framework")); + } + + if (!FileAccess::exists(path)) { // Load .dylib or framework from a standard iOS location. path = get_framework_executable(get_executable_path().get_base_dir().path_join("Frameworks").path_join(p_path.get_file())); } + if (!FileAccess::exists(path)) { + // Load .dylib converted to framework from a standard iOS location. + path = get_framework_executable(get_executable_path().get_base_dir().path_join("Frameworks").path_join(p_path.get_file().get_basename() + ".framework")); + } + 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() + "."); @@ -302,7 +312,7 @@ Error OS_IOS::shell_open(String p_uri) { return ERR_CANT_OPEN; } - printf("opening url %s\n", p_uri.utf8().get_data()); + print_verbose(vformat("Opening URL %s", p_uri)); [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; diff --git a/platform/ios/tts_ios.h b/platform/ios/tts_ios.h index 2d104de8ae..7f9d30b22b 100644 --- a/platform/ios/tts_ios.h +++ b/platform/ios/tts_ios.h @@ -38,8 +38,8 @@ #endif #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/templates/list.h" -#include "core/templates/rb_map.h" #include "core/variant/array.h" #include "servers/display_server.h" diff --git a/platform/ios/view_controller.mm b/platform/ios/view_controller.mm index a5aba201d7..8709252623 100644 --- a/platform/ios/view_controller.mm +++ b/platform/ios/view_controller.mm @@ -150,7 +150,7 @@ - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; - printf("*********** did receive memory warning!\n"); + print_verbose("Did receive memory warning!"); } - (void)viewDidLoad { @@ -165,11 +165,11 @@ } - (void)observeKeyboard { - printf("******** setting up keyboard input view\n"); + print_verbose("Setting up keyboard input view."); self.keyboardView = [GodotKeyboardInputView new]; [self.view addSubview:self.keyboardView]; - printf("******** adding observer for keyboard show/hide\n"); + print_verbose("Adding observer for keyboard show/hide."); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardOnScreen:) diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 54351757cd..dadc03685b 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -56,6 +56,16 @@ def get_opts(): ] +def get_doc_classes(): + return [ + "EditorExportPlatformLinuxBSD", + ] + + +def get_doc_path(): + return "doc_classes" + + def get_flags(): return [ ("arch", detect_arch()), @@ -453,6 +463,9 @@ def configure(env: "Environment"): else: env.Append(LINKFLAGS=["-T", "platform/linuxbsd/pck_embed.legacy.ld"]) + if platform.system() == "FreeBSD": + env.Append(LINKFLAGS=["-lkvm"]) + ## Cross-compilation # TODO: Support cross-compilation on architectures other than x86. host_is_64_bit = sys.maxsize > 2**32 diff --git a/platform/linuxbsd/doc_classes/EditorExportPlatformLinuxBSD.xml b/platform/linuxbsd/doc_classes/EditorExportPlatformLinuxBSD.xml new file mode 100644 index 0000000000..4ab2464929 --- /dev/null +++ b/platform/linuxbsd/doc_classes/EditorExportPlatformLinuxBSD.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformLinuxBSD" inherits="EditorExportPlatformPC" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for Linux/BSD. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for Linux">$DOCS_URL/tutorials/export/exporting_for_linux.html</link> + </tutorials> + <members> + <member name="binary_format/architecture" type="String" setter="" getter=""> + Application executable architecture. + Supported architectures: [code]x86_32[/code], [code]x86_64[/code], [code]arm64[/code], [code]arm32[/code], [code]rv64[/code], [code]ppc64[/code], and [code]ppc32[/code]. + Official export templates include [code]x86_32[/code] and [code]x86_64[/code] binaries only. + </member> + <member name="binary_format/embed_pck" type="bool" setter="" getter=""> + If [code]true[/code], project resources are embedded into the executable. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="debug/export_console_script" type="int" setter="" getter=""> + If [code]true[/code], a console wrapper script is exported alongside the main executable, which allows running the project with enabled console output. + </member> + <member name="ssh_remote_deploy/cleanup_script" type="String" setter="" getter=""> + Script code to execute on the remote host when app is finished. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="ssh_remote_deploy/enabled" type="bool" setter="" getter=""> + Enables remote deploy using SSH/SCP. + </member> + <member name="ssh_remote_deploy/extra_args_scp" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SCP. + </member> + <member name="ssh_remote_deploy/extra_args_ssh" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SSH. + </member> + <member name="ssh_remote_deploy/host" type="String" setter="" getter=""> + Remote host SSH user name and address, in [code]user@address[/code] format. + </member> + <member name="ssh_remote_deploy/port" type="String" setter="" getter=""> + Remote host SSH port number. + </member> + <member name="ssh_remote_deploy/run_script" type="String" setter="" getter=""> + Script code to execute on the remote host when running the app. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="texture_format/bptc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the BPTC format. + </member> + <member name="texture_format/etc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the ETC format. + </member> + <member name="texture_format/etc2" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the ETC2 format. + </member> + <member name="texture_format/s3tc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the S3TC format. + </member> + </members> +</class> diff --git a/platform/linuxbsd/export/export.cpp b/platform/linuxbsd/export/export.cpp index 2c5a945b6c..09f354246d 100644 --- a/platform/linuxbsd/export/export.cpp +++ b/platform/linuxbsd/export/export.cpp @@ -33,6 +33,10 @@ #include "editor/export/editor_export.h" #include "export_plugin.h" +void register_linuxbsd_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformLinuxBSD); +} + void register_linuxbsd_exporter() { Ref<EditorExportPlatformLinuxBSD> platform; platform.instantiate(); diff --git a/platform/linuxbsd/export/export.h b/platform/linuxbsd/export/export.h index a2d70a73e3..f493047815 100644 --- a/platform/linuxbsd/export/export.h +++ b/platform/linuxbsd/export/export.h @@ -31,6 +31,7 @@ #ifndef LINUXBSD_EXPORT_H #define LINUXBSD_EXPORT_H +void register_linuxbsd_exporter_types(); void register_linuxbsd_exporter(); #endif // LINUXBSD_EXPORT_H diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 2528bb2b99..9544cc761d 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -34,6 +34,7 @@ #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/editor_scale.h" +#include "editor/export/editor_export.h" #include "platform/linuxbsd/logo_svg.gen.h" #include "platform/linuxbsd/run_icon_svg.gen.h" @@ -142,7 +143,18 @@ List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<Edito return list; } -void EditorExportPlatformLinuxBSD::get_export_options(List<ExportOption> *r_options) { +bool EditorExportPlatformLinuxBSD::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + if (p_preset) { + // Hide SSH options. + bool ssh = p_preset->get("ssh_remote_deploy/enabled"); + if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) { + return false; + } + } + return true; +} + +void EditorExportPlatformLinuxBSD::get_export_options(List<ExportOption> *r_options) const { EditorExportPlatformPC::get_export_options(r_options); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "x86_64,x86_32,arm64,arm32,rv64,ppc64,ppc32"), "x86_64")); @@ -156,7 +168,7 @@ void EditorExportPlatformLinuxBSD::get_export_options(List<ExportOption> *r_opti "kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")\n" "rm -rf \"{temp_dir}\""; - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22")); @@ -503,22 +515,24 @@ Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, } EditorExportPlatformLinuxBSD::EditorExportPlatformLinuxBSD() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _linuxbsd_logo_svg, EDSCALE, upsample, false); - set_logo(ImageTexture::create_from_image(img)); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _linuxbsd_logo_svg, EDSCALE, upsample, false); + set_logo(ImageTexture::create_from_image(img)); - img_loader.create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _linuxbsd_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) { - stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); - } else { - stop_icon.instantiate(); + Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); + if (theme.is_valid()) { + stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); + } else { + stop_icon.instantiate(); + } } } diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h index 4f860c3fd0..cef714e86e 100644 --- a/platform/linuxbsd/export/export_plugin.h +++ b/platform/linuxbsd/export/export_plugin.h @@ -37,6 +37,8 @@ #include "scene/resources/texture.h" class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { + GDCLASS(EditorExportPlatformLinuxBSD, EditorExportPlatformPC); + HashMap<String, String> extensions; struct SSHCleanupCommand { @@ -69,8 +71,9 @@ class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { 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 void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index ec1fcf6698..6dfa8ed93c 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -138,6 +138,22 @@ FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() { #else unsupported = false; #endif + + if (unsupported) { + return; + } + + bool ver_ok = false; + int version_major = 0; + int version_minor = 0; + int version_rev = 0; + dbus_get_version(&version_major, &version_minor, &version_rev); + ver_ok = (version_major == 1 && version_minor >= 10) || (version_major > 1); // 1.10.0 + print_verbose(vformat("PortalDesktop: DBus %d.%d.%d detected.", version_major, version_minor, version_rev)); + if (!ver_ok) { + print_verbose("PortalDesktop: Unsupported DBus library version!"); + unsupported = true; + } } #endif // DBUS_ENABLED diff --git a/platform/linuxbsd/freedesktop_screensaver.cpp b/platform/linuxbsd/freedesktop_screensaver.cpp index d07e781a5f..cf179b5735 100644 --- a/platform/linuxbsd/freedesktop_screensaver.cpp +++ b/platform/linuxbsd/freedesktop_screensaver.cpp @@ -141,6 +141,22 @@ FreeDesktopScreenSaver::FreeDesktopScreenSaver() { #else unsupported = false; #endif + + if (unsupported) { + return; + } + + bool ver_ok = false; + int version_major = 0; + int version_minor = 0; + int version_rev = 0; + dbus_get_version(&version_major, &version_minor, &version_rev); + ver_ok = (version_major == 1 && version_minor >= 10) || (version_major > 1); // 1.10.0 + print_verbose(vformat("ScreenSaver: DBus %d.%d.%d detected.", version_major, version_minor, version_rev)); + if (!ver_ok) { + print_verbose("ScreenSaver:: Unsupported DBus library version!"); + unsupported = true; + } } #endif // DBUS_ENABLED diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 0256af0a59..ab79885fb4 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -32,6 +32,8 @@ #include "joypad_linux.h" +#include "core/os/os.h" + #include <dirent.h> #include <errno.h> #include <fcntl.h> @@ -72,6 +74,28 @@ void JoypadLinux::Joypad::reset() { events.clear(); } +#ifdef UDEV_ENABLED +// This function is derived from SDL: +// https://github.com/libsdl-org/SDL/blob/main/src/core/linux/SDL_sandbox.c#L28-L45 +static bool detect_sandbox() { + if (access("/.flatpak-info", F_OK) == 0) { + return true; + } + + // For Snap, we check multiple variables because they might be set for + // unrelated reasons. This is the same thing WebKitGTK does. + if (OS::get_singleton()->has_environment("SNAP") && OS::get_singleton()->has_environment("SNAP_NAME") && OS::get_singleton()->has_environment("SNAP_REVISION")) { + return true; + } + + if (access("/run/host/container-manager", F_OK) == 0) { + return true; + } + + return false; +} +#endif // UDEV_ENABLED + JoypadLinux::JoypadLinux(Input *in) { #ifdef UDEV_ENABLED #ifdef SOWRAP_ENABLED @@ -80,11 +104,25 @@ JoypadLinux::JoypadLinux(Input *in) { #else int dylibloader_verbose = 0; #endif - use_udev = initialize_libudev(dylibloader_verbose) == 0; - if (use_udev) { - print_verbose("JoypadLinux: udev enabled and loaded successfully."); + if (detect_sandbox()) { + // Linux binaries in sandboxes / containers need special handling because + // libudev doesn't work there. So we need to fallback to manual parsing + // of /dev/input in such case. + use_udev = false; + print_verbose("JoypadLinux: udev enabled, but detected incompatible sandboxed mode. Falling back to /dev/input to detect joypads."); } else { - print_verbose("JoypadLinux: udev enabled, but couldn't be loaded. Falling back to /dev/input to detect joypads."); + use_udev = initialize_libudev(dylibloader_verbose) == 0; + if (use_udev) { + if (!udev_new || !udev_unref || !udev_enumerate_new || !udev_enumerate_add_match_subsystem || !udev_enumerate_scan_devices || !udev_enumerate_get_list_entry || !udev_list_entry_get_next || !udev_list_entry_get_name || !udev_device_new_from_syspath || !udev_device_get_devnode || !udev_device_get_action || !udev_device_unref || !udev_enumerate_unref || !udev_monitor_new_from_netlink || !udev_monitor_filter_add_match_subsystem_devtype || !udev_monitor_enable_receiving || !udev_monitor_get_fd || !udev_monitor_receive_device || !udev_monitor_unref) { + // There's no API to check version, check if functions are available instead. + use_udev = false; + print_verbose("JoypadLinux: Unsupported udev library version!"); + } else { + print_verbose("JoypadLinux: udev enabled and loaded successfully."); + } + } else { + print_verbose("JoypadLinux: udev enabled, but couldn't be loaded. Falling back to /dev/input to detect joypads."); + } } #endif #else diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 88c3d2cc14..2c093b00e7 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -30,6 +30,7 @@ #include "os_linuxbsd.h" +#include "core/io/certs_compressed.gen.h" #include "core/io/dir_access.h" #include "main/main.h" #include "servers/display_server.h" @@ -195,6 +196,10 @@ void OS_LinuxBSD::set_main_loop(MainLoop *p_main_loop) { main_loop = p_main_loop; } +String OS_LinuxBSD::get_identifier() const { + return "linuxbsd"; +} + String OS_LinuxBSD::get_name() const { #ifdef __linux__ return "Linux"; @@ -210,39 +215,40 @@ String OS_LinuxBSD::get_name() const { } String OS_LinuxBSD::get_systemd_os_release_info_value(const String &key) const { - static String info; - if (info.is_empty()) { - Ref<FileAccess> f = FileAccess::open("/etc/os-release", FileAccess::READ); - if (f.is_valid()) { - while (!f->eof_reached()) { - const String line = f->get_line(); - if (line.find(key) != -1) { - return line.split("=")[1].strip_edges(); - } + Ref<FileAccess> f = FileAccess::open("/etc/os-release", FileAccess::READ); + if (f.is_valid()) { + while (!f->eof_reached()) { + const String line = f->get_line(); + if (line.find(key) != -1) { + String value = line.split("=")[1].strip_edges(); + value = value.trim_prefix("\""); + return value.trim_suffix("\""); } } } - return info; + return ""; } String OS_LinuxBSD::get_distribution_name() const { - static String systemd_name = get_systemd_os_release_info_value("NAME"); // returns a value for systemd users, otherwise an empty string. - if (!systemd_name.is_empty()) { - return systemd_name; + static String distribution_name = get_systemd_os_release_info_value("NAME"); // returns a value for systemd users, otherwise an empty string. + if (!distribution_name.is_empty()) { + return distribution_name; } struct utsname uts; // returns a decent value for BSD family. uname(&uts); - return uts.sysname; + distribution_name = uts.sysname; + return distribution_name; } String OS_LinuxBSD::get_version() const { - static String systemd_version = get_systemd_os_release_info_value("VERSION"); // returns a value for systemd users, otherwise an empty string. - if (!systemd_version.is_empty()) { - return systemd_version; + static String release_version = get_systemd_os_release_info_value("VERSION"); // returns a value for systemd users, otherwise an empty string. + if (!release_version.is_empty()) { + return release_version; } struct utsname uts; // returns a decent value for BSD family. uname(&uts); - return uts.version; + release_version = uts.version; + return release_version; } Vector<String> OS_LinuxBSD::get_video_adapter_driver_info() const { @@ -250,6 +256,11 @@ Vector<String> OS_LinuxBSD::get_video_adapter_driver_info() const { return Vector<String>(); } + static Vector<String> info; + if (!info.is_empty()) { + return info; + } + const String rendering_device_name = RenderingServer::get_singleton()->get_rendering_device()->get_device_name(); // e.g. `NVIDIA GeForce GTX 970` const String rendering_device_vendor = RenderingServer::get_singleton()->get_rendering_device()->get_device_vendor_name(); // e.g. `NVIDIA` const String card_name = rendering_device_name.trim_prefix(rendering_device_vendor).strip_edges(); // -> `GeForce GTX 970` @@ -315,8 +326,8 @@ Vector<String> OS_LinuxBSD::get_video_adapter_driver_info() const { Vector<String> class_display_device_drivers = OS_LinuxBSD::lspci_get_device_value(class_display_device_candidates, kernel_lit, dummys); Vector<String> class_3d_device_drivers = OS_LinuxBSD::lspci_get_device_value(class_3d_device_candidates, kernel_lit, dummys); - static String driver_name; - static String driver_version; + String driver_name; + String driver_version; // Use first valid value: for (const String &driver : class_3d_device_drivers) { @@ -336,7 +347,6 @@ Vector<String> OS_LinuxBSD::get_video_adapter_driver_info() const { } } - Vector<String> info; info.push_back(driver_name); String modinfo; @@ -490,6 +500,11 @@ bool OS_LinuxBSD::_check_internal_feature_support(const String &p_feature) { return true; } + // Match against the specific OS (linux, freebsd, etc). + if (p_feature == get_name().to_lower()) { + return true; + } + return false; } @@ -1076,6 +1091,40 @@ Error OS_LinuxBSD::move_to_trash(const String &p_path) { return OK; } +String OS_LinuxBSD::get_system_ca_certificates() { + String certfile; + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + + // Compile time preferred certificates path. + if (!String(_SYSTEM_CERTS_PATH).is_empty() && da->file_exists(_SYSTEM_CERTS_PATH)) { + certfile = _SYSTEM_CERTS_PATH; + } else if (da->file_exists("/etc/ssl/certs/ca-certificates.crt")) { + // Debian/Ubuntu + certfile = "/etc/ssl/certs/ca-certificates.crt"; + } else if (da->file_exists("/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")) { + // Fedora + certfile = "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"; + } else if (da->file_exists("/etc/ca-certificates/extracted/tls-ca-bundle.pem")) { + // Arch Linux + certfile = "/etc/ca-certificates/extracted/tls-ca-bundle.pem"; + } else if (da->file_exists("/var/lib/ca-certificates/ca-bundle.pem")) { + // openSUSE + certfile = "/var/lib/ca-certificates/ca-bundle.pem"; + } else if (da->file_exists("/etc/ssl/cert.pem")) { + // FreeBSD/OpenBSD + certfile = "/etc/ssl/cert.pem"; + } + + if (certfile.is_empty()) { + return ""; + } + + Ref<FileAccess> f = FileAccess::open(certfile, FileAccess::READ); + ERR_FAIL_COND_V_MSG(f.is_null(), "", vformat("Failed to open system CA certificates file: '%s'", certfile)); + + return f->get_as_text(); +} + OS_LinuxBSD::OS_LinuxBSD() { main_loop = nullptr; @@ -1103,6 +1152,16 @@ OS_LinuxBSD::OS_LinuxBSD() { font_config_initialized = true; #endif if (font_config_initialized) { + bool ver_ok = false; + int version = FcGetVersion(); + ver_ok = ((version / 100 / 100) == 2 && (version / 100 % 100) >= 11) || ((version / 100 / 100) > 2); // 2.11.0 + print_verbose(vformat("FontConfig %d.%d.%d detected.", version / 100 / 100, version / 100 % 100, version % 100)); + if (!ver_ok) { + font_config_initialized = false; + } + } + + if (font_config_initialized) { config = FcInitLoadConfigAndFonts(); if (!config) { font_config_initialized = false; diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 9423514944..c1e735b0d4 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -96,6 +96,7 @@ protected: virtual void set_main_loop(MainLoop *p_main_loop) override; public: + virtual String get_identifier() const override; virtual String get_name() const override; virtual String get_distribution_name() const override; virtual String get_version() const override; @@ -132,6 +133,8 @@ public: virtual Error move_to_trash(const String &p_path) override; + virtual String get_system_ca_certificates() override; + OS_LinuxBSD(); ~OS_LinuxBSD(); }; diff --git a/platform/linuxbsd/tts_linux.cpp b/platform/linuxbsd/tts_linux.cpp index ce0199e87f..8bc83f5b9c 100644 --- a/platform/linuxbsd/tts_linux.cpp +++ b/platform/linuxbsd/tts_linux.cpp @@ -48,6 +48,11 @@ void TTS_Linux::speech_init_thread_func(void *p_userdata) { if (initialize_speechd(dylibloader_verbose) != 0) { print_verbose("Text-to-Speech: Cannot load Speech Dispatcher library!"); } else { + if (!spd_open || !spd_set_notification_on || !spd_list_synthesis_voices || !free_spd_voices || !spd_set_synthesis_voice || !spd_set_volume || !spd_set_voice_pitch || !spd_set_voice_rate || !spd_set_data_mode || !spd_say || !spd_pause || !spd_resume || !spd_cancel) { + // There's no API to check version, check if functions are available instead. + print_verbose("Text-to-Speech: Unsupported Speech Dispatcher library version!"); + return; + } #else { #endif @@ -96,6 +101,24 @@ void TTS_Linux::speech_event_callback(size_t p_msg_id, size_t p_client_id, SPDNo } } +void TTS_Linux::_load_voices() { + if (!voices_loaded) { + SPDVoice **spd_voices = spd_list_synthesis_voices(synth); + if (spd_voices != nullptr) { + SPDVoice **voices_ptr = spd_voices; + while (*voices_ptr != nullptr) { + VoiceInfo vi; + vi.language = String::utf8((*voices_ptr)->language); + vi.variant = String::utf8((*voices_ptr)->variant); + voices[String::utf8((*voices_ptr)->name)] = vi; + voices_ptr++; + } + free_spd_voices(spd_voices); + } + voices_loaded = true; + } +} + void TTS_Linux::_speech_event(size_t p_msg_id, size_t p_client_id, int p_type) { _THREAD_SAFE_METHOD_ @@ -118,18 +141,13 @@ void TTS_Linux::_speech_event(size_t p_msg_id, size_t p_client_id, int p_type) { // Inject index mark after each word. String text; String language; - SPDVoice **voices = spd_list_synthesis_voices(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); + + _load_voices(); + const VoiceInfo *voice = voices.getptr(message.voice); + if (voice) { + language = voice->language; } + PackedInt32Array breaks = TS->string_get_word_breaks(message.text, language); for (int i = 0; i < breaks.size(); i += 2) { const int start = breaks[i]; @@ -169,21 +187,17 @@ Array TTS_Linux::get_voices() const { _THREAD_SAFE_METHOD_ ERR_FAIL_COND_V(!synth, Array()); + const_cast<TTS_Linux *>(this)->_load_voices(); + 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); + for (const KeyValue<String, VoiceInfo> &E : voices) { + Dictionary voice_d; + voice_d["name"] = E.key; + voice_d["id"] = E.key; + voice_d["language"] = E.value.language + "_" + E.value.variant; + list.push_back(voice_d); } + return list; } diff --git a/platform/linuxbsd/tts_linux.h b/platform/linuxbsd/tts_linux.h index 4134f8fa2f..ec68219689 100644 --- a/platform/linuxbsd/tts_linux.h +++ b/platform/linuxbsd/tts_linux.h @@ -34,8 +34,8 @@ #include "core/os/thread.h" #include "core/os/thread_safe.h" #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/templates/list.h" -#include "core/templates/rb_map.h" #include "core/variant/array.h" #include "servers/display_server.h" @@ -55,6 +55,13 @@ class TTS_Linux : public Object { int last_msg_id = -1; HashMap<int, int> ids; + struct VoiceInfo { + String language; + String variant; + }; + bool voices_loaded = false; + HashMap<String, VoiceInfo> voices; + Thread init_thread; static void speech_init_thread_func(void *p_userdata); @@ -64,6 +71,7 @@ class TTS_Linux : public Object { static TTS_Linux *singleton; protected: + void _load_voices(); void _speech_event(size_t p_msg_id, size_t p_client_id, int p_type); void _speech_index_mark(size_t p_msg_id, size_t p_client_id, int p_type, const String &p_index_mark); diff --git a/platform/linuxbsd/x11/detect_prime_x11.cpp b/platform/linuxbsd/x11/detect_prime_x11.cpp index 3d07be1c76..78778a8b56 100644 --- a/platform/linuxbsd/x11/detect_prime_x11.cpp +++ b/platform/linuxbsd/x11/detect_prime_x11.cpp @@ -60,6 +60,9 @@ typedef GLXContext (*GLXCREATECONTEXTATTRIBSARBPROC)(Display *, GLXFBConfig, GLXContext, Bool, const int *); +// To prevent shadowing warnings +#undef glGetString + struct vendor { const char *glxvendor = nullptr; int priority = 0; diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 552c15bf01..fa35340ebb 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -186,6 +186,7 @@ bool DisplayServerX11::_refresh_device_info() { xi.absolute_devices.clear(); xi.touch_devices.clear(); xi.pen_inverted_devices.clear(); + xi.last_relative_time = 0; int dev_count; XIDeviceInfo *info = XIQueryDevice(x11_display, XIAllDevices, &dev_count); @@ -302,37 +303,37 @@ void DisplayServerX11::_flush_mouse_motion() { #ifdef SPEECHD_ENABLED bool DisplayServerX11::tts_is_speaking() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return tts->is_speaking(); } bool DisplayServerX11::tts_is_paused() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return tts->is_paused(); } TypedArray<Dictionary> DisplayServerX11::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); + ERR_FAIL_COND_V_MSG(!tts, TypedArray<Dictionary>(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); 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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); 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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->pause(); } void DisplayServerX11::tts_resume() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->resume(); } void DisplayServerX11::tts_stop() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->stop(); } @@ -605,7 +606,7 @@ String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, A success = true; } } else { - printf("Failed to get selection data chunk.\n"); + print_verbose("Failed to get selection data chunk."); done = true; } @@ -632,7 +633,7 @@ String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, A if (result == Success) { ret.parse_utf8((const char *)data); } else { - printf("Failed to get selection data.\n"); + print_verbose("Failed to get selection data."); } if (data) { @@ -753,23 +754,56 @@ int DisplayServerX11::get_screen_count() const { } int DisplayServerX11::get_primary_screen() const { - return XDefaultScreen(x11_display); + int event_base, error_base; + if (XineramaQueryExtension(x11_display, &event_base, &error_base)) { + return 0; + } else { + return XDefaultScreen(x11_display); + } } -Rect2i DisplayServerX11::_screen_get_rect(int p_screen) const { - Rect2i rect(0, 0, 0, 0); +int DisplayServerX11::get_keyboard_focus_screen() const { + int count = get_screen_count(); + if (count < 2) { + // Early exit with single monitor. + return 0; + } - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; + Window focus = 0; + int revert_to = 0; + + XGetInputFocus(x11_display, &focus, &revert_to); + if (focus) { + Window focus_child = 0; + int x = 0, y = 0; + XTranslateCoordinates(x11_display, focus, DefaultRootWindow(x11_display), 0, 0, &x, &y, &focus_child); + + XWindowAttributes xwa; + XGetWindowAttributes(x11_display, focus, &xwa); + Rect2i window_rect = Rect2i(x, y, xwa.width, xwa.height); + + // Find which monitor has the largest overlap with the given window. + int screen_index = 0; + int max_area = 0; + for (int i = 0; i < count; i++) { + Rect2i screen_rect = _screen_get_rect(i); + Rect2i intersection = screen_rect.intersection(window_rect); + int area = intersection.get_area(); + if (area > max_area) { + max_area = area; + screen_index = i; + } + } + return screen_index; } + return get_primary_screen(); +} + +Rect2i DisplayServerX11::_screen_get_rect(int p_screen) const { + Rect2i rect(0, 0, 0, 0); + + p_screen = _get_screen_index(p_screen); ERR_FAIL_COND_V(p_screen < 0, rect); // Using Xinerama Extension. @@ -852,17 +886,7 @@ int bad_window_error_handler(Display *display, XErrorEvent *error) { Rect2i DisplayServerX11::screen_get_usable_rect(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); int screen_count = get_screen_count(); // Check if screen is valid. @@ -1139,18 +1163,7 @@ Rect2i DisplayServerX11::screen_get_usable_rect(int p_screen) const { int DisplayServerX11::screen_get_dpi(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - - //invalid screen? + p_screen = _get_screen_index(p_screen); ERR_FAIL_INDEX_V(p_screen, get_screen_count(), 0); //Get physical monitor Dimensions through XRandR and calculate dpi @@ -1211,8 +1224,8 @@ Color DisplayServerX11::screen_get_pixel(const Point2i &p_position) const { return Color(); } -float DisplayServerX11::screen_get_refresh_rate(int p_screen) const { - _THREAD_SAFE_METHOD_ +Ref<Image> DisplayServerX11::screen_get_image(int p_screen) const { + ERR_FAIL_INDEX_V(p_screen, get_screen_count(), Ref<Image>()); switch (p_screen) { case SCREEN_PRIMARY: { @@ -1225,7 +1238,95 @@ float DisplayServerX11::screen_get_refresh_rate(int p_screen) const { break; } - //invalid screen? + ERR_FAIL_COND_V(p_screen < 0, Ref<Image>()); + + XImage *image = nullptr; + + int event_base, error_base; + if (XineramaQueryExtension(x11_display, &event_base, &error_base)) { + int xin_count; + XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &xin_count); + if (p_screen < xin_count) { + int x_count = XScreenCount(x11_display); + for (int i = 0; i < x_count; i++) { + Window root = XRootWindow(x11_display, i); + XWindowAttributes root_attrs; + XGetWindowAttributes(x11_display, root, &root_attrs); + if ((xsi[p_screen].x_org >= root_attrs.x) && (xsi[p_screen].x_org <= root_attrs.x + root_attrs.width) && (xsi[p_screen].y_org >= root_attrs.y) && (xsi[p_screen].y_org <= root_attrs.y + root_attrs.height)) { + image = XGetImage(x11_display, root, xsi[p_screen].x_org, xsi[p_screen].y_org, xsi[p_screen].width, xsi[p_screen].height, AllPlanes, ZPixmap); + break; + } + } + } else { + ERR_FAIL_V_MSG(Ref<Image>(), "Invalid screen index: " + itos(p_screen) + "(count: " + itos(xin_count) + ")."); + } + } else { + int x_count = XScreenCount(x11_display); + if (p_screen < x_count) { + Window root = XRootWindow(x11_display, p_screen); + + XWindowAttributes root_attrs; + XGetWindowAttributes(x11_display, root, &root_attrs); + + image = XGetImage(x11_display, root, root_attrs.x, root_attrs.y, root_attrs.width, root_attrs.height, AllPlanes, ZPixmap); + } else { + ERR_FAIL_V_MSG(Ref<Image>(), "Invalid screen index: " + itos(p_screen) + "(count: " + itos(x_count) + ")."); + } + } + + Ref<Image> img; + if (image) { + int width = image->width; + int height = image->height; + + Vector<uint8_t> img_data; + img_data.resize(height * width * 4); + + uint8_t *sr = (uint8_t *)image->data; + uint8_t *wr = (uint8_t *)img_data.ptrw(); + + if (image->bits_per_pixel == 24 && image->red_mask == 0xff0000 && image->green_mask == 0x00ff00 && image->blue_mask == 0x0000ff) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + wr[(y * width + x) * 4 + 0] = sr[(y * width + x) * 3 + 2]; + wr[(y * width + x) * 4 + 1] = sr[(y * width + x) * 3 + 1]; + wr[(y * width + x) * 4 + 2] = sr[(y * width + x) * 3 + 0]; + wr[(y * width + x) * 4 + 3] = 255; + } + } + } else if (image->bits_per_pixel == 24 && image->red_mask == 0x0000ff && image->green_mask == 0x00ff00 && image->blue_mask == 0xff0000) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + wr[(y * width + x) * 4 + 0] = sr[(y * width + x) * 3 + 2]; + wr[(y * width + x) * 4 + 1] = sr[(y * width + x) * 3 + 1]; + wr[(y * width + x) * 4 + 2] = sr[(y * width + x) * 3 + 0]; + wr[(y * width + x) * 4 + 3] = 255; + } + } + } else if (image->bits_per_pixel == 32 && image->red_mask == 0xff0000 && image->green_mask == 0x00ff00 && image->blue_mask == 0x0000ff) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + wr[(y * width + x) * 4 + 0] = sr[(y * width + x) * 4 + 2]; + wr[(y * width + x) * 4 + 1] = sr[(y * width + x) * 4 + 1]; + wr[(y * width + x) * 4 + 2] = sr[(y * width + x) * 4 + 0]; + wr[(y * width + x) * 4 + 3] = 255; + } + } + } else { + XFree(image); + ERR_FAIL_V_MSG(Ref<Image>(), vformat("XImage with RGB mask %x %x %x and depth %d is not supported.", (uint64_t)image->red_mask, (uint64_t)image->green_mask, (uint64_t)image->blue_mask, (int64_t)image->bits_per_pixel)); + } + img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data); + XFree(image); + } + + return img; +} + +float DisplayServerX11::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + p_screen = _get_screen_index(p_screen); ERR_FAIL_INDEX_V(p_screen, get_screen_count(), SCREEN_REFRESH_RATE_FALLBACK); //Use xrandr to get screen refresh rate. @@ -1372,7 +1473,7 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { } XDestroyWindow(x11_display, wd.x11_xim_window); #ifdef XKB_ENABLED - if (xkb_loaded) { + if (xkb_loaded_v05p) { if (wd.xkb_state) { xkb_compose_state_unref(wd.xkb_state); wd.xkb_state = nullptr; @@ -1588,18 +1689,7 @@ void DisplayServerX11::window_set_current_screen(int p_screen, WindowID p_window ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd = windows[p_window]; - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - - // Check if screen is valid + p_screen = _get_screen_index(p_screen); ERR_FAIL_INDEX(p_screen, get_screen_count()); if (window_get_current_screen(p_window) == p_screen) { @@ -2672,16 +2762,12 @@ void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu } Ref<Texture2D> texture = p_cursor; + ERR_FAIL_COND(!texture.is_valid()); Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; Size2i texture_size; Rect2i atlas_rect; - if (texture.is_valid()) { - image = texture->get_image(); - } - - if (!image.is_valid() && atlas_texture.is_valid()) { + if (atlas_texture.is_valid()) { texture = atlas_texture->get_atlas(); atlas_rect.size.width = texture->get_width(); @@ -2691,19 +2777,23 @@ void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu texture_size.width = atlas_texture->get_region().size.x; texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { + } else { texture_size.width = texture->get_width(); texture_size.height = texture->get_height(); } - ERR_FAIL_COND(!texture.is_valid()); ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - image = texture->get_image(); + Ref<Image> image = texture->get_image(); ERR_FAIL_COND(!image.is_valid()); + if (image->is_compressed()) { + image = image->duplicate(true); + Error err = image->decompress(); + ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); + } // Create the cursor structure XcursorImage *cursor_image = XcursorImageCreate(texture_size.width, texture_size.height); @@ -2752,8 +2842,8 @@ void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu XcursorImageDestroy(cursor_image); } else { // Reset to default system cursor - if (img[p_shape]) { - cursors[p_shape] = XcursorImageLoadCursor(x11_display, img[p_shape]); + if (cursor_img[p_shape]) { + cursors[p_shape] = XcursorImageLoadCursor(x11_display, cursor_img[p_shape]); } CursorShape c = current_cursor; @@ -2866,7 +2956,7 @@ Key DisplayServerX11::keyboard_get_keycode_from_physical(Key p_keycode) const { Key modifiers = p_keycode & KeyModifierMask::MODIFIER_MASK; Key keycode_no_mod = p_keycode & KeyModifierMask::CODE_MASK; unsigned int xkeycode = KeyMappingX11::get_xlibcode(keycode_no_mod); - KeySym xkeysym = XkbKeycodeToKeysym(x11_display, xkeycode, 0, 0); + KeySym xkeysym = XkbKeycodeToKeysym(x11_display, xkeycode, keyboard_get_current_layout(), 0); if (is_ascii_lower_case(xkeysym)) { xkeysym -= ('a' - 'A'); } @@ -3005,7 +3095,7 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, String keysym; #ifdef XKB_ENABLED - if (xkb_loaded) { + if (xkb_loaded_v08p) { KeySym keysym_unicode_nm = 0; // keysym used to find unicode XLookupString(&xkeyevent_no_mod, nullptr, 0, &keysym_unicode_nm, nullptr); keysym = String::chr(xkb_keysym_to_utf32(xkb_keysym_to_upper(keysym_unicode_nm))); @@ -3100,7 +3190,7 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, } while (status == XBufferOverflow); #endif #ifdef XKB_ENABLED - } else if (xkeyevent->type == KeyPress && wd.xkb_state && xkb_loaded) { + } else if (xkeyevent->type == KeyPress && wd.xkb_state && xkb_loaded_v05p) { xkb_compose_feed_result res = xkb_compose_state_feed(wd.xkb_state, keysym_unicode); if (res == XKB_COMPOSE_FEED_ACCEPTED) { if (xkb_compose_state_get_status(wd.xkb_state) == XKB_COMPOSE_COMPOSED) { @@ -3361,7 +3451,7 @@ Atom DisplayServerX11::_process_selection_request_target(Atom p_target, Window p return p_property; } else { char *target_name = XGetAtomName(x11_display, p_target); - printf("Target '%s' not supported.\n", target_name); + print_verbose(vformat("Target '%s' not supported.", target_name)); if (target_name) { XFree(target_name); } @@ -3525,8 +3615,17 @@ void DisplayServerX11::_window_changed(XEvent *event) { // Query display server about a possible new window state. wd.fullscreen = _window_fullscreen_check(window_id); - wd.minimized = _window_minimize_check(window_id); - wd.maximized = _window_maximize_check(window_id, "_NET_WM_STATE"); + wd.maximized = _window_maximize_check(window_id, "_NET_WM_STATE") && !wd.fullscreen; + wd.minimized = _window_minimize_check(window_id) && !wd.fullscreen && !wd.maximized; + + // Readjusting the window position if the window is being reparented by the window manager for decoration + Window root, parent, *children; + unsigned int nchildren; + if (XQueryTree(x11_display, wd.x11_window, &root, &parent, &children, &nchildren) && wd.parent != parent) { + wd.parent = parent; + window_set_position(wd.position, window_id); + } + XFree(children); { //the position in xconfigure is not useful here, obtain it manually @@ -5012,12 +5111,13 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, V { wd.x11_window = XCreateWindow(x11_display, RootWindow(x11_display, visualInfo.screen), win_rect.position.x, win_rect.position.y, win_rect.size.width > 0 ? win_rect.size.width : 1, win_rect.size.height > 0 ? win_rect.size.height : 1, 0, visualInfo.depth, InputOutput, visualInfo.visual, valuemask, &windowAttributes); + wd.parent = RootWindow(x11_display, visualInfo.screen); XSetWindowAttributes window_attributes_ime = {}; window_attributes_ime.event_mask = KeyPressMask | KeyReleaseMask | StructureNotifyMask | ExposureMask; wd.x11_xim_window = XCreateWindow(x11_display, wd.x11_window, 0, 0, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, CWEventMask, &window_attributes_ime); #ifdef XKB_ENABLED - if (dead_tbl && xkb_loaded) { + if (dead_tbl && xkb_loaded_v05p) { wd.xkb_state = xkb_compose_state_new(dead_tbl, XKB_COMPOSE_STATE_NO_FLAGS); } #endif @@ -5301,9 +5401,16 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode ERR_FAIL_MSG("Can't load XCursor dynamically."); } #ifdef XKB_ENABLED - xkb_loaded = (initialize_xkbcommon(dylibloader_verbose) == 0); - if (!xkb_context_new || !xkb_compose_table_new_from_locale || !xkb_compose_table_unref || !xkb_context_unref || !xkb_compose_state_feed || !xkb_compose_state_unref || !xkb_compose_state_new || !xkb_compose_state_get_status || !xkb_compose_state_get_utf8 || !xkb_keysym_to_utf32 || !xkb_keysym_to_upper) { - xkb_loaded = false; + bool xkb_loaded = (initialize_xkbcommon(dylibloader_verbose) == 0); + xkb_loaded_v05p = xkb_loaded; + if (!xkb_context_new || !xkb_compose_table_new_from_locale || !xkb_compose_table_unref || !xkb_context_unref || !xkb_compose_state_feed || !xkb_compose_state_unref || !xkb_compose_state_new || !xkb_compose_state_get_status || !xkb_compose_state_get_utf8) { + xkb_loaded_v05p = false; + print_verbose("Detected XKBcommon library version older than 0.5, dead key composition and Unicode key labels disabled."); + } + xkb_loaded_v08p = xkb_loaded; + if (!xkb_keysym_to_utf32 || !xkb_keysym_to_upper) { + xkb_loaded_v08p = false; + print_verbose("Detected XKBcommon library version older than 0.8, Unicode key labels disabled."); } #endif if (initialize_xext(dylibloader_verbose) != 0) { @@ -5359,9 +5466,18 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode r_error = OK; + { + if (!XcursorImageCreate || !XcursorImageLoadCursor || !XcursorImageDestroy || !XcursorGetDefaultSize || !XcursorGetTheme || !XcursorLibraryLoadImage) { + // There's no API to check version, check if functions are available instead. + ERR_PRINT("Unsupported Xcursor library version."); + r_error = ERR_UNAVAILABLE; + return; + } + } + for (int i = 0; i < CURSOR_MAX; i++) { cursors[i] = None; - img[i] = nullptr; + cursor_img[i] = nullptr; } XInitThreads(); //always use threads @@ -5375,6 +5491,71 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode return; } + { + int version_major = 0; + int version_minor = 0; + int rc = XShapeQueryVersion(x11_display, &version_major, &version_minor); + print_verbose(vformat("Xshape %d.%d detected.", version_major, version_minor)); + if (rc != 1 || version_major < 1) { + ERR_PRINT("Unsupported Xshape library version."); + r_error = ERR_UNAVAILABLE; + XCloseDisplay(x11_display); + return; + } + } + + { + int version_major = 0; + int version_minor = 0; + int rc = XineramaQueryVersion(x11_display, &version_major, &version_minor); + print_verbose(vformat("Xinerama %d.%d detected.", version_major, version_minor)); + if (rc != 1 || version_major < 1) { + ERR_PRINT("Unsupported Xinerama library version."); + r_error = ERR_UNAVAILABLE; + XCloseDisplay(x11_display); + return; + } + } + + { + int version_major = 0; + int version_minor = 0; + int rc = XRRQueryVersion(x11_display, &version_major, &version_minor); + print_verbose(vformat("Xrandr %d.%d detected.", version_major, version_minor)); + if (rc != 1 || (version_major == 1 && version_minor < 3) || (version_major < 1)) { + ERR_PRINT("Unsupported Xrandr library version."); + r_error = ERR_UNAVAILABLE; + XCloseDisplay(x11_display); + return; + } + } + + { + int version_major = 0; + int version_minor = 0; + int rc = XRenderQueryVersion(x11_display, &version_major, &version_minor); + print_verbose(vformat("Xrender %d.%d detected.", version_major, version_minor)); + if (rc != 1 || (version_major == 0 && version_minor < 11)) { + ERR_PRINT("Unsupported Xrender library version."); + r_error = ERR_UNAVAILABLE; + XCloseDisplay(x11_display); + return; + } + } + + { + int version_major = 2; // Report 2.2 as supported by engine, but should work with 2.1 or 2.0 library as well. + int version_minor = 2; + int rc = XIQueryVersion(x11_display, &version_major, &version_minor); + print_verbose(vformat("Xinput %d.%d detected.", version_major, version_minor)); + if (rc != Success || (version_major < 2)) { + ERR_PRINT("Unsupported Xinput2 library version."); + r_error = ERR_UNAVAILABLE; + XCloseDisplay(x11_display); + return; + } + } + char *modifiers = nullptr; Bool xkb_dar = False; XAutoRepeatOn(x11_display); @@ -5489,7 +5670,10 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode #ifdef SPEECHD_ENABLED // Init TTS - tts = memnew(TTS_Linux); + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + tts = memnew(TTS_Linux); + } #endif //!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -5653,8 +5837,8 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode "question_arrow" }; - img[i] = XcursorLibraryLoadImage(cursor_file[i], cursor_theme, cursor_size); - if (!img[i]) { + cursor_img[i] = XcursorLibraryLoadImage(cursor_file[i], cursor_theme, cursor_size); + if (!cursor_img[i]) { const char *fallback = nullptr; switch (i) { @@ -5692,7 +5876,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode fallback = "bd_double_arrow"; break; case CURSOR_MOVE: - img[i] = img[CURSOR_DRAG]; + cursor_img[i] = cursor_img[CURSOR_DRAG]; break; case CURSOR_VSPLIT: fallback = "sb_v_double_arrow"; @@ -5705,11 +5889,11 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode break; } if (fallback != nullptr) { - img[i] = XcursorLibraryLoadImage(fallback, cursor_theme, cursor_size); + cursor_img[i] = XcursorLibraryLoadImage(fallback, cursor_theme, cursor_size); } } - if (img[i]) { - cursors[i] = XcursorImageLoadCursor(x11_display, img[i]); + if (cursor_img[i]) { + cursors[i] = XcursorImageLoadCursor(x11_display, cursor_img[i]); } else { print_verbose("Failed loading custom cursor: " + String(cursor_file[i])); } @@ -5798,7 +5982,7 @@ DisplayServerX11::~DisplayServerX11() { } XDestroyWindow(x11_display, wd.x11_xim_window); #ifdef XKB_ENABLED - if (xkb_loaded) { + if (xkb_loaded_v05p) { if (wd.xkb_state) { xkb_compose_state_unref(wd.xkb_state); wd.xkb_state = nullptr; @@ -5810,7 +5994,7 @@ DisplayServerX11::~DisplayServerX11() { } #ifdef XKB_ENABLED - if (xkb_loaded) { + if (xkb_loaded_v05p) { if (dead_tbl) { xkb_compose_table_unref(dead_tbl); } @@ -5849,8 +6033,8 @@ DisplayServerX11::~DisplayServerX11() { if (cursors[i] != None) { XFreeCursor(x11_display, cursors[i]); } - if (img[i] != nullptr) { - XcursorImageDestroy(img[i]); + if (cursor_img[i] != nullptr) { + XcursorImageDestroy(cursor_img[i]); } } @@ -5864,7 +6048,9 @@ DisplayServerX11::~DisplayServerX11() { } #ifdef SPEECHD_ENABLED - memdelete(tts); + if (tts) { + memdelete(tts); + } #endif #ifdef DBUS_ENABLED diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index c98409253e..fd3a5dccfa 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -119,8 +119,7 @@ typedef struct _xrr_monitor_info { #undef CursorShape class DisplayServerX11 : public DisplayServer { - //No need to register, it's platform-specific and nothing is added - //GDCLASS(DisplayServerX11, DisplayServer) + // No need to register with GDCLASS, it's platform-specific and nothing is added. _THREAD_SAFE_CLASS_ @@ -160,6 +159,7 @@ class DisplayServerX11 : public DisplayServer { struct WindowData { Window x11_window; Window x11_xim_window; + Window parent; ::XIC xic; bool ime_active = false; bool ime_in_progress = false; @@ -211,7 +211,8 @@ class DisplayServerX11 : public DisplayServer { String im_text; #ifdef XKB_ENABLED - bool xkb_loaded = false; + bool xkb_loaded_v05p = false; + bool xkb_loaded_v08p = false; xkb_context *xkb_ctx = nullptr; xkb_compose_table *dead_tbl = nullptr; #endif @@ -305,7 +306,7 @@ class DisplayServerX11 : public DisplayServer { const char *cursor_theme = nullptr; int cursor_size = 0; - XcursorImage *img[CURSOR_MAX]; + XcursorImage *cursor_img[CURSOR_MAX]; Cursor cursors[CURSOR_MAX]; Cursor null_cursor; CursorShape current_cursor = CURSOR_ARROW; @@ -404,12 +405,14 @@ public: virtual int get_screen_count() const override; virtual int get_primary_screen() const override; + virtual int get_keyboard_focus_screen() 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_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Color screen_get_pixel(const Point2i &p_position) const override; + virtual Ref<Image> screen_get_image(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; #if defined(DBUS_ENABLED) virtual void screen_set_keep_on(bool p_enable) override; diff --git a/platform/linuxbsd/x11/gl_manager_x11.cpp b/platform/linuxbsd/x11/gl_manager_x11.cpp index ee767dfa80..1e579c9f01 100644 --- a/platform/linuxbsd/x11/gl_manager_x11.cpp +++ b/platform/linuxbsd/x11/gl_manager_x11.cpp @@ -44,6 +44,9 @@ typedef GLXContext (*GLXCREATECONTEXTATTRIBSARBPROC)(Display *, GLXFBConfig, GLXContext, Bool, const int *); +// To prevent shadowing warnings +#undef glXCreateContextAttribsARB + struct GLManager_X11_Private { ::GLXContext glx_context; }; diff --git a/platform/macos/detect.py b/platform/macos/detect.py index ae8749354e..7b8d3fd853 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -39,6 +39,16 @@ def get_opts(): ] +def get_doc_classes(): + return [ + "EditorExportPlatformMacOS", + ] + + +def get_doc_path(): + return "doc_classes" + + def get_flags(): return [ ("arch", detect_arch()), @@ -108,10 +118,10 @@ def configure(env: "Environment"): env.Append(CCFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) env.Append(LINKFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) elif env["arch"] == "x86_64": - print("Building for macOS 10.12+.") - env.Append(ASFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) - env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) - env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) + print("Building for macOS 10.13+.") + env.Append(ASFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"]) + env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"]) + env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"]) env.Append(CCFLAGS=["-fobjc-arc"]) @@ -225,6 +235,8 @@ def configure(env: "Environment"): "CoreMedia", "-framework", "QuartzCore", + "-framework", + "Security", ] ) env.Append(LIBS=["pthread", "z"]) diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index f7c5b0b847..495dc43c55 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -56,7 +56,7 @@ #undef CursorShape class DisplayServerMacOS : public DisplayServer { - GDCLASS(DisplayServerMacOS, DisplayServer) + // No need to register with GDCLASS, it's platform-specific and nothing is added. _THREAD_SAFE_CLASS_ @@ -188,7 +188,7 @@ private: Variant tag; Callable callback; }; - Vector<MenuCall> deferred_menu_calls; + List<MenuCall> deferred_menu_calls; const NSMenu *_get_menu_root(const String &p_menu_root) const; NSMenu *_get_menu_root(const String &p_menu_root); @@ -327,6 +327,7 @@ public: virtual int get_screen_count() const override; virtual int get_primary_screen() const override; + virtual int get_keyboard_focus_screen() 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 int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; @@ -335,6 +336,7 @@ public: virtual Rect2i screen_get_usable_rect(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 Color screen_get_pixel(const Point2i &p_position) const override; + virtual Ref<Image> screen_get_image(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void screen_set_keep_on(bool p_enable) override; virtual bool screen_is_kept_on() const override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 322c3f85bf..c24115d705 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -1741,37 +1741,37 @@ void DisplayServerMacOS::global_menu_clear(const String &p_menu_root) { } bool DisplayServerMacOS::tts_is_speaking() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts isSpeaking]; } bool DisplayServerMacOS::tts_is_paused() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts isPaused]; } TypedArray<Dictionary> DisplayServerMacOS::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, Array()); + ERR_FAIL_COND_V_MSG(!tts, Array(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts getVoices]; } void DisplayServerMacOS::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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [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 DisplayServerMacOS::tts_pause() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts pauseSpeaking]; } void DisplayServerMacOS::tts_resume() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts resumeSpeaking]; } void DisplayServerMacOS::tts_stop() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); [tts stopSpeaking]; } @@ -1840,9 +1840,9 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect if (!p_callback.is_null()) { Variant button = button_pressed; Variant *buttonp = &button; - Variant ret; + Variant fun_ret; Callable::CallError ce; - p_callback.callp((const Variant **)&buttonp, 1, ret, ce); + p_callback.callp((const Variant **)&buttonp, 1, fun_ret, ce); } return OK; @@ -1872,9 +1872,9 @@ Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description if (!p_callback.is_null()) { Variant text = ret; Variant *textp = &text; - Variant ret; + Variant fun_ret; Callable::CallError ce; - p_callback.callp((const Variant **)&textp, 1, ret, ce); + p_callback.callp((const Variant **)&textp, 1, fun_ret, ce); } return OK; @@ -1897,7 +1897,7 @@ void DisplayServerMacOS::mouse_set_mode(MouseMode p_mode) { bool previously_shown = (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED); if (show_cursor && !previously_shown) { - WindowID window_id = get_window_at_screen_position(mouse_get_position()); + window_id = get_window_at_screen_position(mouse_get_position()); if (window_id != INVALID_WINDOW_ID) { send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_ENTER); } @@ -2112,20 +2112,15 @@ int DisplayServerMacOS::get_primary_screen() const { return 0; } +int DisplayServerMacOS::get_keyboard_focus_screen() const { + const NSUInteger index = [[NSScreen screens] indexOfObject:[NSScreen mainScreen]]; + return (index == NSNotFound) ? 0 : index; +} + Point2i DisplayServerMacOS::screen_get_position(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); Point2i position = _get_native_screen_position(p_screen) - _get_screens_origin(); // macOS native y-coordinate relative to _get_screens_origin() is negative, // Godot expects a positive value. @@ -2136,17 +2131,7 @@ Point2i DisplayServerMacOS::screen_get_position(int p_screen) const { Size2i DisplayServerMacOS::screen_get_size(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { // Note: Use frame to get the whole screen size. @@ -2160,17 +2145,7 @@ Size2i DisplayServerMacOS::screen_get_size(int p_screen) const { int DisplayServerMacOS::screen_get_dpi(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; @@ -2191,17 +2166,7 @@ int DisplayServerMacOS::screen_get_dpi(int p_screen) const { float DisplayServerMacOS::screen_get_scale(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); if (OS::get_singleton()->is_hidpi_allowed()) { NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { @@ -2224,17 +2189,7 @@ float DisplayServerMacOS::screen_get_max_scale() const { Rect2i DisplayServerMacOS::screen_get_usable_rect(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { const float scale = screen_get_max_scale(); @@ -2279,8 +2234,8 @@ Color DisplayServerMacOS::screen_get_pixel(const Point2i &p_position) const { return Color(); } -float DisplayServerMacOS::screen_get_refresh_rate(int p_screen) const { - _THREAD_SAFE_METHOD_ +Ref<Image> DisplayServerMacOS::screen_get_image(int p_screen) const { + ERR_FAIL_INDEX_V(p_screen, get_screen_count(), Ref<Image>()); switch (p_screen) { case SCREEN_PRIMARY: { @@ -2293,6 +2248,39 @@ float DisplayServerMacOS::screen_get_refresh_rate(int p_screen) const { break; } + Ref<Image> img; + NSArray *screenArray = [NSScreen screens]; + if ((NSUInteger)p_screen < [screenArray count]) { + NSRect nsrect = [[screenArray objectAtIndex:p_screen] frame]; + NSDictionary *screenDescription = [[screenArray objectAtIndex:p_screen] deviceDescription]; + CGDirectDisplayID display_id = [[screenDescription objectForKey:@"NSScreenNumber"] unsignedIntValue]; + CGImageRef image = CGDisplayCreateImageForRect(display_id, CGRectMake(0, 0, nsrect.size.width, nsrect.size.height)); + if (image) { + CGColorSpaceRef color_space = CGColorSpaceCreateDeviceRGB(); + if (color_space) { + NSUInteger width = CGImageGetWidth(image); + NSUInteger height = CGImageGetHeight(image); + + Vector<uint8_t> img_data; + img_data.resize(height * width * 4); + CGContextRef context = CGBitmapContextCreate(img_data.ptrw(), width, height, 8, 4 * width, color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + if (context) { + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data); + CGContextRelease(context); + } + CGColorSpaceRelease(color_space); + } + CGImageRelease(image); + } + } + return img; +} + +float DisplayServerMacOS::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + p_screen = _get_screen_index(p_screen); NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; @@ -3328,16 +3316,12 @@ void DisplayServerMacOS::cursor_set_custom_image(const Ref<Resource> &p_cursor, } Ref<Texture2D> texture = p_cursor; + ERR_FAIL_COND(!texture.is_valid()); Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; Size2 texture_size; Rect2 atlas_rect; - if (texture.is_valid()) { - image = texture->get_image(); - } - - if (!image.is_valid() && atlas_texture.is_valid()) { + if (atlas_texture.is_valid()) { texture = atlas_texture->get_atlas(); atlas_rect.size.width = texture->get_width(); @@ -3347,19 +3331,23 @@ void DisplayServerMacOS::cursor_set_custom_image(const Ref<Resource> &p_cursor, texture_size.width = atlas_texture->get_region().size.x; texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { + } else { texture_size.width = texture->get_width(); texture_size.height = texture->get_height(); } - ERR_FAIL_COND(!texture.is_valid()); ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - image = texture->get_image(); + Ref<Image> image = texture->get_image(); ERR_FAIL_COND(!image.is_valid()); + if (image->is_compressed()) { + image = image->duplicate(true); + Error err = image->decompress(); + ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); + } NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nullptr @@ -3521,14 +3509,16 @@ void DisplayServerMacOS::process_events() { } // Process "menu_callback"s. - for (MenuCall &E : deferred_menu_calls) { - Variant tag = E.tag; + while (List<MenuCall>::Element *call_p = deferred_menu_calls.front()) { + MenuCall call = call_p->get(); + deferred_menu_calls.pop_front(); // Remove before call to avoid infinite loop in case callback is using `process_events` (e.g. EditorProgress). + + Variant tag = call.tag; Variant *tagp = &tag; Variant ret; Callable::CallError ce; - E.callback.callp((const Variant **)&tagp, 1, ret, ce); + call.callback.callp((const Variant **)&tagp, 1, ret, ce); } - deferred_menu_calls.clear(); if (!drop_events) { _process_key_events(); @@ -3855,7 +3845,10 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM CGDisplayRegisterReconfigurationCallback(_displays_arrangement_changed, nullptr); // Init TTS - tts = [[TTS_MacOS alloc] init]; + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + tts = [[TTS_MacOS alloc] init]; + } NSMenuItem *menu_item; NSString *title; @@ -3918,7 +3911,6 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM gl_manager = nullptr; r_error = ERR_UNAVAILABLE; ERR_FAIL_MSG("Could not initialize OpenGL"); - return; } } #endif diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml new file mode 100644 index 0000000000..d030995ee7 --- /dev/null +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -0,0 +1,299 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformMacOS" inherits="EditorExportPlatform" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for macOS. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for macOS">$DOCS_URL/tutorials/export/exporting_for_macos.html</link> + <link title="Running Godot apps on macOS">$DOCS_URL/tutorials//export/running_on_macos.html</link> + </tutorials> + <members> + <member name="application/app_category" type="String" setter="" getter=""> + Application category for the App Store. + </member> + <member name="application/bundle_identifier" type="String" setter="" getter=""> + Unique application identifier in a reverse-DNS format, can only contain alphanumeric characters ([code]A-Z[/code], [code]a-z[/code], and [code]0-9[/code]), hyphens ([code]-[/code]), and periods ([code].[/code]). + </member> + <member name="application/copyright" type="String" setter="" getter=""> + Copyright notice for the bundle visible to the user (in English). + </member> + <member name="application/copyright_localized" type="Dictionary" setter="" getter=""> + Copyright notice for the bundle visible to the user (localized). + </member> + <member name="application/icon" type="String" setter="" getter=""> + Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/macos_native_icon], and then to [member ProjectSettings.application/config/icon]. + </member> + <member name="application/icon_interpolation" type="int" setter="" getter=""> + Interpolation method used to resize application icon. + </member> + <member name="application/min_macos_version" type="String" setter="" getter=""> + Minimum version of macOS required for this application to run in the [code]major.minor.patch[/code] or [code]major.minor[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). + </member> + <member name="application/short_version" type="String" setter="" getter=""> + Application version visible to the user, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). + </member> + <member name="application/signature" type="String" setter="" getter=""> + A four-character creator code that is specific to the bundle. Optional. + </member> + <member name="application/version" type="String" setter="" getter=""> + Machine-readable application version, in the [code]major.minor.patch[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). + </member> + <member name="binary_format/architecture" type="String" setter="" getter=""> + Application executable architecture. + Supported architectures: [code]x86_64[/code], [code]arm64[/code], and [code]universal[/code] ([code]x86_64 + arm64[/code]). + Official export templates include [code]universal[/code] binaries only. + </member> + <member name="codesign/apple_team_id" type="String" setter="" getter=""> + Apple Team ID, unique 10-character string. To locate your Team ID check "Membership details" section in your Apple developer account dashboard, or "Organizational Unit" of your code signing certificate. See [url=https://developer.apple.com/help/account/manage-your-team/locate-your-team-id]Locate your Team ID[/url]. + </member> + <member name="codesign/certificate_file" type="String" setter="" getter=""> + PKCS #12 certificate file used to sign [code].app[/code] bundle. + Can be overridden with the environment variable [code]GODOT_MACOS_CODESIGN_CERTIFICATE_FILE[/code]. + </member> + <member name="codesign/certificate_password" type="String" setter="" getter=""> + Password for the certificate file used to sign [code].app[/code] bundle. + Can be overridden with the environment variable [code]GODOT_MACOS_CODESIGN_CERTIFICATE_PASSWORD[/code]. + </member> + <member name="codesign/codesign" type="int" setter="" getter=""> + Tool to use for code signing. + </member> + <member name="codesign/custom_options" type="PackedStringArray" setter="" getter=""> + Array of the additional command line arguments passed to the code signing tool. + </member> + <member name="codesign/entitlements/address_book" type="bool" setter="" getter=""> + Enable to allow access to contacts in the user's address book, if it's enabled you should also provide usage message in the [code]privacy/address_book_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_addressbook]com.apple.security.personal-information.addressbook[/url]. + </member> + <member name="codesign/entitlements/allow_dyld_environment_variables" type="bool" setter="" getter=""> + Allows app to use dynamic linker environment variables to inject code. If you are using add-ons with dynamic or self-modifying native code, enable them according to the add-on documentation. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-dyld-environment-variables]com.apple.security.cs.allow-dyld-environment-variables[/url]. + </member> + <member name="codesign/entitlements/allow_jit_code_execution" type="bool" setter="" getter=""> + Allows creating writable and executable memory for JIT code. If you are using add-ons with dynamic or self-modifying native code, enable them according to the add-on documentation. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit]com.apple.security.cs.allow-jit[/url]. + </member> + <member name="codesign/entitlements/allow_unsigned_executable_memory" type="bool" setter="" getter=""> + Allows creating writable and executable memory without JIT restrictions. If you are using add-ons with dynamic or self-modifying native code, enable them according to the add-on documentation. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-unsigned-executable-memory]com.apple.security.cs.allow-unsigned-executable-memory[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/device_bluetooth" type="bool" setter="" getter=""> + Enable to allow app to interact with Bluetooth devices. This entitlement is required to use wireless controllers. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_device_bluetooth]com.apple.security.device.bluetooth[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/device_usb" type="bool" setter="" getter=""> + Enable to allow app to interact with USB devices. This entitlement is required to use wired controllers. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_device_usb]com.apple.security.device.usb[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/enabled" type="bool" setter="" getter=""> + Enables App Sandbox. The App Sandbox restricts access to user data, networking, and devices. Sandboxed apps can't access most of the file system, can't use custom file dialogs and execute binaries outside the .app bundle. See [url=https://developer.apple.com/documentation/security/app_sandbox]App Sandbox[/url]. + [b]Note:[/b] To distribute an app through the App Store, you must enable the App Sandbox. + </member> + <member name="codesign/entitlements/app_sandbox/files_downloads" type="int" setter="" getter=""> + Allows read or write access to the user's "Downloads" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_downloads_read-write]com.apple.security.files.downloads.read-write[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/files_movies" type="int" setter="" getter=""> + Allows read or write access to the user's "Movies" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_movies_read-write]com.apple.security.files.movies.read-write[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/files_music" type="int" setter="" getter=""> + Allows read or write access to the user's "Music" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_music_read-write]com.apple.security.files.music.read-write[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/files_pictures" type="int" setter="" getter=""> + Allows read or write access to the user's "Pictures" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_pictures_read-write]com.apple.security.files.pictures.read-write[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/helper_executables" type="Array" setter="" getter=""> + List of helper executables to embedded to the app bundle. Sandboxed app are limited to execute only these executable. See [url=https://developer.apple.com/documentation/xcode/embedding-a-helper-tool-in-a-sandboxed-app]Embedding a command-line tool in a sandboxed app[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/network_client" type="bool" setter="" getter=""> + Enable to allow app to establish outgoing network connections. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_network_client]com.apple.security.network.client[/url]. + </member> + <member name="codesign/entitlements/app_sandbox/network_server" type="bool" setter="" getter=""> + Enable to allow app to listen for incoming network connections. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_network_server]com.apple.security.network.server[/url]. + </member> + <member name="codesign/entitlements/apple_events" type="bool" setter="" getter=""> + Enable to allow app to send Apple events to other apps. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_automation_apple-events]com.apple.security.automation.apple-events[/url]. + </member> + <member name="codesign/entitlements/audio_input" type="bool" setter="" getter=""> + Enable if you need to use the microphone or other audio input sources, if it's enabled you should also provide usage message in the [code]privacy/microphone_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_device_audio-input]com.apple.security.device.audio-input[/url]. + </member> + <member name="codesign/entitlements/calendars" type="bool" setter="" getter=""> + Enable to allow access to the user's calendar, if it's enabled you should also provide usage message in the [code]privacy/calendar_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_calendars]com.apple.security.personal-information.calendars[/url]. + </member> + <member name="codesign/entitlements/camera" type="bool" setter="" getter=""> + Enable if you need to use the camera, if it's enabled you should also provide usage message in the [code]privacy/camera_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_device_camera]com.apple.security.device.camera[/url]. + </member> + <member name="codesign/entitlements/custom_file" type="String" setter="" getter=""> + Custom entitlements [code].plist[/code] file, if specified the rest of entitlements in the export config are ignored. + </member> + <member name="codesign/entitlements/debugging" type="bool" setter="" getter=""> + You can temporarily enable this entitlement to use native debugger (GDB, LLDB) with the exported app. This entitlement should be disabled for production export. See [url=https://developer.apple.com/documentation/xcode/embedding-a-helper-tool-in-a-sandboxed-app]Embedding a command-line tool in a sandboxed app[/url]. + </member> + <member name="codesign/entitlements/disable_library_validation" type="bool" setter="" getter=""> + Allows app to load arbitrary libraries and frameworks (not signed with the same Team ID as the main executable or by Apple). Enable it if you are using GDExtension add-ons or ad-hoc signing, or want to support user-provided external add-ons. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation]com.apple.security.cs.disable-library-validation[/url]. + </member> + <member name="codesign/entitlements/location" type="bool" setter="" getter=""> + Enable if you need to use location information from Location Services, if it's enabled you should also provide usage message in the [code]privacy/location_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_location]com.apple.security.personal-information.location[/url]. + </member> + <member name="codesign/entitlements/photos_library" type="bool" setter="" getter=""> + Enable to allow access to the user's Photos library, if it's enabled you should also provide usage message in the [code]privacy/photos_library_usage_description[/code] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_photos-library]com.apple.security.personal-information.photos-library[/url]. + </member> + <member name="codesign/identity" type="String" setter="" getter=""> + The "Full Name", "Common Name" or SHA-1 hash of the signing identity used to sign [code].app[/code] bundle. + </member> + <member name="codesign/installer_identity" type="String" setter="" getter=""> + The "Full Name", "Common Name" or SHA-1 hash of the signing identity used to sign [code].pkg[/code] installer package for App Store distribution, use [code]3rd Party Mac Developer Installer: Name.[/code] identity. + </member> + <member name="codesign/provisioning_profile" type="String" setter="" getter=""> + Provisioning profile file downloaded from Apple developer account dashboard. See [url=https://developer.apple.com/help/account/manage-profiles/edit-download-or-delete-profiles]Edit, download, or delete provisioning profiles[/url]. + Can be overridden with the environment variable [code]GODOT_MACOS_CODESIGN_PROVISIONING_PROFILE[/code]. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="debug/export_console_script" type="int" setter="" getter=""> + If enabled, a script file that can be used to run the application with console output is created alongside the exported application. + </member> + <member name="display/high_res" type="bool" setter="" getter=""> + If [code]true[/code], the application is rendered at native display resolution, otherwise it is always rendered at loHPI resolution and upscaled by OS when required. + </member> + <member name="export/distribution_type" type="int" setter="" getter=""> + Application distribution target. + </member> + <member name="notarization/api_key" type="String" setter="" getter=""> + Apple App Store Connect API issuer key file. + Can be overridden with the environment variable [code]GODOT_MACOS_NOTARIZATION_API_KEY[/code]. + </member> + <member name="notarization/api_key_id" type="String" setter="" getter=""> + Apple App Store Connect API issuer key ID. + Can be overridden with the environment variable [code]GODOT_MACOS_NOTARIZATION_API_KEY_ID[/code]. + </member> + <member name="notarization/api_uuid" type="String" setter="" getter=""> + Apple App Store Connect API issuer UUID. + Can be overridden with the environment variable [code]GODOT_MACOS_NOTARIZATION_API_UUID[/code]. + </member> + <member name="notarization/apple_id_name" type="String" setter="" getter=""> + Apple ID account name (email address). + Can be overridden with the environment variable [code]GODOT_MACOS_NOTARIZATION_APPLE_ID_NAME[/code]. + </member> + <member name="notarization/apple_id_password" type="String" setter="" getter=""> + Apple ID app-specific password. + Can be overridden with the environment variable [code]GODOT_MACOS_NOTARIZATION_APPLE_ID_PASSWORD[/code]. + </member> + <member name="notarization/notarization" type="int" setter="" getter=""> + Tool to use for notarization. + </member> + <member name="privacy/address_book_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's contacts (in English). + </member> + <member name="privacy/address_book_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's contacts (localized). + </member> + <member name="privacy/calendar_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's calendar data (in English). + </member> + <member name="privacy/calendar_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's calendar data (localized). + </member> + <member name="privacy/camera_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the device's camera (in English). + </member> + <member name="privacy/camera_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the device's camera (localized). + </member> + <member name="privacy/desktop_folder_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's "Desktop" folder (in English). + </member> + <member name="privacy/desktop_folder_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's "Desktop" folder (localized). + </member> + <member name="privacy/documents_folder_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's "Documents" folder (in English). + </member> + <member name="privacy/documents_folder_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's "Documents" folder (localized). + </member> + <member name="privacy/downloads_folder_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's "Downloads" folder (in English). + </member> + <member name="privacy/downloads_folder_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's "Downloads" folder (localized). + </member> + <member name="privacy/location_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's location information (in English). + </member> + <member name="privacy/location_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's location information (localized). + </member> + <member name="privacy/microphone_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the device's microphone (in English). + </member> + <member name="privacy/microphone_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the device's microphone (localized). + </member> + <member name="privacy/network_volumes_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's network drives (in English). + </member> + <member name="privacy/network_volumes_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's network drives (localized). + </member> + <member name="privacy/photos_library_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's photo library (in English). + </member> + <member name="privacy/photos_library_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's photo library (localized). + </member> + <member name="privacy/removable_volumes_usage_description" type="String" setter="" getter=""> + A message displayed when requesting access to the user's removable drives (in English). + </member> + <member name="privacy/removable_volumes_usage_description_localized" type="Dictionary" setter="" getter=""> + A message displayed when requesting access to the user's removable drives (localized). + </member> + <member name="ssh_remote_deploy/cleanup_script" type="String" setter="" getter=""> + Script code to execute on the remote host when app is finished. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="ssh_remote_deploy/enabled" type="bool" setter="" getter=""> + Enables remote deploy using SSH/SCP. + </member> + <member name="ssh_remote_deploy/extra_args_scp" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SCP. + </member> + <member name="ssh_remote_deploy/extra_args_ssh" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SSH. + </member> + <member name="ssh_remote_deploy/host" type="String" setter="" getter=""> + Remote host SSH user name and address, in [code]user@address[/code] format. + </member> + <member name="ssh_remote_deploy/port" type="String" setter="" getter=""> + Remote host SSH port number. + </member> + <member name="ssh_remote_deploy/run_script" type="String" setter="" getter=""> + Script code to execute on the remote host when running the app. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="xcode/platform_build" type="String" setter="" getter=""> + macOS build number used to build application executable. + </member> + <member name="xcode/sdk_build" type="String" setter="" getter=""> + macOS SDK build number used to build application executable. + </member> + <member name="xcode/sdk_name" type="String" setter="" getter=""> + macOS SDK name used to build application executable. + </member> + <member name="xcode/sdk_version" type="String" setter="" getter=""> + macOS SDK version used to build application executable in the [code]major.minor[/code] format. + </member> + <member name="xcode/xcode_build" type="String" setter="" getter=""> + Xcode build number used to build application executable. + </member> + <member name="xcode/xcode_version" type="String" setter="" getter=""> + Xcode version used to build application executable. + </member> + </members> +</class> diff --git a/platform/macos/export/export.cpp b/platform/macos/export/export.cpp index 5a2850a6b7..8930974df9 100644 --- a/platform/macos/export/export.cpp +++ b/platform/macos/export/export.cpp @@ -32,6 +32,10 @@ #include "export_plugin.h" +void register_macos_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformMacOS); +} + void register_macos_exporter() { #ifndef ANDROID_ENABLED EDITOR_DEF("export/macos/rcodesign", ""); diff --git a/platform/macos/export/export.h b/platform/macos/export/export.h index 384dab826e..a7a7dcad16 100644 --- a/platform/macos/export/export.h +++ b/platform/macos/export/export.h @@ -31,6 +31,7 @@ #ifndef MACOS_EXPORT_H #define MACOS_EXPORT_H +void register_macos_exporter_types(); void register_macos_exporter(); #endif // MACOS_EXPORT_H diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 5a097adf39..f0fa5f2d36 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -62,13 +62,191 @@ void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset } } -bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const { +String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + if (p_preset) { + int dist_type = p_preset->get("export/distribution_type"); + bool ad_hoc = false; + int codesign_tool = p_preset->get("codesign/codesign"); + int notary_tool = p_preset->get("notarization/notarization"); + switch (codesign_tool) { + case 1: { // built-in ad-hoc + ad_hoc = true; + } break; + case 2: { // "rcodesign" + ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty(); + } break; +#ifdef MACOS_ENABLED + case 3: { // "codesign" + ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); + } break; +#endif + default: { + }; + } + + if (p_name == "application/bundle_identifier") { + String identifier = p_preset->get("application/bundle_identifier"); + String pn_err; + if (!is_package_name_valid(identifier, &pn_err)) { + return TTR("Invalid bundle identifier:") + " " + pn_err; + } + } + + if (p_name == "codesign/certificate_file" || p_name == "codesign/certificate_password" || p_name == "codesign/identity") { + if (dist_type == 2) { + if (ad_hoc) { + return TTR("App Store distribution with ad-hoc code signing is not supported."); + } + } else if (notary_tool > 0 && ad_hoc) { + return TTR("Notarization with an ad-hoc signature is not supported."); + } + } + + if (p_name == "codesign/apple_team_id") { + String team_id = p_preset->get("codesign/apple_team_id"); + if (team_id.is_empty()) { + if (dist_type == 2) { + return TTR("Apple Team ID is required for App Store distribution."); + } else if (notary_tool > 0) { + return TTR("Apple Team ID is required for notarization."); + } + } + } + + if (p_name == "codesign/provisioning_profile" && dist_type == 2) { + String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE); + if (pprof.is_empty()) { + return TTR("Provisioning profile is required for App Store distribution."); + } + } + + if (p_name == "codesign/installer_identity" && dist_type == 2) { + String ident = p_preset->get("codesign/installer_identity"); + if (ident.is_empty()) { + return TTR("Installer signing identity is required for App Store distribution."); + } + } + + if (p_name == "codesign/entitlements/app_sandbox/enabled" && dist_type == 2) { + bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); + if (!sandbox) { + return TTR("App sandbox is required for App Store distribution."); + } + } + + if (p_name == "codesign/codesign") { + if (dist_type == 2) { + if (codesign_tool == 0) { + return TTR("Code signing is required for App Store distribution."); + } + if (codesign_tool == 1) { + return TTR("App Store distribution with ad-hoc code signing is not supported."); + } + } else if (notary_tool > 0) { + if (codesign_tool == 0) { + return TTR("Code signing is required for notarization."); + } + if (codesign_tool == 1) { + return TTR("Notarization with an ad-hoc signature is not supported."); + } + } + } + + if (notary_tool == 2 || notary_tool == 3) { + if (p_name == "notarization/apple_id_name" || p_name == "notarization/api_uuid") { + String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID); + String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID); + if (apple_id.is_empty() && api_uuid.is_empty()) { + return TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified."); + } + if (!apple_id.is_empty() && !api_uuid.is_empty()) { + return TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time."); + } + } + if (p_name == "notarization/apple_id_password") { + String apple_id = p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID); + String apple_pass = p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS); + if (!apple_id.is_empty() && apple_pass.is_empty()) { + return TTR("Apple ID password not specified."); + } + } + if (p_name == "notarization/api_key_id") { + String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID); + String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID); + if (!api_uuid.is_empty() && api_key.is_empty()) { + return TTR("App Store Connect API key ID not specified."); + } + } + } else if (notary_tool == 1) { + if (p_name == "notarization/api_uuid") { + String api_uuid = p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID); + if (api_uuid.is_empty()) { + return TTR("App Store Connect issuer ID name not specified."); + } + } + if (p_name == "notarization/api_key_id") { + String api_key = p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID); + if (api_key.is_empty()) { + return TTR("App Store Connect API key ID not specified."); + } + } + } + + if (codesign_tool > 0) { + if (p_name == "privacy/microphone_usage_description") { + String discr = p_preset->get("privacy/microphone_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/audio_input"); + if (enabled && discr.is_empty()) { + return TTR("Microphone access is enabled, but usage description is not specified."); + } + } + if (p_name == "privacy/camera_usage_description") { + String discr = p_preset->get("privacy/camera_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/camera"); + if (enabled && discr.is_empty()) { + return TTR("Camera access is enabled, but usage description is not specified."); + } + } + if (p_name == "privacy/location_usage_description") { + String discr = p_preset->get("privacy/location_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/location"); + if (enabled && discr.is_empty()) { + return TTR("Location information access is enabled, but usage description is not specified."); + } + } + if (p_name == "privacy/address_book_usage_description") { + String discr = p_preset->get("privacy/address_book_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/address_book"); + if (enabled && discr.is_empty()) { + return TTR("Address book access is enabled, but usage description is not specified."); + } + } + if (p_name == "privacy/calendar_usage_description") { + String discr = p_preset->get("privacy/calendar_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/calendars"); + if (enabled && discr.is_empty()) { + return TTR("Calendar access is enabled, but usage description is not specified."); + } + } + if (p_name == "privacy/photos_library_usage_description") { + String discr = p_preset->get("privacy/photos_library_usage_description"); + bool enabled = p_preset->get("codesign/entitlements/photos_library"); + if (enabled && discr.is_empty()) { + return TTR("Photo library access is enabled, but usage description is not specified."); + } + } + } + } + return String(); +} + +bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { // Hide irrelevant code signing options. if (p_preset) { int codesign_tool = p_preset->get("codesign/codesign"); switch (codesign_tool) { case 1: { // built-in ad-hoc - if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options") { + if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option == "codesign/team_id") { return false; } } break; @@ -85,17 +263,48 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP } break; #endif default: { // disabled - if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements")) { + if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements") || p_option == "codesign/team_id") { return false; } } break; } + // Distribution type. + int dist_type = p_preset->get("export/distribution_type"); + if (dist_type != 2 && p_option == "codesign/installer_identity") { + return false; + } + + if (dist_type == 2 && p_option.begins_with("notarization/")) { + return false; + } + + if (dist_type != 2 && p_option == "codesign/provisioning_profile") { + return false; + } + + String custom_prof = p_preset->get("codesign/entitlements/custom_file"); + if (!custom_prof.is_empty() && p_option != "codesign/entitlements/custom_file" && p_option.begins_with("codesign/entitlements/")) { + return false; + } + + // Hide sandbox entitlements. + bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); + if (!sandbox && p_option != "codesign/entitlements/app_sandbox/enabled" && p_option.begins_with("codesign/entitlements/app_sandbox/")) { + return false; + } + + // Hide SSH options. + bool ssh = p_preset->get("ssh_remote_deploy/enabled"); + if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) { + return false; + } + // Hide irrelevant notarization options. int notary_tool = p_preset->get("notarization/notarization"); switch (notary_tool) { case 1: { // "rcodesign" - if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id") { + if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password") { return false; } } break; @@ -106,7 +315,7 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP // All options are visible. } break; default: { // disabled - if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key" || p_option == "notarization/api_key_id") { + if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key" || p_option == "notarization/api_key_id") { return false; } } break; @@ -122,7 +331,39 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP return true; } -void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options) { +List<String> EditorExportPlatformMacOS::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { + List<String> list; + + if (p_preset.is_valid()) { + int dist_type = p_preset->get("export/distribution_type"); + if (dist_type == 0) { +#ifdef MACOS_ENABLED + list.push_back("dmg"); +#endif + list.push_back("zip"); + list.push_back("app"); + } else if (dist_type == 1) { +#ifdef MACOS_ENABLED + list.push_back("dmg"); +#endif + list.push_back("zip"); + } else if (dist_type == 2) { +#ifdef MACOS_ENABLED + list.push_back("pkg"); +#endif + } + } + + return list; +} + +void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options) const { +#ifdef MACOS_ENABLED + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution,App Store"), 1, true)); +#else + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "export/distribution_type", PROPERTY_HINT_ENUM, "Testing,Distribution"), 1, true)); +#endif + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "universal,x86_64,arm64", PROPERTY_USAGE_STORAGE), "universal")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); @@ -130,27 +371,39 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_script", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.icns,*.png,*.webp,*.svg"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4)); - 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/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/signature"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/app_category", PROPERTY_HINT_ENUM, "Business,Developer-tools,Education,Entertainment,Finance,Games,Action-games,Adventure-games,Arcade-games,Board-games,Card-games,Casino-games,Dice-games,Educational-games,Family-games,Kids-games,Music-games,Puzzle-games,Racing-games,Role-playing-games,Simulation-games,Sports-games,Strategy-games,Trivia-games,Word-games,Graphics-design,Healthcare-fitness,Lifestyle,Medical,Music,News,Photography,Productivity,Reference,Social-networking,Sports,Travel,Utilities,Video,Weather"), "Games")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_macos_version"), "10.12")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/platform_build"), "14C18")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_version"), "13.1")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_build"), "22C55")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_name"), "macosx13.1")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_version"), "1420")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/xcode_build"), "14C18")); + #ifdef MACOS_ENABLED r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign,Xcode codesign"), 3, true)); #else - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign"), 1, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),rcodesign"), 1, true, true)); #endif + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/installer_identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "3rd Party Mac Developer Installer: (ID)"), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "ID"), "", false, true)); // "codesign" only options: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), "")); // "rcodesign" only options: - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); // "codesign" and "rcodesign" only options: - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/provisioning_profile", PROPERTY_HINT_GLOBAL_FILE, "*.provisionprofile", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); + + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "", true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false)); @@ -163,7 +416,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/photos_library"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/apple_events"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/debugging"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false, true, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_server"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_client"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/device_usb"), false)); @@ -181,35 +434,34 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,rcodesign"), 0, true)); #endif // "altool" and "notarytool" only options: - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide team ID if your Apple ID belongs to multiple teams"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); // "altool", "notarytool" and "rcodesign" only options: - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID UUID"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_GLOBAL_FILE, "*.p8"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID UUID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_GLOBAL_FILE, "*.p8", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "", false, true)); - 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::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); String run_script = "#!/usr/bin/env bash\n" @@ -220,7 +472,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options "kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")\n" "rm -rf \"{temp_dir}\""; - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22")); @@ -424,8 +676,22 @@ void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_pres strnew += lines[i].replace("$app_category", cat.to_lower()) + "\n"; } else if (lines[i].find("$copyright") != -1) { strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n"; + } else if (lines[i].find("$min_version") != -1) { + strnew += lines[i].replace("$min_version", p_preset->get("application/min_macos_version")) + "\n"; } else if (lines[i].find("$highres") != -1) { strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n"; + } else if (lines[i].find("$platfbuild") != -1) { + strnew += lines[i].replace("$platfbuild", p_preset->get("xcode/platform_build")) + "\n"; + } else if (lines[i].find("$sdkver") != -1) { + strnew += lines[i].replace("$sdkver", p_preset->get("xcode/sdk_version")) + "\n"; + } else if (lines[i].find("$sdkname") != -1) { + strnew += lines[i].replace("$sdkname", p_preset->get("xcode/sdk_name")) + "\n"; + } else if (lines[i].find("$sdkbuild") != -1) { + strnew += lines[i].replace("$sdkbuild", p_preset->get("xcode/sdk_build")) + "\n"; + } else if (lines[i].find("$xcodever") != -1) { + strnew += lines[i].replace("$xcodever", p_preset->get("xcode/xcode_version")) + "\n"; + } else if (lines[i].find("$xcodebuild") != -1) { + strnew += lines[i].replace("$xcodebuild", p_preset->get("xcode/xcode_build")) + "\n"; } else if (lines[i].find("$usage_descriptions") != -1) { String descriptions; if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { @@ -510,24 +776,24 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres args.push_back("notary-submit"); - if (p_preset->get("notarization/api_uuid") == "") { + if (p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified.")); return Error::FAILED; } - if (p_preset->get("notarization/api_key") == "") { + if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); return Error::FAILED; } args.push_back("--api-issuer"); - args.push_back(p_preset->get("notarization/api_uuid")); + args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID)); args.push_back("--api-key"); - args.push_back(p_preset->get("notarization/api_key_id")); + args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID)); - if (!p_preset->get("notarization/api_key").operator String().is_empty()) { + if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) { args.push_back("--api-key-path"); - args.push_back(p_preset->get("notarization/api_key")); + args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY)); } args.push_back(p_path); @@ -574,47 +840,47 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres args.push_back(p_path); - if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.")); return Error::FAILED; } - if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.")); return Error::FAILED; } - if (p_preset->get("notarization/apple_id_name") != "") { - if (p_preset->get("notarization/apple_id_password") == "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") { + if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified.")); return Error::FAILED; } args.push_back("--apple-id"); - args.push_back(p_preset->get("notarization/apple_id_name")); + args.push_back(p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID)); args.push_back("--password"); - args.push_back(p_preset->get("notarization/apple_id_password")); + args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS)); } else { - if (p_preset->get("notarization/api_key_id") == "") { + if (p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); return Error::FAILED; } args.push_back("--issuer"); - args.push_back(p_preset->get("notarization/api_uuid")); + args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID)); - if (!p_preset->get("notarization/api_key").operator String().is_empty()) { + if (!p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY).operator String().is_empty()) { args.push_back("--key"); - args.push_back(p_preset->get("notarization/api_key")); + args.push_back(p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY)); } args.push_back("--key-id"); - args.push_back(p_preset->get("notarization/api_key_id")); + args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID)); } args.push_back("--no-progress"); - if (p_preset->get("notarization/apple_team_id")) { + if (p_preset->get("codesign/apple_team_id")) { args.push_back("--team-id"); - args.push_back(p_preset->get("notarization/apple_team_id")); + args.push_back(p_preset->get("codesign/apple_team_id")); } String str; @@ -659,43 +925,43 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres args.push_back("--primary-bundle-id"); args.push_back(p_preset->get("application/bundle_identifier")); - if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) == "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.")); return Error::FAILED; } - if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "" && p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID) != "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.")); return Error::FAILED; } - if (p_preset->get("notarization/apple_id_name") != "") { - if (p_preset->get("notarization/apple_id_password") == "") { + if (p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID) != "") { + if (p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified.")); return Error::FAILED; } args.push_back("--username"); - args.push_back(p_preset->get("notarization/apple_id_name")); + args.push_back(p_preset->get_or_env("notarization/apple_id_name", ENV_MAC_NOTARIZATION_APPLE_ID)); args.push_back("--password"); - args.push_back(p_preset->get("notarization/apple_id_password")); + args.push_back(p_preset->get_or_env("notarization/apple_id_password", ENV_MAC_NOTARIZATION_APPLE_PASS)); } else { - if (p_preset->get("notarization/api_key") == "") { + if (p_preset->get_or_env("notarization/api_key", ENV_MAC_NOTARIZATION_KEY) == "") { add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); return Error::FAILED; } args.push_back("--apiIssuer"); - args.push_back(p_preset->get("notarization/api_uuid")); + args.push_back(p_preset->get_or_env("notarization/api_uuid", ENV_MAC_NOTARIZATION_UUID)); args.push_back("--apiKey"); - args.push_back(p_preset->get("notarization/api_key_id")); + args.push_back(p_preset->get_or_env("notarization/api_key_id", ENV_MAC_NOTARIZATION_KEY_ID)); } args.push_back("--type"); args.push_back("osx"); - if (p_preset->get("notarization/apple_team_id")) { + if (p_preset->get("codesign/apple_team_id")) { args.push_back("--asc-provider"); - args.push_back(p_preset->get("notarization/apple_team_id")); + args.push_back(p_preset->get("codesign/apple_team_id")); } args.push_back("--file"); @@ -766,9 +1032,9 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back(p_ent_path); } - String certificate_file = p_preset->get("codesign/certificate_file"); - String certificate_pass = p_preset->get("codesign/certificate_password"); - if (!certificate_file.is_empty() && !certificate_file.is_empty()) { + String certificate_file = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE); + String certificate_pass = p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS); + if (!certificate_file.is_empty() && !certificate_pass.is_empty()) { args.push_back("--p12-file"); args.push_back(certificate_file); args.push_back("--p12-password"); @@ -978,6 +1244,42 @@ Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugi return error; } +Error EditorExportPlatformMacOS::_create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name) { + List<String> args; + + if (FileAccess::exists(p_pkg_path)) { + OS::get_singleton()->move_to_trash(p_pkg_path); + } + + args.push_back("productbuild"); + args.push_back("--component"); + args.push_back(p_app_path_name); + args.push_back("/Applications"); + String ident = p_preset->get("codesign/installer_identity"); + if (!ident.is_empty()) { + args.push_back("--timestamp"); + args.push_back("--sign"); + args.push_back(ident); + } + args.push_back("--quiet"); + args.push_back(p_pkg_path); + + String str; + Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("Could not start productbuild executable.")); + return err; + } + + print_verbose("productbuild returned: " + str); + if (str.find("productbuild: error:") != -1) { + add_message(EXPORT_MESSAGE_ERROR, TTR("PKG Creation"), TTR("`productbuild` failed.")); + return FAILED; + } + + return OK; +} + Error EditorExportPlatformMacOS::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) { List<String> args; @@ -1106,12 +1408,16 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name); String export_format; - if (use_dmg() && p_path.ends_with("dmg")) { - export_format = "dmg"; - } else if (p_path.ends_with("zip")) { + if (p_path.ends_with("zip")) { export_format = "zip"; } else if (p_path.ends_with("app")) { export_format = "app"; +#ifdef MACOS_ENABLED + } else if (p_path.ends_with("dmg")) { + export_format = "dmg"; + } else if (p_path.ends_with("pkg")) { + export_format = "pkg"; +#endif } else { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Invalid export format.")); return ERR_CANT_CREATE; @@ -1368,16 +1674,18 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (file == "Contents/Resources/icon.icns") { // See if there is an icon. - String iconpath; + String icon_path; if (p_preset->get("application/icon") != "") { - iconpath = p_preset->get("application/icon"); + icon_path = p_preset->get("application/icon"); + } else if (GLOBAL_GET("application/config/macos_native_icon") != "") { + icon_path = GLOBAL_GET("application/config/macos_native_icon"); } else { - iconpath = GLOBAL_GET("application/config/icon"); + icon_path = GLOBAL_GET("application/config/icon"); } - if (!iconpath.is_empty()) { - if (iconpath.get_extension() == "icns") { - Ref<FileAccess> icon = FileAccess::open(iconpath, FileAccess::READ); + if (!icon_path.is_empty()) { + if (icon_path.get_extension() == "icns") { + Ref<FileAccess> icon = FileAccess::open(icon_path, FileAccess::READ); if (icon.is_valid()) { data.resize(icon->get_length()); icon->get_buffer(&data.write[0], icon->get_length()); @@ -1385,7 +1693,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } else { Ref<Image> icon; icon.instantiate(); - err = ImageLoader::load_image(iconpath, icon); + err = ImageLoader::load_image(icon_path, icon); if (err == OK && !icon->is_empty()) { _make_icon(p_preset, icon, data); } @@ -1457,7 +1765,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p ad_hoc = true; } break; case 2: { // "rcodesign" - ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty(); + ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty(); } break; #ifdef MACOS_ENABLED case 3: { // "codesign" @@ -1549,6 +1857,19 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p ent_f->store_line("<true/>"); } + int dist_type = p_preset->get("export/distribution_type"); + if (dist_type == 2) { + String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE); + String teamid = p_preset->get("codesign/apple_team_id"); + String bid = p_preset->get("application/bundle_identifier"); + if (!pprof.is_empty() && !teamid.is_empty()) { + ent_f->store_line("<key>com.apple.developer.team-identifier</key>"); + ent_f->store_line("<string>" + teamid + "</string>"); + ent_f->store_line("<key>com.apple.application-identifier</key>"); + ent_f->store_line("<string>" + teamid + "." + bid + "</string>"); + } + } + if ((bool)p_preset->get("codesign/entitlements/app_sandbox/enabled")) { ent_f->store_line("<key>com.apple.security.app-sandbox</key>"); ent_f->store_line("<true/>"); @@ -1669,6 +1990,15 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } if (err == OK && sign_enabled) { + int dist_type = p_preset->get("export/distribution_type"); + if (dist_type == 2) { + String pprof = p_preset->get_or_env("codesign/provisioning_profile", ENV_MAC_CODESIGN_PROFILE).operator String(); + if (!pprof.is_empty()) { + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + err = da->copy(pprof, tmp_app_path_name + "/Contents/embedded.provisionprofile"); + } + } + if (ep.step(TTR("Code signing bundle"), 2)) { return ERR_SKIP; } @@ -1690,6 +2020,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } err = _code_sign(p_preset, p_path, ent_path, false); } + } else if (export_format == "pkg") { + // Create a Installer. + if (err == OK) { + if (ep.step(TTR("Making PKG installer"), 3)) { + return ERR_SKIP; + } + err = _create_pkg(p_preset, p_path, tmp_app_path_name); + } } else if (export_format == "zip") { // Create ZIP. if (err == OK) { @@ -1712,7 +2050,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p bool noto_enabled = (p_preset->get("notarization/notarization").operator int() > 0); if (err == OK && noto_enabled) { - if (export_format == "app") { + if (export_format == "app" || export_format == "pkg") { 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)) { @@ -1802,21 +2140,16 @@ bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<Editor String err; bool valid = true; - String identifier = p_preset->get("application/bundle_identifier"); - String pn_err; - if (!is_package_name_valid(identifier, &pn_err)) { - err += TTR("Invalid bundle identifier:") + " " + pn_err + "\n"; - valid = false; - } - + int dist_type = p_preset->get("export/distribution_type"); bool ad_hoc = false; int codesign_tool = p_preset->get("codesign/codesign"); + int notary_tool = p_preset->get("notarization/notarization"); switch (codesign_tool) { case 1: { // built-in ad-hoc ad_hoc = true; } break; case 2: { // "rcodesign" - ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty(); + ad_hoc = p_preset->get_or_env("codesign/certificate_file", ENV_MAC_CODESIGN_CERT_FILE).operator String().is_empty() || p_preset->get_or_env("codesign/certificate_password", ENV_MAC_CODESIGN_CERT_PASS).operator String().is_empty(); } break; #ifdef MACOS_ENABLED case 3: { // "codesign" @@ -1826,67 +2159,41 @@ bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<Editor default: { }; } - int notary_tool = p_preset->get("notarization/notarization"); - if (notary_tool > 0) { - if (ad_hoc) { - err += TTR("Notarization: Notarization with an ad-hoc signature is not supported.") + "\n"; - valid = false; - } - if (codesign_tool == 0) { - err += TTR("Notarization: Code signing is required for notarization.") + "\n"; - valid = false; - } - if (notary_tool == 2 || notary_tool == 3) { - if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) { - err += TTR("Notarization: Xcode command line tools are not installed.") + "\n"; - valid = false; + List<ExportOption> options; + get_export_options(&options); + for (const EditorExportPlatform::ExportOption &E : options) { + if (get_export_option_visibility(p_preset.ptr(), E.option.name)) { + String warn = get_export_option_warning(p_preset.ptr(), E.option.name); + if (!warn.is_empty()) { + err += warn + "\n"; + if (E.required) { + valid = false; + } } - if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") { - err += TTR("Notarization: Neither Apple ID name nor App Store Connect issuer ID name not specified.") + "\n"; - valid = false; - } else if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") { - err += TTR("Notarization: Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.") + "\n"; - valid = false; - } else { - if (p_preset->get("notarization/apple_id_name") != "") { - if (p_preset->get("notarization/apple_id_password") == "") { - err += TTR("Notarization: Apple ID password not specified.") + "\n"; - valid = false; - } + } + } + + if (dist_type != 2) { + if (notary_tool > 0) { + if (notary_tool == 2 || notary_tool == 3) { + if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) { + err += TTR("Notarization: Xcode command line tools are not installed.") + "\n"; + valid = false; } - if (p_preset->get("notarization/api_uuid") != "") { - if (p_preset->get("notarization/api_key_id") == "") { - err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n"; - valid = false; - } + } else if (notary_tool == 1) { + String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String(); + if (rcodesign.is_empty()) { + err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n"; + valid = false; } } - if (notary_tool == 2 && p_preset->get("notarization/apple_team_id") == "") { - err += TTR("Notarization: Apple Team ID not specified.") + "\n"; - valid = false; - } - } else if (notary_tool == 1) { - if (p_preset->get("notarization/api_uuid") == "") { - err += TTR("Notarization: App Store Connect issuer ID name not specified.") + "\n"; - valid = false; - } - if (p_preset->get("notarization/api_key_id") == "") { - err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n"; - valid = false; - } - - String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String(); - if (rcodesign.is_empty()) { - err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n"; - valid = false; + } else { + err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n"; + if (codesign_tool == 0) { + err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; } } - } else { - err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n"; - if (codesign_tool == 0) { - err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; - } } if (codesign_tool > 0) { @@ -1905,30 +2212,6 @@ bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<Editor valid = false; } } - if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { - err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } - if ((bool)p_preset->get("codesign/entitlements/camera") && ((String)p_preset->get("privacy/camera_usage_description")).is_empty()) { - err += TTR("Privacy: Camera access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } - if ((bool)p_preset->get("codesign/entitlements/location") && ((String)p_preset->get("privacy/location_usage_description")).is_empty()) { - err += TTR("Privacy: Location information access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } - if ((bool)p_preset->get("codesign/entitlements/address_book") && ((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) { - err += TTR("Privacy: Address book access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } - if ((bool)p_preset->get("codesign/entitlements/calendars") && ((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) { - err += TTR("Privacy: Calendar access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } - if ((bool)p_preset->get("codesign/entitlements/photos_library") && ((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) { - err += TTR("Privacy: Photo library access is enabled, but usage description is not specified.") + "\n"; - valid = false; - } } if (!err.is_empty()) { @@ -2157,22 +2440,24 @@ Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, in } EditorExportPlatformMacOS::EditorExportPlatformMacOS() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _macos_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); - img_loader.create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _macos_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) { - stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); - } else { - stop_icon.instantiate(); + Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); + if (theme.is_valid()) { + stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); + } else { + stop_icon.instantiate(); + } } } diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index c10192949c..0477a8c0cc 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -43,6 +43,17 @@ #include <sys/stat.h> +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_MAC_CODESIGN_CERT_FILE = "GODOT_MACOS_CODESIGN_CERTIFICATE_FILE"; +const String ENV_MAC_CODESIGN_CERT_PASS = "GODOT_MACOS_CODESIGN_CERTIFICATE_PASSWORD"; +const String ENV_MAC_CODESIGN_PROFILE = "GODOT_MACOS_CODESIGN_PROVISIONING_PROFILE"; +const String ENV_MAC_NOTARIZATION_UUID = "GODOT_MACOS_NOTARIZATION_API_UUID"; +const String ENV_MAC_NOTARIZATION_KEY = "GODOT_MACOS_NOTARIZATION_API_KEY"; +const String ENV_MAC_NOTARIZATION_KEY_ID = "GODOT_MACOS_NOTARIZATION_API_KEY_ID"; +const String ENV_MAC_NOTARIZATION_APPLE_ID = "GODOT_MACOS_NOTARIZATION_APPLE_ID_NAME"; +const String ENV_MAC_NOTARIZATION_APPLE_PASS = "GODOT_MACOS_NOTARIZATION_APPLE_ID_PASSWORD"; + class EditorExportPlatformMacOS : public EditorExportPlatform { GDCLASS(EditorExportPlatformMacOS, EditorExportPlatform); @@ -87,18 +98,10 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); + Error _create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name); Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); bool use_codesign() const { return true; } -#ifdef MACOS_ENABLED - bool use_dmg() const { - return true; - } -#else - bool use_dmg() const { - return false; - } -#endif bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { String pname = p_package; @@ -126,8 +129,9 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; + virtual void get_export_options(List<ExportOption> *r_options) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; public: virtual String get_name() const override { @@ -141,15 +145,7 @@ public: } virtual bool is_executable(const String &p_path) const override; - virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { - List<String> list; - if (use_dmg()) { - list.push_back("dmg"); - } - list.push_back("zip"); - list.push_back("app"); - return list; - } + 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 bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm index 485f80a22e..9f5bb2ef5c 100644 --- a/platform/macos/godot_content_view.mm +++ b/platform/macos/godot_content_view.mm @@ -115,13 +115,7 @@ self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft; - if (@available(macOS 10.13, *)) { - [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]]; -#if !defined(__aarch64__) // Do not build deprectead 10.13 code on ARM. - } else { - [self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; -#endif - } + [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]]; marked_text = [[NSMutableAttributedString alloc] init]; return self; } @@ -321,20 +315,11 @@ Vector<String> files; NSPasteboard *pboard = [sender draggingPasteboard]; - if (@available(macOS 10.13, *)) { - NSArray *items = pboard.pasteboardItems; - for (NSPasteboardItem *item in items) { - NSString *url = [item stringForType:NSPasteboardTypeFileURL]; - NSString *file = [NSURL URLWithString:url].path; - files.push_back(String::utf8([file UTF8String])); - } -#if !defined(__aarch64__) // Do not build deprectead 10.13 code on ARM. - } else { - NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType]; - for (NSString *file in filenames) { - files.push_back(String::utf8([file UTF8String])); - } -#endif + NSArray *items = pboard.pasteboardItems; + for (NSPasteboardItem *item in items) { + NSString *url = [item stringForType:NSPasteboardTypeFileURL]; + NSString *file = [NSURL URLWithString:url].path; + files.push_back(String::utf8([file UTF8String])); } Variant v = files; diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index c539f7f529..07bae479be 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -98,6 +98,7 @@ public: virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; virtual Error shell_open(String p_uri) override; + virtual Error shell_show_in_file_manager(String p_path, bool p_open_folder) override; virtual String get_locale() const override; @@ -118,6 +119,8 @@ public: virtual Error move_to_trash(const String &p_path) override; + virtual String get_system_ca_certificates() override; + void run(); OS_MacOS(); diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 71a250153b..838ae742fd 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -30,6 +30,7 @@ #include "os_macos.h" +#include "core/crypto/crypto_core.h" #include "core/version_generated.gen.h" #include "main/main.h" @@ -287,6 +288,27 @@ String OS_MacOS::get_system_dir(SystemDir p_dir, bool p_shared_storage) const { return ret; } +Error OS_MacOS::shell_show_in_file_manager(String p_path, bool p_open_folder) { + bool open_folder = false; + if (DirAccess::dir_exists_absolute(p_path) && p_open_folder) { + open_folder = true; + } + + if (!p_path.begins_with("file://")) { + p_path = String("file://") + p_path; + } + + NSString *string = [NSString stringWithUTF8String:p_path.utf8().get_data()]; + NSURL *uri = [[NSURL alloc] initWithString:[string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]]; + + if (open_folder) { + [[NSWorkspace sharedWorkspace] openURL:uri]; + } else { + [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[ uri ]]; + } + return OK; +} + Error OS_MacOS::shell_open(String p_uri) { NSString *string = [NSString stringWithUTF8String:p_uri.utf8().get_data()]; NSURL *uri = [[NSURL alloc] initWithString:string]; @@ -650,6 +672,34 @@ Error OS_MacOS::move_to_trash(const String &p_path) { return OK; } +String OS_MacOS::get_system_ca_certificates() { + CFArrayRef result; + SecCertificateRef item; + CFDataRef der; + + OSStatus ret = SecTrustCopyAnchorCertificates(&result); + ERR_FAIL_COND_V(ret != noErr, ""); + + CFIndex l = CFArrayGetCount(result); + String certs; + PackedByteArray pba; + for (CFIndex i = 0; i < l; i++) { + item = (SecCertificateRef)CFArrayGetValueAtIndex(result, i); + der = SecCertificateCopyData(item); + int derlen = CFDataGetLength(der); + if (pba.size() < derlen * 3) { + pba.resize(derlen * 3); + } + size_t b64len = 0; + Error err = CryptoCore::b64_encode(pba.ptrw(), pba.size(), &b64len, (unsigned char *)CFDataGetBytePtr(der), derlen); + CFRelease(der); + ERR_CONTINUE(err != OK); + certs += "-----BEGIN CERTIFICATE-----\n" + String((char *)pba.ptr(), b64len) + "\n-----END CERTIFICATE-----\n"; + } + CFRelease(result); + return certs; +} + void OS_MacOS::run() { if (!main_loop) { return; diff --git a/platform/macos/tts_macos.h b/platform/macos/tts_macos.h index 2153a4c692..35205bd1ae 100644 --- a/platform/macos/tts_macos.h +++ b/platform/macos/tts_macos.h @@ -32,8 +32,8 @@ #define TTS_MACOS_H #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/templates/list.h" -#include "core/templates/rb_map.h" #include "core/variant/array.h" #include "servers/display_server.h" diff --git a/platform/uwp/export/export.cpp b/platform/uwp/export/export.cpp index f2b2fa30ce..c20e3316a5 100644 --- a/platform/uwp/export/export.cpp +++ b/platform/uwp/export/export.cpp @@ -31,8 +31,13 @@ #include "export.h" #include "editor/editor_settings.h" +#include "editor/export/editor_export.h" #include "export_plugin.h" +void register_uwp_exporter_types() { + // GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformUWP); +} + void register_uwp_exporter() { #ifdef WINDOWS_ENABLED EDITOR_DEF("export/uwp/signtool", ""); diff --git a/platform/uwp/export/export.h b/platform/uwp/export/export.h index 09449fc53b..d2053e02b1 100644 --- a/platform/uwp/export/export.h +++ b/platform/uwp/export/export.h @@ -31,6 +31,7 @@ #ifndef UWP_EXPORT_H #define UWP_EXPORT_H +void register_uwp_exporter_types(); void register_uwp_exporter(); #endif // UWP_EXPORT_H diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index f810cb0ca9..aac61184b1 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -62,7 +62,7 @@ void EditorExportPlatformUWP::get_preset_features(const Ref<EditorExportPreset> r_features->push_back(p_preset->get("binary_format/architecture")); } -void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) { +void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) const { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); @@ -80,8 +80,8 @@ void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "identity/product_guid", PROPERTY_HINT_PLACEHOLDER_TEXT, "00000000-0000-0000-0000-000000000000"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "identity/publisher_guid", PROPERTY_HINT_PLACEHOLDER_TEXT, "00000000-0000-0000-0000-000000000000"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "signing/certificate", PROPERTY_HINT_GLOBAL_FILE, "*.pfx"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "signing/password"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "signing/certificate", PROPERTY_HINT_GLOBAL_FILE, "*.pfx", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "signing/password", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "signing/algorithm", PROPERTY_HINT_ENUM, "MD5,SHA1,SHA256"), 2)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "version/major"), 1)); @@ -465,8 +465,8 @@ Error EditorExportPlatformUWP::export_project(const Ref<EditorExportPreset> &p_p int cert_alg = EDITOR_GET("export/uwp/debug_algorithm"); if (!p_debug) { - cert_path = p_preset->get("signing/certificate"); - cert_pass = p_preset->get("signing/password"); + cert_path = p_preset->get_or_env("signing/certificate", ENV_UWP_SIGNING_CERT); + cert_pass = p_preset->get_or_env("signing/password", ENV_UWP_SIGNING_PASS); cert_alg = p_preset->get("signing/algorithm"); } diff --git a/platform/uwp/export/export_plugin.h b/platform/uwp/export/export_plugin.h index d015fcabcd..b42a2ae6d9 100644 --- a/platform/uwp/export/export_plugin.h +++ b/platform/uwp/export/export_plugin.h @@ -85,6 +85,11 @@ static const char *uwp_device_capabilities[] = { nullptr }; +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_UWP_SIGNING_CERT = "GODOT_UWP_SIGNING_CERTIFICATE"; +const String ENV_UWP_SIGNING_PASS = "GODOT_UWP_SIGNING_PASSWORD"; + class EditorExportPlatformUWP : public EditorExportPlatform { GDCLASS(EditorExportPlatformUWP, EditorExportPlatform); @@ -427,7 +432,7 @@ public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index 38df68c764..df7923660c 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -450,8 +450,7 @@ String OS_UWP::get_distribution_name() const { String OS_UWP::get_version() const { winrt::hstring df_version = VersionInfo().DeviceFamilyVersion(); - static String version = String(winrt::to_string(df_version).c_str()); - return version; + return String(winrt::to_string(df_version).c_str()); } OS::DateTime OS_UWP::get_datetime(bool p_utc) const { diff --git a/platform/web/api/web_tools_editor_plugin.cpp b/platform/web/api/web_tools_editor_plugin.cpp index 146a48db81..213204ff33 100644 --- a/platform/web/api/web_tools_editor_plugin.cpp +++ b/platform/web/api/web_tools_editor_plugin.cpp @@ -75,7 +75,7 @@ void WebToolsEditorPlugin::_download_zip() { const String project_name_safe = project_name.to_lower().replace(" ", "_"); const String datetime_safe = Time::get_singleton()->get_datetime_string_from_system(false, true).replace(" ", "_"); - const String output_name = OS::get_singleton()->get_safe_dir_name(vformat("%s_%s.zip")); + const String output_name = OS::get_singleton()->get_safe_dir_name(vformat("%s_%s.zip", project_name_safe, datetime_safe)); const String output_path = String("/tmp").path_join(output_name); zipFile zip = zipOpen2(output_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index 1d7b96d707..c6c67db3de 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -103,7 +103,7 @@ void AudioDriverWeb::_audio_driver_capture(int p_from, int p_samples) { Error AudioDriverWeb::init() { int latency = GLOBAL_GET("audio/driver/output_latency"); if (!audio_context.inited) { - audio_context.mix_rate = GLOBAL_GET("audio/driver/mix_rate"); + audio_context.mix_rate = _get_configured_mix_rate(); audio_context.channel_count = godot_audio_init(&audio_context.mix_rate, latency, &_state_change_callback, &_latency_update_callback); audio_context.inited = true; } diff --git a/platform/web/detect.py b/platform/web/detect.py index 08c1ff7b4a..419d8918f2 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -48,6 +48,16 @@ def get_opts(): ] +def get_doc_classes(): + return [ + "EditorExportPlatformWeb", + ] + + +def get_doc_path(): + return "doc_classes" + + def get_flags(): return [ ("arch", "wasm32"), diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 565d439a92..e870f0da29 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -30,6 +30,7 @@ #include "display_server_web.h" +#include "core/config/project_settings.h" #ifdef GLES3_ENABLED #include "drivers/gles3/rasterizer_gles3.h" #endif @@ -298,10 +299,12 @@ const char *DisplayServerWeb::godot2dom_cursor(DisplayServer::CursorShape p_shap } bool DisplayServerWeb::tts_is_speaking() const { + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return godot_js_tts_is_speaking(); } bool DisplayServerWeb::tts_is_paused() const { + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return godot_js_tts_is_paused(); } @@ -320,11 +323,13 @@ void DisplayServerWeb::update_voices_callback(int p_size, const char **p_voice) } TypedArray<Dictionary> DisplayServerWeb::tts_get_voices() const { + ERR_FAIL_COND_V_MSG(!tts, TypedArray<Dictionary>(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); godot_js_tts_get_voices(update_voices_callback); return voices; } void DisplayServerWeb::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_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (p_interrupt) { tts_stop(); } @@ -341,14 +346,17 @@ void DisplayServerWeb::tts_speak(const String &p_text, const String &p_voice, in } void DisplayServerWeb::tts_pause() { + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); godot_js_tts_pause(); } void DisplayServerWeb::tts_resume() { + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); godot_js_tts_resume(); } void DisplayServerWeb::tts_stop() { + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); for (const KeyValue<int, CharString> &E : utterance_ids) { tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, E.key); } @@ -398,16 +406,12 @@ void DisplayServerWeb::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu ERR_FAIL_INDEX(p_shape, CURSOR_MAX); if (p_cursor.is_valid()) { Ref<Texture2D> texture = p_cursor; + ERR_FAIL_COND(!texture.is_valid()); Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; Size2 texture_size; Rect2 atlas_rect; - if (texture.is_valid()) { - image = texture->get_image(); - } - - if (!image.is_valid() && atlas_texture.is_valid()) { + if (atlas_texture.is_valid()) { texture = atlas_texture->get_atlas(); atlas_rect.size.width = texture->get_width(); @@ -417,21 +421,25 @@ void DisplayServerWeb::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu texture_size.width = atlas_texture->get_region().size.x; texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { + } else { texture_size.width = texture->get_width(); texture_size.height = texture->get_height(); } - ERR_FAIL_COND(!texture.is_valid()); ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - image = texture->get_image(); + Ref<Image> image = texture->get_image(); ERR_FAIL_COND(!image.is_valid()); - image = image->duplicate(); + image = image->duplicate(true); + + if (image->is_compressed()) { + Error err = image->decompress(); + ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); + } if (atlas_texture.is_valid()) { image->crop_from_point( @@ -771,6 +779,8 @@ DisplayServer *DisplayServerWeb::create_func(const String &p_rendering_driver, W DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Error &r_error) { r_error = OK; // Always succeeds for now. + tts = GLOBAL_GET("audio/general/text_to_speech"); + // Ensure the canvas ID. godot_js_config_canvas_id_get(canvas_id, 256); @@ -866,7 +876,7 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const { case FEATURE_VIRTUAL_KEYBOARD: return godot_js_display_vk_available() != 0; case FEATURE_TEXT_TO_SPEECH: - return godot_js_display_tts_available() != 0; + return tts && (godot_js_display_tts_available() != 0); default: return false; } @@ -1070,6 +1080,10 @@ bool DisplayServerWeb::can_any_window_draw() const { return true; } +DisplayServer::VSyncMode DisplayServerWeb::window_get_vsync_mode(WindowID p_vsync_mode) const { + return DisplayServer::VSYNC_ENABLED; +} + void DisplayServerWeb::process_events() { Input::get_singleton()->flush_buffered_events(); if (godot_js_input_gamepad_sample() == OK) { diff --git a/platform/web/display_server_web.h b/platform/web/display_server_web.h index 2e50a6bbc8..3b03b102cd 100644 --- a/platform/web/display_server_web.h +++ b/platform/web/display_server_web.h @@ -37,6 +37,8 @@ #include <emscripten/html5.h> class DisplayServerWeb : public DisplayServer { + // No need to register with GDCLASS, it's platform-specific and nothing is added. + private: struct JSTouchEvent { uint32_t identifier[32] = { 0 }; @@ -79,6 +81,7 @@ private: MouseButton last_click_button_index = MouseButton::NONE; bool swap_cancel_ok = false; + bool tts = false; // utilities static void dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod, Key p_keycode); @@ -213,6 +216,8 @@ public: virtual bool can_any_window_draw() const override; + virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override; + // events virtual void process_events() override; diff --git a/platform/web/doc_classes/EditorExportPlatformWeb.xml b/platform/web/doc_classes/EditorExportPlatformWeb.xml new file mode 100644 index 0000000000..6e5a2ac078 --- /dev/null +++ b/platform/web/doc_classes/EditorExportPlatformWeb.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformWeb" inherits="EditorExportPlatform" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for the Web. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for the Web">$DOCS_URL/tutorials/export/exporting_for_web.html</link> + </tutorials> + <members> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="html/canvas_resize_policy" type="int" setter="" getter=""> + The canvas resize policy determines how the canvas should be resized by Godot. + </member> + <member name="html/custom_html_shell" type="String" setter="" getter=""> + </member> + <member name="html/experimental_virtual_keyboard" type="bool" setter="" getter=""> + </member> + <member name="html/export_icon" type="bool" setter="" getter=""> + </member> + <member name="html/focus_canvas_on_start" type="bool" setter="" getter=""> + </member> + <member name="html/head_include" type="String" setter="" getter=""> + </member> + <member name="progressive_web_app/background_color" type="Color" setter="" getter=""> + </member> + <member name="progressive_web_app/display" type="int" setter="" getter=""> + </member> + <member name="progressive_web_app/enabled" type="bool" setter="" getter=""> + </member> + <member name="progressive_web_app/icon_144x144" type="String" setter="" getter=""> + </member> + <member name="progressive_web_app/icon_180x180" type="String" setter="" getter=""> + </member> + <member name="progressive_web_app/icon_512x512" type="String" setter="" getter=""> + </member> + <member name="progressive_web_app/offline_page" type="String" setter="" getter=""> + </member> + <member name="progressive_web_app/orientation" type="int" setter="" getter=""> + </member> + <member name="variant/extensions_support" type="bool" setter="" getter=""> + </member> + <member name="vram_texture_compression/for_desktop" type="bool" setter="" getter=""> + </member> + <member name="vram_texture_compression/for_mobile" type="bool" setter="" getter=""> + </member> + </members> +</class> diff --git a/platform/web/dom_keys.inc b/platform/web/dom_keys.inc index ae3b2fc1a5..cd94b779c0 100644 --- a/platform/web/dom_keys.inc +++ b/platform/web/dom_keys.inc @@ -33,7 +33,7 @@ // See https://w3c.github.io/uievents-code/#code-value-tables Key dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) { #define DOM2GODOT(p_str, p_godot_code) \ - if (memcmp((const void *)p_str, (void *)(p_physical ? p_key : p_code), strlen(p_str) + 1) == 0) { \ + if (memcmp((const void *)p_str, (void *)(p_physical ? p_code : p_key), strlen(p_str) + 1) == 0) { \ return Key::p_godot_code; \ } diff --git a/platform/web/export/export.cpp b/platform/web/export/export.cpp index 11e728ea16..80c29024a8 100644 --- a/platform/web/export/export.cpp +++ b/platform/web/export/export.cpp @@ -31,8 +31,13 @@ #include "export.h" #include "editor/editor_settings.h" +#include "editor/export/editor_export.h" #include "export_plugin.h" +void register_web_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformWeb); +} + void register_web_exporter() { #ifndef ANDROID_ENABLED EDITOR_DEF("export/web/http_host", "localhost"); diff --git a/platform/web/export/export.h b/platform/web/export/export.h index 8d2bbfff26..da02bd8d93 100644 --- a/platform/web/export/export.h +++ b/platform/web/export/export.h @@ -31,6 +31,7 @@ #ifndef WEB_EXPORT_H #define WEB_EXPORT_H +void register_web_exporter_types(); void register_web_exporter(); #endif // WEB_EXPORT_H diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index d8e04904c7..2fff628c85 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" +#include "editor/export/editor_export.h" #include "platform/web/logo_svg.gen.h" #include "platform/web/run_icon_svg.gen.h" @@ -320,7 +321,7 @@ void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset> r_features->push_back("wasm32"); } -void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options) { +void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options) const { r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); @@ -656,31 +657,37 @@ void EditorExportPlatformWeb::_server_thread_poll(void *data) { } EditorExportPlatformWeb::EditorExportPlatformWeb() { - server.instantiate(); - server_thread.start(_server_thread_poll, this); + if (EditorNode::get_singleton()) { + server.instantiate(); + server_thread.start(_server_thread_poll, this); #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false); - logo = ImageTexture::create_from_image(img); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false); + logo = ImageTexture::create_from_image(img); - img_loader.create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) { - stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); - } else { - stop_icon.instantiate(); + Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); + if (theme.is_valid()) { + stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); + } else { + stop_icon.instantiate(); + } } } EditorExportPlatformWeb::~EditorExportPlatformWeb() { - server->stop(); + if (server.is_valid()) { + server->stop(); + } server_quit = true; - server_thread.wait_to_finish(); + if (server_thread.is_started()) { + server_thread.wait_to_finish(); + } } diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index e74c945837..334f12d64d 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -99,7 +99,7 @@ class EditorExportPlatformWeb : public EditorExportPlatform { public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; - virtual void get_export_options(List<ExportOption> *r_options) override; + virtual void get_export_options(List<ExportOption> *r_options) const override; virtual String get_name() const override; virtual String get_os_name() const override; diff --git a/platform/web/godot_js.h b/platform/web/godot_js.h index 3a41f63fa3..660822e291 100644 --- a/platform/web/godot_js.h +++ b/platform/web/godot_js.h @@ -49,6 +49,7 @@ extern void godot_js_os_fs_sync(void (*p_callback)()); extern int godot_js_os_execute(const char *p_json); extern void godot_js_os_shell_open(const char *p_uri); extern int godot_js_os_hw_concurrency_get(); +extern int godot_js_os_has_feature(const char *p_ftr); extern int godot_js_pwa_cb(void (*p_callback)()); extern int godot_js_pwa_update(); diff --git a/platform/web/js/libs/library_godot_fetch.js b/platform/web/js/libs/library_godot_fetch.js index b50012c1e2..1bb48bfd6a 100644 --- a/platform/web/js/libs/library_godot_fetch.js +++ b/platform/web/js/libs/library_godot_fetch.js @@ -50,17 +50,22 @@ const GodotFetch = { return; } let chunked = false; + let bodySize = -1; response.headers.forEach(function (value, header) { const v = value.toLowerCase().trim(); const h = header.toLowerCase().trim(); if (h === 'transfer-encoding' && v === 'chunked') { chunked = true; } + if (h === 'content-length') { + bodySize = parseInt(v, 10); + } }); obj.status = response.status; obj.response = response; obj.reader = response.body.getReader(); obj.chunked = chunked; + obj.bodySize = bodySize; }, onerror: function (id, err) { diff --git a/platform/web/js/libs/library_godot_os.js b/platform/web/js/libs/library_godot_os.js index c4c45a3036..00ae399583 100644 --- a/platform/web/js/libs/library_godot_os.js +++ b/platform/web/js/libs/library_godot_os.js @@ -291,6 +291,28 @@ const GodotOS = { }); }, + godot_js_os_has_feature__sig: 'ii', + godot_js_os_has_feature: function (p_ftr) { + const ftr = GodotRuntime.parseString(p_ftr); + const ua = navigator.userAgent; + if (ftr === 'web_macos') { + return (ua.indexOf('Mac') !== -1) ? 1 : 0; + } + if (ftr === 'web_windows') { + return (ua.indexOf('Windows') !== -1) ? 1 : 0; + } + if (ftr === 'web_android') { + return (ua.indexOf('Android') !== -1) ? 1 : 0; + } + if (ftr === 'web_ios') { + return ((ua.indexOf('iPhone') !== -1) || (ua.indexOf('iPad') !== -1) || (ua.indexOf('iPod') !== -1)) ? 1 : 0; + } + if (ftr === 'web_linuxbsd') { + return ((ua.indexOf('CrOS') !== -1) || (ua.indexOf('BSD') !== -1) || (ua.indexOf('Linux') !== -1) || (ua.indexOf('X11') !== -1)) ? 1 : 0; + } + return 0; + }, + godot_js_os_execute__sig: 'ii', godot_js_os_execute: function (p_json) { const json_args = GodotRuntime.parseString(p_json); diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 964bce01da..0f84e7e841 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -136,6 +136,9 @@ bool OS_Web::_check_internal_feature_support(const String &p_feature) { if (p_feature == "web") { return true; } + if (godot_js_os_has_feature(p_feature.utf8().get_data())) { + return true; + } return false; } diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index 867f1d537c..e32fbd47dd 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -47,9 +47,6 @@ #include <psapi.h> -#pragma comment(lib, "psapi.lib") -#pragma comment(lib, "dbghelp.lib") - // Some versions of imagehlp.dll lack the proper packing directives themselves // so we need to do it. #pragma pack(push, before_imagehlp, 8) diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 1b55574b19..3a6ca66449 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -192,6 +192,16 @@ def get_opts(): ] +def get_doc_classes(): + return [ + "EditorExportPlatformWindows", + ] + + +def get_doc_path(): + return "doc_classes" + + def get_flags(): arch = detect_build_env_arch() or detect_arch() @@ -403,12 +413,16 @@ def configure_msvc(env, vcvars_msvc_config): "dxguid", "imm32", "bcrypt", + "Crypt32", "Avrt", "dwmapi", "dwrite", "wbemuuid", ] + if env.debug_features: + LIBS += ["psapi", "dbghelp"] + if env["vulkan"]: env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED"]) if not env["use_volk"]: @@ -456,6 +470,7 @@ def configure_msvc(env, vcvars_msvc_config): env["BUILDERS"]["ProgramOriginal"] = env["BUILDERS"]["Program"] env["BUILDERS"]["Program"] = methods.precious_program + env.Append(LINKFLAGS=["/NATVIS:platform\windows\godot.natvis"]) env.AppendUnique(LINKFLAGS=["/STACK:" + str(STACK_SIZE)]) @@ -579,6 +594,7 @@ def configure_mingw(env): "ksuser", "imm32", "bcrypt", + "crypt32", "avrt", "uuid", "dwmapi", @@ -587,6 +603,9 @@ def configure_mingw(env): ] ) + if env.debug_features: + env.Append(LIBS=["psapi", "dbghelp"]) + if env["vulkan"]: env.Append(CPPDEFINES=["VULKAN_ENABLED"]) if not env["use_volk"]: diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 01aca246ca..f971846959 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -179,37 +179,37 @@ void DisplayServerWindows::_register_raw_input_devices(WindowID p_target_window) } bool DisplayServerWindows::tts_is_speaking() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return tts->is_speaking(); } bool DisplayServerWindows::tts_is_paused() const { - ERR_FAIL_COND_V(!tts, false); + ERR_FAIL_COND_V_MSG(!tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return tts->is_paused(); } TypedArray<Dictionary> DisplayServerWindows::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); + ERR_FAIL_COND_V_MSG(!tts, TypedArray<Dictionary>(), "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); 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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); 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); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->pause(); } void DisplayServerWindows::tts_resume() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->resume(); } void DisplayServerWindows::tts_stop() { - ERR_FAIL_COND(!tts); + ERR_FAIL_COND_MSG(!tts, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); tts->stop(); } @@ -388,6 +388,17 @@ int DisplayServerWindows::get_primary_screen() const { return data.screen; } +int DisplayServerWindows::get_keyboard_focus_screen() const { + HWND hwnd = GetForegroundWindow(); + if (hwnd) { + EnumScreenData data = { 0, 0, MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcScreen, (LPARAM)&data); + return data.screen; + } else { + return get_primary_screen(); + } +} + typedef struct { int count; int screen; @@ -424,17 +435,7 @@ Point2i DisplayServerWindows::_get_screens_origin() const { Point2i DisplayServerWindows::screen_get_position(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); EnumPosData data = { 0, p_screen, Point2() }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcPos, (LPARAM)&data); return data.pos - _get_screens_origin(); @@ -472,17 +473,7 @@ static BOOL CALLBACK _MonitorEnumProcSize(HMONITOR hMonitor, HDC hdcMonitor, LPR Size2i DisplayServerWindows::screen_get_size(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); EnumSizeData data = { 0, p_screen, Size2() }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcSize, (LPARAM)&data); return data.size; @@ -529,17 +520,7 @@ static BOOL CALLBACK _MonitorEnumProcRefreshRate(HMONITOR hMonitor, HDC hdcMonit Rect2i DisplayServerWindows::screen_get_usable_rect(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); EnumRectData data = { 0, p_screen, Rect2i() }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcUsableSize, (LPARAM)&data); data.rect.position -= _get_screens_origin(); @@ -617,17 +598,7 @@ static BOOL CALLBACK _MonitorEnumProcDpi(HMONITOR hMonitor, HDC hdcMonitor, LPRE int DisplayServerWindows::screen_get_dpi(int p_screen) const { _THREAD_SAFE_METHOD_ - switch (p_screen) { - case SCREEN_PRIMARY: { - p_screen = get_primary_screen(); - } break; - case SCREEN_OF_MAIN_WINDOW: { - p_screen = window_get_current_screen(MAIN_WINDOW_ID); - } break; - default: - break; - } - + p_screen = _get_screen_index(p_screen); EnumDpiData data = { 0, p_screen, 72 }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcDpi, (LPARAM)&data); return data.dpi; @@ -643,17 +614,20 @@ Color DisplayServerWindows::screen_get_pixel(const Point2i &p_position) const { win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p); } HDC dc = GetDC(0); - COLORREF col = GetPixel(dc, p.x, p.y); - if (col != CLR_INVALID) { - return Color(float(col & 0x000000FF) / 256.0, float((col & 0x0000FF00) >> 8) / 256.0, float((col & 0x00FF0000) >> 16) / 256.0, 1.0); + if (dc) { + COLORREF col = GetPixel(dc, p.x, p.y); + if (col != CLR_INVALID) { + ReleaseDC(NULL, dc); + return Color(float(col & 0x000000FF) / 256.0, float((col & 0x0000FF00) >> 8) / 256.0, float((col & 0x00FF0000) >> 16) / 256.0, 1.0); + } + ReleaseDC(NULL, dc); } - ReleaseDC(NULL, dc); return Color(); } -float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const { - _THREAD_SAFE_METHOD_ +Ref<Image> DisplayServerWindows::screen_get_image(int p_screen) const { + ERR_FAIL_INDEX_V(p_screen, get_screen_count(), Ref<Image>()); switch (p_screen) { case SCREEN_PRIMARY: { @@ -666,6 +640,65 @@ float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const { break; } + Point2i pos = screen_get_position(p_screen) + _get_screens_origin(); + Size2i size = screen_get_size(p_screen); + + POINT p1; + p1.x = pos.x; + p1.y = pos.y; + + POINT p2; + p2.x = pos.x + size.x; + p2.y = pos.y + size.y; + if (win81p_LogicalToPhysicalPointForPerMonitorDPI) { + win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p1); + win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p2); + } + + Ref<Image> img; + HDC dc = GetDC(0); + if (dc) { + HDC hdc = CreateCompatibleDC(dc); + int width = p2.x - p1.x; + int height = p2.y - p1.y; + if (hdc) { + HBITMAP hbm = CreateCompatibleBitmap(dc, width, height); + if (hbm) { + SelectObject(hdc, hbm); + BitBlt(hdc, 0, 0, width, height, dc, p1.x, p1.y, SRCCOPY); + + BITMAPINFO bmp_info = {}; + bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader); + bmp_info.bmiHeader.biWidth = width; + bmp_info.bmiHeader.biHeight = -height; + bmp_info.bmiHeader.biPlanes = 1; + bmp_info.bmiHeader.biBitCount = 32; + bmp_info.bmiHeader.biCompression = BI_RGB; + + Vector<uint8_t> img_data; + img_data.resize(width * height * 4); + GetDIBits(hdc, hbm, 0, height, img_data.ptrw(), &bmp_info, DIB_RGB_COLORS); + + uint8_t *wr = (uint8_t *)img_data.ptrw(); + for (int i = 0; i < width * height; i++) { + SWAP(wr[i * 4 + 0], wr[i * 4 + 2]); + } + img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data); + + DeleteObject(hbm); + } + DeleteDC(hdc); + } + ReleaseDC(NULL, dc); + } + + return img; +} + +float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const { + _THREAD_SAFE_METHOD_ + + p_screen = _get_screen_index(p_screen); EnumRefreshRateData data = { 0, p_screen, SCREEN_REFRESH_RATE_FALLBACK }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcRefreshRate, (LPARAM)&data); return data.rate; @@ -1755,38 +1788,37 @@ void DisplayServerWindows::cursor_set_custom_image(const Ref<Resource> &p_cursor } Ref<Texture2D> texture = p_cursor; + ERR_FAIL_COND(!texture.is_valid()); Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; Size2 texture_size; Rect2 atlas_rect; - if (texture.is_valid()) { - image = texture->get_image(); - } - - if (!image.is_valid() && atlas_texture.is_valid()) { + if (atlas_texture.is_valid()) { texture = atlas_texture->get_atlas(); atlas_rect.size.width = texture->get_width(); atlas_rect.size.height = texture->get_height(); atlas_rect.position.x = atlas_texture->get_region().position.x; atlas_rect.position.y = atlas_texture->get_region().position.y; - texture_size.width = atlas_texture->get_region().size.x; texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { + } else { texture_size.width = texture->get_width(); texture_size.height = texture->get_height(); } - ERR_FAIL_COND(!texture.is_valid()); ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - image = texture->get_image(); + Ref<Image> image = texture->get_image(); ERR_FAIL_COND(!image.is_valid()); + if (image->is_compressed()) { + image = image->duplicate(true); + Error err = image->decompress(); + ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); + } UINT image_size = texture_size.width * texture_size.height; @@ -3439,9 +3471,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA gr_mem = alt_mem; } } - if (wParam == VK_LWIN || wParam == VK_RWIN) { - meta_mem = (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN); - } if (windows[window_id].ime_suppress_next_keyup && (uMsg == WM_KEYUP || uMsg == WM_SYSKEYUP)) { windows[window_id].ime_suppress_next_keyup = false; @@ -4123,7 +4152,10 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win rendering_driver = p_rendering_driver; // Init TTS - tts = memnew(TTS_Windows); + bool tts_enabled = GLOBAL_GET("audio/general/text_to_speech"); + if (tts_enabled) { + tts = memnew(TTS_Windows); + } // Enforce default keep screen on value. screen_set_keep_on(GLOBAL_GET("display/window/energy_saving/keep_screen_on")); @@ -4415,5 +4447,4 @@ DisplayServerWindows::~DisplayServerWindows() { if (tts) { memdelete(tts); } - CoUninitialize(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 1b36b0951e..4a1619f331 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -280,8 +280,7 @@ typedef struct { } ICONDIR, *LPICONDIR; class DisplayServerWindows : public DisplayServer { - //No need to register, it's platform-specific and nothing is added - //GDCLASS(DisplayServerWindows, DisplayServer) + // No need to register with GDCLASS, it's platform-specific and nothing is added. _THREAD_SAFE_CLASS_ @@ -523,12 +522,14 @@ public: virtual int get_screen_count() const override; virtual int get_primary_screen() const override; + virtual int get_keyboard_focus_screen() 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_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual Color screen_get_pixel(const Point2i &p_position) const override; + virtual Ref<Image> screen_get_image(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual void screen_set_keep_on(bool p_enable) override; //disable screensaver virtual bool screen_is_kept_on() const override; diff --git a/platform/windows/doc_classes/EditorExportPlatformWindows.xml b/platform/windows/doc_classes/EditorExportPlatformWindows.xml new file mode 100644 index 0000000000..4bdeed19ad --- /dev/null +++ b/platform/windows/doc_classes/EditorExportPlatformWindows.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformWindows" inherits="EditorExportPlatformPC" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Exporter for Windows. + </brief_description> + <description> + </description> + <tutorials> + <link title="Exporting for Windows">$DOCS_URL/tutorials/export/exporting_for_windows.html</link> + </tutorials> + <members> + <member name="application/company_name" type="String" setter="" getter=""> + Company that produced the application. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/console_wrapper_icon" type="String" setter="" getter=""> + Console wrapper icon file. If left empty, it will fallback to [member application/icon], then to [member ProjectSettings.application/config/windows_native_icon], and lastly, [member ProjectSettings.application/config/icon]. + </member> + <member name="application/copyright" type="String" setter="" getter=""> + Copyright notice for the bundle visible to the user. Optional. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/file_description" type="String" setter="" getter=""> + File description to be presented to users. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/file_version" type="String" setter="" getter=""> + Version number of the file. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/icon" type="String" setter="" getter=""> + Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/windows_native_icon], and then to [member ProjectSettings.application/config/icon]. + </member> + <member name="application/icon_interpolation" type="int" setter="" getter=""> + Interpolation method used to resize application icon. + </member> + <member name="application/modify_resources" type="bool" setter="" getter=""> + If enabled, icon and metadata of the exported executable is set according to the other [code]application/*[/code] values. + </member> + <member name="application/product_name" type="String" setter="" getter=""> + Name of the application. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/product_version" type="String" setter="" getter=""> + Application version visible to the user. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="application/trademarks" type="String" setter="" getter=""> + Trademarks and registered trademarks that apply to the file. Optional. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. + </member> + <member name="binary_format/architecture" type="String" setter="" getter=""> + Application executable architecture. + Supported architectures: [code]x86_32[/code], [code]x86_64[/code], and [code]arm64[/code]. + Official export templates include [code]x86_32[/code] and [code]x86_64[/code] binaries only. + </member> + <member name="binary_format/embed_pck" type="bool" setter="" getter=""> + If [code]true[/code], project resources are embedded into the executable. + </member> + <member name="codesign/custom_options" type="PackedStringArray" setter="" getter=""> + Array of the additional command line arguments passed to the code signing tool. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + </member> + <member name="codesign/description" type="String" setter="" getter=""> + Description of the signed content. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + </member> + <member name="codesign/digest_algorithm" type="int" setter="" getter=""> + Digest algorithm to use for creating signature. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + </member> + <member name="codesign/enable" type="bool" setter="" getter=""> + If [code]true[/code], executable signing is enabled. + </member> + <member name="codesign/identity" type="String" setter="" getter=""> + PKCS #12 certificate file used to sign executable or certificate SHA-1 hash (if [member codesign/identity_type] is set to "Use certificate store"). See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + Can be overridden with the environment variable [code]GODOT_WINDOWS_CODESIGN_IDENTITY[/code]. + </member> + <member name="codesign/identity_type" type="int" setter="" getter=""> + Type of identity to use. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + Can be overridden with the environment variable [code]GODOT_WINDOWS_CODESIGN_IDENTITY_TYPE[/code]. + </member> + <member name="codesign/password" type="String" setter="" getter=""> + Password for the certificate file used to sign executable. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + Can be overridden with the environment variable [code]GODOT_WINDOWS_CODESIGN_PASSWORD[/code]. + </member> + <member name="codesign/timestamp" type="bool" setter="" getter=""> + If [code]true[/code], time-stamp is added to the signature. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + </member> + <member name="codesign/timestamp_server_url" type="String" setter="" getter=""> + URL of the time stamp server. If left empty, the default server is used. See [url=https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe]Sign Tool[/url]. + </member> + <member name="custom_template/debug" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="custom_template/release" type="String" setter="" getter=""> + Path to the custom export template. If left empty, default template is used. + </member> + <member name="debug/export_console_script" type="int" setter="" getter=""> + If [code]true[/code], a console wrapper executable is exported alongside the main executable, which allows running the project with enabled console output. + </member> + <member name="ssh_remote_deploy/cleanup_script" type="String" setter="" getter=""> + Script code to execute on the remote host when app is finished. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="ssh_remote_deploy/enabled" type="bool" setter="" getter=""> + Enables remote deploy using SSH/SCP. + </member> + <member name="ssh_remote_deploy/extra_args_scp" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SCP. + </member> + <member name="ssh_remote_deploy/extra_args_ssh" type="String" setter="" getter=""> + Array of the additional command line arguments passed to the SSH. + </member> + <member name="ssh_remote_deploy/host" type="String" setter="" getter=""> + Remote host SSH user name and address, in [code]user@address[/code] format. + </member> + <member name="ssh_remote_deploy/port" type="String" setter="" getter=""> + Remote host SSH port number. + </member> + <member name="ssh_remote_deploy/run_script" type="String" setter="" getter=""> + Script code to execute on the remote host when running the app. + The following variables can be used in the script: + - [code]{temp_dir}[/code] - Path of temporary folder on the remote, used to upload app and scripts to. + - [code]{archive_name}[/code] - Name of the ZIP containing uploaded application. + - [code]{exe_name}[/code] - Name of application executable. + - [code]{cmd_args}[/code] - Array of the command line argument for the application. + </member> + <member name="texture_format/bptc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the BPTC format. + </member> + <member name="texture_format/etc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the ETC format. + </member> + <member name="texture_format/etc2" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the ETC2 format. + </member> + <member name="texture_format/s3tc" type="bool" setter="" getter=""> + If [code]true[/code], project textures are exported in the S3TC format. + </member> + </members> +</class> diff --git a/platform/windows/export/export.cpp b/platform/windows/export/export.cpp index 4112bb84b5..08c620e1e9 100644 --- a/platform/windows/export/export.cpp +++ b/platform/windows/export/export.cpp @@ -33,6 +33,10 @@ #include "editor/export/editor_export.h" #include "export_plugin.h" +void register_windows_exporter_types() { + GDREGISTER_VIRTUAL_CLASS(EditorExportPlatformWindows); +} + void register_windows_exporter() { #ifndef ANDROID_ENABLED EDITOR_DEF("export/windows/rcedit", ""); diff --git a/platform/windows/export/export.h b/platform/windows/export/export.h index f5bf83bb48..6c4020f0c8 100644 --- a/platform/windows/export/export.h +++ b/platform/windows/export/export.h @@ -31,6 +31,7 @@ #ifndef WINDOWS_EXPORT_H #define WINDOWS_EXPORT_H +void register_windows_exporter_types(); void register_windows_exporter(); #endif // WINDOWS_EXPORT_H diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index 941e72c8f3..2ac41af3a2 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -35,6 +35,7 @@ #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/editor_scale.h" +#include "editor/export/editor_export.h" #include "platform/windows/logo_svg.gen.h" #include "platform/windows/run_icon_svg.gen.h" @@ -262,35 +263,86 @@ List<String> EditorExportPlatformWindows::get_binary_extensions(const Ref<Editor return list; } -bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const { +String EditorExportPlatformWindows::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + if (p_preset) { + if (p_name == "application/icon") { + String icon_path = ProjectSettings::get_singleton()->globalize_path(p_preset->get("application/icon")); + if (!icon_path.is_empty() && !FileAccess::exists(icon_path)) { + return TTR("Invalid icon path."); + } + } else if (p_name == "application/file_version") { + String file_version = p_preset->get("application/file_version"); + if (!file_version.is_empty()) { + PackedStringArray version_array = file_version.split(".", false); + if (version_array.size() != 4 || !version_array[0].is_valid_int() || + !version_array[1].is_valid_int() || !version_array[2].is_valid_int() || + !version_array[3].is_valid_int() || file_version.find("-") > -1) { + return TTR("Invalid file version."); + } + } + } else if (p_name == "application/product_version") { + String product_version = p_preset->get("application/product_version"); + if (!product_version.is_empty()) { + PackedStringArray version_array = product_version.split(".", false); + if (version_array.size() != 4 || !version_array[0].is_valid_int() || + !version_array[1].is_valid_int() || !version_array[2].is_valid_int() || + !version_array[3].is_valid_int() || product_version.find("-") > -1) { + return TTR("Invalid product version."); + } + } + } + } + return EditorExportPlatformPC::get_export_option_warning(p_preset, p_name); +} + +bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) 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; } + + // Hide codesign. + bool codesign = p_preset->get("codesign/enable"); + if (!codesign && p_option != "codesign/enable" && p_option.begins_with("codesign/")) { + return false; + } + + // Hide resources. + bool mod_res = p_preset->get("application/modify_resources"); + if (!mod_res && p_option != "application/modify_resources" && p_option.begins_with("application/")) { + return false; + } + + // Hide SSH options. + bool ssh = p_preset->get("ssh_remote_deploy/enabled"); + if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) { + return false; + } + return true; } -void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_options) { +void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_options) const { EditorExportPlatformPC::get_export_options(r_options); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "binary_format/architecture", PROPERTY_HINT_ENUM, "x86_64,x86_32,arm64"), "x86_64")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/identity_type", PROPERTY_HINT_ENUM, "Select automatically,Use PKCS12 file (specify *.PFX/*.P12 file),Use certificate store (specify SHA1 hash)"), 0)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/password", PROPERTY_HINT_PASSWORD), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/identity_type", PROPERTY_HINT_ENUM, "Select automatically,Use PKCS12 file (specify *.PFX/*.P12 file),Use certificate store (specify SHA-1 hash)", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), 0)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/password", PROPERTY_HINT_PASSWORD, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SECRET), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/timestamp_server_url"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/digest_algorithm", PROPERTY_HINT_ENUM, "SHA1,SHA256"), 1)); 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,*.png,*.webp,*.svg"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/modify_resources"), true, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.ico,*.png,*.webp,*.svg"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/console_wrapper_icon", PROPERTY_HINT_FILE, "*.ico,*.png,*.webp,*.svg"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4)); - 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"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/file_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "", false, true)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/product_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "1.0.0.0"), "", false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/company_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Company Name"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/product_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Game Name"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/file_description"), "")); @@ -311,7 +363,7 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio "Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue\n" "Remove-Item -Recurse -Force '{temp_dir}'"; - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "ssh_remote_deploy/enabled"), false, true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/host"), "user@host_ip")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "ssh_remote_deploy/port"), "22")); @@ -347,7 +399,16 @@ Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset } #endif - String icon_path = ProjectSettings::get_singleton()->globalize_path(p_preset->get("application/icon")); + String icon_path; + if (p_preset->get("application/icon") != "") { + icon_path = p_preset->get("application/icon"); + } else if (GLOBAL_GET("application/config/windows_native_icon") != "") { + icon_path = GLOBAL_GET("application/config/windows_native_icon"); + } else { + icon_path = GLOBAL_GET("application/config/icon"); + } + icon_path = ProjectSettings::get_singleton()->globalize_path(icon_path); + if (p_console_icon) { String console_icon_path = ProjectSettings::get_singleton()->globalize_path(p_preset->get("application/console_wrapper_icon")); if (!console_icon_path.is_empty() && FileAccess::exists(console_icon_path)) { @@ -466,21 +527,21 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p //identity #ifdef WINDOWS_ENABLED - int id_type = p_preset->get("codesign/identity_type"); + int id_type = p_preset->get_or_env("codesign/identity_type", ENV_WIN_CODESIGN_ID_TYPE); if (id_type == 0) { //auto select args.push_back("/a"); } else if (id_type == 1) { //pkcs12 - if (p_preset->get("codesign/identity") != "") { + if (p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID) != "") { args.push_back("/f"); - args.push_back(p_preset->get("codesign/identity")); + args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; } } else if (id_type == 2) { //Windows certificate store - if (p_preset->get("codesign/identity") != "") { + if (p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID) != "") { args.push_back("/sha1"); - args.push_back(p_preset->get("codesign/identity")); + args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; @@ -491,9 +552,9 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p } #else int id_type = 1; - if (p_preset->get("codesign/identity") != "") { + if (p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID) != "") { args.push_back("-pkcs12"); - args.push_back(p_preset->get("codesign/identity")); + args.push_back(p_preset->get_or_env("codesign/identity", ENV_WIN_CODESIGN_ID)); } else { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); return FAILED; @@ -501,13 +562,13 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p #endif //password - if ((id_type == 1) && (p_preset->get("codesign/password") != "")) { + if ((id_type == 1) && (p_preset->get_or_env("codesign/password", ENV_WIN_CODESIGN_PASS) != "")) { #ifdef WINDOWS_ENABLED args.push_back("/p"); #else args.push_back("-pass"); #endif - args.push_back(p_preset->get("codesign/password")); + args.push_back(p_preset->get_or_env("codesign/password", ENV_WIN_CODESIGN_PASS)); } //timestamp @@ -632,30 +693,17 @@ bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<Edit String err = ""; bool valid = true; - String icon_path = ProjectSettings::get_singleton()->globalize_path(p_preset->get("application/icon")); - if (!icon_path.is_empty() && !FileAccess::exists(icon_path)) { - err += TTR("Invalid icon path:") + " " + icon_path + "\n"; - } - - // Only non-negative integers can exist in the version string. - - String file_version = p_preset->get("application/file_version"); - if (!file_version.is_empty()) { - PackedStringArray version_array = file_version.split(".", false); - if (version_array.size() != 4 || !version_array[0].is_valid_int() || - !version_array[1].is_valid_int() || !version_array[2].is_valid_int() || - !version_array[3].is_valid_int() || file_version.find("-") > -1) { - err += TTR("Invalid file version:") + " " + file_version + "\n"; - } - } - - String product_version = p_preset->get("application/product_version"); - if (!product_version.is_empty()) { - PackedStringArray version_array = product_version.split(".", false); - if (version_array.size() != 4 || !version_array[0].is_valid_int() || - !version_array[1].is_valid_int() || !version_array[2].is_valid_int() || - !version_array[3].is_valid_int() || product_version.find("-") > -1) { - err += TTR("Invalid product version:") + " " + product_version + "\n"; + List<ExportOption> options; + get_export_options(&options); + for (const EditorExportPlatform::ExportOption &E : options) { + if (get_export_option_visibility(p_preset.ptr(), E.option.name)) { + String warn = get_export_option_warning(p_preset.ptr(), E.option.name); + if (!warn.is_empty()) { + err += warn + "\n"; + if (E.required) { + valid = false; + } + } } } @@ -957,22 +1005,24 @@ Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, } EditorExportPlatformWindows::EditorExportPlatformWindows() { + if (EditorNode::get_singleton()) { #ifdef MODULE_SVG_ENABLED - Ref<Image> img = memnew(Image); - const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); + Ref<Image> img = memnew(Image); + const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE); - ImageLoaderSVG img_loader; - img_loader.create_image_from_string(img, _windows_logo_svg, EDSCALE, upsample, false); - set_logo(ImageTexture::create_from_image(img)); + ImageLoaderSVG img_loader; + img_loader.create_image_from_string(img, _windows_logo_svg, EDSCALE, upsample, false); + set_logo(ImageTexture::create_from_image(img)); - img_loader.create_image_from_string(img, _windows_run_icon_svg, EDSCALE, upsample, false); - run_icon = ImageTexture::create_from_image(img); + img_loader.create_image_from_string(img, _windows_run_icon_svg, EDSCALE, upsample, false); + run_icon = ImageTexture::create_from_image(img); #endif - Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) { - stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); - } else { - stop_icon.instantiate(); + Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); + if (theme.is_valid()) { + stop_icon = theme->get_icon(SNAME("Stop"), SNAME("EditorIcons")); + } else { + stop_icon.instantiate(); + } } } diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index fa75a17a1f..184b2f96f8 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -36,7 +36,15 @@ #include "editor/editor_settings.h" #include "editor/export/editor_export_platform_pc.h" +// Optional environment variables for defining confidential information. If any +// of these is set, they will override the values set in the credentials file. +const String ENV_WIN_CODESIGN_ID_TYPE = "GODOT_WINDOWS_CODESIGN_IDENTITY_TYPE"; +const String ENV_WIN_CODESIGN_ID = "GODOT_WINDOWS_CODESIGN_IDENTITY"; +const String ENV_WIN_CODESIGN_PASS = "GODOT_WINDOWS_CODESIGN_PASSWORD"; + class EditorExportPlatformWindows : public EditorExportPlatformPC { + GDCLASS(EditorExportPlatformWindows, EditorExportPlatformPC); + struct SSHCleanupCommand { String host; String port; @@ -70,10 +78,12 @@ public: 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 void get_export_options(List<ExportOption> *r_options) const override; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; - virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; + virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 7ae26e6cf4..91efe09160 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -516,11 +516,13 @@ void JoypadWindows::joypad_vibration_stop_xinput(int p_device, uint64_t p_timest void JoypadWindows::load_xinput() { xinput_get_state = &_xinput_get_state; xinput_set_state = &_xinput_set_state; + bool legacy_xinput = false; xinput_dll = LoadLibrary("XInput1_4.dll"); if (!xinput_dll) { xinput_dll = LoadLibrary("XInput1_3.dll"); if (!xinput_dll) { xinput_dll = LoadLibrary("XInput9_1_0.dll"); + legacy_xinput = true; } } @@ -529,7 +531,9 @@ void JoypadWindows::load_xinput() { return; } - XInputGetState_t func = (XInputGetState_t)GetProcAddress((HMODULE)xinput_dll, "XInputGetState"); + // (LPCSTR)100 is the magic number to get XInputGetStateEx, which also provides the state for the guide button + LPCSTR get_state_func_name = legacy_xinput ? "XInputGetState" : (LPCSTR)100; + XInputGetState_t func = (XInputGetState_t)GetProcAddress((HMODULE)xinput_dll, get_state_func_name); XInputSetState_t set_func = (XInputSetState_t)GetProcAddress((HMODULE)xinput_dll, "XInputSetState"); if (!func || !set_func) { unload_xinput(); diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 6e219fb929..2653efed71 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -51,9 +51,17 @@ #include <direct.h> #include <knownfolders.h> #include <process.h> +#include <psapi.h> #include <regstr.h> #include <shlobj.h> #include <wbemcli.h> +#include <wincrypt.h> + +#ifdef DEBUG_ENABLED +#pragma pack(push, before_imagehlp, 8) +#include <imagehlp.h> +#pragma pack(pop, before_imagehlp) +#endif extern "C" { __declspec(dllexport) DWORD NvOptimusEnablement = 1; @@ -88,7 +96,7 @@ static String format_error_message(DWORD id) { LocalFree(messageBuffer); - return msg; + return msg.replace("\r", "").replace("\n", ""); } void RedirectStream(const char *p_file_name, const char *p_mode, FILE *p_cpp_stream, const DWORD p_std_handle) { @@ -195,7 +203,6 @@ void OS_Windows::initialize() { IPUnix::make_default(); main_loop = nullptr; - CoInitialize(nullptr); HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast<IUnknown **>(&dwrite_factory)); if (SUCCEEDED(hr)) { hr = dwrite_factory->GetSystemFontCollection(&font_collection, false); @@ -277,6 +284,73 @@ Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) { return OK; } +#ifdef DEBUG_ENABLED +void debug_dynamic_library_check_dependencies(const String &p_root_path, const String &p_path, HashSet<String> &r_checked, HashSet<String> &r_missing) { + if (r_checked.has(p_path)) { + return; + } + r_checked.insert(p_path); + + LOADED_IMAGE loaded_image; + HANDLE file = CreateFileW((LPCWSTR)p_path.utf16().get_data(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); + if (file != INVALID_HANDLE_VALUE) { + HANDLE file_mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY | SEC_COMMIT, 0, 0, nullptr); + if (file_mapping != INVALID_HANDLE_VALUE) { + PVOID mapping = MapViewOfFile(file_mapping, FILE_MAP_READ, 0, 0, 0); + if (mapping) { + PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)mapping; + PIMAGE_NT_HEADERS nt_header = nullptr; + if (dos_header->e_magic == IMAGE_DOS_SIGNATURE) { + PCHAR nt_header_ptr; + nt_header_ptr = ((PCHAR)mapping) + dos_header->e_lfanew; + nt_header = (PIMAGE_NT_HEADERS)nt_header_ptr; + if (nt_header->Signature != IMAGE_NT_SIGNATURE) { + nt_header = nullptr; + } + } + if (nt_header) { + loaded_image.ModuleName = nullptr; + loaded_image.hFile = file; + loaded_image.MappedAddress = (PUCHAR)mapping; + loaded_image.FileHeader = nt_header; + loaded_image.Sections = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt_header->OptionalHeader + nt_header->FileHeader.SizeOfOptionalHeader); + loaded_image.NumberOfSections = nt_header->FileHeader.NumberOfSections; + loaded_image.SizeOfImage = GetFileSize(file, nullptr); + loaded_image.Characteristics = nt_header->FileHeader.Characteristics; + loaded_image.LastRvaSection = loaded_image.Sections; + loaded_image.fSystemImage = false; + loaded_image.fDOSImage = false; + loaded_image.Links.Flink = &loaded_image.Links; + loaded_image.Links.Blink = &loaded_image.Links; + + ULONG size = 0; + const IMAGE_IMPORT_DESCRIPTOR *import_desc = (const IMAGE_IMPORT_DESCRIPTOR *)ImageDirectoryEntryToData((HMODULE)loaded_image.MappedAddress, false, IMAGE_DIRECTORY_ENTRY_IMPORT, &size); + if (import_desc) { + for (; import_desc->Name && import_desc->FirstThunk; import_desc++) { + char16_t full_name_wc[MAX_PATH]; + const char *name_cs = (const char *)ImageRvaToVa(loaded_image.FileHeader, loaded_image.MappedAddress, import_desc->Name, 0); + String name = String(name_cs); + if (name.begins_with("api-ms-win-")) { + r_checked.insert(name); + } else if (SearchPathW(nullptr, (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) { + debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing); + } else if (SearchPathW((LPCWSTR)(p_path.get_base_dir().utf16().get_data()), (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) { + debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing); + } else { + r_missing.insert(name); + } + } + } + } + UnmapViewOfFile(mapping); + } + CloseHandle(file_mapping); + } + CloseHandle(file); + } +} +#endif + 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("/", "\\"); @@ -299,7 +373,29 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han } p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); - ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + format_error_message(GetLastError()) + "."); +#ifdef DEBUG_ENABLED + if (!p_library_handle) { + DWORD err_code = GetLastError(); + + HashSet<String> checekd_libs; + HashSet<String> missing_libs; + debug_dynamic_library_check_dependencies(path, path, checekd_libs, missing_libs); + if (!missing_libs.is_empty()) { + String missing; + for (const String &E : missing_libs) { + if (!missing.is_empty()) { + missing += ", "; + } + missing += E; + } + ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, missing dependencies: (%s), error: \"%s\".", p_path, missing, format_error_message(err_code))); + } else { + ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, error: \"%s\"." + p_path, format_error_message(err_code))); + } + } +#else + ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s, error: \"%s\"." + p_path, format_error_message(GetLastError()))); +#endif if (cookie) { remove_dll_directory(cookie); @@ -323,7 +419,7 @@ Error OS_Windows::get_dynamic_library_symbol_handle(void *p_library_handle, cons p_symbol_handle = (void *)GetProcAddress((HMODULE)p_library_handle, p_name.utf8().get_data()); if (!p_symbol_handle) { if (!p_optional) { - ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, "Can't resolve symbol " + p_name + ", error: " + String::num(GetLastError()) + "."); + ERR_FAIL_V_MSG(ERR_CANT_RESOLVE, vformat("Can't resolve symbol %s, error: \"%s\".", p_name, format_error_message(GetLastError()))); } else { return ERR_CANT_RESOLVE; } @@ -340,8 +436,6 @@ String OS_Windows::get_distribution_name() const { } String OS_Windows::get_version() const { - typedef LONG NTSTATUS; - typedef NTSTATUS(WINAPI * RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); RtlGetVersionPtr version_ptr = (RtlGetVersionPtr)GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlGetVersion"); if (version_ptr != nullptr) { RTL_OSVERSIONINFOW fow; @@ -359,22 +453,25 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { return Vector<String>(); } + static Vector<String> info; + if (!info.is_empty()) { + return info; + } + REFCLSID clsid = CLSID_WbemLocator; // Unmarshaler CLSID REFIID uuid = IID_IWbemLocator; // Interface UUID IWbemLocator *wbemLocator = NULL; // to get the services IWbemServices *wbemServices = NULL; // to get the class IEnumWbemClassObject *iter = NULL; IWbemClassObject *pnpSDriverObject[1]; // contains driver name, version, etc. - static String driver_name; - static String driver_version; + String driver_name; + String driver_version; const String device_name = RenderingServer::get_singleton()->get_rendering_device()->get_device_name(); if (device_name.is_empty()) { return Vector<String>(); } - CoInitialize(nullptr); - HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); if (hr != S_OK) { return Vector<String>(); @@ -444,7 +541,6 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { SAFE_RELEASE(wbemServices) SAFE_RELEASE(iter) - Vector<String> info; info.push_back(driver_name); info.push_back(driver_version); @@ -461,9 +557,9 @@ OS::DateTime OS_Windows::get_datetime(bool p_utc) const { //Get DST information from Windows, but only if p_utc is false. TIME_ZONE_INFORMATION info; - bool daylight = false; + bool is_daylight = false; if (!p_utc && GetTimeZoneInformation(&info) == TIME_ZONE_ID_DAYLIGHT) { - daylight = true; + is_daylight = true; } DateTime dt; @@ -474,20 +570,20 @@ OS::DateTime OS_Windows::get_datetime(bool p_utc) const { dt.hour = systemtime.wHour; dt.minute = systemtime.wMinute; dt.second = systemtime.wSecond; - dt.dst = daylight; + dt.dst = is_daylight; return dt; } OS::TimeZoneInfo OS_Windows::get_time_zone_info() const { TIME_ZONE_INFORMATION info; - bool daylight = false; + bool is_daylight = false; if (GetTimeZoneInformation(&info) == TIME_ZONE_ID_DAYLIGHT) { - daylight = true; + is_daylight = true; } // Daylight Bias needs to be added to the bias if DST is in effect, or else it will not properly update. TimeZoneInfo ret; - if (daylight) { + if (is_daylight) { ret.name = info.DaylightName; ret.bias = info.Bias + info.DaylightBias; } else { @@ -591,6 +687,43 @@ static void _append_to_pipe(char *p_bytes, int p_size, String *r_pipe, Mutex *p_ } } +Dictionary OS_Windows::get_memory_info() const { + Dictionary meminfo; + + meminfo["physical"] = -1; + meminfo["free"] = -1; + meminfo["available"] = -1; + meminfo["stack"] = -1; + + PERFORMANCE_INFORMATION pref_info; + pref_info.cb = sizeof(pref_info); + GetPerformanceInfo(&pref_info, sizeof(pref_info)); + + typedef void(WINAPI * PGetCurrentThreadStackLimits)(PULONG_PTR, PULONG_PTR); + PGetCurrentThreadStackLimits GetCurrentThreadStackLimits = (PGetCurrentThreadStackLimits)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetCurrentThreadStackLimits"); + + ULONG_PTR LowLimit = 0; + ULONG_PTR HighLimit = 0; + if (GetCurrentThreadStackLimits) { + GetCurrentThreadStackLimits(&LowLimit, &HighLimit); + } + + if (pref_info.PhysicalTotal * pref_info.PageSize != 0) { + meminfo["physical"] = static_cast<int64_t>(pref_info.PhysicalTotal * pref_info.PageSize); + } + if (pref_info.PhysicalAvailable * pref_info.PageSize != 0) { + meminfo["free"] = static_cast<int64_t>(pref_info.PhysicalAvailable * pref_info.PageSize); + } + if (pref_info.CommitLimit * pref_info.PageSize != 0) { + meminfo["available"] = static_cast<int64_t>(pref_info.CommitLimit * pref_info.PageSize); + } + if (HighLimit - LowLimit != 0) { + meminfo["stack"] = static_cast<int64_t>(HighLimit - LowLimit); + } + + return meminfo; +} + 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); @@ -1221,6 +1354,51 @@ Error OS_Windows::shell_open(String p_uri) { } } +Error OS_Windows::shell_show_in_file_manager(String p_path, bool p_open_folder) { + p_path = p_path.trim_suffix("file://"); + + bool open_folder = false; + if (DirAccess::dir_exists_absolute(p_path) && p_open_folder) { + open_folder = true; + } + + if (p_path.begins_with("\"")) { + p_path = String("\"") + p_path; + } + if (p_path.ends_with("\"")) { + p_path = p_path + String("\""); + } + p_path = p_path.replace("/", "\\"); + + INT_PTR ret = OK; + if (open_folder) { + ret = (INT_PTR)ShellExecuteW(nullptr, nullptr, L"explorer.exe", LPCWSTR(p_path.utf16().get_data()), nullptr, SW_SHOWNORMAL); + } else { + ret = (INT_PTR)ShellExecuteW(nullptr, nullptr, L"explorer.exe", LPCWSTR((String("/select,") + p_path).utf16().get_data()), nullptr, SW_SHOWNORMAL); + } + + if (ret > 32) { + return OK; + } else { + switch (ret) { + case ERROR_FILE_NOT_FOUND: + case SE_ERR_DLLNOTFOUND: + return ERR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return ERR_FILE_BAD_PATH; + case ERROR_BAD_FORMAT: + return ERR_FILE_CORRUPT; + case SE_ERR_ACCESSDENIED: + return ERR_UNAUTHORIZED; + case 0: + case SE_ERR_OOM: + return ERR_OUT_OF_MEMORY; + default: + return FAILED; + } + } +} + String OS_Windows::get_locale() const { const _WinLocale *wl = &_win_locales[0]; @@ -1502,9 +1680,41 @@ Error OS_Windows::move_to_trash(const String &p_path) { return OK; } +String OS_Windows::get_system_ca_certificates() { + HCERTSTORE cert_store = CertOpenSystemStoreA(0, "ROOT"); + ERR_FAIL_COND_V_MSG(!cert_store, "", "Failed to read the root certificate store."); + + FILETIME curr_time; + GetSystemTimeAsFileTime(&curr_time); + + String certs; + PCCERT_CONTEXT curr = CertEnumCertificatesInStore(cert_store, nullptr); + while (curr) { + FILETIME ft; + DWORD size = sizeof(ft); + // Check if the certificate is disallowed. + if (CertGetCertificateContextProperty(curr, CERT_DISALLOWED_FILETIME_PROP_ID, &ft, &size) && CompareFileTime(&curr_time, &ft) != -1) { + curr = CertEnumCertificatesInStore(cert_store, curr); + continue; + } + // Encode and add to certificate list. + bool success = CryptBinaryToStringA(curr->pbCertEncoded, curr->cbCertEncoded, CRYPT_STRING_BASE64HEADER | CRYPT_STRING_NOCR, nullptr, &size); + ERR_CONTINUE(!success); + PackedByteArray pba; + pba.resize(size); + CryptBinaryToStringA(curr->pbCertEncoded, curr->cbCertEncoded, CRYPT_STRING_BASE64HEADER | CRYPT_STRING_NOCR, (char *)pba.ptrw(), &size); + certs += String((char *)pba.ptr(), size); + curr = CertEnumCertificatesInStore(cert_store, curr); + } + CertCloseStore(cert_store, 0); + return certs; +} + OS_Windows::OS_Windows(HINSTANCE _hInstance) { hInstance = _hInstance; + CoInitializeEx(nullptr, COINIT_MULTITHREADED); + #ifdef WASAPI_ENABLED AudioDriverManager::add_driver(&driver_wasapi); #endif @@ -1532,4 +1742,5 @@ OS_Windows::OS_Windows(HINSTANCE _hInstance) { } OS_Windows::~OS_Windows() { + CoUninitialize(); } diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 05110c2614..c5f95870b3 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -178,6 +178,8 @@ public: virtual void delay_usec(uint32_t p_usec) const override; virtual uint64_t get_ticks_usec() const override; + virtual Dictionary get_memory_info() const override; + virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error kill(const ProcessID &p_pid) override; @@ -212,6 +214,7 @@ public: virtual String get_unique_id() const override; virtual Error shell_open(String p_uri) override; + virtual Error shell_show_in_file_manager(String p_path, bool p_open_folder) override; void run(); @@ -223,6 +226,8 @@ public: virtual Error move_to_trash(const String &p_path) override; + virtual String get_system_ca_certificates() override; + void set_main_window(HWND p_main_window) { main_window = p_main_window; } HINSTANCE get_hinstance() { return hInstance; } diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp index 3a143d0ecb..907096d890 100644 --- a/platform/windows/tts_windows.cpp +++ b/platform/windows/tts_windows.cpp @@ -36,15 +36,16 @@ 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)) { + uint32_t stream_num = (uint32_t)event.ulStreamNum; + if (tts->ids.has(stream_num)) { if (event.eEventId == SPEI_START_INPUT_STREAM) { - DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, tts->ids[event.ulStreamNum].id); + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_STARTED, tts->ids[stream_num].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); + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_ENDED, tts->ids[stream_num].id); + tts->ids.erase(stream_num); tts->_update_tts(); } else if (event.eEventId == SPEI_WORD_BOUNDARY) { - const Char16String &string = tts->ids[event.ulStreamNum].string; + const Char16String &string = tts->ids[stream_num].string; int pos = 0; for (int i = 0; i < MIN(event.lParam, string.length()); i++) { char16_t c = string[i]; @@ -53,7 +54,7 @@ void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) } pos++; } - DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[event.ulStreamNum].id, pos - tts->ids[event.ulStreamNum].offset); + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_BOUNDARY, tts->ids[stream_num].id, pos - tts->ids[stream_num].offset); } } } @@ -106,7 +107,7 @@ void TTS_Windows::_update_tts() { synth->SetRate(10.f * log10(message.rate) / log10(3.f)); synth->Speak((LPCWSTR)ut.string.get_data(), flags, &stream_number); - ids[stream_number] = ut; + ids[(uint32_t)stream_number] = ut; queue.pop_front(); } @@ -117,7 +118,7 @@ bool TTS_Windows::is_speaking() const { SPVOICESTATUS status; synth->GetStatus(&status, nullptr); - return (status.dwRunningState == SPRS_IS_SPEAKING); + return (status.dwRunningState == SPRS_IS_SPEAKING || status.dwRunningState == 0 /* Waiting To Speak */); } bool TTS_Windows::is_paused() const { @@ -230,9 +231,10 @@ void TTS_Windows::stop() { 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); + uint32_t current_stream = (uint32_t)status.ulCurrentStream; + if (ids.has(current_stream)) { + DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, ids[current_stream].id); + ids.erase(current_stream); } for (DisplayServer::TTSUtterance &message : queue) { DisplayServer::get_singleton()->tts_post_utterance_event(DisplayServer::TTS_UTTERANCE_CANCELED, message.id); @@ -249,7 +251,6 @@ TTS_Windows *TTS_Windows::get_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); diff --git a/platform/windows/tts_windows.h b/platform/windows/tts_windows.h index f0538a097c..33b597c612 100644 --- a/platform/windows/tts_windows.h +++ b/platform/windows/tts_windows.h @@ -32,8 +32,8 @@ #define TTS_WINDOWS_H #include "core/string/ustring.h" +#include "core/templates/hash_map.h" #include "core/templates/list.h" -#include "core/templates/rb_map.h" #include "core/variant/array.h" #include "servers/display_server.h" @@ -54,7 +54,7 @@ class TTS_Windows { int offset; int id; }; - RBMap<ULONG, UTData> ids; + HashMap<uint32_t, UTData> ids; static void __stdcall speech_event_callback(WPARAM wParam, LPARAM lParam); void _update_tts(); diff --git a/platform/windows/vulkan_context_win.cpp b/platform/windows/vulkan_context_win.cpp index cf4383fc33..4c1e6eebe4 100644 --- a/platform/windows/vulkan_context_win.cpp +++ b/platform/windows/vulkan_context_win.cpp @@ -55,6 +55,10 @@ Error VulkanContextWindows::window_create(DisplayServer::WindowID p_window_id, D } VulkanContextWindows::VulkanContextWindows() { + // Workaround for Vulkan not working on setups with AMD integrated graphics + NVIDIA dedicated GPU (GH-57708). + // This prevents using AMD integrated graphics with Vulkan entirely, but it allows the engine to start + // even on outdated/broken driver setups. + OS::get_singleton()->set_environment("DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1", "1"); } VulkanContextWindows::~VulkanContextWindows() { |