Path: blob/master/platform/web/export/export_plugin.cpp
10278 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 "core/config/project_settings.h"36#include "editor/editor_string_names.h"37#include "editor/export/editor_export.h"38#include "editor/import/resource_importer_texture_settings.h"39#include "editor/settings/editor_settings.h"40#include "editor/themes/editor_scale.h"41#include "scene/resources/image_texture.h"4243#include "modules/modules_enabled.gen.h" // For mono.44#include "modules/svg/image_loader_svg.h"4546Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {47Ref<FileAccess> io_fa;48zlib_filefunc_def io = zipio_create_io(&io_fa);49unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io);5051if (!pkg) {52add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template));53return ERR_FILE_NOT_FOUND;54}5556if (unzGoToFirstFile(pkg) != UNZ_OK) {57add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template));58unzClose(pkg);59return ERR_FILE_CORRUPT;60}6162do {63//get filename64unz_file_info info;65char fname[16384];66unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);6768String file = String::utf8(fname);6970// Skip folders.71if (file.ends_with("/")) {72continue;73}7475// Skip service worker and offline page if not exporting pwa.76if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) {77continue;78}79Vector<uint8_t> data;80data.resize(info.uncompressed_size);8182//read83unzOpenCurrentFile(pkg);84unzReadCurrentFile(pkg, data.ptrw(), data.size());85unzCloseCurrentFile(pkg);8687//write88String dst = p_dir.path_join(file.replace("godot", p_name));89Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE);90if (f.is_null()) {91add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst));92unzClose(pkg);93return ERR_FILE_CANT_WRITE;94}95f->store_buffer(data.ptr(), data.size());9697} while (unzGoToNextFile(pkg) == UNZ_OK);98unzClose(pkg);99return OK;100}101102Error EditorExportPlatformWeb::_write_or_error(const uint8_t *p_content, int p_size, String p_path) {103Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);104if (f.is_null()) {105add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path));106return ERR_FILE_CANT_WRITE;107}108f->store_buffer(p_content, p_size);109return OK;110}111112void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template) {113String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());114String out;115Vector<String> lines = str_template.split("\n");116for (int i = 0; i < lines.size(); i++) {117String current_line = lines[i];118for (const KeyValue<String, String> &E : p_replaces) {119current_line = current_line.replace(E.key, E.value);120}121out += current_line + "\n";122}123CharString cs = out.utf8();124r_template.resize(cs.length());125for (int i = 0; i < cs.length(); i++) {126r_template.write[i] = cs[i];127}128}129130void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, BitField<EditorExportPlatform::DebugFlags> p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes) {131// Engine.js config132Dictionary config;133Array libs;134for (int i = 0; i < p_shared_objects.size(); i++) {135libs.push_back(p_shared_objects[i].path.get_file());136}137Vector<String> flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT));138Array args;139for (int i = 0; i < flags.size(); i++) {140args.push_back(flags[i]);141}142config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy");143config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard");144config["focusCanvas"] = p_preset->get("html/focus_canvas_on_start");145config["gdextensionLibs"] = libs;146config["executable"] = p_name;147config["args"] = args;148config["fileSizes"] = p_file_sizes;149config["ensureCrossOriginIsolationHeaders"] = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");150151config["godotPoolSize"] = p_preset->get("threads/godot_pool_size");152config["emscriptenPoolSize"] = p_preset->get("threads/emscripten_pool_size");153154String head_include;155if (p_preset->get("html/export_icon")) {156head_include += "<link id=\"-gd-engine-icon\" rel=\"icon\" type=\"image/png\" href=\"" + p_name + ".icon.png\" />\n";157head_include += "<link rel=\"apple-touch-icon\" href=\"" + p_name + ".apple-touch-icon.png\"/>\n";158}159if (p_preset->get("progressive_web_app/enabled")) {160head_include += "<link rel=\"manifest\" href=\"" + p_name + ".manifest.json\">\n";161config["serviceWorker"] = p_name + ".service.worker.js";162}163164// Replaces HTML string165const String str_config = Variant(config).to_json_string();166const String custom_head_include = p_preset->get("html/head_include");167HashMap<String, String> replaces;168replaces["$GODOT_URL"] = p_name + ".js";169replaces["$GODOT_PROJECT_NAME"] = get_project_setting(p_preset, "application/config/name");170replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;171replaces["$GODOT_CONFIG"] = str_config;172replaces["$GODOT_SPLASH_COLOR"] = "#" + Color(get_project_setting(p_preset, "application/boot_splash/bg_color")).to_html(false);173174LocalVector<String> godot_splash_classes;175godot_splash_classes.push_back("show-image--" + String(get_project_setting(p_preset, "application/boot_splash/show_image")));176godot_splash_classes.push_back("fullsize--" + String(get_project_setting(p_preset, "application/boot_splash/fullsize")));177godot_splash_classes.push_back("use-filter--" + String(get_project_setting(p_preset, "application/boot_splash/use_filter")));178replaces["$GODOT_SPLASH_CLASSES"] = String(" ").join(godot_splash_classes);179replaces["$GODOT_SPLASH"] = p_name + ".png";180181if (p_preset->get("variant/thread_support")) {182replaces["$GODOT_THREADS_ENABLED"] = "true";183} else {184replaces["$GODOT_THREADS_ENABLED"] = "false";185}186187_replace_strings(replaces, p_html);188}189190Error EditorExportPlatformWeb::_add_manifest_icon(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_icon, int p_size, Array &r_arr) {191const String name = p_path.get_file().get_basename();192const String icon_name = vformat("%s.%dx%d.png", name, p_size, p_size);193const String icon_dest = p_path.get_base_dir().path_join(icon_name);194195Ref<Image> icon;196if (!p_icon.is_empty()) {197Error err = OK;198icon = _load_icon_or_splash_image(p_icon, &err);199if (err != OK || icon.is_null() || icon->is_empty()) {200add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon));201return err;202}203if (icon->get_width() != p_size || icon->get_height() != p_size) {204icon->resize(p_size, p_size);205}206} else {207icon = _get_project_icon(p_preset);208icon->resize(p_size, p_size);209}210const Error err = icon->save_png(icon_dest);211if (err != OK) {212add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest));213return err;214}215Dictionary icon_dict;216icon_dict["sizes"] = vformat("%dx%d", p_size, p_size);217icon_dict["type"] = "image/png";218icon_dict["src"] = icon_name;219r_arr.push_back(icon_dict);220return err;221}222223Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {224String proj_name = get_project_setting(p_preset, "application/config/name");225if (proj_name.is_empty()) {226proj_name = "Godot Game";227}228229// Service worker230const String dir = p_path.get_base_dir();231const String name = p_path.get_file().get_basename();232bool extensions = (bool)p_preset->get("variant/extensions_support");233bool ensure_crossorigin_isolation_headers = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");234HashMap<String, String> replaces;235replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());236replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);237replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";238replaces["___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___"] = ensure_crossorigin_isolation_headers ? "true" : "false";239240// Files cached during worker install.241Array cache_files = {242name + ".html",243name + ".js",244name + ".offline.html"245};246if (p_preset->get("html/export_icon")) {247cache_files.push_back(name + ".icon.png");248cache_files.push_back(name + ".apple-touch-icon.png");249}250251cache_files.push_back(name + ".audio.worklet.js");252cache_files.push_back(name + ".audio.position.worklet.js");253replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();254255// Heavy files that are cached on demand.256Array opt_cache_files = {257name + ".wasm",258name + ".pck"259};260if (extensions) {261opt_cache_files.push_back(name + ".side.wasm");262for (int i = 0; i < p_shared_objects.size(); i++) {263opt_cache_files.push_back(p_shared_objects[i].path.get_file());264}265}266replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();267268const String sw_path = dir.path_join(name + ".service.worker.js");269Vector<uint8_t> sw;270{271Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ);272if (f.is_null()) {273add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path));274return ERR_FILE_CANT_READ;275}276sw.resize(f->get_length());277f->get_buffer(sw.ptrw(), sw.size());278}279_replace_strings(replaces, sw);280Error err = _write_or_error(sw.ptr(), sw.size(), dir.path_join(name + ".service.worker.js"));281if (err != OK) {282// Message is supplied by the subroutine method.283return err;284}285286// Custom offline page287const String offline_page = p_preset->get("progressive_web_app/offline_page");288if (!offline_page.is_empty()) {289Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);290const String offline_dest = dir.path_join(name + ".offline.html");291err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest);292if (err != OK) {293add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest));294return err;295}296}297298// Manifest299const char *modes[4] = { "fullscreen", "standalone", "minimal-ui", "browser" };300const char *orientations[3] = { "any", "landscape", "portrait" };301const int display = CLAMP(int(p_preset->get("progressive_web_app/display")), 0, 4);302const int orientation = CLAMP(int(p_preset->get("progressive_web_app/orientation")), 0, 3);303304Dictionary manifest;305manifest["name"] = proj_name;306manifest["start_url"] = "./" + name + ".html";307manifest["display"] = String::utf8(modes[display]);308manifest["orientation"] = String::utf8(orientations[orientation]);309manifest["background_color"] = "#" + p_preset->get("progressive_web_app/background_color").operator Color().to_html(false);310311Array icons_arr;312const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");313err = _add_manifest_icon(p_preset, p_path, icon144_path, 144, icons_arr);314if (err != OK) {315// Message is supplied by the subroutine method.316return err;317}318const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");319err = _add_manifest_icon(p_preset, p_path, icon180_path, 180, icons_arr);320if (err != OK) {321// Message is supplied by the subroutine method.322return err;323}324const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");325err = _add_manifest_icon(p_preset, p_path, icon512_path, 512, icons_arr);326if (err != OK) {327// Message is supplied by the subroutine method.328return err;329}330manifest["icons"] = icons_arr;331332CharString cs = Variant(manifest).to_json_string().utf8();333err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.path_join(name + ".manifest.json"));334if (err != OK) {335// Message is supplied by the subroutine method.336return err;337}338339return OK;340}341342void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {343if (p_preset->get("vram_texture_compression/for_desktop")) {344r_features->push_back("s3tc");345r_features->push_back("bptc");346}347if (p_preset->get("vram_texture_compression/for_mobile")) {348r_features->push_back("etc2");349r_features->push_back("astc");350}351if (p_preset->get("variant/thread_support").operator bool()) {352r_features->push_back("threads");353} else {354r_features->push_back("nothreads");355}356if (p_preset->get("variant/extensions_support").operator bool()) {357r_features->push_back("web_extensions");358} else {359r_features->push_back("web_noextensions");360}361r_features->push_back("wasm32");362}363364void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options) const {365r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));366r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));367368r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // GDExtension support.369r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), false, true)); // Thread support (i.e. run with or without COEP/COOP headers).370r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC371r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer372373r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/export_icon"), true));374r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), ""));375r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), ""));376r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2));377r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/focus_canvas_on_start"), true));378r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));379r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));380r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/ensure_cross_origin_isolation_headers"), true));381r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));382r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));383r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));384r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_144x144", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));385r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_180x180", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));386r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_512x512", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));387r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));388389r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/emscripten_pool_size"), 8));390r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/godot_pool_size"), 4));391}392393bool EditorExportPlatformWeb::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {394bool advanced_options_enabled = p_preset->are_advanced_options_enabled();395if (p_option == "custom_template/debug" || p_option == "custom_template/release") {396return advanced_options_enabled;397}398399if (p_option == "threads/godot_pool_size" || p_option == "threads/emscripten_pool_size") {400return p_preset->get("variant/thread_support").operator bool();401}402403return true;404}405406String EditorExportPlatformWeb::get_name() const {407return "Web";408}409410String EditorExportPlatformWeb::get_os_name() const {411return "Web";412}413414Ref<Texture2D> EditorExportPlatformWeb::get_logo() const {415return logo;416}417418bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {419#ifdef MODULE_MONO_ENABLED420// Don't check for additional errors, as this particular error cannot be resolved.421r_error += TTR("Exporting to Web is currently not supported in Godot 4 when using C#/.NET. Use Godot 3 to target Web with C#/Mono instead.") + "\n";422r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";423return false;424#else425426String err;427bool valid = false;428bool extensions = (bool)p_preset->get("variant/extensions_support");429bool thread_support = (bool)p_preset->get("variant/thread_support");430431// Look for export templates (first official, and if defined custom templates).432bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);433bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);434435if (p_preset->get("custom_template/debug") != "") {436dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));437if (!dvalid) {438err += TTR("Custom debug template not found.") + "\n";439}440}441if (p_preset->get("custom_template/release") != "") {442rvalid = FileAccess::exists(p_preset->get("custom_template/release"));443if (!rvalid) {444err += TTR("Custom release template not found.") + "\n";445}446}447448valid = dvalid || rvalid;449r_missing_templates = !valid;450451if (!err.is_empty()) {452r_error = err;453}454455return valid;456#endif // !MODULE_MONO_ENABLED457}458459bool EditorExportPlatformWeb::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {460String err;461bool valid = true;462463// Validate the project configuration.464465if (p_preset->get("vram_texture_compression/for_mobile")) {466if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {467valid = false;468}469}470471if (!err.is_empty()) {472r_error = err;473}474475return valid;476}477478List<String> EditorExportPlatformWeb::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {479List<String> list;480list.push_back("html");481return list;482}483484Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {485ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);486487const String custom_debug = p_preset->get("custom_template/debug");488const String custom_release = p_preset->get("custom_template/release");489const String custom_html = p_preset->get("html/custom_html_shell");490const bool export_icon = p_preset->get("html/export_icon");491const bool pwa = p_preset->get("progressive_web_app/enabled");492493const String base_dir = p_path.get_base_dir();494const String base_path = p_path.get_basename();495const String base_name = p_path.get_file().get_basename();496497if (!DirAccess::exists(base_dir)) {498add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));499return ERR_FILE_BAD_PATH;500}501502// Find the correct template503String template_path = p_debug ? custom_debug : custom_release;504template_path = template_path.strip_edges();505if (template_path.is_empty()) {506bool extensions = (bool)p_preset->get("variant/extensions_support");507bool thread_support = (bool)p_preset->get("variant/thread_support");508template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));509}510511if (!template_path.is_empty() && !FileAccess::exists(template_path)) {512add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path));513return ERR_FILE_NOT_FOUND;514}515516// Export pck and shared objects517Vector<SharedObject> shared_objects;518String pck_path = base_path + ".pck";519Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects);520if (error != OK) {521add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path));522return error;523}524525{526Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);527for (int i = 0; i < shared_objects.size(); i++) {528String dst = base_dir.path_join(shared_objects[i].path.get_file());529error = da->copy(shared_objects[i].path, dst);530if (error != OK) {531add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file()));532return error;533}534}535}536537// Extract templates.538error = _extract_template(template_path, base_dir, base_name, pwa);539if (error) {540// Message is supplied by the subroutine method.541return error;542}543544// Parse generated file sizes (pck and wasm, to help show a meaningful loading bar).545Dictionary file_sizes;546Ref<FileAccess> f = FileAccess::open(pck_path, FileAccess::READ);547if (f.is_valid()) {548file_sizes[pck_path.get_file()] = (uint64_t)f->get_length();549}550f = FileAccess::open(base_path + ".wasm", FileAccess::READ);551if (f.is_valid()) {552file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length();553}554555// Read the HTML shell file (custom or from template).556const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html;557Vector<uint8_t> html;558f = FileAccess::open(html_path, FileAccess::READ);559if (f.is_null()) {560add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path));561return ERR_FILE_CANT_READ;562}563html.resize(f->get_length());564f->get_buffer(html.ptrw(), html.size());565f.unref(); // close file.566567// Generate HTML file with replaced strings.568_fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);569Error err = _write_or_error(html.ptr(), html.size(), p_path);570if (err != OK) {571// Message is supplied by the subroutine method.572return err;573}574html.resize(0);575576// Export splash (why?)577Ref<Image> splash = _get_project_splash(p_preset);578const String splash_png_path = base_path + ".png";579if (splash->save_png(splash_png_path) != OK) {580add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path));581return ERR_FILE_CANT_WRITE;582}583584// Save a favicon that can be accessed without waiting for the project to finish loading.585// This way, the favicon can be displayed immediately when loading the page.586if (export_icon) {587Ref<Image> favicon = _get_project_icon(p_preset);588const String favicon_png_path = base_path + ".icon.png";589if (favicon->save_png(favicon_png_path) != OK) {590add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path));591return ERR_FILE_CANT_WRITE;592}593favicon->resize(180, 180);594const String apple_icon_png_path = base_path + ".apple-touch-icon.png";595if (favicon->save_png(apple_icon_png_path) != OK) {596add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path));597return ERR_FILE_CANT_WRITE;598}599}600601// Generate the PWA worker and manifest602if (pwa) {603err = _build_pwa(p_preset, p_path, shared_objects);604if (err != OK) {605// Message is supplied by the subroutine method.606return err;607}608}609610return OK;611}612613bool EditorExportPlatformWeb::poll_export() {614Ref<EditorExportPreset> preset;615616for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {617Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);618if (ep->is_runnable() && ep->get_platform() == this) {619preset = ep;620break;621}622}623624RemoteDebugState prev_remote_debug_state = remote_debug_state;625remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE;626627if (preset.is_valid()) {628const bool debug = true;629// Throwaway variables to pass to `can_export`.630String err;631bool missing_templates;632633if (can_export(preset, err, missing_templates, debug)) {634if (server->is_listening()) {635remote_debug_state = REMOTE_DEBUG_STATE_SERVING;636} else {637remote_debug_state = REMOTE_DEBUG_STATE_AVAILABLE;638}639}640}641642if (remote_debug_state != REMOTE_DEBUG_STATE_SERVING && server->is_listening()) {643server->stop();644}645646return remote_debug_state != prev_remote_debug_state;647}648649Ref<Texture2D> EditorExportPlatformWeb::get_option_icon(int p_index) const {650Ref<Texture2D> play_icon = EditorExportPlatform::get_option_icon(p_index);651652switch (remote_debug_state) {653case REMOTE_DEBUG_STATE_UNAVAILABLE: {654return nullptr;655} break;656657case REMOTE_DEBUG_STATE_AVAILABLE: {658switch (p_index) {659case 0:660case 1:661return play_icon;662default:663ERR_FAIL_V(nullptr);664}665} break;666667case REMOTE_DEBUG_STATE_SERVING: {668switch (p_index) {669case 0:670return play_icon;671case 1:672return restart_icon;673case 2:674return stop_icon;675default:676ERR_FAIL_V(nullptr);677}678} break;679}680681return nullptr;682}683684int EditorExportPlatformWeb::get_options_count() const {685switch (remote_debug_state) {686case REMOTE_DEBUG_STATE_UNAVAILABLE: {687return 0;688} break;689690case REMOTE_DEBUG_STATE_AVAILABLE: {691return 2;692} break;693694case REMOTE_DEBUG_STATE_SERVING: {695return 3;696} break;697}698699return 0;700}701702String EditorExportPlatformWeb::get_option_label(int p_index) const {703String run_in_browser = TTR("Run in Browser");704String start_http_server = TTR("Start HTTP Server");705String reexport_project = TTR("Re-export Project");706String stop_http_server = TTR("Stop HTTP Server");707708switch (remote_debug_state) {709case REMOTE_DEBUG_STATE_UNAVAILABLE:710return "";711712case REMOTE_DEBUG_STATE_AVAILABLE: {713switch (p_index) {714case 0:715return run_in_browser;716case 1:717return start_http_server;718default:719ERR_FAIL_V("");720}721} break;722723case REMOTE_DEBUG_STATE_SERVING: {724switch (p_index) {725case 0:726return run_in_browser;727case 1:728return reexport_project;729case 2:730return stop_http_server;731default:732ERR_FAIL_V("");733}734} break;735}736737return "";738}739740String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {741String run_in_browser = TTR("Run exported HTML in the system's default browser.");742String start_http_server = TTR("Start the HTTP server.");743String reexport_project = TTR("Export project again to account for updates.");744String stop_http_server = TTR("Stop the HTTP server.");745746switch (remote_debug_state) {747case REMOTE_DEBUG_STATE_UNAVAILABLE:748return "";749750case REMOTE_DEBUG_STATE_AVAILABLE: {751switch (p_index) {752case 0:753return run_in_browser;754case 1:755return start_http_server;756default:757ERR_FAIL_V("");758}759} break;760761case REMOTE_DEBUG_STATE_SERVING: {762switch (p_index) {763case 0:764return run_in_browser;765case 1:766return reexport_project;767case 2:768return stop_http_server;769default:770ERR_FAIL_V("");771}772} break;773}774775return "";776}777778Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {779const uint16_t bind_port = EDITOR_GET("export/web/http_port");780// Resolve host if needed.781const String bind_host = EDITOR_GET("export/web/http_host");782const bool use_tls = EDITOR_GET("export/web/use_tls");783784switch (remote_debug_state) {785case REMOTE_DEBUG_STATE_UNAVAILABLE: {786return FAILED;787} break;788789case REMOTE_DEBUG_STATE_AVAILABLE: {790switch (p_option) {791// Run in Browser.792case 0: {793Error err = _export_project(p_preset, p_debug_flags);794if (err != OK) {795return err;796}797err = _start_server(bind_host, bind_port, use_tls);798if (err != OK) {799return err;800}801return _launch_browser(bind_host, bind_port, use_tls);802} break;803804// Start HTTP Server.805case 1: {806Error err = _export_project(p_preset, p_debug_flags);807if (err != OK) {808return err;809}810return _start_server(bind_host, bind_port, use_tls);811} break;812813default: {814ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));815}816}817} break;818819case REMOTE_DEBUG_STATE_SERVING: {820switch (p_option) {821// Run in Browser.822case 0: {823Error err = _export_project(p_preset, p_debug_flags);824if (err != OK) {825return err;826}827return _launch_browser(bind_host, bind_port, use_tls);828} break;829830// Re-export Project.831case 1: {832return _export_project(p_preset, p_debug_flags);833} break;834835// Stop HTTP Server.836case 2: {837return _stop_server();838} break;839840default: {841ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));842}843}844} break;845}846847return FAILED;848}849850Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {851const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("web");852Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);853if (!da->dir_exists(dest)) {854Error err = da->make_dir_recursive(dest);855if (err != OK) {856add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest));857return err;858}859}860861const String basepath = dest.path_join("tmp_js_export");862Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);863if (err != OK) {864// Export generates several files, clean them up on failure.865DirAccess::remove_file_or_error(basepath + ".html");866DirAccess::remove_file_or_error(basepath + ".offline.html");867DirAccess::remove_file_or_error(basepath + ".js");868DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");869DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js");870DirAccess::remove_file_or_error(basepath + ".service.worker.js");871DirAccess::remove_file_or_error(basepath + ".pck");872DirAccess::remove_file_or_error(basepath + ".png");873DirAccess::remove_file_or_error(basepath + ".side.wasm");874DirAccess::remove_file_or_error(basepath + ".wasm");875DirAccess::remove_file_or_error(basepath + ".icon.png");876DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");877}878return err;879}880881Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {882OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html"));883// FIXME: Find out how to clean up export files after running the successfully884// exported game. Might not be trivial.885return OK;886}887888Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {889IPAddress bind_ip;890if (p_bind_host.is_valid_ip_address()) {891bind_ip = p_bind_host;892} else {893bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host);894}895ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + p_bind_host + "'. Try using '127.0.0.1'.");896897const String tls_key = EDITOR_GET("export/web/tls_key");898const String tls_cert = EDITOR_GET("export/web/tls_certificate");899900// Restart server.901server->stop();902Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert);903if (err != OK) {904add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err));905}906return err;907}908909Error EditorExportPlatformWeb::_stop_server() {910server->stop();911return OK;912}913914Ref<Texture2D> EditorExportPlatformWeb::get_run_icon() const {915return run_icon;916}917918EditorExportPlatformWeb::EditorExportPlatformWeb() {919if (EditorNode::get_singleton()) {920server.instantiate();921922Ref<Image> img = memnew(Image);923const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);924925ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false);926logo = ImageTexture::create_from_image(img);927928ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);929run_icon = ImageTexture::create_from_image(img);930931Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();932if (theme.is_valid()) {933stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));934restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons));935} else {936stop_icon.instantiate();937restart_icon.instantiate();938}939}940}941942EditorExportPlatformWeb::~EditorExportPlatformWeb() {943}944945946