diff options
author | Fabio Alessandrelli <fabio.alessandrelli@gmail.com> | 2020-10-23 18:33:20 +0200 |
---|---|---|
committer | Fabio Alessandrelli <fabio.alessandrelli@gmail.com> | 2020-11-10 11:42:51 +0100 |
commit | e2083871eb57e56fe637c3d8f6647ddb4ff539e0 (patch) | |
tree | a58669c68065541e0062f82d7edb45789d8f354f | |
parent | 54cda5c3b8622c9168fcd5d1c68964ef7697b27e (diff) |
[HTML5] Port JavaScript inline code to libraries.
The API is implemented in javascript, and generates C functions that can
be called from godot.
This allows much cleaner code replacing all `EM_ASM` calls in our C++
code with plain C function calls.
This also gets rid of few hacks and comes with few optimizations (e.g.
custom cursor shapes should be much faster now).
33 files changed, 1995 insertions, 1461 deletions
diff --git a/modules/webrtc/SCsub b/modules/webrtc/SCsub index 20b4c8f8d2..4f870ddb2f 100644 --- a/modules/webrtc/SCsub +++ b/modules/webrtc/SCsub @@ -12,4 +12,8 @@ if use_gdnative: # GDNative is retained in Javascript for export compatibility env_webrtc.Append(CPPDEFINES=["WEBRTC_GDNATIVE_ENABLED"]) env_webrtc.Prepend(CPPPATH=["#modules/gdnative/include/"]) +if env["platform"] == "javascript": + # Our JavaScript/C++ interface. + env.AddJSLibraries(["library_godot_webrtc.js"]) + env_webrtc.add_source_files(env.modules_sources, "*.cpp") diff --git a/modules/webrtc/library_godot_webrtc.js b/modules/webrtc/library_godot_webrtc.js new file mode 100644 index 0000000000..b75996b1f3 --- /dev/null +++ b/modules/webrtc/library_godot_webrtc.js @@ -0,0 +1,407 @@ +/*************************************************************************/ +/* library_godot_webrtc.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +var GodotRTCDataChannel = { + // Our socket implementation that forwards events to C++. + $GodotRTCDataChannel__deps: ['$IDHandler', '$GodotOS'], + $GodotRTCDataChannel: { + + connect: function(p_id, p_on_open, p_on_message, p_on_error, p_on_close) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + + ref.binaryType = 'arraybuffer'; + ref.onopen = function (event) { + p_on_open(); + }; + ref.onclose = function (event) { + p_on_close(); + }; + ref.onerror = function (event) { + p_on_error(); + }; + ref.onmessage = function(event) { + var buffer; + var is_string = 0; + if (event.data instanceof ArrayBuffer) { + buffer = new Uint8Array(event.data); + } else if (event.data instanceof Blob) { + console.error("Blob type not supported"); + return; + } else if (typeof event.data === "string") { + is_string = 1; + var enc = new TextEncoder("utf-8"); + buffer = new Uint8Array(enc.encode(event.data)); + } else { + console.error("Unknown message type"); + return; + } + var len = buffer.length*buffer.BYTES_PER_ELEMENT; + var out = _malloc(len); + HEAPU8.set(buffer, out); + p_on_message(out, len, is_string); + _free(out); + } + }, + + close: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + ref.onopen = null; + ref.onmessage = null; + ref.onerror = null; + ref.onclose = null; + ref.close(); + }, + + get_prop: function(p_id, p_prop, p_def) { + const ref = IDHandler.get(p_id); + return (ref && ref[p_prop] !== undefined) ? ref[p_prop] : p_def; + }, + }, + + godot_js_rtc_datachannel_ready_state_get: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return 3; // CLOSED + } + + switch(ref.readyState) { + case "connecting": + return 0; + case "open": + return 1; + case "closing": + return 2; + case "closed": + return 3; + } + return 3; // CLOSED + }, + + godot_js_rtc_datachannel_send: function(p_id, p_buffer, p_length, p_raw) { + const ref = IDHandler.get(p_id); + if (!ref) { + return 1; + } + + const bytes_array = new Uint8Array(p_length); + for (var i = 0; i < p_length; i++) { + bytes_array[i] = getValue(p_buffer + i, 'i8'); + } + + if (p_raw) { + ref.send(bytes_array.buffer); + } else { + const string = new TextDecoder('utf-8').decode(bytes_array); + ref.send(string); + } + }, + + godot_js_rtc_datachannel_is_ordered: function(p_id) { + return IDHandler.get_prop(p_id, 'ordered', true); + }, + + godot_js_rtc_datachannel_id_get: function(p_id) { + return IDHandler.get_prop(p_id, 'id', 65535); + }, + + godot_js_rtc_datachannel_max_packet_lifetime_get: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return 65535; + } + if (ref['maxPacketLifeTime'] !== undefined) { + return ref['maxPacketLifeTime']; + } else if (ref['maxRetransmitTime'] !== undefined) { + // Guess someone didn't appreciate the standardization process. + return ref['maxRetransmitTime']; + } + return 65535; + }, + + godot_js_rtc_datachannel_max_retransmits_get: function(p_id) { + return IDHandler.get_prop(p_id, 'maxRetransmits', 65535); + }, + + godot_js_rtc_datachannel_is_negotiated: function(p_id, p_def) { + return IDHandler.get_prop(p_id, 'negotiated', 65535); + }, + + godot_js_rtc_datachannel_label_get: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref || !ref.label) { + return 0; + } + return GodotOS.allocString(ref.label); + }, + + godot_js_rtc_datachannel_protocol_get: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref || !ref.protocol) { + return 0; + } + return GodotOS.allocString(ref.protocol); + }, + + godot_js_rtc_datachannel_destroy: function(p_id) { + GodotRTCDataChannel.close(p_id); + IDHandler.remove(p_id); + }, + + godot_js_rtc_datachannel_connect: function(p_id, p_ref, p_on_open, p_on_message, p_on_error, p_on_close) { + const onopen = GodotOS.get_func(p_on_open).bind(null, p_ref); + const onmessage = GodotOS.get_func(p_on_message).bind(null, p_ref); + const onerror = GodotOS.get_func(p_on_error).bind(null, p_ref); + const onclose = GodotOS.get_func(p_on_close).bind(null, p_ref); + GodotRTCDataChannel.connect(p_id, onopen, onmessage, onerror, onclose); + }, + + godot_js_rtc_datachannel_close: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + GodotRTCDataChannel.close(p_id); + }, +}; + +autoAddDeps(GodotRTCDataChannel, '$GodotRTCDataChannel'); +mergeInto(LibraryManager.library, GodotRTCDataChannel); + +var GodotRTCPeerConnection = { + + $GodotRTCPeerConnection__deps: ['$IDHandler', '$GodotOS', '$GodotRTCDataChannel'], + $GodotRTCPeerConnection: { + onstatechange: function(p_id, p_conn, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + var state = 5; // CLOSED + switch(p_conn.iceConnectionState) { + case "new": + state = 0; + case "checking": + state = 1; + case "connected": + case "completed": + state = 2; + case "disconnected": + state = 3; + case "failed": + state = 4; + case "closed": + state = 5; + } + callback(state); + }, + + onicecandidate: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref || !event.candidate) { + return; + } + + let c = event.candidate; + let candidate_str = GodotOS.allocString(c.candidate); + let mid_str = GodotOS.allocString(c.sdpMid); + callback(mid_str, c.sdpMLineIndex, candidate_str); + _free(candidate_str); + _free(mid_str); + }, + + ondatachannel: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + + const cid = IDHandler.add(event.channel); + callback(cid); + }, + + onsession: function(p_id, callback, session) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + let type_str = GodotOS.allocString(session.type); + let sdp_str = GodotOS.allocString(session.sdp); + callback(type_str, sdp_str); + _free(type_str); + _free(sdp_str); + }, + + onerror: function(p_id, callback, error) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + console.error(error); + callback(); + }, + }, + + godot_js_rtc_pc_create: function(p_config, p_ref, p_on_state_change, p_on_candidate, p_on_datachannel) { + const onstatechange = GodotOS.get_func(p_on_state_change).bind(null, p_ref); + const oncandidate = GodotOS.get_func(p_on_candidate).bind(null, p_ref); + const ondatachannel = GodotOS.get_func(p_on_datachannel).bind(null, p_ref); + + var config = JSON.parse(UTF8ToString(p_config)); + var conn = null; + try { + conn = new RTCPeerConnection(config); + } catch (e) { + console.error(e); + return 0; + } + + const base = GodotRTCPeerConnection; + const id = IDHandler.add(conn); + conn.oniceconnectionstatechange = base.onstatechange.bind(null, id, conn, onstatechange); + conn.onicecandidate = base.onicecandidate.bind(null, id, oncandidate); + conn.ondatachannel = base.ondatachannel.bind(null, id, ondatachannel); + return id; + }, + + godot_js_rtc_pc_close: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + ref.close(); + }, + + godot_js_rtc_pc_destroy: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + ref.oniceconnectionstatechange = null; + ref.onicecandidate = null; + ref.ondatachannel = null; + IDHandler.remove(p_id); + }, + + godot_js_rtc_pc_offer_create: function(p_id, p_obj, p_on_session, p_on_error) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + const onsession = GodotOS.get_func(p_on_session).bind(null, p_obj); + const onerror = GodotOS.get_func(p_on_error).bind(null, p_obj); + ref.createOffer().then(function(session) { + GodotRTCPeerConnection.onsession(p_id, onsession, session); + }).catch(function(error) { + GodotRTCPeerConnection.onerror(p_id, onerror, error); + }); + }, + + godot_js_rtc_pc_local_description_set: function(p_id, p_type, p_sdp, p_obj, p_on_error) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + const type = UTF8ToString(p_type); + const sdp = UTF8ToString(p_sdp); + const onerror = GodotOS.get_func(p_on_error).bind(null, p_obj); + ref.setLocalDescription({ + 'sdp': sdp, + 'type': type + }).catch(function(error) { + GodotRTCPeerConnection.onerror(p_id, onerror, error); + }); + }, + + godot_js_rtc_pc_remote_description_set: function(p_id, p_type, p_sdp, p_obj, p_session_created, p_on_error) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + const type = UTF8ToString(p_type); + const sdp = UTF8ToString(p_sdp); + const onerror = GodotOS.get_func(p_on_error).bind(null, p_obj); + const onsession = GodotOS.get_func(p_session_created).bind(null, p_obj); + ref.setRemoteDescription({ + 'sdp': sdp, + 'type': type + }).then(function() { + if (type != 'offer') { + return; + } + return ref.createAnswer().then(function(session) { + GodotRTCPeerConnection.onsession(p_id, onsession, session); + }); + }).catch(function(error) { + GodotRTCPeerConnection.onerror(p_id, onerror, error); + }); + }, + + godot_js_rtc_pc_ice_candidate_add: function(p_id, p_mid_name, p_mline_idx, p_sdp) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + var sdpMidName = UTF8ToString(p_mid_name); + var sdpName = UTF8ToString(p_sdp); + ref.addIceCandidate(new RTCIceCandidate({ + "candidate": sdpName, + "sdpMid": sdpMidName, + "sdpMlineIndex": p_mline_idx, + })); + }, + + godot_js_rtc_pc_datachannel_create__deps: ['$GodotRTCDataChannel'], + godot_js_rtc_pc_datachannel_create: function(p_id, p_label, p_config) { + try { + const ref = IDHandler.get(p_id); + if (!ref) { + return 0; + } + + const label = UTF8ToString(p_label); + const config = JSON.parse(UTF8ToString(p_config)); + + const channel = ref.createDataChannel(label, config); + return IDHandler.add(channel); + } catch (e) { + console.error(e); + return 0; + } + }, +}; + +autoAddDeps(GodotRTCPeerConnection, '$GodotRTCPeerConnection') +mergeInto(LibraryManager.library, GodotRTCPeerConnection); diff --git a/modules/webrtc/webrtc_data_channel_js.cpp b/modules/webrtc/webrtc_data_channel_js.cpp index 2c648ba9f9..3a63001a56 100644 --- a/modules/webrtc/webrtc_data_channel_js.cpp +++ b/modules/webrtc/webrtc_data_channel_js.cpp @@ -34,65 +34,58 @@ #include "emscripten.h" extern "C" { -EMSCRIPTEN_KEEPALIVE void _emrtc_on_ch_error(void *obj) { - WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(obj); - peer->_on_error(); -} +typedef void (*RTCChOnOpen)(void *p_obj); +typedef void (*RTCChOnMessage)(void *p_obj, const uint8_t *p_buffer, int p_size, int p_is_string); +typedef void (*RTCChOnClose)(void *p_obj); +typedef void (*RTCChOnError)(void *p_obj); -EMSCRIPTEN_KEEPALIVE void _emrtc_on_ch_open(void *obj) { - WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(obj); - peer->_on_open(); +extern int godot_js_rtc_datachannel_ready_state_get(int p_id); +extern int godot_js_rtc_datachannel_send(int p_id, const uint8_t *p_buffer, int p_length, int p_raw); +extern int godot_js_rtc_datachannel_is_ordered(int p_id); +extern int godot_js_rtc_datachannel_id_get(int p_id); +extern int godot_js_rtc_datachannel_max_packet_lifetime_get(int p_id); +extern int godot_js_rtc_datachannel_max_retransmits_get(int p_id); +extern int godot_js_rtc_datachannel_is_negotiated(int p_id); +extern char *godot_js_rtc_datachannel_label_get(int p_id); // Must free the returned string. +extern char *godot_js_rtc_datachannel_protocol_get(int p_id); // Must free the returned string. +extern void godot_js_rtc_datachannel_destroy(int p_id); +extern void godot_js_rtc_datachannel_connect(int p_id, void *p_obj, RTCChOnOpen p_on_open, RTCChOnMessage p_on_message, RTCChOnError p_on_error, RTCChOnClose p_on_close); +extern void godot_js_rtc_datachannel_close(int p_id); } -EMSCRIPTEN_KEEPALIVE void _emrtc_on_ch_close(void *obj) { - WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(obj); - peer->_on_close(); +void WebRTCDataChannelJS::_on_open(void *p_obj) { + WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(p_obj); + peer->in_buffer.resize(peer->_in_buffer_shift); } -EMSCRIPTEN_KEEPALIVE void _emrtc_on_ch_message(void *obj, uint8_t *p_data, uint32_t p_size, bool p_is_string) { - WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(obj); - peer->_on_message(p_data, p_size, p_is_string); -} +void WebRTCDataChannelJS::_on_close(void *p_obj) { + WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(p_obj); + peer->close(); } -void WebRTCDataChannelJS::_on_open() { - in_buffer.resize(_in_buffer_shift); +void WebRTCDataChannelJS::_on_error(void *p_obj) { + WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(p_obj); + peer->close(); } -void WebRTCDataChannelJS::_on_close() { - close(); -} +void WebRTCDataChannelJS::_on_message(void *p_obj, const uint8_t *p_data, int p_size, int p_is_string) { + WebRTCDataChannelJS *peer = static_cast<WebRTCDataChannelJS *>(p_obj); + RingBuffer<uint8_t> &in_buffer = peer->in_buffer; -void WebRTCDataChannelJS::_on_error() { - close(); -} - -void WebRTCDataChannelJS::_on_message(uint8_t *p_data, uint32_t p_size, bool p_is_string) { ERR_FAIL_COND_MSG(in_buffer.space_left() < (int)(p_size + 5), "Buffer full! Dropping data."); uint8_t is_string = p_is_string ? 1 : 0; in_buffer.write((uint8_t *)&p_size, 4); in_buffer.write((uint8_t *)&is_string, 1); in_buffer.write(p_data, p_size); - queue_count++; + peer->queue_count++; } void WebRTCDataChannelJS::close() { in_buffer.resize(0); queue_count = 0; _was_string = false; - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - if (!dict) return; - var channel = dict["channel"]; - channel.onopen = null; - channel.onclose = null; - channel.onerror = null; - channel.onmessage = null; - channel.close(); - }, _js_id); - /* clang-format on */ + godot_js_rtc_datachannel_close(_js_id); } Error WebRTCDataChannelJS::poll() { @@ -100,24 +93,7 @@ Error WebRTCDataChannelJS::poll() { } WebRTCDataChannelJS::ChannelState WebRTCDataChannelJS::get_ready_state() const { - /* clang-format off */ - return (ChannelState) EM_ASM_INT({ - var dict = Module.IDHandler.get($0); - if (!dict) return 3; // CLOSED - var channel = dict["channel"]; - switch(channel.readyState) { - case "connecting": - return 0; - case "open": - return 1; - case "closing": - return 2; - case "closed": - return 3; - } - return 3; // CLOSED - }, _js_id); - /* clang-format on */ + return (ChannelState)godot_js_rtc_datachannel_ready_state_get(_js_id); } int WebRTCDataChannelJS::get_available_packet_count() const { @@ -157,27 +133,7 @@ Error WebRTCDataChannelJS::put_packet(const uint8_t *p_buffer, int p_buffer_size ERR_FAIL_COND_V(get_ready_state() != STATE_OPEN, ERR_UNCONFIGURED); int is_bin = _write_mode == WebRTCDataChannel::WRITE_MODE_BINARY ? 1 : 0; - - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var channel = dict["channel"]; - var bytes_array = new Uint8Array($2); - var i = 0; - - for(i=0; i<$2; i++) { - bytes_array[i] = getValue($1+i, 'i8'); - } - - if ($3) { - channel.send(bytes_array.buffer); - } else { - var string = new TextDecoder("utf-8").decode(bytes_array); - channel.send(string); - } - }, _js_id, p_buffer, p_buffer_size, is_bin); - /* clang-format on */ - + godot_js_rtc_datachannel_send(_js_id, p_buffer, p_buffer_size, is_bin); return OK; } @@ -201,46 +157,20 @@ String WebRTCDataChannelJS::get_label() const { return _label; } -/* clang-format off */ -#define _JS_GET(PROP, DEF) \ -EM_ASM_INT({ \ - var dict = Module.IDHandler.get($0); \ - if (!dict || !dict["channel"]) { \ - return DEF; \ - } \ - var out = dict["channel"].PROP; \ - return out === null ? DEF : out; \ -}, _js_id) -/* clang-format on */ - bool WebRTCDataChannelJS::is_ordered() const { - return _JS_GET(ordered, true); + return godot_js_rtc_datachannel_is_ordered(_js_id); } int WebRTCDataChannelJS::get_id() const { - return _JS_GET(id, 65535); + return godot_js_rtc_datachannel_id_get(_js_id); } int WebRTCDataChannelJS::get_max_packet_life_time() const { - // Can't use macro, webkit workaround. - /* clang-format off */ - return EM_ASM_INT({ - var dict = Module.IDHandler.get($0); - if (!dict || !dict["channel"]) { - return 65535; - } - if (dict["channel"].maxRetransmitTime !== undefined) { - // Guess someone didn't appreciate the standardization process. - return dict["channel"].maxRetransmitTime; - } - var out = dict["channel"].maxPacketLifeTime; - return out === null ? 65535 : out; - }, _js_id); - /* clang-format on */ + return godot_js_rtc_datachannel_max_packet_lifetime_get(_js_id); } int WebRTCDataChannelJS::get_max_retransmits() const { - return _JS_GET(maxRetransmits, 65535); + return godot_js_rtc_datachannel_max_retransmits_get(_js_id); } String WebRTCDataChannelJS::get_protocol() const { @@ -248,7 +178,7 @@ String WebRTCDataChannelJS::get_protocol() const { } bool WebRTCDataChannelJS::is_negotiated() const { - return _JS_GET(negotiated, false); + return godot_js_rtc_datachannel_is_negotiated(_js_id); } WebRTCDataChannelJS::WebRTCDataChannelJS() { @@ -264,101 +194,22 @@ WebRTCDataChannelJS::WebRTCDataChannelJS(int js_id) { _write_mode = WRITE_MODE_BINARY; _js_id = js_id; - /* clang-format off */ - EM_ASM({ - var c_ptr = $0; - var dict = Module.IDHandler.get($1); - if (!dict) return; - var channel = dict["channel"]; - dict["ptr"] = c_ptr; - - channel.binaryType = "arraybuffer"; - channel.onopen = function (evt) { - ccall("_emrtc_on_ch_open", - "void", - ["number"], - [c_ptr] - ); - }; - channel.onclose = function (evt) { - ccall("_emrtc_on_ch_close", - "void", - ["number"], - [c_ptr] - ); - }; - channel.onerror = function (evt) { - ccall("_emrtc_on_ch_error", - "void", - ["number"], - [c_ptr] - ); - }; - channel.onmessage = function(event) { - var buffer; - var is_string = 0; - if (event.data instanceof ArrayBuffer) { - buffer = new Uint8Array(event.data); - } else if (event.data instanceof Blob) { - console.error("Blob type not supported"); - return; - } else if (typeof event.data === "string") { - is_string = 1; - var enc = new TextEncoder("utf-8"); - buffer = new Uint8Array(enc.encode(event.data)); - } else { - console.error("Unknown message type"); - return; - } - var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = _malloc(len); - HEAPU8.set(buffer, out); - ccall("_emrtc_on_ch_message", - "void", - ["number", "number", "number", "number"], - [c_ptr, out, len, is_string] - ); - _free(out); - } - - }, this, js_id); + godot_js_rtc_datachannel_connect(js_id, this, &_on_open, &_on_message, &_on_error, &_on_close); // Parse label - char *str; - str = (char *)EM_ASM_INT({ - var dict = Module.IDHandler.get($0); - if (!dict || !dict["channel"]) return 0; - var str = dict["channel"].label; - var len = lengthBytesUTF8(str)+1; - var ptr = _malloc(str); - stringToUTF8(str, ptr, len+1); - return ptr; - }, js_id); - if(str != nullptr) { - _label.parse_utf8(str); - EM_ASM({ _free($0) }, str); + char *label = godot_js_rtc_datachannel_label_get(js_id); + if (label) { + _label.parse_utf8(label); + free(label); } - str = (char *)EM_ASM_INT({ - var dict = Module.IDHandler.get($0); - if (!dict || !dict["channel"]) return 0; - var str = dict["channel"].protocol; - var len = lengthBytesUTF8(str)+1; - var ptr = _malloc(str); - stringToUTF8(str, ptr, len+1); - return ptr; - }, js_id); - if(str != nullptr) { - _protocol.parse_utf8(str); - EM_ASM({ _free($0) }, str); + char *protocol = godot_js_rtc_datachannel_protocol_get(js_id); + if (protocol) { + _protocol.parse_utf8(protocol); + free(protocol); } - /* clang-format on */ } WebRTCDataChannelJS::~WebRTCDataChannelJS() { close(); - /* clang-format off */ - EM_ASM({ - Module.IDHandler.remove($0); - }, _js_id); - /* clang-format on */ -}; + godot_js_rtc_datachannel_destroy(_js_id); +} #endif diff --git a/modules/webrtc/webrtc_data_channel_js.h b/modules/webrtc/webrtc_data_channel_js.h index 7545910e66..e251760019 100644 --- a/modules/webrtc/webrtc_data_channel_js.h +++ b/modules/webrtc/webrtc_data_channel_js.h @@ -54,12 +54,12 @@ private: int queue_count; uint8_t packet_buffer[PACKET_BUFFER_SIZE]; -public: - void _on_open(); - void _on_close(); - void _on_error(); - void _on_message(uint8_t *p_data, uint32_t p_size, bool p_is_string); + static void _on_open(void *p_obj); + static void _on_close(void *p_obj); + static void _on_error(void *p_obj); + static void _on_message(void *p_obj, const uint8_t *p_data, int p_size, int p_is_string); +public: virtual void set_write_mode(WriteMode mode) override; virtual WriteMode get_write_mode() const override; virtual bool was_string_packet() const override; diff --git a/modules/webrtc/webrtc_peer_connection_js.cpp b/modules/webrtc/webrtc_peer_connection_js.cpp index 593c3a5162..ad9b46a8af 100644 --- a/modules/webrtc/webrtc_peer_connection_js.cpp +++ b/modules/webrtc/webrtc_peer_connection_js.cpp @@ -37,116 +37,32 @@ #include "core/io/json.h" #include "emscripten.h" -extern "C" { -EMSCRIPTEN_KEEPALIVE void _emrtc_on_ice_candidate(void *obj, char *p_MidName, int p_MlineIndexName, char *p_sdpName) { - WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(obj); - peer->emit_signal("ice_candidate_created", String(p_MidName), p_MlineIndexName, String(p_sdpName)); +void WebRTCPeerConnectionJS::_on_ice_candidate(void *p_obj, const char *p_mid_name, int p_mline_idx, const char *p_candidate) { + WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(p_obj); + peer->emit_signal("ice_candidate_created", String(p_mid_name), p_mline_idx, String(p_candidate)); } -EMSCRIPTEN_KEEPALIVE void _emrtc_session_description_created(void *obj, char *p_type, char *p_offer) { - WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(obj); - peer->emit_signal("session_description_created", String(p_type), String(p_offer)); +void WebRTCPeerConnectionJS::_on_session_created(void *p_obj, const char *p_type, const char *p_session) { + WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(p_obj); + peer->emit_signal("session_description_created", String(p_type), String(p_session)); } -EMSCRIPTEN_KEEPALIVE void _emrtc_on_connection_state_changed(void *obj) { - WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(obj); - peer->_on_connection_state_changed(); +void WebRTCPeerConnectionJS::_on_connection_state_changed(void *p_obj, int p_state) { + WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(p_obj); + peer->_conn_state = (ConnectionState)p_state; } -EMSCRIPTEN_KEEPALIVE void _emrtc_on_error() { +void WebRTCPeerConnectionJS::_on_error(void *p_obj) { ERR_PRINT("RTCPeerConnection error!"); } -EMSCRIPTEN_KEEPALIVE void _emrtc_emit_channel(void *obj, int p_id) { - WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(obj); +void WebRTCPeerConnectionJS::_on_data_channel(void *p_obj, int p_id) { + WebRTCPeerConnectionJS *peer = static_cast<WebRTCPeerConnectionJS *>(p_obj); peer->emit_signal("data_channel_received", Ref<WebRTCDataChannelJS>(new WebRTCDataChannelJS(p_id))); } -} - -void _emrtc_create_pc(int p_id, const Dictionary &p_config) { - String config = JSON::print(p_config); - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var c_ptr = dict["ptr"]; - var config = JSON.parse(UTF8ToString($1)); - // Setup local connaction - var conn = null; - try { - conn = new RTCPeerConnection(config); - } catch (e) { - console.log(e); - return; - } - conn.oniceconnectionstatechange = function(event) { - if (!Module.IDHandler.get($0)) return; - ccall("_emrtc_on_connection_state_changed", "void", ["number"], [c_ptr]); - }; - conn.onicecandidate = function(event) { - if (!Module.IDHandler.get($0)) return; - if (!event.candidate) return; - - var c = event.candidate; - // should emit on ice candidate - ccall("_emrtc_on_ice_candidate", - "void", - ["number", "string", "number", "string"], - [c_ptr, c.sdpMid, c.sdpMLineIndex, c.candidate] - ); - }; - conn.ondatachannel = function (evt) { - var dict = Module.IDHandler.get($0); - if (!dict) { - return; - } - var id = Module.IDHandler.add({"channel": evt.channel, "ptr": null}); - ccall("_emrtc_emit_channel", - "void", - ["number", "number"], - [c_ptr, id] - ); - }; - dict["conn"] = conn; - }, p_id, config.utf8().get_data()); - /* clang-format on */ -} - -void WebRTCPeerConnectionJS::_on_connection_state_changed() { - /* clang-format off */ - _conn_state = (ConnectionState)EM_ASM_INT({ - var dict = Module.IDHandler.get($0); - if (!dict) return 5; // CLOSED - var conn = dict["conn"]; - switch(conn.iceConnectionState) { - case "new": - return 0; - case "checking": - return 1; - case "connected": - case "completed": - return 2; - case "disconnected": - return 3; - case "failed": - return 4; - case "closed": - return 5; - } - return 5; // CLOSED - }, _js_id); - /* clang-format on */ -} void WebRTCPeerConnectionJS::close() { - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - if (!dict) return; - if (dict["conn"]) { - dict["conn"].close(); - } - }, _js_id); - /* clang-format on */ + godot_js_rtc_pc_close(_js_id); _conn_state = STATE_CLOSED; } @@ -154,46 +70,12 @@ Error WebRTCPeerConnectionJS::create_offer() { ERR_FAIL_COND_V(_conn_state != STATE_NEW, FAILED); _conn_state = STATE_CONNECTING; - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var conn = dict["conn"]; - var c_ptr = dict["ptr"]; - var onError = function(error) { - console.error(error); - ccall("_emrtc_on_error", "void", [], []); - }; - var onCreated = function(offer) { - ccall("_emrtc_session_description_created", - "void", - ["number", "string", "string"], - [c_ptr, offer.type, offer.sdp] - ); - }; - conn.createOffer().then(onCreated).catch(onError); - }, _js_id); - /* clang-format on */ + godot_js_rtc_pc_offer_create(_js_id, this, &_on_session_created, &_on_error); return OK; } Error WebRTCPeerConnectionJS::set_local_description(String type, String sdp) { - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var conn = dict["conn"]; - var c_ptr = dict["ptr"]; - var type = UTF8ToString($1); - var sdp = UTF8ToString($2); - var onError = function(error) { - console.error(error); - ccall("_emrtc_on_error", "void", [], []); - }; - conn.setLocalDescription({ - "sdp": sdp, - "type": type - }).catch(onError); - }, _js_id, type.utf8().get_data(), sdp.utf8().get_data()); - /* clang-format on */ + godot_js_rtc_pc_local_description_set(_js_id, type.utf8().get_data(), sdp.utf8().get_data(), this, &_on_error); return OK; } @@ -202,83 +84,32 @@ Error WebRTCPeerConnectionJS::set_remote_description(String type, String sdp) { ERR_FAIL_COND_V(_conn_state != STATE_NEW, FAILED); _conn_state = STATE_CONNECTING; } - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var conn = dict["conn"]; - var c_ptr = dict["ptr"]; - var type = UTF8ToString($1); - var sdp = UTF8ToString($2); - - var onError = function(error) { - console.error(error); - ccall("_emrtc_on_error", "void", [], []); - }; - var onCreated = function(offer) { - ccall("_emrtc_session_description_created", - "void", - ["number", "string", "string"], - [c_ptr, offer.type, offer.sdp] - ); - }; - var onSet = function() { - if (type != "offer") { - return; - } - conn.createAnswer().then(onCreated); - }; - conn.setRemoteDescription({ - "sdp": sdp, - "type": type - }).then(onSet).catch(onError); - }, _js_id, type.utf8().get_data(), sdp.utf8().get_data()); - /* clang-format on */ + godot_js_rtc_pc_remote_description_set(_js_id, type.utf8().get_data(), sdp.utf8().get_data(), this, &_on_session_created, &_on_error); return OK; } Error WebRTCPeerConnectionJS::add_ice_candidate(String sdpMidName, int sdpMlineIndexName, String sdpName) { - /* clang-format off */ - EM_ASM({ - var dict = Module.IDHandler.get($0); - var conn = dict["conn"]; - var c_ptr = dict["ptr"]; - var sdpMidName = UTF8ToString($1); - var sdpMlineIndexName = UTF8ToString($2); - var sdpName = UTF8ToString($3); - conn.addIceCandidate(new RTCIceCandidate({ - "candidate": sdpName, - "sdpMid": sdpMidName, - "sdpMlineIndex": sdpMlineIndexName - })); - }, _js_id, sdpMidName.utf8().get_data(), sdpMlineIndexName, sdpName.utf8().get_data()); - /* clang-format on */ + godot_js_rtc_pc_ice_candidate_add(_js_id, sdpMidName.utf8().get_data(), sdpMlineIndexName, sdpName.utf8().get_data()); return OK; } Error WebRTCPeerConnectionJS::initialize(Dictionary p_config) { - _emrtc_create_pc(_js_id, p_config); - return OK; + if (_js_id) { + godot_js_rtc_pc_destroy(_js_id); + _js_id = 0; + } + _conn_state = STATE_NEW; + + String config = JSON::print(p_config); + _js_id = godot_js_rtc_pc_create(config.utf8().get_data(), this, &_on_connection_state_changed, &_on_ice_candidate, &_on_data_channel); + return _js_id ? OK : FAILED; } Ref<WebRTCDataChannel> WebRTCPeerConnectionJS::create_data_channel(String p_channel, Dictionary p_channel_config) { + ERR_FAIL_COND_V(_conn_state != STATE_NEW, nullptr); + String config = JSON::print(p_channel_config); - /* clang-format off */ - int id = EM_ASM_INT({ - try { - var dict = Module.IDHandler.get($0); - if (!dict) return 0; - var label = UTF8ToString($1); - var config = JSON.parse(UTF8ToString($2)); - var conn = dict["conn"]; - return Module.IDHandler.add({ - "channel": conn.createDataChannel(label, config), - "ptr": null - }) - } catch (e) { - return 0; - } - }, _js_id, p_channel.utf8().get_data(), config.utf8().get_data()); - /* clang-format on */ + int id = godot_js_rtc_pc_datachannel_create(_js_id, p_channel.utf8().get_data(), config.utf8().get_data()); ERR_FAIL_COND_V(id == 0, nullptr); return memnew(WebRTCDataChannelJS(id)); } @@ -293,22 +124,17 @@ WebRTCPeerConnection::ConnectionState WebRTCPeerConnectionJS::get_connection_sta WebRTCPeerConnectionJS::WebRTCPeerConnectionJS() { _conn_state = STATE_NEW; + _js_id = 0; - /* clang-format off */ - _js_id = EM_ASM_INT({ - return Module.IDHandler.add({"conn": null, "ptr": $0}); - }, this); - /* clang-format on */ Dictionary config; - _emrtc_create_pc(_js_id, config); + initialize(config); } WebRTCPeerConnectionJS::~WebRTCPeerConnectionJS() { close(); - /* clang-format off */ - EM_ASM({ - Module.IDHandler.remove($0); - }, _js_id); - /* clang-format on */ + if (_js_id) { + godot_js_rtc_pc_destroy(_js_id); + _js_id = 0; + } }; #endif diff --git a/modules/webrtc/webrtc_peer_connection_js.h b/modules/webrtc/webrtc_peer_connection_js.h index cdaf3068e3..e33dd5f259 100644 --- a/modules/webrtc/webrtc_peer_connection_js.h +++ b/modules/webrtc/webrtc_peer_connection_js.h @@ -35,16 +35,37 @@ #include "webrtc_peer_connection.h" +extern "C" { +typedef void (*RTCOnIceConnectionStateChange)(void *p_obj, int p_state); +typedef void (*RTCOnIceCandidate)(void *p_obj, const char *p_mid, int p_mline_idx, const char *p_candidate); +typedef void (*RTCOnDataChannel)(void *p_obj, int p_id); +typedef void (*RTCOnSession)(void *p_obj, const char *p_type, const char *p_sdp); +typedef void (*RTCOnError)(void *p_obj); +extern int godot_js_rtc_pc_create(const char *p_config, void *p_obj, RTCOnIceConnectionStateChange p_on_state_change, RTCOnIceCandidate p_on_candidate, RTCOnDataChannel p_on_datachannel); +extern void godot_js_rtc_pc_close(int p_id); +extern void godot_js_rtc_pc_destroy(int p_id); +extern void godot_js_rtc_pc_offer_create(int p_id, void *p_obj, RTCOnSession p_on_session, RTCOnError p_on_error); +extern void godot_js_rtc_pc_local_description_set(int p_id, const char *p_type, const char *p_sdp, void *p_obj, RTCOnError p_on_error); +extern void godot_js_rtc_pc_remote_description_set(int p_id, const char *p_type, const char *p_sdp, void *p_obj, RTCOnSession p_on_session, RTCOnError p_on_error); +extern void godot_js_rtc_pc_ice_candidate_add(int p_id, const char *p_mid_name, int p_mline_idx, const char *p_sdo); +extern int godot_js_rtc_pc_datachannel_create(int p_id, const char *p_label, const char *p_config); +} + class WebRTCPeerConnectionJS : public WebRTCPeerConnection { private: int _js_id; ConnectionState _conn_state; + static void _on_connection_state_changed(void *p_obj, int p_state); + static void _on_ice_candidate(void *p_obj, const char *p_mid_name, int p_mline_idx, const char *p_candidate); + static void _on_data_channel(void *p_obj, int p_channel); + static void _on_session_created(void *p_obj, const char *p_type, const char *p_session); + static void _on_error(void *p_obj); + public: static WebRTCPeerConnection *_create() { return memnew(WebRTCPeerConnectionJS); } static void make_default() { WebRTCPeerConnection::_create = WebRTCPeerConnectionJS::_create; } - void _on_connection_state_changed(); virtual ConnectionState get_connection_state() const; virtual Error initialize(Dictionary configuration = Dictionary()); diff --git a/modules/websocket/SCsub b/modules/websocket/SCsub index af60055855..13e51a39c0 100644 --- a/modules/websocket/SCsub +++ b/modules/websocket/SCsub @@ -3,11 +3,13 @@ Import("env") Import("env_modules") -# Thirdparty source files - env_ws = env_modules.Clone() -if env["builtin_wslay"] and not env["platform"] == "javascript": # already builtin for javascript +if env["platform"] == "javascript": + # Our JavaScript/C++ interface. + env.AddJSLibraries(["library_godot_websocket.js"]) +elif env["builtin_wslay"]: + # Thirdparty source files wslay_dir = "#thirdparty/wslay/" wslay_sources = [ "wslay_net.c", diff --git a/modules/websocket/emws_client.cpp b/modules/websocket/emws_client.cpp index 93d60dca08..d6e00a26af 100644 --- a/modules/websocket/emws_client.cpp +++ b/modules/websocket/emws_client.cpp @@ -35,14 +35,13 @@ #include "core/io/ip.h" #include "emscripten.h" -extern "C" { -EMSCRIPTEN_KEEPALIVE void _esws_on_connect(void *obj, char *proto) { +void EMWSClient::_esws_on_connect(void *obj, char *proto) { EMWSClient *client = static_cast<EMWSClient *>(obj); client->_is_connecting = false; client->_on_connect(String(proto)); } -EMSCRIPTEN_KEEPALIVE void _esws_on_message(void *obj, uint8_t *p_data, int p_data_size, int p_is_string) { +void EMWSClient::_esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string) { EMWSClient *client = static_cast<EMWSClient *>(obj); Error err = static_cast<EMWSPeer *>(*client->get_peer(1))->read_msg(p_data, p_data_size, p_is_string == 1); @@ -50,21 +49,26 @@ EMSCRIPTEN_KEEPALIVE void _esws_on_message(void *obj, uint8_t *p_data, int p_dat client->_on_peer_packet(); } -EMSCRIPTEN_KEEPALIVE void _esws_on_error(void *obj) { +void EMWSClient::_esws_on_error(void *obj) { EMWSClient *client = static_cast<EMWSClient *>(obj); client->_is_connecting = false; client->_on_error(); } -EMSCRIPTEN_KEEPALIVE void _esws_on_close(void *obj, int code, char *reason, int was_clean) { +void EMWSClient::_esws_on_close(void *obj, int code, const char *reason, int was_clean) { EMWSClient *client = static_cast<EMWSClient *>(obj); client->_on_close_request(code, String(reason)); client->_is_connecting = false; + client->disconnect_from_host(); client->_on_disconnect(was_clean != 0); } -} Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, const Vector<String> p_protocols, const Vector<String> p_custom_headers) { + if (_js_id) { + godot_js_websocket_destroy(_js_id); + _js_id = 0; + } + String proto_string; for (int i = 0; i < p_protocols.size(); i++) { if (i != 0) @@ -84,106 +88,17 @@ Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, } } str += p_host + ":" + itos(p_port) + p_path; - _is_connecting = true; - /* clang-format off */ - int peer_sock = EM_ASM_INT({ - var proto_str = UTF8ToString($2); - var socket = null; - try { - if (proto_str) { - socket = new WebSocket(UTF8ToString($1), proto_str.split(",")); - } else { - socket = new WebSocket(UTF8ToString($1)); - } - } catch (e) { - return -1; - } - var c_ptr = Module.IDHandler.get($0); - socket.binaryType = "arraybuffer"; - - // Connection opened - socket.addEventListener("open", function (event) { - if (!Module.IDHandler.has($0)) - return; // Godot Object is gone! - ccall("_esws_on_connect", - "void", - ["number", "string"], - [c_ptr, socket.protocol] - ); - }); - - // Listen for messages - socket.addEventListener("message", function (event) { - if (!Module.IDHandler.has($0)) - return; // Godot Object is gone! - var buffer; - var is_string = 0; - if (event.data instanceof ArrayBuffer) { - - buffer = new Uint8Array(event.data); - - } else if (event.data instanceof Blob) { - - alert("Blob type not supported"); - return; - - } else if (typeof event.data === "string") { - - is_string = 1; - var enc = new TextEncoder("utf-8"); - buffer = new Uint8Array(enc.encode(event.data)); - - } else { - - alert("Unknown message type"); - return; - - } - var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = _malloc(len); - HEAPU8.set(buffer, out); - ccall("_esws_on_message", - "void", - ["number", "number", "number", "number"], - [c_ptr, out, len, is_string] - ); - _free(out); - }); - - socket.addEventListener("error", function (event) { - if (!Module.IDHandler.has($0)) - return; // Godot Object is gone! - ccall("_esws_on_error", - "void", - ["number"], - [c_ptr] - ); - }); - - socket.addEventListener("close", function (event) { - if (!Module.IDHandler.has($0)) - return; // Godot Object is gone! - var was_clean = 0; - if (event.wasClean) - was_clean = 1; - ccall("_esws_on_close", - "void", - ["number", "number", "string", "number"], - [c_ptr, event.code, event.reason, was_clean] - ); - }); - - return Module.IDHandler.add(socket); - }, _js_id, str.utf8().get_data(), proto_string.utf8().get_data()); - /* clang-format on */ - if (peer_sock == -1) + + _js_id = godot_js_websocket_create(this, str.utf8().get_data(), proto_string.utf8().get_data(), &_esws_on_connect, &_esws_on_message, &_esws_on_error, &_esws_on_close); + if (!_js_id) { return FAILED; + } - static_cast<Ref<EMWSPeer>>(_peer)->set_sock(peer_sock, _in_buf_size, _in_pkt_size); + static_cast<Ref<EMWSPeer>>(_peer)->set_sock(_js_id, _in_buf_size, _in_pkt_size); return OK; -}; +} void EMWSClient::poll() { } @@ -200,19 +115,19 @@ NetworkedMultiplayerPeer::ConnectionStatus EMWSClient::get_connection_status() c } return CONNECTION_DISCONNECTED; -}; +} void EMWSClient::disconnect_from_host(int p_code, String p_reason) { _peer->close(p_code, p_reason); -}; +} IP_Address EMWSClient::get_connected_host() const { ERR_FAIL_V_MSG(IP_Address(), "Not supported in HTML5 export."); -}; +} uint16_t EMWSClient::get_connected_port() const { ERR_FAIL_V_MSG(0, "Not supported in HTML5 export."); -}; +} int EMWSClient::get_max_packet_size() const { return (1 << _in_buf_size) - PROTO_SIZE; @@ -227,24 +142,17 @@ Error EMWSClient::set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffe EMWSClient::EMWSClient() { _in_buf_size = DEF_BUF_SHIFT; _in_pkt_size = DEF_PKT_SHIFT; - _is_connecting = false; _peer = Ref<EMWSPeer>(memnew(EMWSPeer)); - /* clang-format off */ - _js_id = EM_ASM_INT({ - return Module.IDHandler.add($0); - }, this); - /* clang-format on */ -}; + _js_id = 0; +} EMWSClient::~EMWSClient() { disconnect_from_host(); _peer = Ref<EMWSPeer>(); - /* clang-format off */ - EM_ASM({ - Module.IDHandler.remove($0); - }, _js_id); - /* clang-format on */ -}; + if (_js_id) { + godot_js_websocket_destroy(_js_id); + } +} #endif // JAVASCRIPT_ENABLED diff --git a/modules/websocket/emws_client.h b/modules/websocket/emws_client.h index 58973c52fa..0123c37457 100644 --- a/modules/websocket/emws_client.h +++ b/modules/websocket/emws_client.h @@ -41,13 +41,17 @@ class EMWSClient : public WebSocketClient { GDCIIMPL(EMWSClient, WebSocketClient); private: + int _js_id; + bool _is_connecting; int _in_buf_size; int _in_pkt_size; - int _js_id; -public: - bool _is_connecting; + static void _esws_on_connect(void *obj, char *proto); + static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); + static void _esws_on_error(void *obj); + static void _esws_on_close(void *obj, int code, const char *reason, int was_clean); +public: Error set_buffers(int p_in_buffer, int p_in_packets, int p_out_buffer, int p_out_packets); Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, const Vector<String> p_protocol = Vector<String>(), const Vector<String> p_custom_headers = Vector<String>()); Ref<WebSocketPeer> get_peer(int p_peer_id) const; diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index 749f45451a..5dcfba5567 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -47,38 +47,14 @@ EMWSPeer::WriteMode EMWSPeer::get_write_mode() const { return write_mode; } -Error EMWSPeer::read_msg(uint8_t *p_data, uint32_t p_size, bool p_is_string) { +Error EMWSPeer::read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string) { uint8_t is_string = p_is_string ? 1 : 0; return _in_buffer.write_packet(p_data, p_size, &is_string); } Error EMWSPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) { int is_bin = write_mode == WebSocketPeer::WRITE_MODE_BINARY ? 1 : 0; - - /* clang-format off */ - EM_ASM({ - var sock = Module.IDHandler.get($0); - var bytes_array = new Uint8Array($2); - var i = 0; - - for(i=0; i<$2; i++) { - bytes_array[i] = getValue($1+i, 'i8'); - } - - try { - if ($3) { - sock.send(bytes_array.buffer); - } else { - var string = new TextDecoder("utf-8").decode(bytes_array); - sock.send(string); - } - } catch (e) { - return 1; - } - return 0; - }, peer_sock, p_buffer, p_buffer_size, is_bin); - /* clang-format on */ - + godot_js_websocket_send(peer_sock, p_buffer, p_buffer_size, is_bin); return OK; }; @@ -110,15 +86,7 @@ bool EMWSPeer::is_connected_to_host() const { void EMWSPeer::close(int p_code, String p_reason) { if (peer_sock != -1) { - /* clang-format off */ - EM_ASM({ - var sock = Module.IDHandler.get($0); - var code = $1; - var reason = UTF8ToString($2); - sock.close(code, reason); - Module.IDHandler.remove($0); - }, peer_sock, p_code, p_reason.utf8().get_data()); - /* clang-format on */ + godot_js_websocket_close(peer_sock, p_code, p_reason.utf8().get_data()); } _is_string = 0; _in_buffer.clear(); diff --git a/modules/websocket/emws_peer.h b/modules/websocket/emws_peer.h index c94d7e9148..2291a32bbc 100644 --- a/modules/websocket/emws_peer.h +++ b/modules/websocket/emws_peer.h @@ -40,6 +40,18 @@ #include "packet_buffer.h" #include "websocket_peer.h" +extern "C" { +typedef void (*WSOnOpen)(void *p_ref, char *p_protocol); +typedef void (*WSOnMessage)(void *p_ref, const uint8_t *p_buf, int p_buf_len, int p_is_string); +typedef void (*WSOnClose)(void *p_ref, int p_code, const char *p_reason, int p_is_clean); +typedef void (*WSOnError)(void *p_ref); + +extern int godot_js_websocket_create(void *p_ref, const char *p_url, const char *p_proto, WSOnOpen p_on_open, WSOnMessage p_on_message, WSOnError p_on_error, WSOnClose p_on_close); +extern int godot_js_websocket_send(int p_id, const uint8_t *p_buf, int p_buf_len, int p_raw); +extern void godot_js_websocket_close(int p_id, int p_code, const char *p_reason); +extern void godot_js_websocket_destroy(int p_id); +} + class EMWSPeer : public WebSocketPeer { GDCIIMPL(EMWSPeer, WebSocketPeer); @@ -52,7 +64,7 @@ private: uint8_t _is_string; public: - Error read_msg(uint8_t *p_data, uint32_t p_size, bool p_is_string); + Error read_msg(const uint8_t *p_data, uint32_t p_size, bool p_is_string); void set_sock(int p_sock, unsigned int p_in_buf_size, unsigned int p_in_pkt_size); virtual int get_available_packet_count() const; virtual Error get_packet(const uint8_t **r_buffer, int &r_buffer_size); diff --git a/modules/websocket/library_godot_websocket.js b/modules/websocket/library_godot_websocket.js new file mode 100644 index 0000000000..f7d3195807 --- /dev/null +++ b/modules/websocket/library_godot_websocket.js @@ -0,0 +1,187 @@ +/*************************************************************************/ +/* library_godot_websocket.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +var GodotWebSocket = { + + // Our socket implementation that forwards events to C++. + $GodotWebSocket__deps: ['$IDHandler'], + $GodotWebSocket: { + // Connection opened, report selected protocol + _onopen: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; // Godot object is gone. + } + let c_str = GodotOS.allocString(ref.protocol); + callback(c_str); + _free(c_str); + }, + + // Message received, report content and type (UTF8 vs binary) + _onmessage: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; // Godot object is gone. + } + var buffer; + var is_string = 0; + if (event.data instanceof ArrayBuffer) { + buffer = new Uint8Array(event.data); + } else if (event.data instanceof Blob) { + alert("Blob type not supported"); + return; + } else if (typeof event.data === "string") { + is_string = 1; + var enc = new TextEncoder("utf-8"); + buffer = new Uint8Array(enc.encode(event.data)); + } else { + alert("Unknown message type"); + return; + } + var len = buffer.length*buffer.BYTES_PER_ELEMENT; + var out = _malloc(len); + HEAPU8.set(buffer, out); + callback(out, len, is_string); + _free(out); + }, + + // An error happened, 'onclose' will be called after this. + _onerror: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; // Godot object is gone. + } + callback(); + }, + + // Connection is closed, this is always fired. Report close code, reason, and clean status. + _onclose: function(p_id, callback, event) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; // Godot object is gone. + } + let c_str = GodotOS.allocString(event.reason); + callback(event.code, c_str, event.wasClean ? 1 : 0); + _free(c_str); + }, + + // Send a message + send: function(p_id, p_data) { + const ref = IDHandler.get(p_id); + if (!ref || ref.readyState != ref.OPEN) { + return 1; // Godot object is gone or socket is not in a ready state. + } + ref.send(p_data); + return 0; + }, + + create: function(socket, p_on_open, p_on_message, p_on_error, p_on_close) { + const id = IDHandler.add(socket); + socket.onopen = GodotWebSocket._onopen.bind(null, id, p_on_open); + socket.onmessage = GodotWebSocket._onmessage.bind(null, id, p_on_message); + socket.onerror = GodotWebSocket._onerror.bind(null, id, p_on_error); + socket.onclose = GodotWebSocket._onclose.bind(null, id, p_on_close); + return id; + }, + + // Closes the JavaScript WebSocket (if not already closing) associated to a given C++ object. + close: function(p_id, p_code, p_reason) { + const ref = IDHandler.get(p_id); + if (ref && ref.readyState < ref.CLOSING) { + const code = p_code; + const reason = UTF8ToString(p_reason); + ref.close(code, reason); + } + }, + + // Deletes the reference to a C++ object (closing any connected socket if necessary). + destroy: function(p_id) { + const ref = IDHandler.get(p_id); + if (!ref) { + return; + } + GodotWebSocket.close(p_id, 1001, ''); + IDHandler.remove(p_id); + ref.onopen = null; + ref.onmessage = null; + ref.onerror = null; + ref.onclose = null; + }, + }, + + godot_js_websocket_create: function(p_ref, p_url, p_proto, p_on_open, p_on_message, p_on_error, p_on_close) { + const on_open = GodotOS.get_func(p_on_open).bind(null, p_ref); + const on_message = GodotOS.get_func(p_on_message).bind(null, p_ref); + const on_error = GodotOS.get_func(p_on_error).bind(null, p_ref); + const on_close = GodotOS.get_func(p_on_close).bind(null, p_ref); + const url = UTF8ToString(p_url); + const protos = UTF8ToString(p_proto); + var socket = null; + try { + if (protos) { + socket = new WebSocket(url, protos.split(",")); + } else { + socket = new WebSocket(url); + } + } catch (e) { + return 0; + } + socket.binaryType = "arraybuffer"; + return GodotWebSocket.create(socket, on_open, on_message, on_error, on_close); + }, + + godot_js_websocket_send: function(p_id, p_buf, p_buf_len, p_raw) { + var bytes_array = new Uint8Array(p_buf_len); + var i = 0; + for(i = 0; i < p_buf_len; i++) { + bytes_array[i] = getValue(p_buf + i, 'i8'); + } + var out = bytes_array; + if (p_raw) { + out = bytes_array.buffer; + } else { + out = new TextDecoder("utf-8").decode(bytes_array); + } + return GodotWebSocket.send(p_id, out); + }, + + godot_js_websocket_close: function(p_id, p_code, p_reason) { + const code = p_code; + const reason = UTF8ToString(p_reason); + GodotWebSocket.close(p_id, code, reason); + }, + + godot_js_websocket_destroy: function(p_id) { + GodotWebSocket.destroy(p_id); + }, +}; + +autoAddDeps(GodotWebSocket, '$GodotWebSocket') +mergeInto(LibraryManager.library, GodotWebSocket); diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 7381ea13b7..f26b314a51 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -18,21 +18,22 @@ if env["threads_enabled"]: build = env.add_program(build_targets, javascript_files) -js_libraries = [ - "native/http_request.js", - "native/library_godot_audio.js", -] -for lib in js_libraries: - env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) -env.Depends(build, js_libraries) +env.AddJSLibraries( + [ + "native/http_request.js", + "native/library_godot_audio.js", + "native/library_godot_display.js", + "native/library_godot_os.js", + ] +) -js_pre = [ - "native/id_handler.js", - "native/utils.js", -] -for js in js_pre: - env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) -env.Depends(build, js_pre) +if env["tools"]: + env.AddJSLibraries(["native/library_godot_editor_tools.js"]) +if env["javascript_eval"]: + env.AddJSLibraries(["native/library_godot_eval.js"]) +for lib in env["JS_LIBS"]: + env.Append(LINKFLAGS=["--js-library", lib]) +env.Depends(build, env["JS_LIBS"]) engine = [ "engine/preloader.js", diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index d44e8139a1..8d781703ed 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -39,6 +39,11 @@ #include <emscripten/emscripten.h> +// JavaScript functions defined in library_godot_editor_tools.js +extern "C" { +extern void godot_js_editor_download_file(const char *p_path, const char *p_name, const char *p_mime); +} + static void _javascript_editor_init_callback() { EditorNode::get_singleton()->add_editor_plugin(memnew(JavaScriptToolsEditorPlugin(EditorNode::get_singleton()))); } @@ -65,25 +70,7 @@ void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, NULL); - EM_ASM({ - const path = "/tmp/project.zip"; - const size = FS.stat(path)["size"]; - const buf = new Uint8Array(size); - const fd = FS.open(path, "r"); - FS.read(fd, buf, 0, size); - FS.close(fd); - FS.unlink(path); - const blob = new Blob([buf], { type: "application/zip" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "project.zip"; - a.style.display = "none"; - document.body.appendChild(a); - a.click(); - a.remove(); - window.URL.revokeObjectURL(url); - }); + godot_js_editor_download_file("/tmp/project.zip", "project.zip", "application/zip"); } void JavaScriptToolsEditorPlugin::_bind_methods() { diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index ab6ed54cc8..2535ad1812 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -55,33 +55,33 @@ void AudioDriverJavaScript::_audio_thread_func(void *p_data) { OS::get_singleton()->delay_usec(1000); // Give the browser some slack. continue; } - obj->_js_driver_process(); + obj->_audio_driver_process(); obj->needs_process = false; obj->unlock(); } } #endif -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_start() { +void AudioDriverJavaScript::_audio_driver_process_start() { #ifndef NO_THREADS - AudioDriverJavaScript::singleton->lock(); + singleton->lock(); #else - AudioDriverJavaScript::singleton->_js_driver_process(); + singleton->_audio_driver_process(); #endif } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_end() { +void AudioDriverJavaScript::_audio_driver_process_end() { #ifndef NO_THREADS - AudioDriverJavaScript::singleton->needs_process = true; - AudioDriverJavaScript::singleton->unlock(); + singleton->needs_process = true; + singleton->unlock(); #endif } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { - AudioDriverJavaScript::singleton->process_capture(sample); +void AudioDriverJavaScript::_audio_driver_process_capture(float p_sample) { + singleton->process_capture(p_sample); } -void AudioDriverJavaScript::_js_driver_process() { +void AudioDriverJavaScript::_audio_driver_process() { int sample_count = memarr_len(internal_buffer) / channel_count; int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); audio_server_process(sample_count, stream_buffer); @@ -122,7 +122,7 @@ void AudioDriverJavaScript::start() { #ifndef NO_THREADS thread = Thread::create(_audio_thread_func, this); #endif - godot_audio_start(internal_buffer); + godot_audio_start(internal_buffer, &_audio_driver_process_start, &_audio_driver_process_end, &_audio_driver_process_capture); } void AudioDriverJavaScript::resume() { @@ -153,18 +153,12 @@ void AudioDriverJavaScript::unlock() { #endif } -void AudioDriverJavaScript::finish_async() { -#ifndef NO_THREADS - quit = true; // Ask thread to quit. -#endif - godot_audio_finish_async(); -} - void AudioDriverJavaScript::finish() { #ifndef NO_THREADS + quit = true; // Ask thread to quit. Thread::wait_to_finish(thread); memdelete(thread); - thread = NULL; + thread = nullptr; #endif if (internal_buffer) { memdelete_arr(internal_buffer); @@ -173,13 +167,13 @@ void AudioDriverJavaScript::finish() { } Error AudioDriverJavaScript::capture_start() { - godot_audio_capture_stop(); input_buffer_init(buffer_length); godot_audio_capture_start(); return OK; } Error AudioDriverJavaScript::capture_stop() { + godot_audio_capture_stop(); input_buffer.clear(); return OK; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 56a7da0307..60ad6480c9 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -44,7 +44,6 @@ private: int mix_rate = 0; int channel_count = 0; -public: #ifndef NO_THREADS Mutex mutex; Thread *thread = nullptr; @@ -53,9 +52,12 @@ public: static void _audio_thread_func(void *p_data); #endif + static void _audio_driver_process_start(); + static void _audio_driver_process_end(); + static void _audio_driver_process_capture(float p_sample); + void _audio_driver_process(); - void _js_driver_process(); - +public: static bool is_available(); void process_capture(float sample); @@ -72,7 +74,6 @@ public: void lock() override; void unlock() override; void finish() override; - void finish_async(); Error capture_start() override; Error capture_stop() override; diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index c401fd7b30..e6e35f6aa9 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,6 @@ import os -from emscripten_helpers import run_closure_compiler, create_engine_file +from emscripten_helpers import run_closure_compiler, create_engine_file, add_js_libraries from SCons.Util import WhereIs @@ -96,6 +96,9 @@ def configure(env): jscc = env.Builder(generator=run_closure_compiler, suffix=".cc.js", src_suffix=".js") env.Append(BUILDERS={"BuildJS": jscc}) + # Add helper method for adding libraries. + env.AddMethod(add_js_libraries, "AddJSLibraries") + # Add method that joins/compiles our Engine files. env.AddMethod(create_engine_file, "CreateEngineFile") @@ -166,6 +169,6 @@ def configure(env): env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"]) # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS. - env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH']"]) + env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']"]) # Add code that allow exiting runtime. env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 8dc33bdf64..768e326e80 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -37,6 +37,7 @@ #include <png.h> #include "dom_keys.inc" +#include "godot_js.h" #define DOM_BUTTON_LEFT 0 #define DOM_BUTTON_MIDDLE 1 @@ -44,28 +45,17 @@ #define DOM_BUTTON_XBUTTON1 3 #define DOM_BUTTON_XBUTTON2 4 -char DisplayServerJavaScript::canvas_id[256] = { 0 }; -static bool cursor_inside_canvas = true; - DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() { return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton()); } // Window (canvas) void DisplayServerJavaScript::focus_canvas() { - /* clang-format off */ - EM_ASM( - Module['canvas'].focus(); - ); - /* clang-format on */ + godot_js_display_canvas_focus(); } bool DisplayServerJavaScript::is_canvas_focused() { - /* clang-format off */ - return EM_ASM_INT_V( - return document.activeElement == Module['canvas']; - ); - /* clang-format on */ + return godot_js_display_canvas_is_focused() != 0; } bool DisplayServerJavaScript::check_size_force_redraw() { @@ -83,19 +73,17 @@ bool DisplayServerJavaScript::check_size_force_redraw() { } Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { - int canvas_x = EM_ASM_INT({ - return Module['canvas'].getBoundingClientRect().x; - }); - int canvas_y = EM_ASM_INT({ - return Module['canvas'].getBoundingClientRect().y; - }); + DisplayServerJavaScript *display = get_singleton(); + int canvas_x; + int canvas_y; + godot_js_display_canvas_bounding_rect_position_get(&canvas_x, &canvas_y); int canvas_width; int canvas_height; - emscripten_get_canvas_element_size(canvas_id, &canvas_width, &canvas_height); + emscripten_get_canvas_element_size(display->canvas_id, &canvas_width, &canvas_height); double element_width; double element_height; - emscripten_get_element_css_size(canvas_id, &element_width, &element_height); + emscripten_get_element_css_size(display->canvas_id, &element_width, &element_height); return Point2((int)(canvas_width / element_width * (p_x - canvas_x)), (int)(canvas_height / element_height * (p_y - canvas_y))); @@ -105,8 +93,7 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co DisplayServerJavaScript *display = get_singleton(); // Empty ID is canvas. String target_id = String::utf8(p_event->id); - String canvas_str_id = String::utf8(canvas_id); - if (target_id.empty() || target_id == canvas_str_id) { + if (target_id.empty() || target_id == String::utf8(display->canvas_id)) { // This event property is the only reliable data on // browser fullscreen state. if (p_event->isFullscreen) { @@ -118,14 +105,15 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co return false; } -// Drag and drop callback (see native/utils.js). -extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); +// Drag and drop callback. +void DisplayServerJavaScript::drop_files_js_callback(char **p_filev, int p_filec) { + DisplayServerJavaScript *ds = get_singleton(); if (!ds) { ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); } - if (ds->drop_files_callback.is_null()) + if (ds->drop_files_callback.is_null()) { return; + } Vector<String> files; for (int i = 0; i < p_filec; i++) { files.push_back(String::utf8(p_filev[i])); @@ -137,6 +125,18 @@ extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p ds->drop_files_callback.call((const Variant **)&vp, 1, ret, ce); } +// JavaScript quit request callback. +void DisplayServerJavaScript::request_quit_callback() { + DisplayServerJavaScript *ds = get_singleton(); + if (ds && !ds->window_event_callback.is_null()) { + Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + // Keys template <typename T> @@ -272,12 +272,13 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E } EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *ds = get_singleton(); Input *input = Input::get_singleton(); int input_mask = input->get_mouse_button_mask(); Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); // For motion outside the canvas, only read mouse movement if dragging // started inside the canvas; imitating desktop app behaviour. - if (!cursor_inside_canvas && !input_mask) + if (!ds->cursor_inside_canvas && !input_mask) return false; Ref<InputEventMouseMotion> ev; @@ -339,35 +340,13 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape } } -void DisplayServerJavaScript::set_css_cursor(const char *p_cursor) { - /* clang-format off */ - EM_ASM_({ - Module['canvas'].style.cursor = UTF8ToString($0); - }, p_cursor); - /* clang-format on */ -} - -bool DisplayServerJavaScript::is_css_cursor_hidden() const { - /* clang-format off */ - return EM_ASM_INT({ - return Module['canvas'].style.cursor === 'none'; - }); - /* clang-format on */ -} - void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - - if (mouse_get_mode() == MOUSE_MODE_VISIBLE) { - if (cursors[p_shape] != "") { - Vector<String> url = cursors[p_shape].split("?"); - set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8()); - } else { - set_css_cursor(godot2dom_cursor(p_shape)); - } + if (cursor_shape == p_shape) { + return; } - cursor_shape = p_shape; + godot_js_display_cursor_set_shape(godot2dom_cursor(cursor_shape)); } DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { @@ -376,17 +355,6 @@ DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); - - if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { - cursor_set_shape(p_shape); - return; - } - - cursors_cache.erase(p_shape); - } - Ref<Texture2D> texture = p_cursor; Ref<AtlasTexture> atlas_texture = p_cursor; Ref<Image> image; @@ -449,53 +417,10 @@ void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, Curso png.resize(len); ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - char *object_url; - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - var PTR = $2; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' }); - var url = URL.createObjectURL(png); - var length_bytes = lengthBytesUTF8(url) + 1; - var string_on_wasm_heap = _malloc(length_bytes); - setValue(PTR, string_on_wasm_heap, '*'); - stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, png.ptr(), len, &object_url); - /* clang-format on */ - - String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); - - /* clang-format off */ - EM_ASM({ _free($0); }, object_url); - /* clang-format on */ - - if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - } - - cursors[p_shape] = url; + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), png.ptr(), len, p_hotspot.x, p_hotspot.y); - Vector<Variant> params; - params.push_back(p_cursor); - params.push_back(p_hotspot); - cursors_cache.insert(p_shape, params); - - } else if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - - cursors_cache.erase(p_shape); + } else { + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), NULL, 0, 0, 0); } cursor_set_shape(cursor_shape); @@ -508,40 +433,37 @@ void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) { return; if (p_mode == MOUSE_MODE_VISIBLE) { - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - cursor_set_shape(cursor_shape); + godot_js_display_cursor_set_visible(1); emscripten_exit_pointerlock(); } else if (p_mode == MOUSE_MODE_HIDDEN) { - set_css_cursor("none"); + godot_js_display_cursor_set_visible(0); emscripten_exit_pointerlock(); } else if (p_mode == MOUSE_MODE_CAPTURED) { - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); + godot_js_display_cursor_set_visible(1); + EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(canvas_id, false); ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - // set_css_cursor must be called before cursor_set_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - cursor_set_shape(cursor_shape); } } DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { - if (is_css_cursor_hidden()) + if (godot_js_display_cursor_is_hidden()) { return MOUSE_MODE_HIDDEN; + } EmscriptenPointerlockChangeEvent ev; emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; + return (ev.isActive && String::utf8(ev.id) == String::utf8(canvas_id)) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; } // Wheel - EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); + DisplayServerJavaScript *ds = get_singleton(); if (!is_canvas_focused()) { - if (cursor_inside_canvas) { + if (ds->cursor_inside_canvas) { focus_canvas(); } else { return false; @@ -632,7 +554,7 @@ EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const Emsc } bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { - return EM_ASM_INT({ return 'ontouchstart' in window; }); + return godot_js_display_touchscreen_is_available(); } // Gamepad @@ -696,53 +618,30 @@ Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() { } // Clipboard -extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { - // Only call set_clipboard from OS (sets local clipboard) - DisplayServerJavaScript::get_singleton()->clipboard = p_text; +void DisplayServerJavaScript::update_clipboard_callback(const char *p_text) { + get_singleton()->clipboard = p_text; } void DisplayServerJavaScript::clipboard_set(const String &p_text) { - /* clang-format off */ - int err = EM_ASM_INT({ - var text = UTF8ToString($0); - if (!navigator.clipboard || !navigator.clipboard.writeText) - return 1; - navigator.clipboard.writeText(text).catch(function(e) { - // Setting OS clipboard is only possible from an input callback. - console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); - }); - return 0; - }, p_text.utf8().get_data()); - /* clang-format on */ + clipboard = p_text; + int err = godot_js_display_clipboard_set(p_text.utf8().get_data()); ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); } String DisplayServerJavaScript::clipboard_get() const { - /* clang-format off */ - EM_ASM({ - try { - navigator.clipboard.readText().then(function (result) { - ccall('update_clipboard', 'void', ['string'], [result]); - }).catch(function (e) { - // Fail graciously. - }); - } catch (e) { - // Fail graciously. - } - }); - /* clang-format on */ + godot_js_display_clipboard_get(update_clipboard_callback); return clipboard; } -extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { +void DisplayServerJavaScript::send_window_event_callback(int p_notification) { + DisplayServerJavaScript *ds = get_singleton(); + if (!ds) { + return; + } if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) { - cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; + ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; } - OS_JavaScript *os = OS_JavaScript::get_singleton(); - if (os->is_finalizing()) - return; // We don't want events anymore. - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); - if (ds && !ds->window_event_callback.is_null()) { + if (!ds->window_event_callback.is_null()) { Variant event = int(p_notification); Variant *eventp = &event; Variant ret; @@ -752,11 +651,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { } void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) { - /* clang-format off */ - EM_ASM_({ - window.alert(UTF8ToString($0)); - }, p_alert.utf8().get_data()); - /* clang-format on */ + godot_js_display_alert(p_alert.utf8().get_data()); } void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { @@ -787,29 +682,11 @@ void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { png.resize(len); ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" }); - var url = URL.createObjectURL(png); - var link = document.getElementById('-gd-engine-icon'); - if (link === null) { - link = document.createElement('link'); - link.rel = 'icon'; - link.id = '-gd-engine-icon'; - document.head.appendChild(link); - } - link.href = url; - }, png.ptr(), len); - /* clang-format on */ + godot_js_display_window_icon_set(png.ptr(), len); } void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) { OS_JavaScript *os = OS_JavaScript::get_singleton(); - if (os->is_finalizing()) - return; // We don't want events anymore. // Resume audio context after input in case autoplay was denied. os->resume_audio(); @@ -831,16 +708,14 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { r_error = OK; // Always succeeds for now. - /* clang-format off */ - swap_cancel_ok = EM_ASM_INT({ - const win = (['Windows', 'Win64', 'Win32', 'WinCE']); - const plat = navigator.platform || ""; - if (win.indexOf(plat) !== -1) { - return 1; - } - return 0; - }) == 1; - /* clang-format on */ + // Ensure the canvas ID. + godot_js_config_canvas_id_get(canvas_id, 256); + + // Check if it's windows. + swap_cancel_ok = godot_js_display_is_swap_ok_cancel() == 1; + + // Expose method for requesting quit. + godot_js_os_request_quit_cb(request_quit_callback); RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? #if 0 @@ -878,10 +753,8 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive video_driver_index = p_video_driver; #endif - /* clang-format off */ window_set_mode(p_mode); - if (EM_ASM_INT_V({ return Module['resizeCanvasOnStart'] })) { - /* clang-format on */ + if (godot_js_config_is_resize_on_start()) { window_set_size(p_resolution); } @@ -899,8 +772,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ EM_CHECK(ev) // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM - // is used below. + // JavaScript APIs. SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) @@ -919,42 +791,20 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive #undef SET_EM_CALLBACK #undef EM_CHECK - /* clang-format off */ - EM_ASM_ARGS({ - // Bind native event listeners. - // Module.listeners, and Module.drop_handler are defined in native/utils.js - const canvas = Module['canvas']; - const send_window_event = cwrap('send_window_event', null, ['number']); - const notifications = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.listeners.add(canvas, event, send_window_event.bind(null, notifications[index]), true); - }); - // Clipboard - const update_clipboard = cwrap('update_clipboard', null, ['string']); - Module.listeners.add(window, 'paste', function(evt) { - update_clipboard(evt.clipboardData.getData('text')); - }, false); - // Drag an drop - Module.listeners.add(canvas, 'dragover', function(ev) { - // Prevent default behavior (which would try to open the file(s)) - ev.preventDefault(); - }, false); - Module.listeners.add(canvas, 'drop', Module.drop_handler, false); - }, - WINDOW_EVENT_MOUSE_ENTER, - WINDOW_EVENT_MOUSE_EXIT, - WINDOW_EVENT_FOCUS_IN, - WINDOW_EVENT_FOCUS_OUT - ); - /* clang-format on */ + // For APIs that are not (sufficiently) exposed, a + // library is used below (implemented in library_godot_display.js). + godot_js_display_notification_cb(&send_window_event_callback, + WINDOW_EVENT_MOUSE_ENTER, + WINDOW_EVENT_MOUSE_EXIT, + WINDOW_EVENT_FOCUS_IN, + WINDOW_EVENT_FOCUS_OUT); + godot_js_display_paste_cb(update_clipboard_callback); + godot_js_display_drop_files_cb(drop_files_js_callback); Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event); } DisplayServerJavaScript::~DisplayServerJavaScript() { - EM_ASM({ - Module.listeners.clear(); - }); //emscripten_webgl_commit_frame(); //emscripten_webgl_destroy_context(webgl_ctx); } @@ -1057,11 +907,7 @@ void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_c } void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) { - /* clang-format off */ - EM_ASM_({ - document.title = UTF8ToString($0); - }, p_title.utf8().get_data()); - /* clang-format on */ + godot_js_display_window_title_set(p_title.utf8().get_data()); } int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const { @@ -1103,9 +949,7 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { last_width = p_size.x; last_height = p_size.y; - double scale = EM_ASM_DOUBLE({ - return window.devicePixelRatio || 1; - }); + double scale = godot_js_display_pixel_ratio_get(); emscripten_set_canvas_element_size(canvas_id, p_size.x * scale, p_size.y * scale); emscripten_set_element_css_size(canvas_id, p_size.x, p_size.y); } @@ -1130,7 +974,7 @@ void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_wind emscripten_exit_fullscreen(); } window_mode = WINDOW_MODE_WINDOWED; - window_set_size(windowed_size); + window_set_size(Size2i(last_width, last_height)); } break; case WINDOW_MODE_FULLSCREEN: { EmscriptenFullscreenStrategy strategy; diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index d7116be36f..1f00295d48 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -37,18 +37,22 @@ #include <emscripten/html5.h> class DisplayServerJavaScript : public DisplayServer { - //int video_driver_index; - - Vector2 windowed_size; - +private: + WindowMode window_mode = WINDOW_MODE_WINDOWED; ObjectID window_attached_instance_id = {}; + Callable window_event_callback; + Callable input_event_callback; + Callable input_text_callback; + Callable drop_files_callback; + + String clipboard; Ref<InputEventKey> deferred_key_event; - CursorShape cursor_shape = CURSOR_ARROW; - String cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; Point2 touches[32]; + char canvas_id[256] = { 0 }; + bool cursor_inside_canvas = true; + CursorShape cursor_shape = CURSOR_ARROW; Point2i last_click_pos = Point2(-100, -100); // TODO check this again. double last_click_ms = 0; int last_click_button_index = -1; @@ -66,8 +70,6 @@ class DisplayServerJavaScript : public DisplayServer { static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event); static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event); static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape); - static void set_css_cursor(const char *p_cursor); - bool is_css_cursor_hidden() const; // events static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); @@ -92,22 +94,17 @@ class DisplayServerJavaScript : public DisplayServer { static void _dispatch_input_event(const Ref<InputEvent> &p_event); + static void request_quit_callback(); + static void update_clipboard_callback(const char *p_text); + static void send_window_event_callback(int p_notification); + static void drop_files_js_callback(char **p_filev, int p_filec); + protected: int get_current_video_driver() const; public: // Override return type to make writing static callbacks less tedious. static DisplayServerJavaScript *get_singleton(); - static char canvas_id[256]; - - WindowMode window_mode = WINDOW_MODE_WINDOWED; - - String clipboard; - - Callable window_event_callback; - Callable input_event_callback; - Callable input_text_callback; - Callable drop_files_callback; // utilities bool check_size_force_redraw(); diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index f6db10fbbd..cc874c432e 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -19,3 +19,9 @@ def create_engine_file(env, target, source, externs): if env["use_closure_compiler"]: return env.BuildJS(target, source, JSEXTERNS=externs) return env.Textfile(target, [env.File(s) for s in source]) + + +def add_js_libraries(env, libraries): + if "JS_LIBS" not in env: + env["JS_LIBS"] = [] + env.Append(JS_LIBS=env.File(libraries)) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index 05a11701c0..3745e04479 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -33,7 +33,7 @@ Function('return this')()['Engine'] = (function() { this.resizeCanvasOnStart = false; this.onExecute = null; this.onExit = null; - this.persistentPaths = []; + this.persistentPaths = ['/userfs']; }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { @@ -114,18 +114,30 @@ Function('return this')()['Engine'] = (function() { locale = navigator.languages ? navigator.languages[0] : navigator.language; locale = locale.split('.')[0]; } - me.rtenv['locale'] = locale; - me.rtenv['canvas'] = me.canvas; + // Emscripten configuration. me.rtenv['thisProgram'] = me.executableName; - me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart; me.rtenv['noExitRuntime'] = true; - me.rtenv['onExecute'] = me.onExecute; - me.rtenv['onExit'] = function(code) { - me.rtenv['deinitFS'](); - if (me.onExit) - me.onExit(code); - me.rtenv = null; - }; + // Godot configuration. + me.rtenv['initConfig']({ + 'resizeCanvasOnStart': me.resizeCanvasOnStart, + 'canvas': me.canvas, + 'locale': locale, + 'onExecute': function(p_args) { + if (me.onExecute) { + me.onExecute(p_args); + return 0; + } + return 1; + }, + 'onExit': function(p_code) { + me.rtenv['deinitFS'](); + if (me.onExit) { + me.onExit(p_code); + } + me.rtenv = null; + }, + }); + return new Promise(function(resolve, reject) { preloader.preloadedFiles.forEach(function(file) { me.rtenv['copyToFS'](file.path, file.buffer); @@ -208,8 +220,6 @@ Function('return this')()['Engine'] = (function() { }; Engine.prototype.setOnExecute = function(onExecute) { - if (this.rtenv) - this.rtenv.onExecute = onExecute; this.onExecute = onExecute; }; diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h index f7f26e5262..120fdbcc59 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.h @@ -42,9 +42,8 @@ extern int godot_audio_is_available(); extern int godot_audio_init(int p_mix_rate, int p_latency); extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); -extern void godot_audio_start(float *r_buffer_ptr); +extern void godot_audio_start(float *r_buffer_ptr, void (*p_start)(), void (*p_end)(), void (*p_input)(float p_sample)); extern void godot_audio_resume(); -extern void godot_audio_finish_async(); extern float godot_audio_get_latency(); diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h new file mode 100644 index 0000000000..23596a0897 --- /dev/null +++ b/platform/javascript/godot_js.h @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* godot_js.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_JS_H +#define GODOT_JS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +// Config +extern void godot_js_config_locale_get(char *p_ptr, int p_ptr_max); +extern void godot_js_config_canvas_id_get(char *p_ptr, int p_ptr_max); +extern int godot_js_config_is_resize_on_start(); + +// OS +extern void godot_js_os_finish_async(void (*p_callback)()); +extern void godot_js_os_request_quit_cb(void (*p_callback)()); +extern int godot_js_os_fs_is_persistent(); +extern void godot_js_os_fs_sync(void (*p_callback)()); +extern int godot_js_os_execute(const char *p_json); +extern void godot_js_os_shell_open(const char *p_uri); + +// Display +extern double godot_js_display_pixel_ratio_get(); +extern void godot_js_display_alert(const char *p_text); +extern int godot_js_display_touchscreen_is_available(); +extern int godot_js_display_is_swap_ok_cancel(); + +// Display canvas +extern void godot_js_display_canvas_focus(); +extern int godot_js_display_canvas_is_focused(); +extern void godot_js_display_canvas_bounding_rect_position_get(int32_t *p_x, int32_t *p_y); + +// Display window +extern void godot_js_display_window_request_fullscreen(); +extern void godot_js_display_window_title_set(const char *p_text); +extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); + +// Display clipboard +extern int godot_js_display_clipboard_set(const char *p_text); +extern int godot_js_display_clipboard_get(void (*p_callback)(const char *p_text)); + +// Display cursor +extern void godot_js_display_cursor_set_shape(const char *p_cursor); +extern int godot_js_display_cursor_is_hidden(); +extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); +extern void godot_js_display_cursor_set_visible(int p_visible); + +// Display listeners +extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); +extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); +extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_JS_H */ diff --git a/platform/javascript/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index 3a72b10dd4..b203253a39 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -33,95 +33,30 @@ #include "api/javascript_eval.h" #include "emscripten.h" -extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { - p_arr->resize(p_len); - *r_write = p_arr->write; - return p_arr->ptrw(); +extern "C" { +union js_eval_ret { + uint32_t b; + double d; + char *s; +}; + +extern int godot_js_eval(const char *p_js, int p_use_global_ctx, union js_eval_ret *p_union_ptr, void *p_byte_arr, void *p_byte_arr_write, void *(*p_callback)(void *p_ptr, void *p_ptr2, int p_len)); } -Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - union { - bool b; - double d; - char *s; - } js_data; +void *resize_PackedByteArray_and_open_write(void *p_arr, void *r_write, int p_len) { + PackedByteArray *arr = (PackedByteArray *)p_arr; + VectorWriteProxy<uint8_t> *write = (VectorWriteProxy<uint8_t> *)r_write; + arr->resize(p_len); + *write = arr->write; + return arr->ptrw(); +} +Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { + union js_eval_ret js_data; PackedByteArray arr; VectorWriteProxy<uint8_t> arr_write; - /* clang-format off */ - Variant::Type return_type = static_cast<Variant::Type>(EM_ASM_INT({ - - const CODE = $0; - const USE_GLOBAL_EXEC_CONTEXT = $1; - const PTR = $2; - const BYTEARRAY_PTR = $3; - const BYTEARRAY_WRITE_PTR = $4; - var eval_ret; - try { - if (USE_GLOBAL_EXEC_CONTEXT) { - // indirect eval call grants global execution context - var global_eval = eval; - eval_ret = global_eval(UTF8ToString(CODE)); - } else { - eval_ret = eval(UTF8ToString(CODE)); - } - } catch (e) { - err(e); - eval_ret = null; - } - - switch (typeof eval_ret) { - - case 'boolean': - setValue(PTR, eval_ret, 'i32'); - return 1; // BOOL - - case 'number': - setValue(PTR, eval_ret, 'double'); - return 3; // FLOAT - - case 'string': - var array_len = lengthBytesUTF8(eval_ret)+1; - var array_ptr = _malloc(array_len); - try { - if (array_ptr===0) { - throw new Error('String allocation failed (probably out of memory)'); - } - setValue(PTR, array_ptr , '*'); - stringToUTF8(eval_ret, array_ptr, array_len); - return 4; // STRING - } catch (e) { - if (array_ptr!==0) { - _free(array_ptr) - } - err(e); - // fall through - } - break; - - case 'object': - if (eval_ret === null) { - break; - } - - if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { - eval_ret = new Uint8Array(eval_ret.buffer); - } - else if (eval_ret instanceof ArrayBuffer) { - eval_ret = new Uint8Array(eval_ret); - } - if (eval_ret instanceof Uint8Array) { - var bytes_ptr = ccall('resize_PackedByteArray_and_open_write', 'number', ['number', 'number' ,'number'], [BYTEARRAY_PTR, BYTEARRAY_WRITE_PTR, eval_ret.length]); - HEAPU8.set(eval_ret, bytes_ptr); - return 20; // PACKED_BYTE_ARRAY - } - break; - } - return 0; // NIL - - }, p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write)); - /* clang-format on */ + Variant::Type return_type = static_cast<Variant::Type>(godot_js_eval(p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write, resize_PackedByteArray_and_open_write)); switch (return_type) { case Variant::BOOL: @@ -130,9 +65,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { return js_data.d; case Variant::STRING: { String str = String::utf8(js_data.s); - /* clang-format off */ - EM_ASM_({ _free($0); }, js_data.s); - /* clang-format on */ + free(js_data.s); // Must free the string allocated in JS. return str; } case Variant::PACKED_BYTE_ARRAY: diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 01722c4bc8..2d28a63566 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -36,20 +36,11 @@ #include <emscripten/emscripten.h> #include <stdlib.h> +#include "godot_js.h" + static OS_JavaScript *os = nullptr; static uint64_t target_ticks = 0; -extern "C" EMSCRIPTEN_KEEPALIVE void _request_quit_callback(char *p_filev[], int p_filec) { - DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); - if (ds) { - Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); - Variant *eventp = &event; - Variant ret; - Callable::CallError ce; - ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); - } -} - void exit_callback() { emscripten_cancel_main_loop(); // After this, we can exit! Main::cleanup(); @@ -59,6 +50,10 @@ void exit_callback() { emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing. } +void cleanup_after_sync() { + emscripten_set_main_loop(exit_callback, -1, false); +} + void main_loop_callback() { uint64_t current_ticks = os->get_ticks_usec(); @@ -74,68 +69,14 @@ void main_loop_callback() { target_ticks += (uint64_t)(1000000 / target_fps); } if (os->main_loop_iterate()) { - emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. - /* clang-format off */ - EM_ASM({ - // This will contain the list of operations that need to complete before cleanup. - Module.async_finish = [ - // Always contains at least one async promise, to avoid firing immediately if nothing is added. - new Promise(function(accept, reject) { - setTimeout(accept, 0); - }) - ]; - }); - /* clang-format on */ - os->get_main_loop()->finish(); - os->finalize_async(); // Will add all the async finish functions. - /* clang-format off */ - EM_ASM({ - Promise.all(Module.async_finish).then(function() { - Module.async_finish = []; - return new Promise(function(accept, reject) { - if (!Module.idbfs) { - accept(); - return; - } - FS.syncfs(function(error) { - if (error) { - err('Failed to save IDB file system: ' + error.message); - } - accept(); - }); - }); - }).then(function() { - ccall("cleanup_after_sync", null, []); - }); - }); - /* clang-format on */ + emscripten_cancel_main_loop(); // Cancel current loop and wait for cleanup_after_sync. + godot_js_os_finish_async(cleanup_after_sync); } } -extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { - emscripten_set_main_loop(exit_callback, -1, false); -} - /// When calling main, it is assumed FS is setup and synced. int main(int argc, char *argv[]) { - // Configure locale. - char locale_ptr[16]; - /* clang-format off */ - EM_ASM({ - stringToUTF8(Module['locale'], $0, 16); - }, locale_ptr); - /* clang-format on */ - setenv("LANG", locale_ptr, true); - - // Ensure the canvas ID. - /* clang-format off */ - EM_ASM({ - stringToUTF8("#" + Module['canvas'].id, $0, 255); - }, DisplayServerJavaScript::canvas_id); - /* clang-format on */ - os = new OS_JavaScript(); - os->set_idb_available((bool)EM_ASM_INT({ return Module.idbfs })); // We must override main when testing is enabled TEST_MAIN_OVERRIDE @@ -147,14 +88,6 @@ int main(int argc, char *argv[]) { Main::start(); os->get_main_loop()->init(); - // Expose method for requesting quit. - /* clang-format off */ - EM_ASM({ - Module['request_quit'] = function() { - ccall("_request_quit_callback", null, []); - }; - }); - /* clang-format on */ emscripten_set_main_loop(main_loop_callback, -1, false); // Immediately run the first iteration. // We are inside an animation frame, we want to immediately draw on the newly setup canvas. diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js index 4e7f3e2af5..1e6f787657 100644 --- a/platform/javascript/native/library_godot_audio.js +++ b/platform/javascript/native/library_godot_audio.js @@ -29,6 +29,7 @@ /*************************************************************************/ var GodotAudio = { + $GodotAudio__deps: ['$GodotOS'], $GodotAudio: { ctx: null, @@ -49,6 +50,27 @@ var GodotAudio = { sampleRate: mix_rate, // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. }); + GodotOS.atexit(function(accept, reject) { + if (!GodotAudio.ctx) { + accept(); + return; + } + if (GodotAudio.script) { + GodotAudio.script.disconnect(); + GodotAudio.script.onaudioprocess = null; + GodotAudio.script = null; + } + if (GodotAudio.input) { + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + GodotAudio.ctx.close().then(function() { + accept(); + }).catch(function(e) { + accept(); + }); + GodotAudio.ctx = null; + }); return GodotAudio.ctx.destination.channelCount; }, @@ -58,10 +80,10 @@ var GodotAudio = { return GodotAudio.script.bufferSize; }, - godot_audio_start: function(buffer_ptr) { - var audioDriverProcessStart = cwrap('audio_driver_process_start'); - var audioDriverProcessEnd = cwrap('audio_driver_process_end'); - var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); + godot_audio_start: function(buffer_ptr, p_process_start, p_process_end, p_process_capture) { + const audioDriverProcessStart = GodotOS.get_func(p_process_start); + const audioDriverProcessEnd = GodotOS.get_func(p_process_end); + const audioDriverProcessCapture = GodotOS.get_func(p_process_capture); GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { audioDriverProcessStart(); @@ -96,29 +118,6 @@ var GodotAudio = { } }, - godot_audio_finish_async: function() { - Module.async_finish.push(new Promise(function(accept, reject) { - if (!GodotAudio.ctx) { - setTimeout(accept, 0); - } else { - if (GodotAudio.script) { - GodotAudio.script.disconnect(); - GodotAudio.script = null; - } - if (GodotAudio.input) { - GodotAudio.input.disconnect(); - GodotAudio.input = null; - } - GodotAudio.ctx.close().then(function() { - accept(); - }).catch(function(e) { - accept(); - }); - GodotAudio.ctx = null; - } - })); - }, - godot_audio_get_latency__proxy: 'sync', godot_audio_get_latency: function() { var latency = 0; @@ -159,9 +158,9 @@ var GodotAudio = { godot_audio_capture_stop__proxy: 'sync', godot_audio_capture_stop: function() { if (GodotAudio.input) { - const tracks = GodotAudio.input.mediaStream.getTracks(); + const tracks = GodotAudio.input['mediaStream']['getTracks'](); for (var i = 0; i < tracks.length; i++) { - tracks[i].stop(); + tracks[i]['stop'](); } GodotAudio.input.disconnect(); GodotAudio.input = null; diff --git a/platform/javascript/native/library_godot_display.js b/platform/javascript/native/library_godot_display.js new file mode 100644 index 0000000000..490b9181d0 --- /dev/null +++ b/platform/javascript/native/library_godot_display.js @@ -0,0 +1,478 @@ +/*************************************************************************/ +/* library_godot_display.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +/* + * Display Server listeners. + * Keeps track of registered event listeners so it can remove them on shutdown. + */ +const GodotDisplayListeners = { + $GodotDisplayListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayListeners.clear(); resolve(); });', + $GodotDisplayListeners: { + handlers: [], + + has: function(target, event, method, capture) { + return GodotDisplayListeners.handlers.findIndex(function(e) { + return e.target === target && e.event === event && e.method === method && e.capture == capture; + }) !== -1; + }, + + add: function(target, event, method, capture) { + if (GodotDisplayListeners.has(target, event, method, capture)) { + return; + } + function Handler(target, event, method, capture) { + this.target = target; + this.event = event; + this.method = method; + this.capture = capture; + }; + GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }, + + clear: function() { + GodotDisplayListeners.handlers.forEach(function(h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + GodotDisplayListeners.handlers.length = 0; + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayListeners); + +/* + * Drag and drop handler. + * This is pretty big, but basically detect dropped files on GodotConfig.canvas, + * process them one by one (recursively for directories), and copies them to + * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot + * event (that requires a string array of paths). + * + * NOTE: The temporary files are removed after the callback. This means that + * deferred callbacks won't be able to access the files. + */ +const GodotDisplayDragDrop = { + + $GodotDisplayDragDrop__deps: ['$FS', '$GodotFS'], + $GodotDisplayDragDrop: { + promises: [], + pending_files: [], + + add_entry: function(entry) { + if (entry.isDirectory) { + GodotDisplayDragDrop.add_dir(entry); + } else if (entry.isFile) { + GodotDisplayDragDrop.add_file(entry); + } else { + console.error("Unrecognized entry...", entry); + } + }, + + add_dir: function(entry) { + GodotDisplayDragDrop.promises.push(new Promise(function(resolve, reject) { + const reader = entry.createReader(); + reader.readEntries(function(entries) { + for (let i = 0; i < entries.length; i++) { + GodotDisplayDragDrop.add_entry(entries[i]); + } + resolve(); + }); + })); + }, + + add_file: function(entry) { + GodotDisplayDragDrop.promises.push(new Promise(function(resolve, reject) { + entry.file(function(file) { + const reader = new FileReader(); + reader.onload = function() { + const f = { + "path": file.relativePath || file.webkitRelativePath, + "name": file.name, + "type": file.type, + "size": file.size, + "data": reader.result + }; + if (!f['path']) { + f['path'] = f['name']; + } + GodotDisplayDragDrop.pending_files.push(f); + resolve() + }; + reader.onerror = function() { + console.log("Error reading file"); + reject(); + } + reader.readAsArrayBuffer(file); + }, function(err) { + console.log("Error!"); + reject(); + }); + })); + }, + + process: function(resolve, reject) { + if (GodotDisplayDragDrop.promises.length == 0) { + resolve(); + return; + } + GodotDisplayDragDrop.promises.pop().then(function() { + setTimeout(function() { + GodotDisplayDragDrop.process(resolve, reject); + }, 0); + }); + }, + + _process_event: function(ev, callback) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + let entry = null; + if ("getAsEntry" in item) { + entry = item.getAsEntry(); + } else if ("webkitGetAsEntry" in item) { + entry = item.webkitGetAsEntry(); + } + if (entry) { + GodotDisplayDragDrop.add_entry(entry); + } + } + } else { + console.error("File upload not supported"); + } + new Promise(GodotDisplayDragDrop.process).then(function() { + const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; + const drops = []; + const files = []; + FS.mkdir(DROP); + GodotDisplayDragDrop.pending_files.forEach((elem) => { + const path = elem['path']; + GodotFS.copy_to_fs(DROP + path, elem['data']); + let idx = path.indexOf("/"); + if (idx == -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + const sub = path.substr(0, idx); + idx = sub.indexOf("/"); + if (idx < 0 && drops.indexOf(DROP + sub) == -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + GodotDisplayDragDrop.promises = []; + GodotDisplayDragDrop.pending_files = []; + callback(drops); + const dirs = [DROP.substr(0, DROP.length -1)]; + // Remove temporary files + files.forEach(function (file) { + FS.unlink(file); + let dir = file.replace(DROP, ""); + let idx = dir.lastIndexOf("/"); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(DROP + dir) == -1) { + dirs.push(DROP + dir); + } + idx = dir.lastIndexOf("/"); + } + }); + // Remove dirs. + dirs.sort(function(a, b) { + const al = (a.match(/\//g) || []).length; + const bl = (b.match(/\//g) || []).length; + if (al > bl) + return -1; + else if (al < bl) + return 1; + return 0; + }).forEach(function(dir) { + FS.rmdir(dir); + }); + }); + }, + + handler: function(callback) { + return function(ev) { + GodotDisplayDragDrop._process_event(ev, callback); + }; + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayDragDrop); + +/* + * Display server cursor helper. + * Keeps track of cursor status and custom shapes. + */ +const GodotDisplayCursor = { + $GodotDisplayCursor__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });', + $GodotDisplayCursor__deps: ['$GodotConfig', '$GodotOS'], + $GodotDisplayCursor: { + shape: 'auto', + visible: true, + cursors: {}, + set_style: function(style) { + GodotConfig.canvas.style.cursor = style; + }, + set_shape: function(shape) { + GodotDisplayCursor.shape = shape; + let css = shape; + if (shape in GodotDisplayCursor.cursors) { + const c = GodotDisplayCursor.cursors[shape]; + css = 'url("' + c.url + '") ' + c.x + ' ' + c.y + ', auto'; + } + if (GodotDisplayCursor.visible) { + GodotDisplayCursor.set_style(css); + } + }, + clear: function() { + GodotDisplayCursor.set_style(''); + GodotDisplayCursor.shape = 'auto'; + GodotDisplayCursor.visible = true; + Object.keys(GodotDisplayCursor.cursors).forEach(function(key) { + URL.revokeObjectURL(GodotDisplayCursor.cursors[key]); + delete GodotDisplayCursor.cursors[key]; + }); + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayCursor); + +/** + * Display server interface. + * + * Exposes all the functions needed by DisplayServer implementation. + */ +const GodotDisplay = { + $GodotDisplay__deps: ['$GodotConfig', '$GodotOS', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop'], + $GodotDisplay: { + window_icon: '', + }, + + godot_js_display_is_swap_ok_cancel: function() { + const win = (['Windows', 'Win64', 'Win32', 'WinCE']); + const plat = navigator.platform || ""; + if (win.indexOf(plat) !== -1) { + return 1; + } + return 0; + }, + + godot_js_display_alert: function(p_text) { + window.alert(UTF8ToString(p_text)); + }, + + godot_js_display_pixel_ratio_get: function() { + return window.devicePixelRatio || 1; + }, + + /* + * Canvas + */ + godot_js_display_canvas_focus: function() { + GodotConfig.canvas.focus(); + }, + + godot_js_display_canvas_is_focused: function() { + return document.activeElement == GodotConfig.canvas; + }, + + godot_js_display_canvas_bounding_rect_position_get: function(r_x, r_y) { + const brect = GodotConfig.canvas.getBoundingClientRect(); + setValue(r_x, brect.x, 'i32'); + setValue(r_y, brect.y, 'i32'); + }, + + /* + * Touchscreen + */ + godot_js_display_touchscreen_is_available: function() { + return 'ontouchstart' in window; + }, + + /* + * Clipboard + */ + godot_js_display_clipboard_set: function(p_text) { + const text = UTF8ToString(p_text); + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return 1; + } + navigator.clipboard.writeText(text).catch(function(e) { + // Setting OS clipboard is only possible from an input callback. + console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); + }); + return 0; + }, + + godot_js_display_clipboard_get_deps: ['$GodotOS'], + godot_js_display_clipboard_get: function(callback) { + const func = GodotOS.get_func(callback); + try { + navigator.clipboard.readText().then(function (result) { + const ptr = allocate(intArrayFromString(result), ALLOC_NORMAL); + func(ptr); + _free(ptr); + }).catch(function (e) { + // Fail graciously. + }); + } catch (e) { + // Fail graciously. + } + }, + + /* + * Window + */ + godot_js_display_window_request_fullscreen: function() { + const canvas = GodotConfig.canvas; + (canvas.requestFullscreen || canvas.msRequestFullscreen || + canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || + canvas.webkitRequestFullscreen + ).call(canvas); + }, + + godot_js_display_window_title_set: function(p_data) { + document.title = UTF8ToString(p_data); + }, + + godot_js_display_window_icon_set: function(p_ptr, p_len) { + let link = document.getElementById('-gd-engine-icon'); + if (link === null) { + link = document.createElement('link'); + link.rel = 'icon'; + link.id = '-gd-engine-icon'; + document.head.appendChild(link); + } + const old_icon = GodotDisplay.window_icon; + const png = new Blob([GodotOS.heapCopy(HEAPU8, p_ptr, p_len)], { type: "image/png" }); + GodotDisplay.window_icon = URL.createObjectURL(png); + link.href = GodotDisplay.window_icon; + if (old_icon) { + URL.revokeObjectURL(old_icon); + } + }, + + /* + * Cursor + */ + godot_js_display_cursor_set_visible: function(p_visible) { + const visible = p_visible != 0; + if (visible == GodotDisplayCursor.visible) { + return; + } + GodotDisplayCursor.visible = visible; + if (visible) { + GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); + } else { + GodotDisplayCursor.set_style('none'); + } + }, + + godot_js_display_cursor_is_hidden: function() { + return !GodotDisplayCursor.visible; + }, + + godot_js_display_cursor_set_shape: function(p_string) { + GodotDisplayCursor.set_shape(UTF8ToString(p_string)); + }, + + godot_js_display_cursor_set_custom_shape: function(p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) { + const shape = UTF8ToString(p_shape); + const old_shape = GodotDisplayCursor.cursors[shape]; + if (p_len > 0) { + const png = new Blob([GodotOS.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); + const url = URL.createObjectURL(png); + GodotDisplayCursor.cursors[shape] = { + url: url, + x: p_hotspot_x, + y: p_hotspot_y, + }; + } else { + delete GodotDisplayCursor.cursors[shape]; + } + if (shape == GodotDisplayCursor.shape) { + GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); + } + if (old_shape) { + URL.revokeObjectURL(old_shape.url); + } + }, + + /* + * Listeners + */ + godot_js_display_notification_cb: function(callback, p_enter, p_exit, p_in, p_out) { + const canvas = GodotConfig.canvas; + const func = GodotOS.get_func(callback); + const notif = [p_enter, p_exit, p_in, p_out]; + ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function(evt_name, idx) { + GodotDisplayListeners.add(canvas, evt_name, function() { + func.bind(null, notif[idx]); + }, true); + }); + }, + + godot_js_display_paste_cb: function(callback) { + const func = GodotOS.get_func(callback); + GodotDisplayListeners.add(window, 'paste', function(evt) { + const text = evt.clipboardData.getData('text'); + const ptr = allocate(intArrayFromString(text), ALLOC_NORMAL); + func(ptr); + _free(ptr); + }, false); + }, + + godot_js_display_drop_files_cb: function(callback) { + const func = GodotOS.get_func(callback) + const dropFiles = function(files) { + const args = files || []; + if (!args.length) { + return; + } + const argc = args.length; + const argv = GodotOS.allocStringArray(args); + func(argv, argc); + GodotOS.freeStringArray(argv, argc); + }; + const canvas = GodotConfig.canvas; + GodotDisplayListeners.add(canvas, 'dragover', function(ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }, false); + GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles)); + }, +}; + +autoAddDeps(GodotDisplay, '$GodotDisplay'); +mergeInto(LibraryManager.library, GodotDisplay); diff --git a/platform/javascript/native/id_handler.js b/platform/javascript/native/library_godot_editor_tools.js index 67d29075b8..bd62bbf4e1 100644 --- a/platform/javascript/native/id_handler.js +++ b/platform/javascript/native/library_godot_editor_tools.js @@ -1,5 +1,5 @@ /*************************************************************************/ -/* id_handler.js */ +/* library_godot_editor_tools.js */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,36 +28,30 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var IDHandler = /** @constructor */ function() { - - var ids = {}; - var size = 0; - - this.has = function(id) { - return ids.hasOwnProperty(id); - } - - this.add = function(obj) { - size += 1; - var id = crypto.getRandomValues(new Int32Array(32))[0]; - ids[id] = obj; - return id; - } - - this.get = function(id) { - return ids[id]; - } - - this.remove = function(id) { - size -= 1; - delete ids[id]; - } - - this.size = function() { - return size; - } - - this.ids = ids; +const GodotEditorTools = { + + godot_js_editor_download_file__deps: ['$FS'], + godot_js_editor_download_file: function(p_path, p_name, p_mime) { + const path = UTF8ToString(p_path); + const name = UTF8ToString(p_name); + const mime = UTF8ToString(p_mime); + const size = FS.stat(path)['size']; + const buf = new Uint8Array(size); + const fd = FS.open(path, 'r'); + FS.read(fd, buf, 0, size); + FS.close(fd); + FS.unlink(path); + const blob = new Blob([buf], { type: mime }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = name; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }, }; -Module.IDHandler = new IDHandler; +mergeInto(LibraryManager.library, GodotEditorTools); diff --git a/platform/javascript/native/library_godot_eval.js b/platform/javascript/native/library_godot_eval.js new file mode 100644 index 0000000000..e83c61dd9d --- /dev/null +++ b/platform/javascript/native/library_godot_eval.js @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* library_godot_eval.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const GodotEval = { + + godot_js_eval__deps: ['$GodotOS'], + godot_js_eval: function(p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) { + const js_code = UTF8ToString(p_js); + let eval_ret = null; + try { + if (p_use_global_ctx) { + // indirect eval call grants global execution context + const global_eval = eval; + eval_ret = global_eval(js_code); + } else { + eval_ret = eval(js_code); + } + } catch (e) { + err(e); + } + + switch (typeof eval_ret) { + + case 'boolean': + setValue(p_union_ptr, eval_ret, 'i32'); + return 1; // BOOL + + case 'number': + setValue(p_union_ptr, eval_ret, 'double'); + return 3; // REAL + + case 'string': + let array_ptr = GodotOS.allocString(eval_ret); + setValue(p_union_ptr, array_ptr , '*'); + return 4; // STRING + + case 'object': + if (eval_ret === null) { + break; + } + + if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { + eval_ret = new Uint8Array(eval_ret.buffer); + } + else if (eval_ret instanceof ArrayBuffer) { + eval_ret = new Uint8Array(eval_ret); + } + if (eval_ret instanceof Uint8Array) { + const func = GodotOS.get_func(p_callback); + const bytes_ptr = func(p_byte_arr, p_byte_arr_write, eval_ret.length); + HEAPU8.set(eval_ret, bytes_ptr); + return 20; // POOL_BYTE_ARRAY + } + break; + } + return 0; // NIL + }, +} + +mergeInto(LibraryManager.library, GodotEval); diff --git a/platform/javascript/native/library_godot_os.js b/platform/javascript/native/library_godot_os.js new file mode 100644 index 0000000000..ed48280674 --- /dev/null +++ b/platform/javascript/native/library_godot_os.js @@ -0,0 +1,313 @@ +/*************************************************************************/ +/* library_godot_os.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const IDHandler = { + $IDHandler: { + _last_id: 0, + _references: {}, + + get: function(p_id) { + return IDHandler._references[p_id]; + }, + + add: function(p_data) { + const id = ++IDHandler._last_id; + IDHandler._references[id] = p_data; + return id; + }, + + remove: function(p_id) { + delete IDHandler._references[p_id]; + }, + }, +}; + +autoAddDeps(IDHandler, "$IDHandler"); +mergeInto(LibraryManager.library, IDHandler); + +const GodotConfig = { + + $GodotConfig__postset: 'Module["initConfig"] = GodotConfig.init_config;', + $GodotConfig: { + canvas: null, + locale: "en", + resize_on_start: false, + on_execute: null, + + init_config: function(p_opts) { + GodotConfig.resize_on_start = p_opts['resizeCanvasOnStart'] ? true : false; + GodotConfig.canvas = p_opts['canvas']; + GodotConfig.locale = p_opts['locale'] || GodotConfig.locale; + GodotConfig.on_execute = p_opts['onExecute']; + // This is called by emscripten, even if undocumented. + Module['onExit'] = p_opts['onExit']; + }, + }, + + godot_js_config_canvas_id_get: function(p_ptr, p_ptr_max) { + stringToUTF8('#' + GodotConfig.canvas.id, p_ptr, p_ptr_max); + }, + + godot_js_config_locale_get: function(p_ptr, p_ptr_max) { + stringToUTF8(GodotConfig.locale, p_ptr, p_ptr_max); + }, + + godot_js_config_is_resize_on_start: function() { + return GodotConfig.resize_on_start ? 1 : 0; + }, +}; + +autoAddDeps(GodotConfig, '$GodotConfig'); +mergeInto(LibraryManager.library, GodotConfig); + +const GodotFS = { + $GodotFS__deps: ['$FS', '$IDBFS'], + $GodotFS__postset: [ + 'Module["initFS"] = GodotFS.init;', + 'Module["deinitFS"] = GodotFS.deinit;', + 'Module["copyToFS"] = GodotFS.copy_to_fs;', + ].join(''), + $GodotFS: { + _idbfs: false, + _syncing: false, + _mount_points: [], + + is_persistent: function() { + return GodotFS._idbfs ? 1 : 0; + }, + + // Initialize godot file system, setting up persistent paths. + // Returns a promise that resolves when the FS is ready. + // We keep track of mount_points, so that we can properly close the IDBFS + // since emscripten is not doing it by itself. (emscripten GH#12516). + init: function(persistentPaths) { + GodotFS._idbfs = false; + if (!Array.isArray(persistentPaths)) { + return Promise.reject(new Error('Persistent paths must be an array')); + } + if (!persistentPaths.length) { + return Promise.resolve(); + } + GodotFS._mount_points = persistentPaths.slice(); + + function createRecursive(dir) { + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + } + + GodotFS._mount_points.forEach(function(path) { + createRecursive(path); + FS.mount(IDBFS, {}, path); + }); + return new Promise(function(resolve, reject) { + FS.syncfs(true, function(err) { + if (err) { + GodotFS._mount_points = []; + GodotFS._idbfs = false; + console.log("IndexedDB not available: " + err.message); + } else { + GodotFS._idbfs = true; + } + resolve(err); + }); + }); + }, + + // Deinit godot file system, making sure to unmount file systems, and close IDBFS(s). + deinit: function() { + GodotFS._mount_points.forEach(function(path) { + try { + FS.unmount(path); + } catch (e) { + console.log("Already unmounted", e); + } + if (GodotFS._idbfs && IDBFS.dbs[path]) { + IDBFS.dbs[path].close(); + delete IDBFS.dbs[path]; + } + }); + GodotFS._mount_points = []; + GodotFS._idbfs = false; + GodotFS._syncing = false; + }, + + sync: function() { + if (GodotFS._syncing) { + err('Already syncing!'); + return Promise.resolve(); + } + GodotFS._syncing = true; + return new Promise(function (resolve, reject) { + FS.syncfs(false, function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + GodotFS._syncing = false; + resolve(error); + }); + }); + }, + + // Copies a buffer to the internal file system. Creating directories recursively. + copy_to_fs: function(path, buffer) { + const idx = path.lastIndexOf("/"); + let dir = "/"; + if (idx > 0) { + dir = path.slice(0, idx); + } + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); + }, + }, +}; +mergeInto(LibraryManager.library, GodotFS); + +const GodotOS = { + $GodotOS__deps: ['$GodotFS'], + $GodotOS__postset: [ + 'Module["request_quit"] = function() { GodotOS.request_quit() };', + 'GodotOS._fs_sync_promise = Promise.resolve();', + ].join(''), + $GodotOS: { + + request_quit: function() {}, + _async_cbs: [], + _fs_sync_promise: null, + + get_func: function(ptr) { + return wasmTable.get(ptr); + }, + + atexit: function(p_promise_cb) { + GodotOS._async_cbs.push(p_promise_cb); + }, + + finish_async: function(callback) { + GodotOS._fs_sync_promise.then(function(err) { + const promises = []; + GodotOS._async_cbs.forEach(function(cb) { + promises.push(new Promise(cb)); + }); + return Promise.all(promises); + }).then(function() { + return GodotFS.sync(); // Final FS sync. + }).then(function(err) { + // Always deferred. + setTimeout(function() { + callback(); + }, 0); + }); + }, + + allocString: function(p_str) { + const length = lengthBytesUTF8(p_str)+1; + const c_str = _malloc(length); + stringToUTF8(p_str, c_str, length); + return c_str; + }, + + allocStringArray: function(strings) { + const size = strings.length; + const c_ptr = _malloc(size * 4); + for (let i = 0; i < size; i++) { + HEAP32[(c_ptr >> 2) + i] = GodotOS.allocString(strings[i]); + } + return c_ptr; + }, + + freeStringArray: function(c_ptr, size) { + for (let i = 0; i < size; i++) { + _free(HEAP32[(c_ptr >> 2) + i]); + } + _free(c_ptr); + }, + + heapSub: function(heap, ptr, size) { + const bytes = heap.BYTES_PER_ELEMENT; + return heap.subarray(ptr / bytes, ptr / bytes + size); + }, + + heapCopy: function(heap, ptr, size) { + const bytes = heap.BYTES_PER_ELEMENT; + return heap.slice(ptr / bytes, ptr / bytes + size); + }, + }, + + godot_js_os_finish_async: function(p_callback) { + const func = GodotOS.get_func(p_callback); + GodotOS.finish_async(func); + }, + + godot_js_os_request_quit_cb: function(p_callback) { + GodotOS.request_quit = GodotOS.get_func(p_callback); + }, + + godot_js_os_fs_is_persistent: function() { + return GodotFS.is_persistent(); + }, + + godot_js_os_fs_sync: function(callback) { + const func = GodotOS.get_func(callback); + GodotOS._fs_sync_promise = GodotFS.sync(); + GodotOS._fs_sync_promise.then(function(err) { + func(); + }); + }, + + godot_js_os_execute: function(p_json) { + const json_args = UTF8ToString(p_json); + const args = JSON.parse(json_args); + if (GodotConfig.on_execute) { + GodotConfig.on_execute(args); + return 0; + } + return 1; + }, + + godot_js_os_shell_open: function(p_uri) { + window.open(UTF8ToString(p_uri), '_blank'); + }, +}; + +autoAddDeps(GodotOS, '$GodotOS'); +mergeInto(LibraryManager.library, GodotOS); diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js deleted file mode 100644 index 8d0beba454..0000000000 --- a/platform/javascript/native/utils.js +++ /dev/null @@ -1,292 +0,0 @@ -/*************************************************************************/ -/* utils.js */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -Module['initFS'] = function(persistentPaths) { - Module.mount_points = ['/userfs'].concat(persistentPaths); - - function createRecursive(dir) { - try { - FS.stat(dir); - } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { - throw e; - } - FS.mkdirTree(dir); - } - } - - Module.mount_points.forEach(function(path) { - createRecursive(path); - FS.mount(IDBFS, {}, path); - }); - return new Promise(function(resolve, reject) { - FS.syncfs(true, function(err) { - if (err) { - Module.mount_points = []; - Module.idbfs = false; - console.log("IndexedDB not available: " + err.message); - } else { - Module.idbfs = true; - } - resolve(err); - }); - }); -}; - -Module['deinitFS'] = function() { - Module.mount_points.forEach(function(path) { - try { - FS.unmount(path); - } catch (e) { - console.log("Already unmounted", e); - } - if (Module.idbfs && IDBFS.dbs[path]) { - IDBFS.dbs[path].close(); - delete IDBFS.dbs[path]; - } - }); - Module.mount_points = []; -}; - -Module['copyToFS'] = function(path, buffer) { - var p = path.lastIndexOf("/"); - var dir = "/"; - if (p > 0) { - dir = path.slice(0, path.lastIndexOf("/")); - } - try { - FS.stat(dir); - } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { - throw e; - } - FS.mkdirTree(dir); - } - // With memory growth, canOwn should be false. - FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); -} - -Module.drop_handler = (function() { - var upload = []; - var uploadPromises = []; - var uploadCallback = null; - - function readFilePromise(entry, path) { - return new Promise(function(resolve, reject) { - entry.file(function(file) { - var reader = new FileReader(); - reader.onload = function() { - var f = { - "path": file.relativePath || file.webkitRelativePath, - "name": file.name, - "type": file.type, - "size": file.size, - "data": reader.result - }; - if (!f['path']) - f['path'] = f['name']; - upload.push(f); - resolve() - }; - reader.onerror = function() { - console.log("Error reading file"); - reject(); - } - - reader.readAsArrayBuffer(file); - - }, function(err) { - console.log("Error!"); - reject(); - }); - }); - } - - function readDirectoryPromise(entry) { - return new Promise(function(resolve, reject) { - var reader = entry.createReader(); - reader.readEntries(function(entries) { - for (var i = 0; i < entries.length; i++) { - var ent = entries[i]; - if (ent.isDirectory) { - uploadPromises.push(readDirectoryPromise(ent)); - } else if (ent.isFile) { - uploadPromises.push(readFilePromise(ent)); - } - } - resolve(); - }); - }); - } - - function processUploadsPromises(resolve, reject) { - if (uploadPromises.length == 0) { - resolve(); - return; - } - uploadPromises.pop().then(function() { - setTimeout(function() { - processUploadsPromises(resolve, reject); - //processUploadsPromises.bind(null, resolve, reject) - }, 0); - }); - } - - function dropFiles(files) { - var args = files || []; - var argc = args.length; - var argv = stackAlloc((argc + 1) * 4); - for (var i = 0; i < argc; i++) { - HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]); - } - HEAP32[(argv >> 2) + argc] = 0; - // Defined in display_server_javascript.cpp - ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]); - } - - return function(ev) { - ev.preventDefault(); - if (ev.dataTransfer.items) { - // Use DataTransferItemList interface to access the file(s) - for (var i = 0; i < ev.dataTransfer.items.length; i++) { - const item = ev.dataTransfer.items[i]; - var entry = null; - if ("getAsEntry" in item) { - entry = item.getAsEntry(); - } else if ("webkitGetAsEntry" in item) { - entry = item.webkitGetAsEntry(); - } - if (!entry) { - console.error("File upload not supported"); - } else if (entry.isDirectory) { - uploadPromises.push(readDirectoryPromise(entry)); - } else if (entry.isFile) { - uploadPromises.push(readFilePromise(entry)); - } else { - console.error("Unrecognized entry...", entry); - } - } - } else { - console.error("File upload not supported"); - } - uploadCallback = new Promise(processUploadsPromises).then(function() { - const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; - var drops = []; - var files = []; - upload.forEach((elem) => { - var path = elem['path']; - Module['copyToFS'](DROP + path, elem['data']); - var idx = path.indexOf("/"); - if (idx == -1) { - // Root file - drops.push(DROP + path); - } else { - // Subdir - var sub = path.substr(0, idx); - idx = sub.indexOf("/"); - if (idx < 0 && drops.indexOf(DROP + sub) == -1) { - drops.push(DROP + sub); - } - } - files.push(DROP + path); - }); - uploadPromises = []; - upload = []; - dropFiles(drops); - var dirs = [DROP.substr(0, DROP.length -1)]; - files.forEach(function (file) { - FS.unlink(file); - var dir = file.replace(DROP, ""); - var idx = dir.lastIndexOf("/"); - while (idx > 0) { - dir = dir.substr(0, idx); - if (dirs.indexOf(DROP + dir) == -1) { - dirs.push(DROP + dir); - } - idx = dir.lastIndexOf("/"); - } - }); - // Remove dirs. - dirs = dirs.sort(function(a, b) { - var al = (a.match(/\//g) || []).length; - var bl = (b.match(/\//g) || []).length; - if (al > bl) - return -1; - else if (al < bl) - return 1; - return 0; - }); - dirs.forEach(function(dir) { - FS.rmdir(dir); - }); - }); - } -})(); - -function EventHandlers() { - function Handler(target, event, method, capture) { - this.target = target; - this.event = event; - this.method = method; - this.capture = capture; - } - - var listeners = []; - - function has(target, event, method, capture) { - return listeners.findIndex(function(e) { - return e.target === target && e.event === event && e.method === method && e.capture == capture; - }) !== -1; - } - - this.add = function(target, event, method, capture) { - if (has(target, event, method, capture)) { - return; - } - listeners.push(new Handler(target, event, method, capture)); - target.addEventListener(event, method, capture); - }; - - this.remove = function(target, event, method, capture) { - if (!has(target, event, method, capture)) { - return; - } - target.removeEventListener(event, method, capture); - }; - - this.clear = function() { - listeners.forEach(function(h) { - h.target.removeEventListener(h.event, h.method, h.capture); - }); - listeners.length = 0; - }; -} - -Module.listeners = new EventHandlers(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index cf5751f384..80723d54fc 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -46,6 +46,8 @@ #include <emscripten.h> #include <stdlib.h> +#include "godot_js.h" + // Lifecycle void OS_JavaScript::initialize() { OS_Unix::initialize_core(); @@ -72,24 +74,15 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } -extern "C" EMSCRIPTEN_KEEPALIVE void _idb_synced() { - OS_JavaScript::get_singleton()->idb_is_syncing = false; +void OS_JavaScript::fs_sync_callback() { + get_singleton()->idb_is_syncing = false; } bool OS_JavaScript::main_loop_iterate() { if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) { idb_is_syncing = true; idb_needs_sync = false; - /* clang-format off */ - EM_ASM({ - FS.syncfs(function(error) { - if (error) { - err('Failed to save IDB file system: ' + error.message); - } - ccall("_idb_synced", 'void', [], []); - }); - }); - /* clang-format on */ + godot_js_os_fs_sync(&fs_sync_callback); } DisplayServer::get_singleton()->process_events(); @@ -104,13 +97,6 @@ void OS_JavaScript::delete_main_loop() { main_loop = nullptr; } -void OS_JavaScript::finalize_async() { - finalizing = true; - if (audio_driver_javascript) { - audio_driver_javascript->finish_async(); - } -} - void OS_JavaScript::finalize() { delete_main_loop(); if (audio_driver_javascript) { @@ -127,17 +113,7 @@ Error OS_JavaScript::execute(const String &p_path, const List<String> &p_argumen args.push_back(E->get()); } String json_args = JSON::print(args); - /* clang-format off */ - int failed = EM_ASM_INT({ - const json_args = UTF8ToString($0); - const args = JSON.parse(json_args); - if (Module["onExecute"]) { - Module["onExecute"](args); - return 0; - } - return 1; - }, json_args.utf8().get_data()); - /* clang-format on */ + int failed = godot_js_os_execute(json_args.utf8().get_data()); ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in JavaScript via 'engine.setOnExecute' if required."); return OK; } @@ -168,11 +144,7 @@ String OS_JavaScript::get_executable_path() const { Error OS_JavaScript::shell_open(String p_uri) { // Open URI in a new tab, browser will deal with it by protocol. - /* clang-format off */ - EM_ASM_({ - window.open(UTF8ToString($0), '_blank'); - }, p_uri.utf8().get_data()); - /* clang-format on */ + godot_js_os_shell_open(p_uri.utf8().get_data()); return OK; } @@ -211,10 +183,6 @@ void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags } } -void OS_JavaScript::set_idb_available(bool p_idb_available) { - idb_available = p_idb_available; -} - bool OS_JavaScript::is_userfs_persistent() const { return idb_available; } @@ -227,11 +195,17 @@ void OS_JavaScript::initialize_joypads() { } OS_JavaScript::OS_JavaScript() { + char locale_ptr[16]; + godot_js_config_locale_get(locale_ptr, 16); + setenv("LANG", locale_ptr, true); + if (AudioDriverJavaScript::is_available()) { audio_driver_javascript = memnew(AudioDriverJavaScript); AudioDriverManager::add_driver(audio_driver_javascript); } + idb_available = godot_js_os_fs_is_persistent(); + Vector<Logger *> loggers; loggers.push_back(memnew(StdLogger)); _set_logger(memnew(CompositeLogger(loggers))); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 85551d708b..03a3053367 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -42,13 +42,14 @@ class OS_JavaScript : public OS_Unix { MainLoop *main_loop = nullptr; AudioDriverJavaScript *audio_driver_javascript = nullptr; - bool finalizing = false; + bool idb_is_syncing = false; bool idb_available = false; bool idb_needs_sync = false; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); + static void fs_sync_callback(); protected: void initialize() override; @@ -61,15 +62,12 @@ protected: bool _check_internal_feature_support(const String &p_feature) override; public: - bool idb_is_syncing = false; - // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); void initialize_joypads() override; MainLoop *get_main_loop() const override; - void finalize_async(); bool main_loop_iterate(); Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr) override; @@ -88,11 +86,9 @@ public: String get_data_path() const override; String get_user_data_dir() const override; - void set_idb_available(bool p_idb_available); bool is_userfs_persistent() const override; void resume_audio(); - bool is_finalizing() { return finalizing; } OS_JavaScript(); }; |