Path: blob/master/platform/web/export/export_plugin.cpp
20898 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 "core/io/dir_access.h"37#include "editor/editor_string_names.h"38#include "editor/export/editor_export.h"39#include "editor/import/resource_importer_texture_settings.h"40#include "editor/settings/editor_settings.h"41#include "editor/themes/editor_scale.h"42#include "scene/resources/image_texture.h"4344#include "modules/modules_enabled.gen.h" // For mono.45#include "modules/svg/image_loader_svg.h"4647Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {48Ref<FileAccess> io_fa;49zlib_filefunc_def io = zipio_create_io(&io_fa);50unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io);5152if (!pkg) {53add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template));54return ERR_FILE_NOT_FOUND;55}5657if (unzGoToFirstFile(pkg) != UNZ_OK) {58add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template));59unzClose(pkg);60return ERR_FILE_CORRUPT;61}6263do {64//get filename65unz_file_info info;66char fname[16384];67unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);6869String file = String::utf8(fname);7071// Skip folders.72if (file.ends_with("/")) {73continue;74}7576// Skip service worker and offline page if not exporting pwa.77if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) {78continue;79}80Vector<uint8_t> data;81data.resize(info.uncompressed_size);8283//read84unzOpenCurrentFile(pkg);85unzReadCurrentFile(pkg, data.ptrw(), data.size());86unzCloseCurrentFile(pkg);8788//write89String dst = p_dir.path_join(file.replace("godot", p_name));90Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE);91if (f.is_null()) {92add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst));93unzClose(pkg);94return ERR_FILE_CANT_WRITE;95}96f->store_buffer(data.ptr(), data.size());9798} while (unzGoToNextFile(pkg) == UNZ_OK);99unzClose(pkg);100return OK;101}102103Error EditorExportPlatformWeb::_write_or_error(const uint8_t *p_content, int p_size, String p_path) {104Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);105if (f.is_null()) {106add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path));107return ERR_FILE_CANT_WRITE;108}109f->store_buffer(p_content, p_size);110return OK;111}112113void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template) {114String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());115String out;116Vector<String> lines = str_template.split("\n");117for (int i = 0; i < lines.size(); i++) {118String current_line = lines[i];119for (const KeyValue<String, String> &E : p_replaces) {120current_line = current_line.replace(E.key, E.value);121}122out += current_line + "\n";123}124CharString cs = out.utf8();125r_template.resize(cs.length());126for (int i = 0; i < cs.length(); i++) {127r_template.write[i] = cs[i];128}129}130131void 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) {132// Engine.js config133Dictionary config;134Array libs;135for (int i = 0; i < p_shared_objects.size(); i++) {136libs.push_back(p_shared_objects[i].path.get_file());137}138Vector<String> flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT));139Array args;140for (int i = 0; i < flags.size(); i++) {141args.push_back(flags[i]);142}143config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy");144config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard");145config["focusCanvas"] = p_preset->get("html/focus_canvas_on_start");146config["gdextensionLibs"] = libs;147config["executable"] = p_name;148config["args"] = args;149config["fileSizes"] = p_file_sizes;150config["ensureCrossOriginIsolationHeaders"] = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");151152config["godotPoolSize"] = p_preset->get("threads/godot_pool_size");153config["emscriptenPoolSize"] = p_preset->get("threads/emscripten_pool_size");154155String head_include;156if (p_preset->get("html/export_icon")) {157head_include += "<link id=\"-gd-engine-icon\" rel=\"icon\" type=\"image/png\" href=\"" + p_name + ".icon.png\" />\n";158head_include += "<link rel=\"apple-touch-icon\" href=\"" + p_name + ".apple-touch-icon.png\"/>\n";159}160if (p_preset->get("progressive_web_app/enabled")) {161head_include += "<link rel=\"manifest\" href=\"" + p_name + ".manifest.json\">\n";162config["serviceWorker"] = p_name + ".service.worker.js";163}164165// Replaces HTML string166const String str_config = Variant(config).to_json_string();167const String custom_head_include = p_preset->get("html/head_include");168HashMap<String, String> replaces;169replaces["$GODOT_URL"] = p_name + ".js";170replaces["$GODOT_PROJECT_NAME"] = get_project_setting(p_preset, "application/config/name");171replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;172replaces["$GODOT_CONFIG"] = str_config;173replaces["$GODOT_SPLASH_COLOR"] = "#" + Color(get_project_setting(p_preset, "application/boot_splash/bg_color")).to_html(false);174175Vector<String> godot_splash_classes;176godot_splash_classes.push_back("show-image--" + String(get_project_setting(p_preset, "application/boot_splash/show_image")));177RenderingServer::SplashStretchMode boot_splash_stretch_mode = get_project_setting(p_preset, "application/boot_splash/stretch_mode");178godot_splash_classes.push_back("fullsize--" + String(((boot_splash_stretch_mode != RenderingServer::SplashStretchMode::SPLASH_STRETCH_MODE_DISABLED) ? "true" : "false")));179godot_splash_classes.push_back("use-filter--" + String(get_project_setting(p_preset, "application/boot_splash/use_filter")));180replaces["$GODOT_SPLASH_CLASSES"] = String(" ").join(godot_splash_classes);181replaces["$GODOT_SPLASH"] = p_name + ".png";182183if (p_preset->get("variant/thread_support")) {184replaces["$GODOT_THREADS_ENABLED"] = "true";185} else {186replaces["$GODOT_THREADS_ENABLED"] = "false";187}188189_replace_strings(replaces, p_html);190}191192Error EditorExportPlatformWeb::_add_manifest_icon(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_icon, int p_size, Array &r_arr) {193const String name = p_path.get_file().get_basename();194const String icon_name = vformat("%s.%dx%d.png", name, p_size, p_size);195const String icon_dest = p_path.get_base_dir().path_join(icon_name);196197Ref<Image> icon;198if (!p_icon.is_empty()) {199Error err = OK;200icon = _load_icon_or_splash_image(p_icon, &err);201if (err != OK || icon.is_null() || icon->is_empty()) {202add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon));203return err;204}205if (icon->get_width() != p_size || icon->get_height() != p_size) {206icon->resize(p_size, p_size);207}208} else {209icon = _get_project_icon(p_preset);210icon->resize(p_size, p_size);211}212const Error err = icon->save_png(icon_dest);213if (err != OK) {214add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest));215return err;216}217Dictionary icon_dict;218icon_dict["sizes"] = vformat("%dx%d", p_size, p_size);219icon_dict["type"] = "image/png";220icon_dict["src"] = icon_name;221r_arr.push_back(icon_dict);222return err;223}224225Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {226String proj_name = get_project_setting(p_preset, "application/config/name");227if (proj_name.is_empty()) {228proj_name = "Godot Game";229}230231// Service worker232const String dir = p_path.get_base_dir();233const String name = p_path.get_file().get_basename();234bool extensions = (bool)p_preset->get("variant/extensions_support");235bool ensure_crossorigin_isolation_headers = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");236HashMap<String, String> replaces;237replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());238replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);239replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";240replaces["___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___"] = ensure_crossorigin_isolation_headers ? "true" : "false";241242// Files cached during worker install.243Array cache_files = {244name + ".html",245name + ".js",246name + ".offline.html"247};248if (p_preset->get("html/export_icon")) {249cache_files.push_back(name + ".icon.png");250cache_files.push_back(name + ".apple-touch-icon.png");251}252253cache_files.push_back(name + ".audio.worklet.js");254cache_files.push_back(name + ".audio.position.worklet.js");255replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();256257// Heavy files that are cached on demand.258Array opt_cache_files = {259name + ".wasm",260name + ".pck"261};262if (extensions) {263opt_cache_files.push_back(name + ".side.wasm");264for (int i = 0; i < p_shared_objects.size(); i++) {265opt_cache_files.push_back(p_shared_objects[i].path.get_file());266}267}268replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();269270const String sw_path = dir.path_join(name + ".service.worker.js");271Vector<uint8_t> sw;272{273Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ);274if (f.is_null()) {275add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path));276return ERR_FILE_CANT_READ;277}278sw.resize(f->get_length());279f->get_buffer(sw.ptrw(), sw.size());280}281_replace_strings(replaces, sw);282Error err = _write_or_error(sw.ptr(), sw.size(), dir.path_join(name + ".service.worker.js"));283if (err != OK) {284// Message is supplied by the subroutine method.285return err;286}287288// Custom offline page289const String offline_page = p_preset->get("progressive_web_app/offline_page");290if (!offline_page.is_empty()) {291Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);292const String offline_dest = dir.path_join(name + ".offline.html");293err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest);294if (err != OK) {295add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest));296return err;297}298}299300// Manifest301const char *modes[4] = { "fullscreen", "standalone", "minimal-ui", "browser" };302const char *orientations[3] = { "any", "landscape", "portrait" };303const int display = CLAMP(int(p_preset->get("progressive_web_app/display")), 0, 4);304const int orientation = CLAMP(int(p_preset->get("progressive_web_app/orientation")), 0, 3);305306Dictionary manifest;307manifest["name"] = proj_name;308manifest["start_url"] = "./" + name + ".html";309manifest["display"] = String::utf8(modes[display]);310manifest["orientation"] = String::utf8(orientations[orientation]);311manifest["background_color"] = "#" + p_preset->get("progressive_web_app/background_color").operator Color().to_html(false);312313Array icons_arr;314const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");315err = _add_manifest_icon(p_preset, p_path, icon144_path, 144, icons_arr);316if (err != OK) {317// Message is supplied by the subroutine method.318return err;319}320const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");321err = _add_manifest_icon(p_preset, p_path, icon180_path, 180, icons_arr);322if (err != OK) {323// Message is supplied by the subroutine method.324return err;325}326const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");327err = _add_manifest_icon(p_preset, p_path, icon512_path, 512, icons_arr);328if (err != OK) {329// Message is supplied by the subroutine method.330return err;331}332manifest["icons"] = icons_arr;333334CharString cs = Variant(manifest).to_json_string().utf8();335err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.path_join(name + ".manifest.json"));336if (err != OK) {337// Message is supplied by the subroutine method.338return err;339}340341return OK;342}343344void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {345if (p_preset->get("vram_texture_compression/for_desktop")) {346r_features->push_back("s3tc");347r_features->push_back("bptc");348}349if (p_preset->get("vram_texture_compression/for_mobile")) {350r_features->push_back("etc2");351r_features->push_back("astc");352}353if (p_preset->get("variant/thread_support").operator bool()) {354r_features->push_back("threads");355} else {356r_features->push_back("nothreads");357}358if (p_preset->get("variant/extensions_support").operator bool()) {359r_features->push_back("web_extensions");360} else {361r_features->push_back("web_noextensions");362}363r_features->push_back("wasm32");364}365366void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options) const {367r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));368r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));369370r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // GDExtension support.371r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), false, true)); // Thread support (i.e. run with or without COEP/COOP headers).372r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC373r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer374375r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/export_icon"), true));376r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), ""));377r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT, "monospace,no_wrap"), ""));378r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2));379r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/focus_canvas_on_start"), true));380r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));381r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));382r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/ensure_cross_origin_isolation_headers"), true));383r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));384r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));385r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));386r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_144x144", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));387r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_180x180", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));388r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_512x512", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));389r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));390391r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/emscripten_pool_size"), 8));392r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/godot_pool_size"), 4));393}394395bool EditorExportPlatformWeb::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {396bool advanced_options_enabled = p_preset->are_advanced_options_enabled();397if (p_option == "custom_template/debug" || p_option == "custom_template/release") {398return advanced_options_enabled;399}400401if (p_option == "threads/godot_pool_size" || p_option == "threads/emscripten_pool_size") {402return p_preset->get("variant/thread_support").operator bool();403}404405return true;406}407408String EditorExportPlatformWeb::get_name() const {409return "Web";410}411412String EditorExportPlatformWeb::get_os_name() const {413return "Web";414}415416Ref<Texture2D> EditorExportPlatformWeb::get_logo() const {417return logo;418}419420bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {421#ifdef MODULE_MONO_ENABLED422// Don't check for additional errors, as this particular error cannot be resolved.423r_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";424r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";425return false;426#else427428String err;429bool valid = false;430bool extensions = (bool)p_preset->get("variant/extensions_support");431bool thread_support = (bool)p_preset->get("variant/thread_support");432433// Look for export templates (first official, and if defined custom templates).434bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);435bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);436437if (p_preset->get("custom_template/debug") != "") {438dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));439if (!dvalid) {440err += TTR("Custom debug template not found.") + "\n";441}442}443if (p_preset->get("custom_template/release") != "") {444rvalid = FileAccess::exists(p_preset->get("custom_template/release"));445if (!rvalid) {446err += TTR("Custom release template not found.") + "\n";447}448}449450valid = dvalid || rvalid;451r_missing_templates = !valid;452453if (!err.is_empty()) {454r_error = err;455}456457return valid;458#endif // !MODULE_MONO_ENABLED459}460461bool EditorExportPlatformWeb::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {462String err;463bool valid = true;464465// Validate the project configuration.466467if (p_preset->get("vram_texture_compression/for_mobile")) {468if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {469valid = false;470}471}472473if (!err.is_empty()) {474r_error = err;475}476477return valid;478}479480List<String> EditorExportPlatformWeb::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {481List<String> list;482list.push_back("html");483return list;484}485486Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {487ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);488489const String custom_debug = p_preset->get("custom_template/debug");490const String custom_release = p_preset->get("custom_template/release");491const String custom_html = p_preset->get("html/custom_html_shell");492const bool export_icon = p_preset->get("html/export_icon");493const bool pwa = p_preset->get("progressive_web_app/enabled");494495const String base_dir = p_path.get_base_dir();496const String base_path = p_path.get_basename();497const String base_name = p_path.get_file().get_basename();498499if (!DirAccess::exists(base_dir)) {500add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));501return ERR_FILE_BAD_PATH;502}503504// Find the correct template505String template_path = p_debug ? custom_debug : custom_release;506template_path = template_path.strip_edges();507if (template_path.is_empty()) {508bool extensions = (bool)p_preset->get("variant/extensions_support");509bool thread_support = (bool)p_preset->get("variant/thread_support");510template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));511}512513if (!template_path.is_empty() && !FileAccess::exists(template_path)) {514add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path));515return ERR_FILE_NOT_FOUND;516}517518// Export pck and shared objects519Vector<SharedObject> shared_objects;520String pck_path = base_path + ".pck";521Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects);522if (error != OK) {523add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path));524return error;525}526527{528Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);529for (int i = 0; i < shared_objects.size(); i++) {530String dst = base_dir.path_join(shared_objects[i].path.get_file());531error = da->copy(shared_objects[i].path, dst);532if (error != OK) {533add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file()));534return error;535}536}537}538539// Extract templates.540error = _extract_template(template_path, base_dir, base_name, pwa);541if (error) {542// Message is supplied by the subroutine method.543return error;544}545546// Parse generated file sizes (pck and wasm, to help show a meaningful loading bar).547Dictionary file_sizes;548Ref<FileAccess> f = FileAccess::open(pck_path, FileAccess::READ);549if (f.is_valid()) {550file_sizes[pck_path.get_file()] = (uint64_t)f->get_length();551}552f = FileAccess::open(base_path + ".wasm", FileAccess::READ);553if (f.is_valid()) {554file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length();555}556557// Read the HTML shell file (custom or from template).558const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html;559Vector<uint8_t> html;560f = FileAccess::open(html_path, FileAccess::READ);561if (f.is_null()) {562add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path));563return ERR_FILE_CANT_READ;564}565html.resize(f->get_length());566f->get_buffer(html.ptrw(), html.size());567f.unref(); // close file.568569// Generate HTML file with replaced strings.570_fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);571Error err = _write_or_error(html.ptr(), html.size(), p_path);572if (err != OK) {573// Message is supplied by the subroutine method.574return err;575}576html.resize(0);577578// Export splash (why?)579Ref<Image> splash = _get_project_splash(p_preset);580const String splash_png_path = base_path + ".png";581if (splash->save_png(splash_png_path) != OK) {582add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path));583return ERR_FILE_CANT_WRITE;584}585586// Save a favicon that can be accessed without waiting for the project to finish loading.587// This way, the favicon can be displayed immediately when loading the page.588if (export_icon) {589Ref<Image> favicon = _get_project_icon(p_preset);590const String favicon_png_path = base_path + ".icon.png";591if (favicon->save_png(favicon_png_path) != OK) {592add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path));593return ERR_FILE_CANT_WRITE;594}595favicon->resize(180, 180);596const String apple_icon_png_path = base_path + ".apple-touch-icon.png";597if (favicon->save_png(apple_icon_png_path) != OK) {598add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path));599return ERR_FILE_CANT_WRITE;600}601}602603// Generate the PWA worker and manifest604if (pwa) {605err = _build_pwa(p_preset, p_path, shared_objects);606if (err != OK) {607// Message is supplied by the subroutine method.608return err;609}610}611612return OK;613}614615bool EditorExportPlatformWeb::poll_export() {616Ref<EditorExportPreset> preset = EditorExport::get_singleton()->get_runnable_preset_for_platform(this);617618RemoteDebugState prev_remote_debug_state = remote_debug_state;619remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE;620621if (preset.is_valid()) {622const bool debug = true;623// Throwaway variables to pass to `can_export`.624String err;625bool missing_templates;626627if (can_export(preset, err, missing_templates, debug)) {628if (server->is_listening()) {629remote_debug_state = REMOTE_DEBUG_STATE_SERVING;630} else {631remote_debug_state = REMOTE_DEBUG_STATE_AVAILABLE;632}633}634}635636if (remote_debug_state != REMOTE_DEBUG_STATE_SERVING && server->is_listening()) {637server->stop();638}639640return remote_debug_state != prev_remote_debug_state;641}642643Ref<Texture2D> EditorExportPlatformWeb::get_option_icon(int p_index) const {644Ref<Texture2D> play_icon = EditorExportPlatform::get_option_icon(p_index);645646switch (remote_debug_state) {647case REMOTE_DEBUG_STATE_UNAVAILABLE: {648return nullptr;649} break;650651case REMOTE_DEBUG_STATE_AVAILABLE: {652switch (p_index) {653case 0:654case 1:655return play_icon;656default:657ERR_FAIL_V(nullptr);658}659} break;660661case REMOTE_DEBUG_STATE_SERVING: {662switch (p_index) {663case 0:664return play_icon;665case 1:666return restart_icon;667case 2:668return stop_icon;669default:670ERR_FAIL_V(nullptr);671}672} break;673}674675return nullptr;676}677678int EditorExportPlatformWeb::get_options_count() const {679switch (remote_debug_state) {680case REMOTE_DEBUG_STATE_UNAVAILABLE: {681return 0;682} break;683684case REMOTE_DEBUG_STATE_AVAILABLE: {685return 2;686} break;687688case REMOTE_DEBUG_STATE_SERVING: {689return 3;690} break;691}692693return 0;694}695696String EditorExportPlatformWeb::get_option_label(int p_index) const {697String run_in_browser = TTR("Run in Browser");698String start_http_server = TTR("Start HTTP Server");699String reexport_project = TTR("Re-export Project");700String stop_http_server = TTR("Stop HTTP Server");701702switch (remote_debug_state) {703case REMOTE_DEBUG_STATE_UNAVAILABLE:704return "";705706case REMOTE_DEBUG_STATE_AVAILABLE: {707switch (p_index) {708case 0:709return run_in_browser;710case 1:711return start_http_server;712default:713ERR_FAIL_V("");714}715} break;716717case REMOTE_DEBUG_STATE_SERVING: {718switch (p_index) {719case 0:720return run_in_browser;721case 1:722return reexport_project;723case 2:724return stop_http_server;725default:726ERR_FAIL_V("");727}728} break;729}730731return "";732}733734String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {735String run_in_browser = TTR("Run exported HTML in the system's default browser.");736String start_http_server = TTR("Start the HTTP server.");737String reexport_project = TTR("Export project again to account for updates.");738String stop_http_server = TTR("Stop the HTTP server.");739740switch (remote_debug_state) {741case REMOTE_DEBUG_STATE_UNAVAILABLE:742return "";743744case REMOTE_DEBUG_STATE_AVAILABLE: {745switch (p_index) {746case 0:747return run_in_browser;748case 1:749return start_http_server;750default:751ERR_FAIL_V("");752}753} break;754755case REMOTE_DEBUG_STATE_SERVING: {756switch (p_index) {757case 0:758return run_in_browser;759case 1:760return reexport_project;761case 2:762return stop_http_server;763default:764ERR_FAIL_V("");765}766} break;767}768769return "";770}771772Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {773const uint16_t bind_port = EDITOR_GET("export/web/http_port");774// Resolve host if needed.775const String bind_host = EDITOR_GET("export/web/http_host");776const bool use_tls = EDITOR_GET("export/web/use_tls");777778switch (remote_debug_state) {779case REMOTE_DEBUG_STATE_UNAVAILABLE: {780return FAILED;781} break;782783case REMOTE_DEBUG_STATE_AVAILABLE: {784switch (p_option) {785// Run in Browser.786case 0: {787Error err = _export_project(p_preset, p_debug_flags);788if (err != OK) {789return err;790}791err = _start_server(bind_host, bind_port, use_tls);792if (err != OK) {793return err;794}795return _launch_browser(bind_host, bind_port, use_tls);796} break;797798// Start HTTP Server.799case 1: {800Error err = _export_project(p_preset, p_debug_flags);801if (err != OK) {802return err;803}804return _start_server(bind_host, bind_port, use_tls);805} break;806807default: {808ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));809}810}811} break;812813case REMOTE_DEBUG_STATE_SERVING: {814switch (p_option) {815// Run in Browser.816case 0: {817Error err = _export_project(p_preset, p_debug_flags);818if (err != OK) {819return err;820}821return _launch_browser(bind_host, bind_port, use_tls);822} break;823824// Re-export Project.825case 1: {826return _export_project(p_preset, p_debug_flags);827} break;828829// Stop HTTP Server.830case 2: {831return _stop_server();832} break;833834default: {835ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));836}837}838} break;839}840841return FAILED;842}843844Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {845const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("web");846Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);847if (!da->dir_exists(dest)) {848Error err = da->make_dir_recursive(dest);849if (err != OK) {850add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest));851return err;852}853}854855const String basepath = dest.path_join("tmp_js_export");856Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);857if (err != OK) {858// Export generates several files, clean them up on failure.859DirAccess::remove_file_or_error(basepath + ".html");860DirAccess::remove_file_or_error(basepath + ".offline.html");861DirAccess::remove_file_or_error(basepath + ".js");862DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");863DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js");864DirAccess::remove_file_or_error(basepath + ".service.worker.js");865DirAccess::remove_file_or_error(basepath + ".pck");866DirAccess::remove_file_or_error(basepath + ".png");867DirAccess::remove_file_or_error(basepath + ".side.wasm");868DirAccess::remove_file_or_error(basepath + ".wasm");869DirAccess::remove_file_or_error(basepath + ".icon.png");870DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");871}872return err;873}874875Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {876OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html"));877// FIXME: Find out how to clean up export files after running the successfully878// exported game. Might not be trivial.879return OK;880}881882Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {883IPAddress bind_ip;884if (p_bind_host.is_valid_ip_address()) {885bind_ip = p_bind_host;886} else {887bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host);888}889ERR_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'.");890891const String tls_key = EDITOR_GET("export/web/tls_key");892const String tls_cert = EDITOR_GET("export/web/tls_certificate");893894// Restart server.895server->stop();896Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert);897if (err != OK) {898add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err));899}900return err;901}902903Error EditorExportPlatformWeb::_stop_server() {904server->stop();905return OK;906}907908Ref<Texture2D> EditorExportPlatformWeb::get_run_icon() const {909return run_icon;910}911912void EditorExportPlatformWeb::initialize() {913if (EditorNode::get_singleton()) {914server.instantiate();915916Ref<Image> img = memnew(Image);917const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);918919ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false);920logo = ImageTexture::create_from_image(img);921922ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);923run_icon = ImageTexture::create_from_image(img);924925Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();926if (theme.is_valid()) {927stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));928restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons));929} else {930stop_icon.instantiate();931restart_icon.instantiate();932}933}934}935936EditorExportPlatformWeb::~EditorExportPlatformWeb() {937}938939940