1 /*************************************************************************/
2 /* export.cpp */
3 /*************************************************************************/
4 /* This file is part of: */
5 /* GODOT ENGINE */
6 /* https://godotengine.org */
7 /*************************************************************************/
8 /* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
9 /* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
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 "core/io/tcp_server.h"
32 #include "core/io/zip_io.h"
33 #include "editor/editor_export.h"
34 #include "editor/editor_node.h"
35 #include "main/splash.gen.h"
36 #include "platform/javascript/logo.gen.h"
37 #include "platform/javascript/run_icon.gen.h"
38
39 #define EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE "webassembly_release.zip"
40 #define EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG "webassembly_debug.zip"
41
42 class EditorHTTPServer : public Reference {
43
44 private:
45 Ref<TCP_Server> server;
46 Ref<StreamPeerTCP> connection;
47 uint64_t time;
48 uint8_t req_buf[4096];
49 int req_pos;
50
_clear_client()51 void _clear_client() {
52 connection = Ref<StreamPeerTCP>();
53 memset(req_buf, 0, sizeof(req_buf));
54 time = 0;
55 req_pos = 0;
56 }
57
58 public:
EditorHTTPServer()59 EditorHTTPServer() {
60 server.instance();
61 stop();
62 }
63
stop()64 void stop() {
65 server->stop();
66 _clear_client();
67 }
68
listen(int p_port,IP_Address p_address)69 Error listen(int p_port, IP_Address p_address) {
70 return server->listen(p_port, p_address);
71 }
72
is_listening() const73 bool is_listening() const {
74 return server->is_listening();
75 }
76
_send_response()77 void _send_response() {
78 Vector<String> psa = String((char *)req_buf).split("\r\n");
79 int len = psa.size();
80 ERR_FAIL_COND_MSG(len < 4, "Not enough response headers, got: " + itos(len) + ", expected >= 4.");
81
82 Vector<String> req = psa[0].split(" ", false);
83 ERR_FAIL_COND_MSG(req.size() < 2, "Invalid protocol or status code.");
84
85 // Wrong protocol
86 ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
87
88 String filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
89 const String basereq = "/tmp_js_export";
90 String ctype = "";
91 if (req[1] == basereq + ".html") {
92 filepath += ".html";
93 ctype = "text/html";
94 } else if (req[1] == basereq + ".js") {
95 filepath += ".js";
96 ctype = "application/javascript";
97 } else if (req[1] == basereq + ".worker.js") {
98 filepath += ".worker.js";
99 ctype = "application/javascript";
100 } else if (req[1] == basereq + ".pck") {
101 filepath += ".pck";
102 ctype = "application/octet-stream";
103 } else if (req[1] == basereq + ".png" || req[1] == "/favicon.png") {
104 // Also allow serving the generated favicon for a smoother loading experience.
105 if (req[1] == "/favicon.png") {
106 filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png");
107 } else {
108 filepath += ".png";
109 }
110 ctype = "image/png";
111 } else if (req[1] == basereq + ".wasm") {
112 filepath += ".wasm";
113 ctype = "application/wasm";
114 } else {
115 String s = "HTTP/1.1 404 Not Found\r\n";
116 s += "Connection: Close\r\n";
117 s += "\r\n";
118 CharString cs = s.utf8();
119 connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
120 return;
121 }
122 FileAccess *f = FileAccess::open(filepath, FileAccess::READ);
123 ERR_FAIL_COND(!f);
124 String s = "HTTP/1.1 200 OK\r\n";
125 s += "Connection: Close\r\n";
126 s += "Content-Type: " + ctype + "\r\n";
127 s += "\r\n";
128 CharString cs = s.utf8();
129 Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
130 if (err != OK) {
131 memdelete(f);
132 ERR_FAIL();
133 }
134
135 while (true) {
136 uint8_t bytes[4096];
137 int read = f->get_buffer(bytes, 4096);
138 if (read < 1) {
139 break;
140 }
141 err = connection->put_data(bytes, read);
142 if (err != OK) {
143 memdelete(f);
144 ERR_FAIL();
145 }
146 }
147 memdelete(f);
148 }
149
poll()150 void poll() {
151 if (!server->is_listening())
152 return;
153 if (connection.is_null()) {
154 if (!server->is_connection_available())
155 return;
156 connection = server->take_connection();
157 time = OS::get_singleton()->get_ticks_usec();
158 }
159 if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
160 _clear_client();
161 return;
162 }
163 if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED)
164 return;
165
166 while (true) {
167
168 char *r = (char *)req_buf;
169 int l = req_pos - 1;
170 if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
171 _send_response();
172 _clear_client();
173 return;
174 }
175
176 int read = 0;
177 ERR_FAIL_COND(req_pos >= 4096);
178 Error err = connection->get_partial_data(&req_buf[req_pos], 1, read);
179 if (err != OK) {
180 // Got an error
181 _clear_client();
182 return;
183 } else if (read != 1) {
184 // Busy, wait next poll
185 return;
186 }
187 req_pos += read;
188 }
189 }
190 };
191
192 class EditorExportPlatformJavaScript : public EditorExportPlatform {
193
194 GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform);
195
196 Ref<ImageTexture> logo;
197 Ref<ImageTexture> run_icon;
198 Ref<ImageTexture> stop_icon;
199 int menu_options;
200
201 void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug);
202
203 private:
204 Ref<EditorHTTPServer> server;
205 bool server_quit;
206 Mutex *server_lock;
207 Thread *server_thread;
208
209 static void _server_thread_poll(void *data);
210
211 public:
212 virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features);
213
214 virtual void get_export_options(List<ExportOption> *r_options);
215
216 virtual String get_name() const;
217 virtual String get_os_name() const;
218 virtual Ref<Texture> get_logo() const;
219
220 virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const;
221 virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const;
222 virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0);
223
224 virtual bool poll_export();
225 virtual int get_options_count() const;
get_option_label(int p_index) const226 virtual String get_option_label(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
get_option_tooltip(int p_index) const227 virtual String get_option_tooltip(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
228 virtual Ref<ImageTexture> get_option_icon(int p_index) const;
229 virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags);
230 virtual Ref<Texture> get_run_icon() const;
231
get_platform_features(List<String> * r_features)232 virtual void get_platform_features(List<String> *r_features) {
233
234 r_features->push_back("web");
235 r_features->push_back(get_os_name());
236 }
237
resolve_platform_feature_priorities(const Ref<EditorExportPreset> & p_preset,Set<String> & p_features)238 virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) {
239 }
240
241 EditorExportPlatformJavaScript();
242 ~EditorExportPlatformJavaScript();
243 };
244
_fix_html(Vector<uint8_t> & p_html,const Ref<EditorExportPreset> & p_preset,const String & p_name,bool p_debug)245 void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug) {
246
247 String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size());
248 String str_export;
249 Vector<String> lines = str_template.split("\n");
250
251 for (int i = 0; i < lines.size(); i++) {
252
253 String current_line = lines[i];
254 current_line = current_line.replace("$GODOT_BASENAME", p_name);
255 current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name"));
256 current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include"));
257 current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false");
258 str_export += current_line + "\n";
259 }
260
261 CharString cs = str_export.utf8();
262 p_html.resize(cs.length());
263 for (int i = 0; i < cs.length(); i++) {
264 p_html.write[i] = cs[i];
265 }
266 }
267
get_preset_features(const Ref<EditorExportPreset> & p_preset,List<String> * r_features)268 void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
269
270 if (p_preset->get("vram_texture_compression/for_desktop")) {
271 r_features->push_back("s3tc");
272 }
273
274 if (p_preset->get("vram_texture_compression/for_mobile")) {
275 String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name");
276 if (driver == "GLES2") {
277 r_features->push_back("etc");
278 } else if (driver == "GLES3") {
279 r_features->push_back("etc2");
280 if (ProjectSettings::get_singleton()->get("rendering/quality/driver/fallback_to_gles2")) {
281 r_features->push_back("etc");
282 }
283 }
284 }
285 }
286
get_export_options(List<ExportOption> * r_options)287 void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_options) {
288
289 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
290 r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
291 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), ""));
292 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), ""));
293 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
294 r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
295 }
296
get_name() const297 String EditorExportPlatformJavaScript::get_name() const {
298
299 return "HTML5";
300 }
301
get_os_name() const302 String EditorExportPlatformJavaScript::get_os_name() const {
303
304 return "HTML5";
305 }
306
get_logo() const307 Ref<Texture> EditorExportPlatformJavaScript::get_logo() const {
308
309 return logo;
310 }
311
can_export(const Ref<EditorExportPreset> & p_preset,String & r_error,bool & r_missing_templates) const312 bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
313
314 String err;
315 bool valid = false;
316
317 // Look for export templates (first official, and if defined custom templates).
318
319 bool dvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG, &err);
320 bool rvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE, &err);
321
322 if (p_preset->get("custom_template/debug") != "") {
323 dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
324 if (!dvalid) {
325 err += TTR("Custom debug template not found.") + "\n";
326 }
327 }
328 if (p_preset->get("custom_template/release") != "") {
329 rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
330 if (!rvalid) {
331 err += TTR("Custom release template not found.") + "\n";
332 }
333 }
334
335 valid = dvalid || rvalid;
336 r_missing_templates = !valid;
337
338 // Validate the rest of the configuration.
339
340 if (p_preset->get("vram_texture_compression/for_mobile")) {
341 String etc_error = test_etc2();
342 if (etc_error != String()) {
343 valid = false;
344 err += etc_error;
345 }
346 }
347
348 if (!err.empty())
349 r_error = err;
350
351 return valid;
352 }
353
get_binary_extensions(const Ref<EditorExportPreset> & p_preset) const354 List<String> EditorExportPlatformJavaScript::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
355
356 List<String> list;
357 list.push_back("html");
358 return list;
359 }
360
export_project(const Ref<EditorExportPreset> & p_preset,bool p_debug,const String & p_path,int p_flags)361 Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
362 ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
363
364 String custom_debug = p_preset->get("custom_template/debug");
365 String custom_release = p_preset->get("custom_template/release");
366 String custom_html = p_preset->get("html/custom_html_shell");
367
368 String template_path = p_debug ? custom_debug : custom_release;
369
370 template_path = template_path.strip_edges();
371
372 if (template_path == String()) {
373
374 if (p_debug)
375 template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG);
376 else
377 template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE);
378 }
379
380 if (!DirAccess::exists(p_path.get_base_dir())) {
381 return ERR_FILE_BAD_PATH;
382 }
383
384 if (template_path != String() && !FileAccess::exists(template_path)) {
385 EditorNode::get_singleton()->show_warning(TTR("Template file not found:") + "\n" + template_path);
386 return ERR_FILE_NOT_FOUND;
387 }
388
389 String pck_path = p_path.get_basename() + ".pck";
390 Error error = save_pack(p_preset, pck_path);
391 if (error != OK) {
392 EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + pck_path);
393 return error;
394 }
395
396 FileAccess *src_f = NULL;
397 zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
398 unzFile pkg = unzOpen2(template_path.utf8().get_data(), &io);
399
400 if (!pkg) {
401
402 EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + template_path);
403 return ERR_FILE_NOT_FOUND;
404 }
405
406 if (unzGoToFirstFile(pkg) != UNZ_OK) {
407 EditorNode::get_singleton()->show_warning(TTR("Invalid export template:") + "\n" + template_path);
408 unzClose(pkg);
409 return ERR_FILE_CORRUPT;
410 }
411
412 do {
413 //get filename
414 unz_file_info info;
415 char fname[16384];
416 unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0);
417
418 String file = fname;
419
420 Vector<uint8_t> data;
421 data.resize(info.uncompressed_size);
422
423 //read
424 unzOpenCurrentFile(pkg);
425 unzReadCurrentFile(pkg, data.ptrw(), data.size());
426 unzCloseCurrentFile(pkg);
427
428 //write
429
430 if (file == "godot.html") {
431
432 if (!custom_html.empty()) {
433 continue;
434 }
435 _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug);
436 file = p_path.get_file();
437
438 } else if (file == "godot.js") {
439
440 file = p_path.get_file().get_basename() + ".js";
441 } else if (file == "godot.worker.js") {
442
443 file = p_path.get_file().get_basename() + ".worker.js";
444
445 } else if (file == "godot.wasm") {
446
447 file = p_path.get_file().get_basename() + ".wasm";
448 }
449
450 String dst = p_path.get_base_dir().plus_file(file);
451 FileAccess *f = FileAccess::open(dst, FileAccess::WRITE);
452 if (!f) {
453 EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst);
454 unzClose(pkg);
455 return ERR_FILE_CANT_WRITE;
456 }
457 f->store_buffer(data.ptr(), data.size());
458 memdelete(f);
459
460 } while (unzGoToNextFile(pkg) == UNZ_OK);
461 unzClose(pkg);
462
463 if (!custom_html.empty()) {
464
465 FileAccess *f = FileAccess::open(custom_html, FileAccess::READ);
466 if (!f) {
467 EditorNode::get_singleton()->show_warning(TTR("Could not read custom HTML shell:") + "\n" + custom_html);
468 return ERR_FILE_CANT_READ;
469 }
470 Vector<uint8_t> buf;
471 buf.resize(f->get_len());
472 f->get_buffer(buf.ptrw(), buf.size());
473 memdelete(f);
474 _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug);
475
476 f = FileAccess::open(p_path, FileAccess::WRITE);
477 if (!f) {
478 EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path);
479 return ERR_FILE_CANT_WRITE;
480 }
481 f->store_buffer(buf.ptr(), buf.size());
482 memdelete(f);
483 }
484
485 Ref<Image> splash;
486 const String splash_path = String(GLOBAL_GET("application/boot_splash/image")).strip_edges();
487 if (!splash_path.empty()) {
488 splash.instance();
489 const Error err = splash->load(splash_path);
490 if (err) {
491 EditorNode::get_singleton()->show_warning(TTR("Could not read boot splash image file:") + "\n" + splash_path + "\n" + TTR("Using default boot splash image."));
492 splash.unref();
493 }
494 }
495 if (splash.is_null()) {
496 splash = Ref<Image>(memnew(Image(boot_splash_png)));
497 }
498 const String splash_png_path = p_path.get_base_dir().plus_file(p_path.get_file().get_basename() + ".png");
499 if (splash->save_png(splash_png_path) != OK) {
500 EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + splash_png_path);
501 return ERR_FILE_CANT_WRITE;
502 }
503
504 // Save a favicon that can be accessed without waiting for the project to finish loading.
505 // This way, the favicon can be displayed immediately when loading the page.
506 Ref<Image> favicon;
507 const String favicon_path = String(GLOBAL_GET("application/config/icon")).strip_edges();
508 if (!favicon_path.empty()) {
509 favicon.instance();
510 const Error err = favicon->load(favicon_path);
511 if (err) {
512 favicon.unref();
513 }
514 }
515
516 if (favicon.is_valid()) {
517 const String favicon_png_path = p_path.get_base_dir().plus_file("favicon.png");
518 if (favicon->save_png(favicon_png_path) != OK) {
519 EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + favicon_png_path);
520 return ERR_FILE_CANT_WRITE;
521 }
522 }
523
524 return OK;
525 }
526
poll_export()527 bool EditorExportPlatformJavaScript::poll_export() {
528
529 Ref<EditorExportPreset> preset;
530
531 for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
532
533 Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
534 if (ep->is_runnable() && ep->get_platform() == this) {
535 preset = ep;
536 break;
537 }
538 }
539
540 int prev = menu_options;
541 menu_options = preset.is_valid();
542 if (server->is_listening()) {
543 if (menu_options == 0) {
544 server_lock->lock();
545 server->stop();
546 server_lock->unlock();
547 } else {
548 menu_options += 1;
549 }
550 }
551 return menu_options != prev;
552 }
553
get_option_icon(int p_index) const554 Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) const {
555 return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
556 }
557
get_options_count() const558 int EditorExportPlatformJavaScript::get_options_count() const {
559
560 return menu_options;
561 }
562
run(const Ref<EditorExportPreset> & p_preset,int p_option,int p_debug_flags)563 Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
564
565 if (p_option == 1) {
566 server_lock->lock();
567 server->stop();
568 server_lock->unlock();
569 return OK;
570 }
571
572 const String basepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
573 Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);
574 if (err != OK) {
575 // Export generates several files, clean them up on failure.
576 DirAccess::remove_file_or_error(basepath + ".html");
577 DirAccess::remove_file_or_error(basepath + ".js");
578 DirAccess::remove_file_or_error(basepath + ".worker.js");
579 DirAccess::remove_file_or_error(basepath + ".pck");
580 DirAccess::remove_file_or_error(basepath + ".png");
581 DirAccess::remove_file_or_error(basepath + ".wasm");
582 DirAccess::remove_file_or_error(EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png"));
583 return err;
584 }
585
586 const uint16_t bind_port = EDITOR_GET("export/web/http_port");
587 // Resolve host if needed.
588 const String bind_host = EDITOR_GET("export/web/http_host");
589 IP_Address bind_ip;
590 if (bind_host.is_valid_ip_address()) {
591 bind_ip = bind_host;
592 } else {
593 bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
594 }
595 ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'.");
596
597 // Restart server.
598 server_lock->lock();
599 server->stop();
600 err = server->listen(bind_port, bind_ip);
601 server_lock->unlock();
602 ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to start HTTP server.");
603
604 OS::get_singleton()->shell_open(String("http://" + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html"));
605 // FIXME: Find out how to clean up export files after running the successfully
606 // exported game. Might not be trivial.
607 return OK;
608 }
609
get_run_icon() const610 Ref<Texture> EditorExportPlatformJavaScript::get_run_icon() const {
611
612 return run_icon;
613 }
614
_server_thread_poll(void * data)615 void EditorExportPlatformJavaScript::_server_thread_poll(void *data) {
616 EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data;
617 while (!ej->server_quit) {
618 OS::get_singleton()->delay_usec(1000);
619 ej->server_lock->lock();
620 ej->server->poll();
621 ej->server_lock->unlock();
622 }
623 }
624
EditorExportPlatformJavaScript()625 EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {
626
627 server.instance();
628 server_quit = false;
629 server_lock = Mutex::create();
630 server_thread = Thread::create(_server_thread_poll, this);
631
632 Ref<Image> img = memnew(Image(_javascript_logo));
633 logo.instance();
634 logo->create_from_image(img);
635
636 img = Ref<Image>(memnew(Image(_javascript_run_icon)));
637 run_icon.instance();
638 run_icon->create_from_image(img);
639
640 Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
641 if (theme.is_valid())
642 stop_icon = theme->get_icon("Stop", "EditorIcons");
643 else
644 stop_icon.instance();
645
646 menu_options = 0;
647 }
648
~EditorExportPlatformJavaScript()649 EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() {
650 server->stop();
651 server_quit = true;
652 Thread::wait_to_finish(server_thread);
653 memdelete(server_lock);
654 memdelete(server_thread);
655 }
656
register_javascript_exporter()657 void register_javascript_exporter() {
658
659 EDITOR_DEF("export/web/http_host", "localhost");
660 EDITOR_DEF("export/web/http_port", 8060);
661 EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1"));
662
663 Ref<EditorExportPlatformJavaScript> platform;
664 platform.instance();
665 EditorExport::get_singleton()->add_export_platform(platform);
666 }
667