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