diff --git a/modules/webrtc/library_godot_webrtc.js b/modules/webrtc/library_godot_webrtc.js index d4c38f15a27..c8a10018e58 100644 --- a/modules/webrtc/library_godot_webrtc.js +++ b/modules/webrtc/library_godot_webrtc.js @@ -28,9 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotRTCDataChannel = { +const GodotRTCDataChannel = { // Our socket implementation that forwards events to C++. - $GodotRTCDataChannel__deps: ['$IDHandler', '$GodotOS'], + $GodotRTCDataChannel__deps: ['$IDHandler', '$GodotRuntime'], $GodotRTCDataChannel: { connect: function(p_id, p_on_open, p_on_message, p_on_error, p_on_close) { const ref = IDHandler.get(p_id); @@ -54,21 +54,21 @@ var GodotRTCDataChannel = { if (event.data instanceof ArrayBuffer) { buffer = new Uint8Array(event.data); } else if (event.data instanceof Blob) { - console.error("Blob type not supported"); + GodotRuntime.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"); + GodotRuntime.error("Unknown message type"); return; } var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = _malloc(len); + var out = GodotRuntime.malloc(len); HEAPU8.set(buffer, out); p_on_message(out, len, is_string); - _free(out); + GodotRuntime.free(out); } }, @@ -104,9 +104,9 @@ var GodotRTCDataChannel = { case "closing": return 2; case "closed": + default: return 3; } - return 3; // CLOSED }, godot_js_rtc_datachannel_send: function(p_id, p_buffer, p_length, p_raw) { @@ -117,7 +117,7 @@ var GodotRTCDataChannel = { const bytes_array = new Uint8Array(p_length); for (var i = 0; i < p_length; i++) { - bytes_array[i] = getValue(p_buffer + i, 'i8'); + bytes_array[i] = GodotRuntime.getHeapValue(p_buffer + i, 'i8'); } if (p_raw) { @@ -126,6 +126,7 @@ var GodotRTCDataChannel = { const string = new TextDecoder('utf-8').decode(bytes_array); ref.send(string); } + return 0; }, godot_js_rtc_datachannel_is_ordered: function(p_id) { @@ -163,7 +164,7 @@ var GodotRTCDataChannel = { if (!ref || !ref.label) { return 0; } - return GodotOS.allocString(ref.label); + return GodotRuntime.allocString(ref.label); }, godot_js_rtc_datachannel_protocol_get: function(p_id) { @@ -171,7 +172,7 @@ var GodotRTCDataChannel = { if (!ref || !ref.protocol) { return 0; } - return GodotOS.allocString(ref.protocol); + return GodotRuntime.allocString(ref.protocol); }, godot_js_rtc_datachannel_destroy: function(p_id) { @@ -180,10 +181,10 @@ var GodotRTCDataChannel = { }, 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); + const onopen = GodotRuntime.get_func(p_on_open).bind(null, p_ref); + const onmessage = GodotRuntime.get_func(p_on_message).bind(null, p_ref); + const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_ref); + const onclose = GodotRuntime.get_func(p_on_close).bind(null, p_ref); GodotRTCDataChannel.connect(p_id, onopen, onmessage, onerror, onclose); }, @@ -199,8 +200,8 @@ var GodotRTCDataChannel = { autoAddDeps(GodotRTCDataChannel, '$GodotRTCDataChannel'); mergeInto(LibraryManager.library, GodotRTCDataChannel); -var GodotRTCPeerConnection = { - $GodotRTCPeerConnection__deps: ['$IDHandler', '$GodotOS', '$GodotRTCDataChannel'], +const GodotRTCPeerConnection = { + $GodotRTCPeerConnection__deps: ['$IDHandler', '$GodotRuntime', '$GodotRTCDataChannel'], $GodotRTCPeerConnection: { onstatechange: function(p_id, p_conn, callback, event) { const ref = IDHandler.get(p_id); @@ -211,17 +212,24 @@ var GodotRTCPeerConnection = { switch(p_conn.iceConnectionState) { case "new": state = 0; + break; case "checking": state = 1; + break; case "connected": case "completed": state = 2; + break; case "disconnected": state = 3; + break; case "failed": state = 4; + break; case "closed": + default: state = 5; + break; } callback(state); }, @@ -233,11 +241,11 @@ var GodotRTCPeerConnection = { } let c = event.candidate; - let candidate_str = GodotOS.allocString(c.candidate); - let mid_str = GodotOS.allocString(c.sdpMid); + let candidate_str = GodotRuntime.allocString(c.candidate); + let mid_str = GodotRuntime.allocString(c.sdpMid); callback(mid_str, c.sdpMLineIndex, candidate_str); - _free(candidate_str); - _free(mid_str); + GodotRuntime.free(candidate_str); + GodotRuntime.free(mid_str); }, ondatachannel: function(p_id, callback, event) { @@ -255,11 +263,11 @@ var GodotRTCPeerConnection = { if (!ref) { return; } - let type_str = GodotOS.allocString(session.type); - let sdp_str = GodotOS.allocString(session.sdp); + let type_str = GodotRuntime.allocString(session.type); + let sdp_str = GodotRuntime.allocString(session.sdp); callback(type_str, sdp_str); - _free(type_str); - _free(sdp_str); + GodotRuntime.free(type_str); + GodotRuntime.free(sdp_str); }, onerror: function(p_id, callback, error) { @@ -267,22 +275,22 @@ var GodotRTCPeerConnection = { if (!ref) { return; } - console.error(error); + GodotRuntime.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); + const onstatechange = GodotRuntime.get_func(p_on_state_change).bind(null, p_ref); + const oncandidate = GodotRuntime.get_func(p_on_candidate).bind(null, p_ref); + const ondatachannel = GodotRuntime.get_func(p_on_datachannel).bind(null, p_ref); - var config = JSON.parse(UTF8ToString(p_config)); + var config = JSON.parse(GodotRuntime.parseString(p_config)); var conn = null; try { conn = new RTCPeerConnection(config); } catch (e) { - console.error(e); + GodotRuntime.error(e); return 0; } @@ -318,8 +326,8 @@ var GodotRTCPeerConnection = { 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); + const onsession = GodotRuntime.get_func(p_on_session).bind(null, p_obj); + const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj); ref.createOffer().then(function(session) { GodotRTCPeerConnection.onsession(p_id, onsession, session); }).catch(function(error) { @@ -332,9 +340,9 @@ var GodotRTCPeerConnection = { 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 type = GodotRuntime.parseString(p_type); + const sdp = GodotRuntime.parseString(p_sdp); + const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj); ref.setLocalDescription({ 'sdp': sdp, 'type': type @@ -348,16 +356,16 @@ var GodotRTCPeerConnection = { 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); + const type = GodotRuntime.parseString(p_type); + const sdp = GodotRuntime.parseString(p_sdp); + const onerror = GodotRuntime.get_func(p_on_error).bind(null, p_obj); + const onsession = GodotRuntime.get_func(p_session_created).bind(null, p_obj); ref.setRemoteDescription({ 'sdp': sdp, 'type': type }).then(function() { - if (type != 'offer') { - return; + if (type !== 'offer') { + return Promise.resolve(); } return ref.createAnswer().then(function(session) { GodotRTCPeerConnection.onsession(p_id, onsession, session); @@ -372,8 +380,8 @@ var GodotRTCPeerConnection = { if (!ref) { return; } - var sdpMidName = UTF8ToString(p_mid_name); - var sdpName = UTF8ToString(p_sdp); + var sdpMidName = GodotRuntime.parseString(p_mid_name); + var sdpName = GodotRuntime.parseString(p_sdp); ref.addIceCandidate(new RTCIceCandidate({ "candidate": sdpName, "sdpMid": sdpMidName, @@ -389,13 +397,13 @@ var GodotRTCPeerConnection = { return 0; } - const label = UTF8ToString(p_label); - const config = JSON.parse(UTF8ToString(p_config)); + const label = GodotRuntime.parseString(p_label); + const config = JSON.parse(GodotRuntime.parseString(p_config)); const channel = ref.createDataChannel(label, config); return IDHandler.add(channel); } catch (e) { - console.error(e); + GodotRuntime.error(e); return 0; } }, diff --git a/modules/websocket/library_godot_websocket.js b/modules/websocket/library_godot_websocket.js index 7076a6f43df..0856cb13e62 100644 --- a/modules/websocket/library_godot_websocket.js +++ b/modules/websocket/library_godot_websocket.js @@ -28,9 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotWebSocket = { +const GodotWebSocket = { // Our socket implementation that forwards events to C++. - $GodotWebSocket__deps: ['$IDHandler'], + $GodotWebSocket__deps: ['$IDHandler', '$GodotRuntime'], $GodotWebSocket: { // Connection opened, report selected protocol _onopen: function(p_id, callback, event) { @@ -38,9 +38,9 @@ var GodotWebSocket = { if (!ref) { return; // Godot object is gone. } - let c_str = GodotOS.allocString(ref.protocol); + let c_str = GodotRuntime.allocString(ref.protocol); callback(c_str); - _free(c_str); + GodotRuntime.free(c_str); }, // Message received, report content and type (UTF8 vs binary) @@ -54,21 +54,21 @@ var GodotWebSocket = { if (event.data instanceof ArrayBuffer) { buffer = new Uint8Array(event.data); } else if (event.data instanceof Blob) { - alert("Blob type not supported"); + GodotRuntime.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 { - alert("Unknown message type"); + GodotRuntime.error("Unknown message type"); return; } var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = _malloc(len); + var out = GodotRuntime.malloc(len); HEAPU8.set(buffer, out); callback(out, len, is_string); - _free(out); + GodotRuntime.free(out); }, // An error happened, 'onclose' will be called after this. @@ -86,15 +86,15 @@ var GodotWebSocket = { if (!ref) { return; // Godot object is gone. } - let c_str = GodotOS.allocString(event.reason); + let c_str = GodotRuntime.allocString(event.reason); callback(event.code, c_str, event.wasClean ? 1 : 0); - _free(c_str); + GodotRuntime.free(c_str); }, // Send a message send: function(p_id, p_data) { const ref = IDHandler.get(p_id); - if (!ref || ref.readyState != ref.OPEN) { + if (!ref || ref.readyState !== ref.OPEN) { return 1; // Godot object is gone or socket is not in a ready state. } ref.send(p_data); @@ -115,7 +115,7 @@ var GodotWebSocket = { const ref = IDHandler.get(p_id); if (ref && ref.readyState < ref.CLOSING) { const code = p_code; - const reason = UTF8ToString(p_reason); + const reason = GodotRuntime.parseString(p_reason); ref.close(code, reason); } }, @@ -136,12 +136,12 @@ var GodotWebSocket = { }, 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); + const on_open = GodotRuntime.get_func(p_on_open).bind(null, p_ref); + const on_message = GodotRuntime.get_func(p_on_message).bind(null, p_ref); + const on_error = GodotRuntime.get_func(p_on_error).bind(null, p_ref); + const on_close = GodotRuntime.get_func(p_on_close).bind(null, p_ref); + const url = GodotRuntime.parseString(p_url); + const protos = GodotRuntime.parseString(p_proto); var socket = null; try { if (protos) { @@ -160,7 +160,7 @@ var GodotWebSocket = { 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'); + bytes_array[i] = GodotRuntime.getHeapValue(p_buf + i, 'i8'); } var out = bytes_array.buffer; if (!p_raw) { @@ -171,7 +171,7 @@ var GodotWebSocket = { godot_js_websocket_close: function(p_id, p_code, p_reason) { const code = p_code; - const reason = UTF8ToString(p_reason); + const reason = GodotRuntime.parseString(p_reason); GodotWebSocket.close(p_id, code, reason); }, diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index a0e6fa0e18a..627ae778b1c 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -20,27 +20,28 @@ build = env.add_program(build_targets, javascript_files) env.AddJSLibraries( [ - "native/http_request.js", - "native/library_godot_audio.js", - "native/library_godot_display.js", - "native/library_godot_os.js", + "js/libs/library_godot_audio.js", + "js/libs/library_godot_display.js", + "js/libs/library_godot_http_request.js", + "js/libs/library_godot_os.js", + "js/libs/library_godot_runtime.js", ] ) if env["tools"]: - env.AddJSLibraries(["native/library_godot_editor_tools.js"]) + env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"]) if env["javascript_eval"]: - env.AddJSLibraries(["native/library_godot_eval.js"]) + env.AddJSLibraries(["js/libs/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", - "engine/utils.js", - "engine/engine.js", + "js/engine/preloader.js", + "js/engine/utils.js", + "js/engine/engine.js", ] -externs = [env.File("#platform/javascript/engine/externs.js")] +externs = [env.File("#platform/javascript/js/engine/engine.externs.js")] js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs) env.Depends(js_engine, externs) @@ -59,7 +60,7 @@ out_files = [ zip_dir.File(binary_name + ".audio.worklet.js"), ] html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" -in_files = [js_wrapped, build[1], html_file, "#platform/javascript/native/audio.worklet.js"] +in_files = [js_wrapped, build[1], html_file, "#platform/javascript/js/libs/audio.worklet.js"] if env["threads_enabled"]: in_files.append(build[2]) out_files.append(zip_dir.File(binary_name + ".worker.js")) diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js deleted file mode 100644 index b3467d009f1..00000000000 --- a/platform/javascript/engine/preloader.js +++ /dev/null @@ -1,135 +0,0 @@ -var Preloader = /** @constructor */ function() { - var DOWNLOAD_ATTEMPTS_MAX = 4; - var progressFunc = null; - var lastProgress = { loaded: 0, total: 0 }; - - var loadingFiles = {}; - this.preloadedFiles = []; - - function loadXHR(resolve, reject, file, tracker) { - var xhr = new XMLHttpRequest; - xhr.open('GET', file); - if (!file.endsWith('.js')) { - xhr.responseType = 'arraybuffer'; - } - ['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) { - xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker)); - }); - xhr.send(); - } - - function onXHREvent(resolve, reject, file, tracker, ev) { - if (this.status >= 400) { - if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - reject(new Error("Failed loading file '" + file + "': " + this.statusText)); - this.abort(); - return; - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - } - - switch (ev.type) { - case 'loadstart': - if (tracker[file] === undefined) { - tracker[file] = { - total: ev.total, - loaded: ev.loaded, - attempts: 0, - final: false, - }; - } - break; - - case 'progress': - tracker[file].loaded = ev.loaded; - tracker[file].total = ev.total; - break; - - case 'load': - tracker[file].final = true; - resolve(this); - break; - - case 'error': - if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - tracker[file].final = true; - reject(new Error("Failed loading file '" + file + "'")); - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - break; - - case 'abort': - tracker[file].final = true; - reject(new Error("Loading file '" + file + "' was aborted.")); - break; - } - } - - this.loadPromise = function(file) { - return new Promise(function(resolve, reject) { - loadXHR(resolve, reject, file, loadingFiles); - }); - } - - this.preload = function(pathOrBuffer, destPath) { - if (pathOrBuffer instanceof ArrayBuffer) { - pathOrBuffer = new Uint8Array(pathOrBuffer); - } else if (ArrayBuffer.isView(pathOrBuffer)) { - pathOrBuffer = new Uint8Array(pathOrBuffer.buffer); - } - if (pathOrBuffer instanceof Uint8Array) { - this.preloadedFiles.push({ - path: destPath, - buffer: pathOrBuffer - }); - return Promise.resolve(); - } else if (typeof pathOrBuffer === 'string') { - var me = this; - return this.loadPromise(pathOrBuffer).then(function(xhr) { - me.preloadedFiles.push({ - path: destPath || pathOrBuffer, - buffer: xhr.response - }); - return Promise.resolve(); - }); - } else { - throw Promise.reject("Invalid object for preloading"); - } - }; - - var animateProgress = function() { - var loaded = 0; - var total = 0; - var totalIsValid = true; - var progressIsFinal = true; - - Object.keys(loadingFiles).forEach(function(file) { - const stat = loadingFiles[file]; - if (!stat.final) { - progressIsFinal = false; - } - if (!totalIsValid || stat.total === 0) { - totalIsValid = false; - total = 0; - } else { - total += stat.total; - } - loaded += stat.loaded; - }); - if (loaded !== lastProgress.loaded || total !== lastProgress.total) { - lastProgress.loaded = loaded; - lastProgress.total = total; - if (typeof progressFunc === 'function') - progressFunc(loaded, total); - } - if (!progressIsFinal) - requestAnimationFrame(animateProgress); - } - this.animateProgress = animateProgress; // Also exposed to start it. - - this.setProgressFunc = function(callback) { - progressFunc = callback; - } -}; diff --git a/platform/javascript/engine/externs.js b/platform/javascript/js/engine/engine.externs.js similarity index 100% rename from platform/javascript/engine/externs.js rename to platform/javascript/js/engine/engine.externs.js diff --git a/platform/javascript/engine/engine.js b/platform/javascript/js/engine/engine.js similarity index 93% rename from platform/javascript/engine/engine.js rename to platform/javascript/js/engine/engine.js index 3745e044793..792ec9cb28f 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/js/engine/engine.js @@ -1,4 +1,4 @@ -Function('return this')()['Engine'] = (function() { +const Engine = (function() { var preloader = new Preloader(); var wasmExt = '.wasm'; @@ -25,7 +25,7 @@ Function('return this')()['Engine'] = (function() { }; /** @constructor */ - function Engine() { + function Engine() { // eslint-disable-line no-shadow this.canvas = null; this.executableName = ''; this.rtenv = null; @@ -90,6 +90,9 @@ Function('return this')()['Engine'] = (function() { if (!(me.canvas instanceof HTMLCanvasElement)) { me.canvas = Utils.findCanvas(); + if (!me.canvas) { + return Promise.reject(new Error('No canvas found in page')); + } } // Canvas can grab focus on click, or key events won't work. @@ -104,7 +107,7 @@ Function('return this')()['Engine'] = (function() { // Until context restoration is implemented warn the user of context loss. me.canvas.addEventListener('webglcontextlost', function(ev) { - alert("WebGL context lost, please reload the page"); + alert("WebGL context lost, please reload the page"); // eslint-disable-line no-alert ev.preventDefault(); }, false); @@ -198,10 +201,11 @@ Function('return this')()['Engine'] = (function() { Engine.prototype.setStdoutFunc = function(func) { var print = function(text) { + let msg = text; if (arguments.length > 1) { - text = Array.prototype.slice.call(arguments).join(" "); + msg = Array.prototype.slice.call(arguments).join(" "); } - func(text); + func(msg); }; if (this.rtenv) this.rtenv.print = print; @@ -210,9 +214,11 @@ Function('return this')()['Engine'] = (function() { Engine.prototype.setStderrFunc = function(func) { var printErr = function(text) { - if (arguments.length > 1) - text = Array.prototype.slice.call(arguments).join(" "); - func(text); + let msg = text + if (arguments.length > 1) { + msg = Array.prototype.slice.call(arguments).join(" "); + } + func(msg); }; if (this.rtenv) this.rtenv.printErr = printErr; @@ -269,3 +275,4 @@ Function('return this')()['Engine'] = (function() { Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; return Engine; })(); +if (typeof window !== 'undefined') window['Engine'] = Engine; diff --git a/platform/javascript/js/engine/preloader.js b/platform/javascript/js/engine/preloader.js new file mode 100644 index 00000000000..8641646e6e1 --- /dev/null +++ b/platform/javascript/js/engine/preloader.js @@ -0,0 +1,129 @@ +var Preloader = /** @constructor */ function() { // eslint-disable-line no-unused-vars + + const loadXHR = function(resolve, reject, file, tracker, attempts) { + const xhr = new XMLHttpRequest(); + tracker[file] = { + total: 0, + loaded: 0, + final: false, + }; + xhr.onerror = function() { + if (attempts <= 1) { + reject(new Error("Failed loading file '" + file + "'")); + } else { + setTimeout(function () { + loadXHR(resolve, reject, file, tracker, attempts - 1); + }, 1000); + } + }; + xhr.onabort = function() { + tracker[file].final = true; + reject(new Error("Loading file '" + file + "' was aborted.")); + }; + xhr.onloadstart = function(ev) { + tracker[file].total = ev.total; + tracker[file].loaded = ev.loaded; + }; + xhr.onprogress = function(ev) { + tracker[file].loaded = ev.loaded; + tracker[file].total = ev.total; + }; + xhr.onload = function() { + if (xhr.status >= 400) { + if (xhr.status < 500 || attempts <= 1) { + reject(new Error("Failed loading file '" + file + "': " + xhr.statusText)); + xhr.abort(); + } else { + setTimeout(function () { + loadXHR(resolve, reject, file, tracker, attempts - 1); + }, 1000); + } + } else { + tracker[file].final = true; + resolve(xhr); + } + }; + // Make request. + xhr.open('GET', file); + if (!file.endsWith('.js')) { + xhr.responseType = 'arraybuffer'; + } + xhr.send(); + }; + + const DOWNLOAD_ATTEMPTS_MAX = 4; + const loadingFiles = {}; + const lastProgress = { loaded: 0, total: 0 }; + let progressFunc = null; + + const animateProgress = function() { + + var loaded = 0; + var total = 0; + var totalIsValid = true; + var progressIsFinal = true; + + Object.keys(loadingFiles).forEach(function(file) { + const stat = loadingFiles[file]; + if (!stat.final) { + progressIsFinal = false; + } + if (!totalIsValid || stat.total === 0) { + totalIsValid = false; + total = 0; + } else { + total += stat.total; + } + loaded += stat.loaded; + }); + if (loaded !== lastProgress.loaded || total !== lastProgress.total) { + lastProgress.loaded = loaded; + lastProgress.total = total; + if (typeof progressFunc === 'function') + progressFunc(loaded, total); + } + if (!progressIsFinal) + requestAnimationFrame(animateProgress); + } + + this.animateProgress = animateProgress; + + this.setProgressFunc = function(callback) { + progressFunc = callback; + } + + + this.loadPromise = function(file) { + return new Promise(function(resolve, reject) { + loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX); + }); + } + + this.preloadedFiles = []; + this.preload = function(pathOrBuffer, destPath) { + let buffer = null; + if (typeof pathOrBuffer === 'string') { + var me = this; + return this.loadPromise(pathOrBuffer).then(function(xhr) { + me.preloadedFiles.push({ + path: destPath || pathOrBuffer, + buffer: xhr.response + }); + return Promise.resolve(); + }); + } else if (pathOrBuffer instanceof ArrayBuffer) { + buffer = new Uint8Array(pathOrBuffer); + } else if (ArrayBuffer.isView(pathOrBuffer)) { + buffer = new Uint8Array(pathOrBuffer.buffer); + } + if (buffer) { + this.preloadedFiles.push({ + path: destPath, + buffer: pathOrBuffer + }); + return Promise.resolve(); + } else { + return Promise.reject(new Error("Invalid object for preloading")); + } + }; +}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/js/engine/utils.js similarity index 86% rename from platform/javascript/engine/utils.js rename to platform/javascript/js/engine/utils.js index 8455739a255..fbab9ba9f90 100644 --- a/platform/javascript/engine/utils.js +++ b/platform/javascript/js/engine/utils.js @@ -1,4 +1,5 @@ -var Utils = { +var Utils = { // eslint-disable-line no-unused-vars + createLocateRewrite: function(execName) { function rw(path) { if (path.endsWith('.worker.js')) { @@ -10,18 +11,20 @@ var Utils = { } else if (path.endsWith('.wasm')) { return execName + '.wasm'; } + return path; } return rw; }, createInstantiatePromise: function(wasmLoader) { + let loader = wasmLoader; function instantiateWasm(imports, onSuccess) { - wasmLoader.then(function(xhr) { + loader.then(function(xhr) { WebAssembly.instantiate(xhr.response, imports).then(function(result) { onSuccess(result['instance'], result['module']); }); }); - wasmLoader = null; + loader = null; return {}; }; @@ -33,10 +36,11 @@ var Utils = { if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { return nodes[0]; } - throw new Error("No canvas found"); + return null; }, isWebGLAvailable: function(majorVersion = 1) { + var testContext = false; try { var testCanvas = document.createElement('canvas'); @@ -45,7 +49,9 @@ var Utils = { } else if (majorVersion === 2) { testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); } - } catch (e) {} + } catch (e) { + // Not available + } return !!testContext; } }; diff --git a/platform/javascript/native/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js similarity index 92% rename from platform/javascript/native/audio.worklet.js rename to platform/javascript/js/libs/audio.worklet.js index f91787b993f..a27035ef224 100644 --- a/platform/javascript/native/audio.worklet.js +++ b/platform/javascript/js/libs/audio.worklet.js @@ -27,6 +27,7 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ + class RingBuffer { constructor(p_buffer, p_state) { this.buffer = p_buffer; @@ -104,7 +105,7 @@ class GodotProcessor extends AudioWorkletProcessor { } parse_message(p_cmd, p_data) { - if (p_cmd == "start" && p_data) { + if (p_cmd === "start" && p_data) { const state = p_data[0]; let idx = 0; this.lock = state.subarray(idx, ++idx); @@ -113,14 +114,14 @@ class GodotProcessor extends AudioWorkletProcessor { const avail_out = state.subarray(idx, ++idx); this.input = new RingBuffer(p_data[1], avail_in); this.output = new RingBuffer(p_data[2], avail_out); - } else if (p_cmd == "stop") { + } else if (p_cmd === "stop") { this.runing = false; this.output = null; this.input = null; } } - array_has_data(arr) { + static array_has_data(arr) { return arr.length && arr[0].length && arr[0][0].length; } @@ -131,30 +132,30 @@ class GodotProcessor extends AudioWorkletProcessor { if (this.output === null) { return true; // Not ready yet, keep processing. } - const process_input = this.array_has_data(inputs); + const process_input = GodotProcessor.array_has_data(inputs); if (process_input) { const input = inputs[0]; const chunk = input[0].length * input.length; - if (this.input_buffer.length != chunk) { + if (this.input_buffer.length !== chunk) { this.input_buffer = new Float32Array(chunk); } if (this.input.space_left() >= chunk) { - this.write_input(this.input_buffer, input); + GodotProcessor.write_input(this.input_buffer, input); this.input.write(this.input_buffer); } else { this.port.postMessage("Input buffer is full! Skipping input frame."); } } - const process_output = this.array_has_data(outputs); + const process_output = GodotProcessor.array_has_data(outputs); if (process_output) { const output = outputs[0]; const chunk = output[0].length * output.length; - if (this.output_buffer.length != chunk) { + if (this.output_buffer.length !== chunk) { this.output_buffer = new Float32Array(chunk); } if (this.output.data_left() >= chunk) { this.output.read(this.output_buffer); - this.write_output(output, this.output_buffer); + GodotProcessor.write_output(output, this.output_buffer); } else { this.port.postMessage("Output buffer has not enough frames! Skipping output frame."); } @@ -163,7 +164,7 @@ class GodotProcessor extends AudioWorkletProcessor { return true; } - write_output(dest, source) { + static write_output(dest, source) { const channels = dest.length; for (let ch = 0; ch < channels; ch++) { for (let sample = 0; sample < dest[ch].length; sample++) { @@ -172,7 +173,7 @@ class GodotProcessor extends AudioWorkletProcessor { } } - write_input(dest, source) { + static write_input(dest, source) { const channels = source.length; for (let ch = 0; ch < channels; ch++) { for (let sample = 0; sample < source[ch].length; sample++) { diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js similarity index 88% rename from platform/javascript/native/library_godot_audio.js rename to platform/javascript/js/libs/library_godot_audio.js index 3a0c8f297ad..a657d0a1259 100644 --- a/platform/javascript/native/library_godot_audio.js +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -29,7 +29,7 @@ /*************************************************************************/ const GodotAudio = { - $GodotAudio__deps: ['$GodotOS'], + $GodotAudio__deps: ['$GodotRuntime', '$GodotOS'], $GodotAudio: { ctx: null, input: null, @@ -42,7 +42,6 @@ const GodotAudio = { // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. }); GodotAudio.ctx = ctx; - onstatechange(ctx.state); // Immeditately notify state. ctx.onstatechange = function() { let state = 0; switch (ctx.state) { @@ -55,19 +54,22 @@ const GodotAudio = { case 'closed': state = 2; break; + + // no default } onstatechange(state); } + ctx.onstatechange(); // Immeditately notify state. // Update computed latency GodotAudio.interval = setInterval(function() { - let latency = 0; + let computed_latency = 0; if (ctx.baseLatency) { - latency += GodotAudio.ctx.baseLatency; + computed_latency += GodotAudio.ctx.baseLatency; } if (ctx.outputLatency) { - latency += GodotAudio.ctx.outputLatency; + computed_latency += GodotAudio.ctx.outputLatency; } - onlatencyupdate(latency); + onlatencyupdate(computed_latency); }, 1000); GodotOS.atexit(GodotAudio.close_async); return ctx.destination.channelCount; @@ -84,14 +86,14 @@ const GodotAudio = { if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ "audio": true - }).then(gotMediaInput, function(e) { out(e) }); + }).then(gotMediaInput, function(e) { GodotRuntime.print(e) }); } else { if (!navigator.getUserMedia) { navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; } navigator.getUserMedia({ "audio": true - }, gotMediaInput, function(e) { out(e) }); + }, gotMediaInput, function(e) { GodotRuntime.print(e) }); } }, @@ -125,7 +127,7 @@ const GodotAudio = { resolve(); }).catch(function(e) { ctx.onstatechange = null; - console.error("Error closing AudioContext", e); + GodotRuntime.error("Error closing AudioContext", e); resolve(); }); }, @@ -140,13 +142,13 @@ const GodotAudio = { }, godot_audio_init: function(p_mix_rate, p_latency, p_state_change, p_latency_update) { - const statechange = GodotOS.get_func(p_state_change); - const latencyupdate = GodotOS.get_func(p_latency_update); + const statechange = GodotRuntime.get_func(p_state_change); + const latencyupdate = GodotRuntime.get_func(p_latency_update); return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); }, godot_audio_resume: function() { - if (GodotAudio.ctx && GodotAudio.ctx.state != 'running') { + if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') { GodotAudio.ctx.resume(); } }, @@ -181,13 +183,13 @@ mergeInto(LibraryManager.library, GodotAudio); * The AudioWorklet API driver, used when threads are available. */ const GodotAudioWorklet = { - $GodotAudioWorklet__deps: ['$GodotAudio'], + $GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'], $GodotAudioWorklet: { promise: null, worklet: null, create: function(channels) { - const path = Module['locateFile']('godot.audio.worklet.js'); + const path = GodotConfig.locate_file('godot.audio.worklet.js'); GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function() { GodotAudioWorklet.worklet = new AudioWorkletNode( GodotAudio.ctx, @@ -210,7 +212,7 @@ const GodotAudioWorklet = { 'data': [state, in_buf, out_buf], }); node.port.onmessage = function(event) { - console.error(event.data); + GodotRuntime.error(event.data); }; }); }, @@ -240,9 +242,9 @@ const GodotAudioWorklet = { }, godot_audio_worklet_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { - const out_buffer = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); - const in_buffer = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); - const state = GodotOS.heapSub(HEAP32, p_state, 4); + const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); + const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); + const state = GodotRuntime.heapSub(HEAP32, p_state, 4); GodotAudioWorklet.start(in_buffer, out_buffer, state); }, @@ -280,7 +282,7 @@ const GodotAudioScript = { start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) { GodotAudioScript.script.onaudioprocess = function(event) { // Read input - const inb = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); + const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); const input = event.inputBuffer; if (GodotAudio.input) { const inlen = input.getChannelData(0).length; @@ -296,7 +298,7 @@ const GodotAudioScript = { onprocess(); // Write the output. - const outb = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); + const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); const output = event.outputBuffer; const channels = output.numberOfChannels; for (let ch = 0; ch < channels; ch++) { @@ -329,7 +331,7 @@ const GodotAudioScript = { }, godot_audio_script_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { - const onprocess = GodotOS.get_func(p_cb); + const onprocess = GodotRuntime.get_func(p_cb); GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); }, }; diff --git a/platform/javascript/native/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js similarity index 85% rename from platform/javascript/native/library_godot_display.js rename to platform/javascript/js/libs/library_godot_display.js index 11bbfbc60dd..28f63ba5576 100644 --- a/platform/javascript/native/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -33,13 +33,14 @@ * Keeps track of registered event listeners so it can remove them on shutdown. */ const GodotDisplayListeners = { + $GodotDisplayListeners__deps: ['$GodotOS'], $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; + return e.target === target && e.event === event && e.method === method && e.capture === capture; }) !== -1; }, @@ -47,11 +48,11 @@ const GodotDisplayListeners = { 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; + function Handler(p_target, p_event, p_method, p_capture) { + this.target = p_target; + this.event = p_event; + this.method = p_method; + this.capture = p_capture; }; GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture)); target.addEventListener(event, method, capture); @@ -89,7 +90,7 @@ const GodotDisplayDragDrop = { } else if (entry.isFile) { GodotDisplayDragDrop.add_file(entry); } else { - console.error("Unrecognized entry...", entry); + GodotRuntime.error("Unrecognized entry...", entry); } }, @@ -124,19 +125,19 @@ const GodotDisplayDragDrop = { resolve() }; reader.onerror = function() { - console.log("Error reading file"); + GodotRuntime.print("Error reading file"); reject(); } reader.readAsArrayBuffer(file); }, function(err) { - console.log("Error!"); + GodotRuntime.print("Error!"); reject(); }); })); }, process: function(resolve, reject) { - if (GodotDisplayDragDrop.promises.length == 0) { + if (GodotDisplayDragDrop.promises.length === 0) { resolve(); return; } @@ -164,10 +165,10 @@ const GodotDisplayDragDrop = { } } } else { - console.error("File upload not supported"); + GodotRuntime.error("File upload not supported"); } new Promise(GodotDisplayDragDrop.process).then(function() { - const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; + const DROP = "/tmp/drop-" + parseInt(Math.random() * (1 << 30), 10) + "/"; const drops = []; const files = []; FS.mkdir(DROP); @@ -175,14 +176,14 @@ const GodotDisplayDragDrop = { const path = elem['path']; GodotFS.copy_to_fs(DROP + path, elem['data']); let idx = path.indexOf("/"); - if (idx == -1) { + 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) { + if (idx < 0 && drops.indexOf(DROP + sub) === -1) { drops.push(DROP + sub); } } @@ -199,7 +200,7 @@ const GodotDisplayDragDrop = { let idx = dir.lastIndexOf("/"); while (idx > 0) { dir = dir.substr(0, idx); - if (dirs.indexOf(DROP + dir) == -1) { + if (dirs.indexOf(DROP + dir) === -1) { dirs.push(DROP + dir); } idx = dir.lastIndexOf("/"); @@ -234,8 +235,8 @@ mergeInto(LibraryManager.library, GodotDisplayDragDrop); * Keeps track of cursor status and custom shapes. */ const GodotDisplayCursor = { + $GodotDisplayCursor__deps: ['$GodotOS', '$GodotConfig'], $GodotDisplayCursor__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });', - $GodotDisplayCursor__deps: ['$GodotConfig', '$GodotOS'], $GodotDisplayCursor: { shape: 'auto', visible: true, @@ -273,7 +274,7 @@ mergeInto(LibraryManager.library, GodotDisplayCursor); * Exposes all the functions needed by DisplayServer implementation. */ const GodotDisplay = { - $GodotDisplay__deps: ['$GodotConfig', '$GodotOS', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop'], + $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop'], $GodotDisplay: { window_icon: '', }, @@ -288,7 +289,7 @@ const GodotDisplay = { }, godot_js_display_alert: function(p_text) { - window.alert(UTF8ToString(p_text)); + window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert }, godot_js_display_pixel_ratio_get: function() { @@ -303,13 +304,13 @@ const GodotDisplay = { }, godot_js_display_canvas_is_focused: function() { - return document.activeElement == GodotConfig.canvas; + 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'); + GodotRuntime.setHeapValue(r_x, brect.x, 'i32'); + GodotRuntime.setHeapValue(r_y, brect.y, 'i32'); }, /* @@ -323,25 +324,24 @@ const GodotDisplay = { * Clipboard */ godot_js_display_clipboard_set: function(p_text) { - const text = UTF8ToString(p_text); + const text = GodotRuntime.parseString(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); + GodotRuntime.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); + const func = GodotRuntime.get_func(callback); try { navigator.clipboard.readText().then(function (result) { - const ptr = allocate(intArrayFromString(result), ALLOC_NORMAL); + const ptr = GodotRuntime.allocString(result); func(ptr); - _free(ptr); + GodotRuntime.free(ptr); }).catch(function (e) { // Fail graciously. }); @@ -362,7 +362,7 @@ const GodotDisplay = { }, godot_js_display_window_title_set: function(p_data) { - document.title = UTF8ToString(p_data); + document.title = GodotRuntime.parseString(p_data); }, godot_js_display_window_icon_set: function(p_ptr, p_len) { @@ -374,7 +374,7 @@ const GodotDisplay = { document.head.appendChild(link); } const old_icon = GodotDisplay.window_icon; - const png = new Blob([GodotOS.heapCopy(HEAPU8, p_ptr, p_len)], { type: "image/png" }); + const png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: "image/png" }); GodotDisplay.window_icon = URL.createObjectURL(png); link.href = GodotDisplay.window_icon; if (old_icon) { @@ -386,8 +386,8 @@ const GodotDisplay = { * Cursor */ godot_js_display_cursor_set_visible: function(p_visible) { - const visible = p_visible != 0; - if (visible == GodotDisplayCursor.visible) { + const visible = p_visible !== 0; + if (visible === GodotDisplayCursor.visible) { return; } GodotDisplayCursor.visible = visible; @@ -403,14 +403,14 @@ const GodotDisplay = { }, godot_js_display_cursor_set_shape: function(p_string) { - GodotDisplayCursor.set_shape(UTF8ToString(p_string)); + GodotDisplayCursor.set_shape(GodotRuntime.parseString(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 shape = GodotRuntime.parseString(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 png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); const url = URL.createObjectURL(png); GodotDisplayCursor.cursors[shape] = { url: url, @@ -420,7 +420,7 @@ const GodotDisplay = { } else { delete GodotDisplayCursor.cursors[shape]; } - if (shape == GodotDisplayCursor.shape) { + if (shape === GodotDisplayCursor.shape) { GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); } if (old_shape) { @@ -433,7 +433,7 @@ const GodotDisplay = { */ 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 func = GodotRuntime.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() { @@ -443,26 +443,26 @@ const GodotDisplay = { }, godot_js_display_paste_cb: function(callback) { - const func = GodotOS.get_func(callback); + const func = GodotRuntime.get_func(callback); GodotDisplayListeners.add(window, 'paste', function(evt) { const text = evt.clipboardData.getData('text'); - const ptr = allocate(intArrayFromString(text), ALLOC_NORMAL); + const ptr = GodotRuntime.allocString(text); func(ptr); - _free(ptr); + GodotRuntime.free(ptr); }, false); }, godot_js_display_drop_files_cb: function(callback) { - const func = GodotOS.get_func(callback) + const func = GodotRuntime.get_func(callback) const dropFiles = function(files) { const args = files || []; if (!args.length) { return; } const argc = args.length; - const argv = GodotOS.allocStringArray(args); + const argv = GodotRuntime.allocStringArray(args); func(argv, argc); - GodotOS.freeStringArray(argv, argc); + GodotRuntime.freeStringArray(argv, argc); }; const canvas = GodotConfig.canvas; GodotDisplayListeners.add(canvas, 'dragover', function(ev) { diff --git a/platform/javascript/native/library_godot_editor_tools.js b/platform/javascript/js/libs/library_godot_editor_tools.js similarity index 95% rename from platform/javascript/native/library_godot_editor_tools.js rename to platform/javascript/js/libs/library_godot_editor_tools.js index 202a198adbf..21e40185aea 100644 --- a/platform/javascript/native/library_godot_editor_tools.js +++ b/platform/javascript/js/libs/library_godot_editor_tools.js @@ -31,9 +31,9 @@ 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 path = GodotRuntime.parseString(p_path); + const name = GodotRuntime.parseString(p_name); + const mime = GodotRuntime.parseString(p_mime); const size = FS.stat(path)['size']; const buf = new Uint8Array(size); const fd = FS.open(path, 'r'); diff --git a/platform/javascript/native/library_godot_eval.js b/platform/javascript/js/libs/library_godot_eval.js similarity index 86% rename from platform/javascript/native/library_godot_eval.js rename to platform/javascript/js/libs/library_godot_eval.js index 44d356a4fb9..4064938d3e9 100644 --- a/platform/javascript/native/library_godot_eval.js +++ b/platform/javascript/js/libs/library_godot_eval.js @@ -29,34 +29,34 @@ /*************************************************************************/ const GodotEval = { - godot_js_eval__deps: ['$GodotOS'], + godot_js_eval__deps: ['$GodotRuntime'], 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); + const js_code = GodotRuntime.parseString(p_js); let eval_ret = null; try { if (p_use_global_ctx) { // indirect eval call grants global execution context - const global_eval = eval; + const global_eval = eval; // eslint-disable-line no-eval eval_ret = global_eval(js_code); } else { - eval_ret = eval(js_code); + eval_ret = eval(js_code); // eslint-disable-line no-eval } } catch (e) { - err(e); + GodotRuntime.error(e); } switch (typeof eval_ret) { + case 'boolean': - setValue(p_union_ptr, eval_ret, 'i32'); + GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'i32'); return 1; // BOOL case 'number': - setValue(p_union_ptr, eval_ret, 'double'); + GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'double'); return 3; // REAL case 'string': - let array_ptr = GodotOS.allocString(eval_ret); - setValue(p_union_ptr, array_ptr , '*'); + GodotRuntime.setHeapValue(p_union_ptr, GodotRuntime.allocString(eval_ret), '*'); return 4; // STRING case 'object': @@ -71,12 +71,14 @@ const GodotEval = { eval_ret = new Uint8Array(eval_ret); } if (eval_ret instanceof Uint8Array) { - const func = GodotOS.get_func(p_callback); + const func = GodotRuntime.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; + + // no default } return 0; // NIL }, diff --git a/platform/javascript/native/http_request.js b/platform/javascript/js/libs/library_godot_http_request.js similarity index 83% rename from platform/javascript/native/http_request.js rename to platform/javascript/js/libs/library_godot_http_request.js index 272154aee3c..6f80f3b9580 100644 --- a/platform/javascript/native/http_request.js +++ b/platform/javascript/js/libs/library_godot_http_request.js @@ -27,7 +27,9 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotHTTPRequest = { + +const GodotHTTPRequest = { + $GodotHTTPRequest__deps: ['$GodotRuntime'], $GodotHTTPRequest: { requests: [], @@ -65,14 +67,14 @@ var GodotHTTPRequest = { GodotHTTPRequest.requests[xhrId] = null; }, - godot_xhr_open: function(xhrId, method, url, user, password) { - user = user > 0 ? UTF8ToString(user) : null; - password = password > 0 ? UTF8ToString(password) : null; - GodotHTTPRequest.requests[xhrId].open(UTF8ToString(method), UTF8ToString(url), true, user, password); + godot_xhr_open: function(xhrId, method, url, p_user, p_password) { + const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null; + const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null; + GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password); }, godot_xhr_set_request_header: function(xhrId, header, value) { - GodotHTTPRequest.requests[xhrId].setRequestHeader(UTF8ToString(header), UTF8ToString(value)); + GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value)); }, godot_xhr_send_null: function(xhrId) { @@ -81,19 +83,19 @@ var GodotHTTPRequest = { godot_xhr_send_string: function(xhrId, strPtr) { if (!strPtr) { - err("Failed to send string per XHR: null pointer"); + GodotRuntime.error("Failed to send string per XHR: null pointer"); return; } - GodotHTTPRequest.requests[xhrId].send(UTF8ToString(strPtr)); + GodotHTTPRequest.requests[xhrId].send(GodotRuntime.parseString(strPtr)); }, godot_xhr_send_data: function(xhrId, ptr, len) { if (!ptr) { - err("Failed to send data per XHR: null pointer"); + GodotRuntime.error("Failed to send data per XHR: null pointer"); return; } if (len < 0) { - err("Failed to send data per XHR: buffer length less than 0"); + GodotRuntime.error("Failed to send data per XHR: buffer length less than 0"); return; } GodotHTTPRequest.requests[xhrId].send(HEAPU8.subarray(ptr, ptr + len)); @@ -113,17 +115,14 @@ var GodotHTTPRequest = { godot_xhr_get_response_headers_length: function(xhrId) { var headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - return headers === null ? 0 : lengthBytesUTF8(headers); + return headers === null ? 0 : GodotRuntime.strlen(headers); }, godot_xhr_get_response_headers: function(xhrId, dst, len) { var str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); if (str === null) return; - var buf = new Uint8Array(len + 1); - stringToUTF8Array(str, buf, 0, buf.length); - buf = buf.subarray(0, -1); - HEAPU8.set(buf, dst); + GodotRuntime.stringToHeap(str, dst, len); }, godot_xhr_get_response_length: function(xhrId) { diff --git a/platform/javascript/native/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js similarity index 83% rename from platform/javascript/native/library_godot_os.js rename to platform/javascript/js/libs/library_godot_os.js index a1424a691ac..582f04cb1b2 100644 --- a/platform/javascript/native/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -54,6 +54,7 @@ mergeInto(LibraryManager.library, IDHandler); const GodotConfig = { $GodotConfig__postset: 'Module["initConfig"] = GodotConfig.init_config;', + $GodotConfig__deps: ['$GodotRuntime'], $GodotConfig: { canvas: null, locale: "en", @@ -66,16 +67,20 @@ const GodotConfig = { 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']; + Module['onExit'] = p_opts['onExit']; // eslint-disable-line no-undef + }, + + locate_file: function(file) { + return Module["locateFile"](file); // eslint-disable-line no-undef }, }, godot_js_config_canvas_id_get: function(p_ptr, p_ptr_max) { - stringToUTF8('#' + GodotConfig.canvas.id, p_ptr, p_ptr_max); + GodotRuntime.stringToHeap('#' + 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); + GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max); }, godot_js_config_is_resize_on_start: function() { @@ -86,8 +91,9 @@ const GodotConfig = { autoAddDeps(GodotConfig, '$GodotConfig'); mergeInto(LibraryManager.library, GodotConfig); + const GodotFS = { - $GodotFS__deps: ['$FS', '$IDBFS'], + $GodotFS__deps: ['$FS', '$IDBFS', '$GodotRuntime'], $GodotFS__postset: [ 'Module["initFS"] = GodotFS.init;', 'Module["deinitFS"] = GodotFS.deinit;', @@ -136,7 +142,7 @@ const GodotFS = { if (err) { GodotFS._mount_points = []; GodotFS._idbfs = false; - console.log("IndexedDB not available: " + err.message); + GodotRuntime.print("IndexedDB not available: " + err.message); } else { GodotFS._idbfs = true; } @@ -151,7 +157,7 @@ const GodotFS = { try { FS.unmount(path); } catch (e) { - console.log("Already unmounted", e); + GodotRuntime.print("Already unmounted", e); } if (GodotFS._idbfs && IDBFS.dbs[path]) { IDBFS.dbs[path].close(); @@ -165,14 +171,14 @@ const GodotFS = { sync: function() { if (GodotFS._syncing) { - err('Already syncing!'); + GodotRuntime.error('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); + GodotRuntime.error('Failed to save IDB file system: ' + error.message); } GodotFS._syncing = false; resolve(error); @@ -202,7 +208,7 @@ const GodotFS = { mergeInto(LibraryManager.library, GodotFS); const GodotOS = { - $GodotOS__deps: ['$GodotFS'], + $GodotOS__deps: ['$GodotFS', '$GodotRuntime'], $GodotOS__postset: [ 'Module["request_quit"] = function() { GodotOS.request_quit() };', 'GodotOS._fs_sync_promise = Promise.resolve();', @@ -212,10 +218,6 @@ const GodotOS = { _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); }, @@ -236,48 +238,15 @@ const GodotOS = { }, 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); + const func = GodotRuntime.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); + GodotOS.request_quit = GodotRuntime.get_func(p_callback); }, godot_js_os_fs_is_persistent: function() { @@ -285,7 +254,7 @@ const GodotOS = { }, godot_js_os_fs_sync: function(callback) { - const func = GodotOS.get_func(callback); + const func = GodotRuntime.get_func(callback); GodotOS._fs_sync_promise = GodotFS.sync(); GodotOS._fs_sync_promise.then(function(err) { func(); @@ -293,7 +262,7 @@ const GodotOS = { }, godot_js_os_execute: function(p_json) { - const json_args = UTF8ToString(p_json); + const json_args = GodotRuntime.parseString(p_json); const args = JSON.parse(json_args); if (GodotConfig.on_execute) { GodotConfig.on_execute(args); @@ -303,7 +272,7 @@ const GodotOS = { }, godot_js_os_shell_open: function(p_uri) { - window.open(UTF8ToString(p_uri), '_blank'); + window.open(GodotRuntime.parseString(p_uri), '_blank'); }, }; diff --git a/platform/javascript/js/libs/library_godot_runtime.js b/platform/javascript/js/libs/library_godot_runtime.js new file mode 100644 index 00000000000..1769f83623e --- /dev/null +++ b/platform/javascript/js/libs/library_godot_runtime.js @@ -0,0 +1,120 @@ +/*************************************************************************/ +/* library_godot_runtime.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 GodotRuntime = { + $GodotRuntime: { + /* + * Functions + */ + get_func: function(ptr) { + return wasmTable.get(ptr); // eslint-disable-line no-undef + }, + + /* + * Prints + */ + error: function() { + err.apply(null, Array.from(arguments)); // eslint-disable-line no-undef + }, + + print: function() { + out.apply(null, Array.from(arguments)); // eslint-disable-line no-undef + }, + + /* + * Memory + */ + malloc: function(p_size) { + return _malloc(p_size); // eslint-disable-line no-undef + }, + + free: function(p_ptr) { + _free(p_ptr); // eslint-disable-line no-undef + }, + + getHeapValue: function (p_ptr, p_type) { + return getValue(p_ptr, p_type); // eslint-disable-line no-undef + }, + + setHeapValue: function(p_ptr, p_value, p_type) { + setValue(p_ptr, p_value, p_type); // eslint-disable-line no-undef + }, + + heapSub: function(p_heap, p_ptr, p_len) { + const bytes = p_heap.BYTES_PER_ELEMENT; + return p_heap.subarray(p_ptr / bytes, p_ptr / bytes + p_len); + }, + + heapCopy: function(p_heap, p_ptr, p_len) { + const bytes = p_heap.BYTES_PER_ELEMENT; + return p_heap.slice(p_ptr / bytes, p_ptr / bytes + p_len); + }, + + /* + * Strings + */ + parseString: function(p_ptr) { + return UTF8ToString(p_ptr); // eslint-disable-line no-undef + }, + + strlen: function(p_str) { + return lengthBytesUTF8(p_str); // eslint-disable-line no-undef + }, + + allocString: function(p_str) { + const length = GodotRuntime.strlen(p_str)+1; + const c_str = GodotRuntime.malloc(length); + stringToUTF8(p_str, c_str, length); // eslint-disable-line no-undef + return c_str; + }, + + allocStringArray: function(p_strings) { + const size = p_strings.length; + const c_ptr = GodotRuntime.malloc(size * 4); + for (let i = 0; i < size; i++) { + HEAP32[(c_ptr >> 2) + i] = GodotRuntime.allocString(p_strings[i]); + } + return c_ptr; + }, + + freeStringArray: function(p_ptr, p_len) { + for (let i = 0; i < p_len; i++) { + GodotRuntime.free(HEAP32[(p_ptr >> 2) + i]); + } + GodotRuntime.free(p_ptr); + }, + + stringToHeap: function (p_str, p_ptr, p_len) { + return stringToUTF8Array(p_str, HEAP8, p_ptr, p_len); // eslint-disable-line no-undef + }, + }, +}; +autoAddDeps(GodotRuntime, "$GodotRuntime"); +mergeInto(LibraryManager.library, GodotRuntime);