Path: blob/master/platform/ios/export/export_plugin.cpp
11353 views
/**************************************************************************/1/* export_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "export_plugin.h"3132#include "logo_svg.gen.h"33#include "run_icon_svg.gen.h"3435#include "editor/editor_node.h"3637Vector<String> EditorExportPlatformIOS::device_types({ "iPhone", "iPad" });3839void EditorExportPlatformIOS::initialize() {40if (EditorNode::get_singleton()) {41EditorExportPlatformAppleEmbedded::_initialize(_ios_logo_svg, _ios_run_icon_svg);42#ifdef MACOS_ENABLED43_start_remote_device_poller_thread();44#endif45}46}4748EditorExportPlatformIOS::~EditorExportPlatformIOS() {49}5051void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) const {52EditorExportPlatformAppleEmbedded::get_export_options(r_options);5354r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/targeted_device_family", PROPERTY_HINT_ENUM, "iPhone,iPad,iPhone & iPad"), 2));55r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_ios_version"), get_minimum_deployment_target()));5657r_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));58r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE_PATH, "*.png,*.jpg,*.jpeg"), ""));59r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE_PATH, "*.png,*.jpg,*.jpeg"), ""));60r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_custom_bg_color"), false));61r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "storyboard/custom_bg_color"), Color()));62}6364bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {65bool valid = EditorExportPlatformAppleEmbedded::has_valid_export_configuration(p_preset, r_error, r_missing_templates, p_debug);6667String err;68String rendering_method = get_project_setting(p_preset, "rendering/renderer/rendering_method.mobile");69String rendering_driver = get_project_setting(p_preset, "rendering/rendering_device/driver." + get_platform_name());70if ((rendering_method == "forward_plus" || rendering_method == "mobile") && rendering_driver == "metal") {71float version = p_preset->get("application/min_ios_version").operator String().to_float();72if (version < 14.0) {73err += TTR("Metal renderer require iOS 14+.") + "\n";74}75}7677if (!err.is_empty()) {78if (!r_error.is_empty()) {79r_error += err;80} else {81r_error = err;82}83}8485return valid;86}8788HashMap<String, Variant> EditorExportPlatformIOS::get_custom_project_settings(const Ref<EditorExportPreset> &p_preset) const {89HashMap<String, Variant> settings;9091int image_scale_mode = p_preset->get("storyboard/image_scale_mode");92String value;9394switch (image_scale_mode) {95case 0: {96String logo_path = get_project_setting(p_preset, "application/boot_splash/image");97bool is_on = get_project_setting(p_preset, "application/boot_splash/fullsize");98// If custom logo is not specified, Godot does not scale default one, so we should do the same.99value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center";100} break;101default: {102value = storyboard_image_scale_mode[image_scale_mode - 1];103}104}105settings["ios/launch_screen_image_mode"] = value;106return settings;107}108109Error EditorExportPlatformIOS::_export_loading_screen_file(const Ref<EditorExportPreset> &p_preset, const String &p_dest_dir) {110const String custom_launch_image_2x = p_preset->get("storyboard/custom_image@2x");111const String custom_launch_image_3x = p_preset->get("storyboard/custom_image@3x");112113if (custom_launch_image_2x.length() > 0 && custom_launch_image_3x.length() > 0) {114String image_path = p_dest_dir.path_join("[email protected]");115Error err = OK;116Ref<Image> image = _load_icon_or_splash_image(custom_launch_image_2x, &err);117118if (err != OK || image.is_null() || image->is_empty()) {119return err;120}121122if (image->save_png(image_path) != OK) {123return ERR_FILE_CANT_WRITE;124}125126image_path = p_dest_dir.path_join("[email protected]");127image = _load_icon_or_splash_image(custom_launch_image_3x, &err);128129if (err != OK || image.is_null() || image->is_empty()) {130return err;131}132133if (image->save_png(image_path) != OK) {134return ERR_FILE_CANT_WRITE;135}136} else {137Error err = OK;138Ref<Image> splash;139140const String splash_path = get_project_setting(p_preset, "application/boot_splash/image");141142if (!splash_path.is_empty()) {143splash = _load_icon_or_splash_image(splash_path, &err);144}145146if (err != OK || splash.is_null() || splash->is_empty()) {147splash.instantiate(boot_splash_png);148}149150// Using same image for both @2x and @3x151// because Godot's own boot logo uses single image for all resolutions.152// Also not using @1x image, because devices using this image variant153// are not supported by iOS 9, which is minimal target.154const String splash_png_path_2x = p_dest_dir.path_join("[email protected]");155const String splash_png_path_3x = p_dest_dir.path_join("[email protected]");156157if (splash->save_png(splash_png_path_2x) != OK) {158return ERR_FILE_CANT_WRITE;159}160161if (splash->save_png(splash_png_path_3x) != OK) {162return ERR_FILE_CANT_WRITE;163}164}165166return OK;167}168169Vector<EditorExportPlatformAppleEmbedded::IconInfo> EditorExportPlatformIOS::get_icon_infos() const {170Vector<EditorExportPlatformAppleEmbedded::IconInfo> icon_infos;171return {172// Settings on iPhone, iPad Pro, iPad, iPad mini173{ PNAME("icons/settings_58x58"), "universal", "Icon-58", "58", "2x", "29x29", false },174{ PNAME("icons/settings_87x87"), "universal", "Icon-87", "87", "3x", "29x29", false },175176// Notifications on iPhone, iPad Pro, iPad, iPad mini177{ PNAME("icons/notification_40x40"), "universal", "Icon-40", "40", "2x", "20x20", false },178{ PNAME("icons/notification_60x60"), "universal", "Icon-60", "60", "3x", "20x20", false },179{ PNAME("icons/notification_76x76"), "universal", "Icon-76", "76", "2x", "38x38", false },180{ PNAME("icons/notification_114x114"), "universal", "Icon-114", "114", "3x", "38x38", false },181182// Spotlight on iPhone, iPad Pro, iPad, iPad mini183{ PNAME("icons/spotlight_80x80"), "universal", "Icon-80", "80", "2x", "40x40", false },184{ PNAME("icons/spotlight_120x120"), "universal", "Icon-120", "120", "3x", "40x40", false },185186// Home Screen on iPhone187{ PNAME("icons/iphone_120x120"), "universal", "Icon-120-1", "120", "2x", "60x60", false },188{ PNAME("icons/iphone_180x180"), "universal", "Icon-180", "180", "3x", "60x60", false },189190// Home Screen on iPad Pro191{ PNAME("icons/ipad_167x167"), "universal", "Icon-167", "167", "2x", "83.5x83.5", false },192193// Home Screen on iPad, iPad mini194{ PNAME("icons/ipad_152x152"), "universal", "Icon-152", "152", "2x", "76x76", false },195196{ PNAME("icons/ios_128x128"), "universal", "Icon-128", "128", "2x", "64x64", false },197{ PNAME("icons/ios_192x192"), "universal", "Icon-192", "192", "3x", "64x64", false },198199{ PNAME("icons/ios_136x136"), "universal", "Icon-136", "136", "2x", "68x68", false },200201// App Store202{ PNAME("icons/app_store_1024x1024"), "universal", "Icon-1024", "1024", "1x", "1024x1024", true },203};204}205206Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir) {207String json_description = "{\"images\":[";208String sizes;209210Ref<DirAccess> da = DirAccess::open(p_iconset_dir);211if (da.is_null()) {212add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not open a directory at path \"%s\"."), p_iconset_dir));213return ERR_CANT_OPEN;214}215216Color boot_bg_color = get_project_setting(p_preset, "application/boot_splash/bg_color");217218enum IconColorMode {219ICON_NORMAL,220ICON_DARK,221ICON_TINTED,222ICON_MAX,223};224225Vector<IconInfo> icon_infos = get_icon_infos();226bool first_icon = true;227for (int i = 0; i < icon_infos.size(); ++i) {228for (int color_mode = ICON_NORMAL; color_mode < ICON_MAX; color_mode++) {229IconInfo info = icon_infos[i];230int side_size = String(info.actual_size_side).to_int();231String key = info.preset_key;232String exp_name = info.export_name;233if (color_mode == ICON_DARK) {234key += "_dark";235exp_name += "_dark";236} else if (color_mode == ICON_TINTED) {237key += "_tinted";238exp_name += "_tinted";239}240exp_name += ".png";241String icon_path = p_preset->get(key);242bool resize_waning = true;243if (icon_path.is_empty()) {244// Load and resize base icon.245key = "icons/icon_1024x1024";246if (color_mode == ICON_DARK) {247key += "_dark";248} else if (color_mode == ICON_TINTED) {249key += "_tinted";250}251icon_path = p_preset->get(key);252resize_waning = false;253}254if (icon_path.is_empty()) {255if (color_mode != ICON_NORMAL) {256continue;257}258// Resize main app icon.259icon_path = get_project_setting(p_preset, "application/config/icon");260Error err = OK;261Ref<Image> img = _load_icon_or_splash_image(icon_path, &err);262if (err != OK || img.is_null() || img->is_empty()) {263add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));264return ERR_UNCONFIGURED;265} else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {266img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));267Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);268new_img->fill(boot_bg_color);269_blend_and_rotate(new_img, img, false);270err = new_img->save_png(p_iconset_dir + exp_name);271} else {272img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));273err = img->save_png(p_iconset_dir + exp_name);274}275if (err) {276add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));277return err;278}279} else {280// Load custom icon and resize if required.281Error err = OK;282Ref<Image> img = _load_icon_or_splash_image(icon_path, &err);283if (err != OK || img.is_null() || img->is_empty()) {284add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path));285return ERR_UNCONFIGURED;286} else if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) {287if (resize_waning) {288add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key));289}290img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));291Ref<Image> new_img = Image::create_empty(side_size, side_size, false, Image::FORMAT_RGBA8);292new_img->fill(boot_bg_color);293_blend_and_rotate(new_img, img, false);294err = new_img->save_png(p_iconset_dir + exp_name);295} else if (img->get_width() != side_size || img->get_height() != side_size) {296if (resize_waning) {297add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size)));298}299img->resize(side_size, side_size, (Image::Interpolation)(p_preset->get("application/icon_interpolation").operator int()));300err = img->save_png(p_iconset_dir + exp_name);301} else if (!icon_path.ends_with(".png")) {302err = img->save_png(p_iconset_dir + exp_name);303} else {304err = da->copy(icon_path, p_iconset_dir + exp_name);305}306307if (err) {308add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path));309return err;310}311}312sizes += String(info.actual_size_side) + "\n";313if (first_icon) {314first_icon = false;315} else {316json_description += ",";317}318json_description += String("{");319if (color_mode != ICON_NORMAL) {320json_description += String("\"appearances\":[{");321json_description += String("\"appearance\":\"luminosity\",");322if (color_mode == ICON_DARK) {323json_description += String("\"value\":\"dark\"");324} else if (color_mode == ICON_TINTED) {325json_description += String("\"value\":\"tinted\"");326}327json_description += String("}],");328}329json_description += String("\"idiom\":") + "\"" + info.idiom + "\",";330json_description += String("\"platform\":\"" + get_platform_name() + "\",");331json_description += String("\"size\":") + "\"" + info.unscaled_size + "\",";332if (String(info.scale) != "1x") {333json_description += String("\"scale\":") + "\"" + info.scale + "\",";334}335json_description += String("\"filename\":") + "\"" + exp_name + "\"";336json_description += String("}");337}338}339json_description += "],\"info\":{\"author\":\"xcode\",\"version\":1}}";340341Ref<FileAccess> json_file = FileAccess::open(p_iconset_dir + "Contents.json", FileAccess::WRITE);342if (json_file.is_null()) {343add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "Contents.json"));344return ERR_CANT_CREATE;345}346347CharString json_utf8 = json_description.utf8();348json_file->store_buffer((const uint8_t *)json_utf8.get_data(), json_utf8.length());349350Ref<FileAccess> sizes_file = FileAccess::open(p_iconset_dir + "sizes", FileAccess::WRITE);351if (sizes_file.is_null()) {352add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat(TTR("Could not write to a file at path \"%s\"."), p_iconset_dir + "sizes"));353return ERR_CANT_CREATE;354}355356CharString sizes_utf8 = sizes.utf8();357sizes_file->store_buffer((const uint8_t *)sizes_utf8.get_data(), sizes_utf8.length());358359return OK;360}361362String EditorExportPlatformIOS::_process_config_file_line(const Ref<EditorExportPreset> &p_preset, const String &p_line, const AppleEmbeddedConfigData &p_config, bool p_debug, const CodeSigningDetails &p_code_signing) {363// Do iOS specific processing first, and call super implementation if there are no matches364365String strnew;366367// Supported Destinations368if (p_line.contains("$targeted_device_family")) {369String xcode_value;370switch ((int)p_preset->get("application/targeted_device_family")) {371case 0: // iPhone372xcode_value = "1";373break;374case 1: // iPad375xcode_value = "2";376break;377case 2: // iPhone & iPad378xcode_value = "1,2";379break;380}381strnew += p_line.replace("$targeted_device_family", xcode_value) + "\n";382383// MoltenVK Framework384} else if (p_line.contains("$moltenvk_buildfile")) {385String value = "9039D3BE24C093AC0020482C /* MoltenVK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9039D3BD24C093AC0020482C /* MoltenVK.xcframework */; };";386strnew += p_line.replace("$moltenvk_buildfile", value) + "\n";387} else if (p_line.contains("$moltenvk_fileref")) {388String value = "9039D3BD24C093AC0020482C /* MoltenVK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MoltenVK; path = MoltenVK.xcframework; sourceTree = \"<group>\"; };";389strnew += p_line.replace("$moltenvk_fileref", value) + "\n";390} else if (p_line.contains("$moltenvk_buildphase")) {391String value = "9039D3BE24C093AC0020482C /* MoltenVK.xcframework in Frameworks */,";392strnew += p_line.replace("$moltenvk_buildphase", value) + "\n";393} else if (p_line.contains("$moltenvk_buildgrp")) {394String value = "9039D3BD24C093AC0020482C /* MoltenVK.xcframework */,";395strnew += p_line.replace("$moltenvk_buildgrp", value) + "\n";396397// Launch Storyboard398} else if (p_line.contains("$plist_launch_screen_name")) {399String value = "<key>UILaunchStoryboardName</key>\n<string>Launch Screen</string>";400strnew += p_line.replace("$plist_launch_screen_name", value) + "\n";401} else if (p_line.contains("$pbx_launch_screen_file_reference")) {402String value = "90DD2D9D24B36E8000717FE1 = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = \"Launch Screen.storyboard\"; sourceTree = \"<group>\"; };";403strnew += p_line.replace("$pbx_launch_screen_file_reference", value) + "\n";404} else if (p_line.contains("$pbx_launch_screen_copy_files")) {405String value = "90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */,";406strnew += p_line.replace("$pbx_launch_screen_copy_files", value) + "\n";407} else if (p_line.contains("$pbx_launch_screen_build_phase")) {408String value = "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */,";409strnew += p_line.replace("$pbx_launch_screen_build_phase", value) + "\n";410} else if (p_line.contains("$pbx_launch_screen_build_reference")) {411String value = "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */; };";412strnew += p_line.replace("$pbx_launch_screen_build_reference", value) + "\n";413414// Launch Storyboard customization415} else if (p_line.contains("$launch_screen_image_mode")) {416int image_scale_mode = p_preset->get("storyboard/image_scale_mode");417String value;418419switch (image_scale_mode) {420case 0: {421String logo_path = get_project_setting(p_preset, "application/boot_splash/image");422bool is_on = get_project_setting(p_preset, "application/boot_splash/fullsize");423// If custom logo is not specified, Godot does not scale default one, so we should do the same.424value = (is_on && logo_path.length() > 0) ? "scaleAspectFit" : "center";425} break;426default: {427value = storyboard_image_scale_mode[image_scale_mode - 1];428}429}430431strnew += p_line.replace("$launch_screen_image_mode", value) + "\n";432} else if (p_line.contains("$launch_screen_background_color")) {433bool use_custom = p_preset->get("storyboard/use_custom_bg_color");434Color color = use_custom ? p_preset->get("storyboard/custom_bg_color") : get_project_setting(p_preset, "application/boot_splash/bg_color");435const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\"";436437Dictionary value_dictionary;438value_dictionary["red"] = color.r;439value_dictionary["green"] = color.g;440value_dictionary["blue"] = color.b;441value_dictionary["alpha"] = color.a;442String value = value_format.format(value_dictionary, "$_");443444strnew += p_line.replace("$launch_screen_background_color", value) + "\n";445446// OS Deployment Target447} else if (p_line.contains("$os_deployment_target")) {448String min_version = p_preset->get("application/min_" + get_platform_name() + "_version");449String value = "IPHONEOS_DEPLOYMENT_TARGET = " + min_version + ";";450strnew += p_line.replace("$os_deployment_target", value) + "\n";451452// Valid Archs453} else if (p_line.contains("$valid_archs")) {454strnew += p_line.replace("$valid_archs", "arm64 x86_64") + "\n";455456// Apple Embedded common457} else {458strnew += EditorExportPlatformAppleEmbedded::_process_config_file_line(p_preset, p_line, p_config, p_debug, p_code_signing);459}460461return strnew;462}463464465