Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/platform/web/export/export_plugin.cpp
20898 views
1
/**************************************************************************/
2
/* export_plugin.cpp */
3
/**************************************************************************/
4
/* This file is part of: */
5
/* GODOT ENGINE */
6
/* https://godotengine.org */
7
/**************************************************************************/
8
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10
/* */
11
/* Permission is hereby granted, free of charge, to any person obtaining */
12
/* a copy of this software and associated documentation files (the */
13
/* "Software"), to deal in the Software without restriction, including */
14
/* without limitation the rights to use, copy, modify, merge, publish, */
15
/* distribute, sublicense, and/or sell copies of the Software, and to */
16
/* permit persons to whom the Software is furnished to do so, subject to */
17
/* the following conditions: */
18
/* */
19
/* The above copyright notice and this permission notice shall be */
20
/* included in all copies or substantial portions of the Software. */
21
/* */
22
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29
/**************************************************************************/
30
31
#include "export_plugin.h"
32
33
#include "logo_svg.gen.h"
34
#include "run_icon_svg.gen.h"
35
36
#include "core/config/project_settings.h"
37
#include "core/io/dir_access.h"
38
#include "editor/editor_string_names.h"
39
#include "editor/export/editor_export.h"
40
#include "editor/import/resource_importer_texture_settings.h"
41
#include "editor/settings/editor_settings.h"
42
#include "editor/themes/editor_scale.h"
43
#include "scene/resources/image_texture.h"
44
45
#include "modules/modules_enabled.gen.h" // For mono.
46
#include "modules/svg/image_loader_svg.h"
47
48
Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {
49
Ref<FileAccess> io_fa;
50
zlib_filefunc_def io = zipio_create_io(&io_fa);
51
unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io);
52
53
if (!pkg) {
54
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not open template for export: \"%s\"."), p_template));
55
return ERR_FILE_NOT_FOUND;
56
}
57
58
if (unzGoToFirstFile(pkg) != UNZ_OK) {
59
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Invalid export template: \"%s\"."), p_template));
60
unzClose(pkg);
61
return ERR_FILE_CORRUPT;
62
}
63
64
do {
65
//get filename
66
unz_file_info info;
67
char fname[16384];
68
unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
69
70
String file = String::utf8(fname);
71
72
// Skip folders.
73
if (file.ends_with("/")) {
74
continue;
75
}
76
77
// Skip service worker and offline page if not exporting pwa.
78
if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) {
79
continue;
80
}
81
Vector<uint8_t> data;
82
data.resize(info.uncompressed_size);
83
84
//read
85
unzOpenCurrentFile(pkg);
86
unzReadCurrentFile(pkg, data.ptrw(), data.size());
87
unzCloseCurrentFile(pkg);
88
89
//write
90
String dst = p_dir.path_join(file.replace("godot", p_name));
91
Ref<FileAccess> f = FileAccess::open(dst, FileAccess::WRITE);
92
if (f.is_null()) {
93
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst));
94
unzClose(pkg);
95
return ERR_FILE_CANT_WRITE;
96
}
97
f->store_buffer(data.ptr(), data.size());
98
99
} while (unzGoToNextFile(pkg) == UNZ_OK);
100
unzClose(pkg);
101
return OK;
102
}
103
104
Error EditorExportPlatformWeb::_write_or_error(const uint8_t *p_content, int p_size, String p_path) {
105
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
106
if (f.is_null()) {
107
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), p_path));
108
return ERR_FILE_CANT_WRITE;
109
}
110
f->store_buffer(p_content, p_size);
111
return OK;
112
}
113
114
void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template) {
115
String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());
116
String out;
117
Vector<String> lines = str_template.split("\n");
118
for (int i = 0; i < lines.size(); i++) {
119
String current_line = lines[i];
120
for (const KeyValue<String, String> &E : p_replaces) {
121
current_line = current_line.replace(E.key, E.value);
122
}
123
out += current_line + "\n";
124
}
125
CharString cs = out.utf8();
126
r_template.resize(cs.length());
127
for (int i = 0; i < cs.length(); i++) {
128
r_template.write[i] = cs[i];
129
}
130
}
131
132
void 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) {
133
// Engine.js config
134
Dictionary config;
135
Array libs;
136
for (int i = 0; i < p_shared_objects.size(); i++) {
137
libs.push_back(p_shared_objects[i].path.get_file());
138
}
139
Vector<String> flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT));
140
Array args;
141
for (int i = 0; i < flags.size(); i++) {
142
args.push_back(flags[i]);
143
}
144
config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy");
145
config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard");
146
config["focusCanvas"] = p_preset->get("html/focus_canvas_on_start");
147
config["gdextensionLibs"] = libs;
148
config["executable"] = p_name;
149
config["args"] = args;
150
config["fileSizes"] = p_file_sizes;
151
config["ensureCrossOriginIsolationHeaders"] = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");
152
153
config["godotPoolSize"] = p_preset->get("threads/godot_pool_size");
154
config["emscriptenPoolSize"] = p_preset->get("threads/emscripten_pool_size");
155
156
String head_include;
157
if (p_preset->get("html/export_icon")) {
158
head_include += "<link id=\"-gd-engine-icon\" rel=\"icon\" type=\"image/png\" href=\"" + p_name + ".icon.png\" />\n";
159
head_include += "<link rel=\"apple-touch-icon\" href=\"" + p_name + ".apple-touch-icon.png\"/>\n";
160
}
161
if (p_preset->get("progressive_web_app/enabled")) {
162
head_include += "<link rel=\"manifest\" href=\"" + p_name + ".manifest.json\">\n";
163
config["serviceWorker"] = p_name + ".service.worker.js";
164
}
165
166
// Replaces HTML string
167
const String str_config = Variant(config).to_json_string();
168
const String custom_head_include = p_preset->get("html/head_include");
169
HashMap<String, String> replaces;
170
replaces["$GODOT_URL"] = p_name + ".js";
171
replaces["$GODOT_PROJECT_NAME"] = get_project_setting(p_preset, "application/config/name");
172
replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
173
replaces["$GODOT_CONFIG"] = str_config;
174
replaces["$GODOT_SPLASH_COLOR"] = "#" + Color(get_project_setting(p_preset, "application/boot_splash/bg_color")).to_html(false);
175
176
Vector<String> godot_splash_classes;
177
godot_splash_classes.push_back("show-image--" + String(get_project_setting(p_preset, "application/boot_splash/show_image")));
178
RenderingServer::SplashStretchMode boot_splash_stretch_mode = get_project_setting(p_preset, "application/boot_splash/stretch_mode");
179
godot_splash_classes.push_back("fullsize--" + String(((boot_splash_stretch_mode != RenderingServer::SplashStretchMode::SPLASH_STRETCH_MODE_DISABLED) ? "true" : "false")));
180
godot_splash_classes.push_back("use-filter--" + String(get_project_setting(p_preset, "application/boot_splash/use_filter")));
181
replaces["$GODOT_SPLASH_CLASSES"] = String(" ").join(godot_splash_classes);
182
replaces["$GODOT_SPLASH"] = p_name + ".png";
183
184
if (p_preset->get("variant/thread_support")) {
185
replaces["$GODOT_THREADS_ENABLED"] = "true";
186
} else {
187
replaces["$GODOT_THREADS_ENABLED"] = "false";
188
}
189
190
_replace_strings(replaces, p_html);
191
}
192
193
Error EditorExportPlatformWeb::_add_manifest_icon(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_icon, int p_size, Array &r_arr) {
194
const String name = p_path.get_file().get_basename();
195
const String icon_name = vformat("%s.%dx%d.png", name, p_size, p_size);
196
const String icon_dest = p_path.get_base_dir().path_join(icon_name);
197
198
Ref<Image> icon;
199
if (!p_icon.is_empty()) {
200
Error err = OK;
201
icon = _load_icon_or_splash_image(p_icon, &err);
202
if (err != OK || icon.is_null() || icon->is_empty()) {
203
add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon));
204
return err;
205
}
206
if (icon->get_width() != p_size || icon->get_height() != p_size) {
207
icon->resize(p_size, p_size);
208
}
209
} else {
210
icon = _get_project_icon(p_preset);
211
icon->resize(p_size, p_size);
212
}
213
const Error err = icon->save_png(icon_dest);
214
if (err != OK) {
215
add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not write file: \"%s\"."), icon_dest));
216
return err;
217
}
218
Dictionary icon_dict;
219
icon_dict["sizes"] = vformat("%dx%d", p_size, p_size);
220
icon_dict["type"] = "image/png";
221
icon_dict["src"] = icon_name;
222
r_arr.push_back(icon_dict);
223
return err;
224
}
225
226
Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {
227
String proj_name = get_project_setting(p_preset, "application/config/name");
228
if (proj_name.is_empty()) {
229
proj_name = "Godot Game";
230
}
231
232
// Service worker
233
const String dir = p_path.get_base_dir();
234
const String name = p_path.get_file().get_basename();
235
bool extensions = (bool)p_preset->get("variant/extensions_support");
236
bool ensure_crossorigin_isolation_headers = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");
237
HashMap<String, String> replaces;
238
replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
239
replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);
240
replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";
241
replaces["___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___"] = ensure_crossorigin_isolation_headers ? "true" : "false";
242
243
// Files cached during worker install.
244
Array cache_files = {
245
name + ".html",
246
name + ".js",
247
name + ".offline.html"
248
};
249
if (p_preset->get("html/export_icon")) {
250
cache_files.push_back(name + ".icon.png");
251
cache_files.push_back(name + ".apple-touch-icon.png");
252
}
253
254
cache_files.push_back(name + ".audio.worklet.js");
255
cache_files.push_back(name + ".audio.position.worklet.js");
256
replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
257
258
// Heavy files that are cached on demand.
259
Array opt_cache_files = {
260
name + ".wasm",
261
name + ".pck"
262
};
263
if (extensions) {
264
opt_cache_files.push_back(name + ".side.wasm");
265
for (int i = 0; i < p_shared_objects.size(); i++) {
266
opt_cache_files.push_back(p_shared_objects[i].path.get_file());
267
}
268
}
269
replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();
270
271
const String sw_path = dir.path_join(name + ".service.worker.js");
272
Vector<uint8_t> sw;
273
{
274
Ref<FileAccess> f = FileAccess::open(sw_path, FileAccess::READ);
275
if (f.is_null()) {
276
add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), sw_path));
277
return ERR_FILE_CANT_READ;
278
}
279
sw.resize(f->get_length());
280
f->get_buffer(sw.ptrw(), sw.size());
281
}
282
_replace_strings(replaces, sw);
283
Error err = _write_or_error(sw.ptr(), sw.size(), dir.path_join(name + ".service.worker.js"));
284
if (err != OK) {
285
// Message is supplied by the subroutine method.
286
return err;
287
}
288
289
// Custom offline page
290
const String offline_page = p_preset->get("progressive_web_app/offline_page");
291
if (!offline_page.is_empty()) {
292
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
293
const String offline_dest = dir.path_join(name + ".offline.html");
294
err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest);
295
if (err != OK) {
296
add_message(EXPORT_MESSAGE_ERROR, TTR("PWA"), vformat(TTR("Could not read file: \"%s\"."), offline_dest));
297
return err;
298
}
299
}
300
301
// Manifest
302
const char *modes[4] = { "fullscreen", "standalone", "minimal-ui", "browser" };
303
const char *orientations[3] = { "any", "landscape", "portrait" };
304
const int display = CLAMP(int(p_preset->get("progressive_web_app/display")), 0, 4);
305
const int orientation = CLAMP(int(p_preset->get("progressive_web_app/orientation")), 0, 3);
306
307
Dictionary manifest;
308
manifest["name"] = proj_name;
309
manifest["start_url"] = "./" + name + ".html";
310
manifest["display"] = String::utf8(modes[display]);
311
manifest["orientation"] = String::utf8(orientations[orientation]);
312
manifest["background_color"] = "#" + p_preset->get("progressive_web_app/background_color").operator Color().to_html(false);
313
314
Array icons_arr;
315
const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");
316
err = _add_manifest_icon(p_preset, p_path, icon144_path, 144, icons_arr);
317
if (err != OK) {
318
// Message is supplied by the subroutine method.
319
return err;
320
}
321
const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");
322
err = _add_manifest_icon(p_preset, p_path, icon180_path, 180, icons_arr);
323
if (err != OK) {
324
// Message is supplied by the subroutine method.
325
return err;
326
}
327
const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");
328
err = _add_manifest_icon(p_preset, p_path, icon512_path, 512, icons_arr);
329
if (err != OK) {
330
// Message is supplied by the subroutine method.
331
return err;
332
}
333
manifest["icons"] = icons_arr;
334
335
CharString cs = Variant(manifest).to_json_string().utf8();
336
err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.path_join(name + ".manifest.json"));
337
if (err != OK) {
338
// Message is supplied by the subroutine method.
339
return err;
340
}
341
342
return OK;
343
}
344
345
void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
346
if (p_preset->get("vram_texture_compression/for_desktop")) {
347
r_features->push_back("s3tc");
348
r_features->push_back("bptc");
349
}
350
if (p_preset->get("vram_texture_compression/for_mobile")) {
351
r_features->push_back("etc2");
352
r_features->push_back("astc");
353
}
354
if (p_preset->get("variant/thread_support").operator bool()) {
355
r_features->push_back("threads");
356
} else {
357
r_features->push_back("nothreads");
358
}
359
if (p_preset->get("variant/extensions_support").operator bool()) {
360
r_features->push_back("web_extensions");
361
} else {
362
r_features->push_back("web_noextensions");
363
}
364
r_features->push_back("wasm32");
365
}
366
367
void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options) const {
368
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
369
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
370
371
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // GDExtension support.
372
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), false, true)); // Thread support (i.e. run with or without COEP/COOP headers).
373
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
374
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
375
376
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/export_icon"), true));
377
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), ""));
378
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT, "monospace,no_wrap"), ""));
379
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2));
380
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/focus_canvas_on_start"), true));
381
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));
382
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));
383
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/ensure_cross_origin_isolation_headers"), true));
384
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));
385
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));
386
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));
387
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_144x144", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));
388
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_180x180", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));
389
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_512x512", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), ""));
390
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
391
392
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/emscripten_pool_size"), 8));
393
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "threads/godot_pool_size"), 4));
394
}
395
396
bool EditorExportPlatformWeb::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
397
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
398
if (p_option == "custom_template/debug" || p_option == "custom_template/release") {
399
return advanced_options_enabled;
400
}
401
402
if (p_option == "threads/godot_pool_size" || p_option == "threads/emscripten_pool_size") {
403
return p_preset->get("variant/thread_support").operator bool();
404
}
405
406
return true;
407
}
408
409
String EditorExportPlatformWeb::get_name() const {
410
return "Web";
411
}
412
413
String EditorExportPlatformWeb::get_os_name() const {
414
return "Web";
415
}
416
417
Ref<Texture2D> EditorExportPlatformWeb::get_logo() const {
418
return logo;
419
}
420
421
bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const {
422
#ifdef MODULE_MONO_ENABLED
423
// Don't check for additional errors, as this particular error cannot be resolved.
424
r_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";
425
r_error += TTR("If this project does not use C#, use a non-C# editor build to export the project.") + "\n";
426
return false;
427
#else
428
429
String err;
430
bool valid = false;
431
bool extensions = (bool)p_preset->get("variant/extensions_support");
432
bool thread_support = (bool)p_preset->get("variant/thread_support");
433
434
// Look for export templates (first official, and if defined custom templates).
435
bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);
436
bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);
437
438
if (p_preset->get("custom_template/debug") != "") {
439
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
440
if (!dvalid) {
441
err += TTR("Custom debug template not found.") + "\n";
442
}
443
}
444
if (p_preset->get("custom_template/release") != "") {
445
rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
446
if (!rvalid) {
447
err += TTR("Custom release template not found.") + "\n";
448
}
449
}
450
451
valid = dvalid || rvalid;
452
r_missing_templates = !valid;
453
454
if (!err.is_empty()) {
455
r_error = err;
456
}
457
458
return valid;
459
#endif // !MODULE_MONO_ENABLED
460
}
461
462
bool EditorExportPlatformWeb::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const {
463
String err;
464
bool valid = true;
465
466
// Validate the project configuration.
467
468
if (p_preset->get("vram_texture_compression/for_mobile")) {
469
if (!ResourceImporterTextureSettings::should_import_etc2_astc()) {
470
valid = false;
471
}
472
}
473
474
if (!err.is_empty()) {
475
r_error = err;
476
}
477
478
return valid;
479
}
480
481
List<String> EditorExportPlatformWeb::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
482
List<String> list;
483
list.push_back("html");
484
return list;
485
}
486
487
Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
488
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
489
490
const String custom_debug = p_preset->get("custom_template/debug");
491
const String custom_release = p_preset->get("custom_template/release");
492
const String custom_html = p_preset->get("html/custom_html_shell");
493
const bool export_icon = p_preset->get("html/export_icon");
494
const bool pwa = p_preset->get("progressive_web_app/enabled");
495
496
const String base_dir = p_path.get_base_dir();
497
const String base_path = p_path.get_basename();
498
const String base_name = p_path.get_file().get_basename();
499
500
if (!DirAccess::exists(base_dir)) {
501
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));
502
return ERR_FILE_BAD_PATH;
503
}
504
505
// Find the correct template
506
String template_path = p_debug ? custom_debug : custom_release;
507
template_path = template_path.strip_edges();
508
if (template_path.is_empty()) {
509
bool extensions = (bool)p_preset->get("variant/extensions_support");
510
bool thread_support = (bool)p_preset->get("variant/thread_support");
511
template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));
512
}
513
514
if (!template_path.is_empty() && !FileAccess::exists(template_path)) {
515
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Template file not found: \"%s\"."), template_path));
516
return ERR_FILE_NOT_FOUND;
517
}
518
519
// Export pck and shared objects
520
Vector<SharedObject> shared_objects;
521
String pck_path = base_path + ".pck";
522
Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects);
523
if (error != OK) {
524
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path));
525
return error;
526
}
527
528
{
529
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
530
for (int i = 0; i < shared_objects.size(); i++) {
531
String dst = base_dir.path_join(shared_objects[i].path.get_file());
532
error = da->copy(shared_objects[i].path, dst);
533
if (error != OK) {
534
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), shared_objects[i].path.get_file()));
535
return error;
536
}
537
}
538
}
539
540
// Extract templates.
541
error = _extract_template(template_path, base_dir, base_name, pwa);
542
if (error) {
543
// Message is supplied by the subroutine method.
544
return error;
545
}
546
547
// Parse generated file sizes (pck and wasm, to help show a meaningful loading bar).
548
Dictionary file_sizes;
549
Ref<FileAccess> f = FileAccess::open(pck_path, FileAccess::READ);
550
if (f.is_valid()) {
551
file_sizes[pck_path.get_file()] = (uint64_t)f->get_length();
552
}
553
f = FileAccess::open(base_path + ".wasm", FileAccess::READ);
554
if (f.is_valid()) {
555
file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length();
556
}
557
558
// Read the HTML shell file (custom or from template).
559
const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html;
560
Vector<uint8_t> html;
561
f = FileAccess::open(html_path, FileAccess::READ);
562
if (f.is_null()) {
563
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not read HTML shell: \"%s\"."), html_path));
564
return ERR_FILE_CANT_READ;
565
}
566
html.resize(f->get_length());
567
f->get_buffer(html.ptrw(), html.size());
568
f.unref(); // close file.
569
570
// Generate HTML file with replaced strings.
571
_fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);
572
Error err = _write_or_error(html.ptr(), html.size(), p_path);
573
if (err != OK) {
574
// Message is supplied by the subroutine method.
575
return err;
576
}
577
html.resize(0);
578
579
// Export splash (why?)
580
Ref<Image> splash = _get_project_splash(p_preset);
581
const String splash_png_path = base_path + ".png";
582
if (splash->save_png(splash_png_path) != OK) {
583
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), splash_png_path));
584
return ERR_FILE_CANT_WRITE;
585
}
586
587
// Save a favicon that can be accessed without waiting for the project to finish loading.
588
// This way, the favicon can be displayed immediately when loading the page.
589
if (export_icon) {
590
Ref<Image> favicon = _get_project_icon(p_preset);
591
const String favicon_png_path = base_path + ".icon.png";
592
if (favicon->save_png(favicon_png_path) != OK) {
593
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), favicon_png_path));
594
return ERR_FILE_CANT_WRITE;
595
}
596
favicon->resize(180, 180);
597
const String apple_icon_png_path = base_path + ".apple-touch-icon.png";
598
if (favicon->save_png(apple_icon_png_path) != OK) {
599
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), apple_icon_png_path));
600
return ERR_FILE_CANT_WRITE;
601
}
602
}
603
604
// Generate the PWA worker and manifest
605
if (pwa) {
606
err = _build_pwa(p_preset, p_path, shared_objects);
607
if (err != OK) {
608
// Message is supplied by the subroutine method.
609
return err;
610
}
611
}
612
613
return OK;
614
}
615
616
bool EditorExportPlatformWeb::poll_export() {
617
Ref<EditorExportPreset> preset = EditorExport::get_singleton()->get_runnable_preset_for_platform(this);
618
619
RemoteDebugState prev_remote_debug_state = remote_debug_state;
620
remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE;
621
622
if (preset.is_valid()) {
623
const bool debug = true;
624
// Throwaway variables to pass to `can_export`.
625
String err;
626
bool missing_templates;
627
628
if (can_export(preset, err, missing_templates, debug)) {
629
if (server->is_listening()) {
630
remote_debug_state = REMOTE_DEBUG_STATE_SERVING;
631
} else {
632
remote_debug_state = REMOTE_DEBUG_STATE_AVAILABLE;
633
}
634
}
635
}
636
637
if (remote_debug_state != REMOTE_DEBUG_STATE_SERVING && server->is_listening()) {
638
server->stop();
639
}
640
641
return remote_debug_state != prev_remote_debug_state;
642
}
643
644
Ref<Texture2D> EditorExportPlatformWeb::get_option_icon(int p_index) const {
645
Ref<Texture2D> play_icon = EditorExportPlatform::get_option_icon(p_index);
646
647
switch (remote_debug_state) {
648
case REMOTE_DEBUG_STATE_UNAVAILABLE: {
649
return nullptr;
650
} break;
651
652
case REMOTE_DEBUG_STATE_AVAILABLE: {
653
switch (p_index) {
654
case 0:
655
case 1:
656
return play_icon;
657
default:
658
ERR_FAIL_V(nullptr);
659
}
660
} break;
661
662
case REMOTE_DEBUG_STATE_SERVING: {
663
switch (p_index) {
664
case 0:
665
return play_icon;
666
case 1:
667
return restart_icon;
668
case 2:
669
return stop_icon;
670
default:
671
ERR_FAIL_V(nullptr);
672
}
673
} break;
674
}
675
676
return nullptr;
677
}
678
679
int EditorExportPlatformWeb::get_options_count() const {
680
switch (remote_debug_state) {
681
case REMOTE_DEBUG_STATE_UNAVAILABLE: {
682
return 0;
683
} break;
684
685
case REMOTE_DEBUG_STATE_AVAILABLE: {
686
return 2;
687
} break;
688
689
case REMOTE_DEBUG_STATE_SERVING: {
690
return 3;
691
} break;
692
}
693
694
return 0;
695
}
696
697
String EditorExportPlatformWeb::get_option_label(int p_index) const {
698
String run_in_browser = TTR("Run in Browser");
699
String start_http_server = TTR("Start HTTP Server");
700
String reexport_project = TTR("Re-export Project");
701
String stop_http_server = TTR("Stop HTTP Server");
702
703
switch (remote_debug_state) {
704
case REMOTE_DEBUG_STATE_UNAVAILABLE:
705
return "";
706
707
case REMOTE_DEBUG_STATE_AVAILABLE: {
708
switch (p_index) {
709
case 0:
710
return run_in_browser;
711
case 1:
712
return start_http_server;
713
default:
714
ERR_FAIL_V("");
715
}
716
} break;
717
718
case REMOTE_DEBUG_STATE_SERVING: {
719
switch (p_index) {
720
case 0:
721
return run_in_browser;
722
case 1:
723
return reexport_project;
724
case 2:
725
return stop_http_server;
726
default:
727
ERR_FAIL_V("");
728
}
729
} break;
730
}
731
732
return "";
733
}
734
735
String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {
736
String run_in_browser = TTR("Run exported HTML in the system's default browser.");
737
String start_http_server = TTR("Start the HTTP server.");
738
String reexport_project = TTR("Export project again to account for updates.");
739
String stop_http_server = TTR("Stop the HTTP server.");
740
741
switch (remote_debug_state) {
742
case REMOTE_DEBUG_STATE_UNAVAILABLE:
743
return "";
744
745
case REMOTE_DEBUG_STATE_AVAILABLE: {
746
switch (p_index) {
747
case 0:
748
return run_in_browser;
749
case 1:
750
return start_http_server;
751
default:
752
ERR_FAIL_V("");
753
}
754
} break;
755
756
case REMOTE_DEBUG_STATE_SERVING: {
757
switch (p_index) {
758
case 0:
759
return run_in_browser;
760
case 1:
761
return reexport_project;
762
case 2:
763
return stop_http_server;
764
default:
765
ERR_FAIL_V("");
766
}
767
} break;
768
}
769
770
return "";
771
}
772
773
Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {
774
const uint16_t bind_port = EDITOR_GET("export/web/http_port");
775
// Resolve host if needed.
776
const String bind_host = EDITOR_GET("export/web/http_host");
777
const bool use_tls = EDITOR_GET("export/web/use_tls");
778
779
switch (remote_debug_state) {
780
case REMOTE_DEBUG_STATE_UNAVAILABLE: {
781
return FAILED;
782
} break;
783
784
case REMOTE_DEBUG_STATE_AVAILABLE: {
785
switch (p_option) {
786
// Run in Browser.
787
case 0: {
788
Error err = _export_project(p_preset, p_debug_flags);
789
if (err != OK) {
790
return err;
791
}
792
err = _start_server(bind_host, bind_port, use_tls);
793
if (err != OK) {
794
return err;
795
}
796
return _launch_browser(bind_host, bind_port, use_tls);
797
} break;
798
799
// Start HTTP Server.
800
case 1: {
801
Error err = _export_project(p_preset, p_debug_flags);
802
if (err != OK) {
803
return err;
804
}
805
return _start_server(bind_host, bind_port, use_tls);
806
} break;
807
808
default: {
809
ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));
810
}
811
}
812
} break;
813
814
case REMOTE_DEBUG_STATE_SERVING: {
815
switch (p_option) {
816
// Run in Browser.
817
case 0: {
818
Error err = _export_project(p_preset, p_debug_flags);
819
if (err != OK) {
820
return err;
821
}
822
return _launch_browser(bind_host, bind_port, use_tls);
823
} break;
824
825
// Re-export Project.
826
case 1: {
827
return _export_project(p_preset, p_debug_flags);
828
} break;
829
830
// Stop HTTP Server.
831
case 2: {
832
return _stop_server();
833
} break;
834
835
default: {
836
ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));
837
}
838
}
839
} break;
840
}
841
842
return FAILED;
843
}
844
845
Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {
846
const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("web");
847
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
848
if (!da->dir_exists(dest)) {
849
Error err = da->make_dir_recursive(dest);
850
if (err != OK) {
851
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Could not create HTTP server directory: %s."), dest));
852
return err;
853
}
854
}
855
856
const String basepath = dest.path_join("tmp_js_export");
857
Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);
858
if (err != OK) {
859
// Export generates several files, clean them up on failure.
860
DirAccess::remove_file_or_error(basepath + ".html");
861
DirAccess::remove_file_or_error(basepath + ".offline.html");
862
DirAccess::remove_file_or_error(basepath + ".js");
863
DirAccess::remove_file_or_error(basepath + ".audio.worklet.js");
864
DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js");
865
DirAccess::remove_file_or_error(basepath + ".service.worker.js");
866
DirAccess::remove_file_or_error(basepath + ".pck");
867
DirAccess::remove_file_or_error(basepath + ".png");
868
DirAccess::remove_file_or_error(basepath + ".side.wasm");
869
DirAccess::remove_file_or_error(basepath + ".wasm");
870
DirAccess::remove_file_or_error(basepath + ".icon.png");
871
DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");
872
}
873
return err;
874
}
875
876
Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
877
OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html"));
878
// FIXME: Find out how to clean up export files after running the successfully
879
// exported game. Might not be trivial.
880
return OK;
881
}
882
883
Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
884
IPAddress bind_ip;
885
if (p_bind_host.is_valid_ip_address()) {
886
bind_ip = p_bind_host;
887
} else {
888
bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host);
889
}
890
ERR_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'.");
891
892
const String tls_key = EDITOR_GET("export/web/tls_key");
893
const String tls_cert = EDITOR_GET("export/web/tls_certificate");
894
895
// Restart server.
896
server->stop();
897
Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert);
898
if (err != OK) {
899
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err));
900
}
901
return err;
902
}
903
904
Error EditorExportPlatformWeb::_stop_server() {
905
server->stop();
906
return OK;
907
}
908
909
Ref<Texture2D> EditorExportPlatformWeb::get_run_icon() const {
910
return run_icon;
911
}
912
913
void EditorExportPlatformWeb::initialize() {
914
if (EditorNode::get_singleton()) {
915
server.instantiate();
916
917
Ref<Image> img = memnew(Image);
918
const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
919
920
ImageLoaderSVG::create_image_from_string(img, _web_logo_svg, EDSCALE, upsample, false);
921
logo = ImageTexture::create_from_image(img);
922
923
ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);
924
run_icon = ImageTexture::create_from_image(img);
925
926
Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
927
if (theme.is_valid()) {
928
stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));
929
restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons));
930
} else {
931
stop_icon.instantiate();
932
restart_icon.instantiate();
933
}
934
}
935
}
936
937
EditorExportPlatformWeb::~EditorExportPlatformWeb() {
938
}
939
940