// Copyright 2010 The Emscripten Authors. All rights reserved. // Emscripten is available under two separate licenses, // the MIT license and the University of Illinois/NCSA Open Source License. // Both these licenses can be found in the LICENSE file. mergeInto(LibraryManager.library, { interop_SaveBlob: function(blob, name) { if (window.navigator.msSaveBlob) { window.navigator.msSaveBlob(blob, name); return; } var url = window.URL.createObjectURL(blob); var elem = document.createElement('a'); elem.href = url; elem.download = name; elem.style.display = 'none'; document.body.appendChild(elem); elem.click(); document.body.removeChild(elem); window.URL.revokeObjectURL(url); }, interop_InitModule: function() { // these are required for older versions of emscripten, but the compiler removes // this by default as no syscalls are used by the C platform code anymore window.ERRNO_CODES={ENOENT:2,EBADF:9,EAGAIN:11,ENOMEM:12,EEXIST:17,EINVAL:22}; }, interop_InitModule__deps: ['interop_SaveBlob', 'interop_callVoidFunc', 'interop_callStringFunc'], interop_TakeScreenshot: function(path) { var name = UTF8ToString(path); var canvas = Module['canvas']; if (canvas.toBlob) { canvas.toBlob(function(blob) { _interop_SaveBlob(blob, name); }); } else if (canvas.msToBlob) { _interop_SaveBlob(canvas.msToBlob(), name); } }, interop_callVoidFunc: function(func) { Module['_' + func](); }, interop_callStringFunc: function(func, str) { var arg = 0; var stackTop = stackSave(); if (str !== null && str !== undefined) { var len = (str.length * 4) + 1; // worst case, 4 bytes to encode a char arg = stackAlloc(len); stringToUTF8(str, arg, len); } Module['_' + func](arg); stackRestore(stackTop); }, //######################################################################################################################## //-----------------------------------------------------------Http--------------------------------------------------------- //######################################################################################################################## interop_DownloadAsync: function(urlStr, method, reqID) { // onFinished = FUNC(data, len, status) // onProgress = FUNC(read, total) var url = UTF8ToString(urlStr); var reqMethod = method == 1 ? 'HEAD' : 'GET'; var onFinished = Module["_Http_OnFinishedAsync"]; var onProgress = Module["_Http_OnUpdateProgress"]; var xhr = new XMLHttpRequest(); try { xhr.open(reqMethod, url); } catch (e) { // DOMException gets thrown when invalid URL provided. Test cases: // http://%7https://www.example.com/test.zip // http://example:app/test.zip console.log(e); return 1; } xhr.responseType = 'arraybuffer'; var getContentLength = function(e) { if (e.total) return e.total; try { var len = xhr.getResponseHeader('Content-Length'); return parseInt(len, 10); } catch (ex) { return 0; } }; xhr.onload = function(e) { var src = new Uint8Array(xhr.response); var len = src.byteLength; var data = _malloc(len); HEAPU8.set(src, data); onFinished(reqID, data, len || getContentLength(e), xhr.status); }; xhr.onerror = function(e) { onFinished(reqID, 0, 0, xhr.status); }; xhr.ontimeout = function(e) { onFinished(reqID, 0, 0, xhr.status); }; xhr.onprogress = function(e) { onProgress(reqID, e.loaded, e.total); }; try { xhr.send(); } catch (e) { onFinished(reqID, 0, 0, 0); } return 0; }, interop_IsHttpsOnly : function() { // If this webpage is https://, browsers deny any http:// downloading return location.protocol === 'https:'; }, //######################################################################################################################## //---------------------------------------------------------Dialogs-------------------------------------------------------- //######################################################################################################################## interop_DownloadFile: function(filename, filters, titles) { try { if (_interop_ShowSaveDialog(filename, filters, titles)) return 0; var name = UTF8ToString(filename); var path = 'Downloads/' + name; _interop_callStringFunc('Window_OnFileUploaded', path); var data = CCFS.readFile(path); var blob = new Blob([data], { type: 'application/octet-stream' }); _interop_SaveBlob(blob, UTF8ToString(filename)); CCFS.unlink(path); return 0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return e.errno; } }, interop_DownloadFile__deps: ['interop_SaveBlob', 'interop_ShowSaveDialog'], interop_ShowSaveDialog: function(filename, filters, titles) { // not supported by all browsers if (!window.showSaveFilePicker) return 0; var fileTypes = []; for (var i = 0; HEAP32[(filters>>2)+i|0]; i++) { var filter = HEAP32[(filters>>2)+i|0]; var title = HEAP32[(titles >>2)+i|0]; var filetype = { description: UTF8ToString(title), accept: {'applicaion/octet-stream': [UTF8ToString(filter)]} }; fileTypes.push(filetype); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises // https://web.dev/file-system-access/ var path = null; var opts = { suggestedName: UTF8ToString(filename), types: fileTypes }; window.showSaveFilePicker(opts) .then(function(fileHandle) { path = 'Downloads/' + fileHandle.name; return fileHandle.createWritable(); }) .then(function(writable) { _interop_callStringFunc('Window_OnFileUploaded', path); var data = CCFS.readFile(path); writable.write(data); return writable.close(); }) .catch(function(error) { _interop_callStringFunc('Platform_LogError', '&cError downloading file'); _interop_callStringFunc('Platform_LogError', ' &c' + error); }) .finally(function(result) { if (path) CCFS.unlink(path); }); return 1; }, //######################################################################################################################## //-------------------------------------------------------Main driver------------------------------------------------------ //######################################################################################################################## fetchTexturePackAsync: function(url, onload, onerror) { var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'arraybuffer'; xhr.onerror = onerror; xhr.onload = function() { if (xhr.status == 200) { onload(xhr.response); } else { onerror(); } }; xhr.send(); }, interop_AsyncDownloadTexturePack__deps: ['fetchTexturePackAsync'], interop_AsyncDownloadTexturePack: function (rawPath) { var path = UTF8ToString(rawPath); var url = '/static/default.zip'; Module.setStatus('Downloading textures.. (1/2)'); _fetchTexturePackAsync(url, function(buffer) { CCFS.writeFile(path, new Uint8Array(buffer)); _interop_callVoidFunc('main_phase1'); }, function() { _interop_callVoidFunc('main_phase1'); } ); }, interop_AsyncLoadIndexedDB__deps: ['IDBFS_loadFS'], interop_AsyncLoadIndexedDB: function() { Module.setStatus('Preloading filesystem.. (2/2)'); _IDBFS_loadFS(function(err) { if (err) window.cc_idbErr = err; Module.setStatus(''); _interop_callVoidFunc('main_phase2'); }); }, //######################################################################################################################## //---------------------------------------------------------Platform------------------------------------------------------- //######################################################################################################################## interop_OpenTab: function(url) { try { window.open(UTF8ToString(url)); } catch (e) { // DOMException gets thrown when invalid URL provided. Test cases: // http://example:app/test.zip console.log(e); return 1; } return 0; }, interop_Log: function(msg, len) { Module.print(UTF8ArrayToString(HEAPU8, msg, len)); }, interop_GetLocalTime: function(time) { var date = new Date(); HEAP32[(time|0 + 0)>>2] = date.getFullYear(); HEAP32[(time|0 + 4)>>2] = date.getMonth() + 1|0; HEAP32[(time|0 + 8)>>2] = date.getDate(); HEAP32[(time|0 + 12)>>2] = date.getHours(); HEAP32[(time|0 + 16)>>2] = date.getMinutes(); HEAP32[(time|0 + 20)>>2] = date.getSeconds(); }, interop_DirectorySetWorking: function (raw) { var path = UTF8ToString(raw); CCFS.chdir(path); }, interop_DirectoryIter: function(raw) { var path = UTF8ToString(raw); try { var entries = CCFS.readdir(path); for (var i = 0; i < entries.length; i++) { var path = entries[i]; // absolute path to root relative path if (path.indexOf(CCFS.currentPath) === 0) { path = path.substring(CCFS.currentPath.length + 1); } _interop_callStringFunc('Directory_IterCallback', path); } return 0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileExists: function (raw) { var path = UTF8ToString(raw); path = CCFS.resolvePath(path); return path in CCFS.entries; }, interop_FileCreate: function(raw, flags) { var path = UTF8ToString(raw); try { var stream = CCFS.open(path, flags); return stream.fd|0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileRead: function(fd, dst, count) { try { var stream = CCFS.getStream(fd); return CCFS.read(stream, HEAP8, dst, count)|0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileWrite: function(fd, src, count) { try { var stream = CCFS.getStream(fd); return CCFS.write(stream, HEAP8, src, count)|0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileSeek: function(fd, offset, whence) { try { var stream = CCFS.getStream(fd); return CCFS.llseek(stream, offset, whence)|0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileLength: function(fd) { try { var stream = CCFS.getStream(fd); return stream.node.usedBytes|0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileClose: function(fd) { try { var stream = CCFS.getStream(fd); CCFS.close(stream); // save writable files to IndexedDB (check for O_RDWR) if ((stream.flags & 3) == 2) _interop_SaveNode(stream.path); return 0; } catch (e) { if (!(e instanceof CCFS.ErrnoError)) abort(e); return -e.errno; } }, interop_FileClose__deps: ['interop_SaveNode'], //######################################################################################################################## //--------------------------------------------------------Filesystem------------------------------------------------------ //######################################################################################################################## interop_InitFilesystem__deps: ['interop_SaveNode'], interop_InitFilesystem: function(buffer) { if (!window.cc_idbErr) return; var msg = 'Error preloading IndexedDB:' + window.cc_idbErr + '\n\nPreviously saved settings/maps will be lost'; _interop_callStringFunc('Platform_LogError', msg); }, interop_LoadIndexedDB: function() { // previously you were required to add interop_LoadIndexedDB to Module.preRun array // to load the indexedDB asynchronously *before* starting ClassiCube, because it // could not load indexedDB asynchronously // however, as ClassiCube now loads IndexedDB asynchronously itself, this is // no longer necessary, but is kept around for backwards compatibility }, interop_SaveNode__deps: ['IDBFS_getDB', 'IDBFS_storeRemoteEntry'], interop_SaveNode: function(path) { var callback = function(err) { if (!err) return; console.log(err); _interop_callStringFunc('Platform_LogError', '&cError saving ' + path); _interop_callStringFunc('Platform_LogError', ' &c' + err); }; var stat, node, entry; try { var lookup = CCFS.lookupPath(path); node = lookup.node; // Performance consideration: storing a normal JavaScript array to a IndexedDB is much slower than storing a typed array. // Therefore always convert the file contents to a typed array first before writing the data to IndexedDB. node.contents = MEMFS.getFileDataAsTypedArray(node); entry = { timestamp: node.timestamp, mode: CCFS.MODE_TYPE_FILE, contents: node.contents }; } catch (err) { return callback(err); } _IDBFS_getDB(function(err, db) { if (err) return callback(err); var transaction, store; // can still throw errors here try { transaction = db.transaction([IDBFS_DB_STORE_NAME], 'readwrite'); store = transaction.objectStore(IDBFS_DB_STORE_NAME); } catch (err) { return callback(err); } transaction.onerror = function(e) { callback(this.error); e.preventDefault(); }; _IDBFS_storeRemoteEntry(store, path, entry, callback); }); }, //######################################################################################################################## //--------------------------------------------------------IndexedDB------------------------------------------------------- //######################################################################################################################## IDBFS_loadFS__deps: ['IDBFS_getRemoteSet', 'IDBFS_reconcile'], IDBFS_loadFS: function(callback) { _IDBFS_getRemoteSet(function(err, remote) { if (err) return callback(err); _IDBFS_reconcile(remote, callback); }); }, IDBFS_getDB: function(callback) { var db = window.IDBFS_db; if (db) return callback(null, db); IDBFS_DB_VERSION = 21; IDBFS_DB_STORE_NAME = "FILE_DATA"; var idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; if (!idb) return callback("IndexedDB unsupported"); var req; try { req = idb.open('/classicube', IDBFS_DB_VERSION); } catch (e) { return callback(e); } if (!req) return callback("Unable to connect to IndexedDB"); req.onupgradeneeded = function(e) { var db = e.target.result; var transaction = e.target.transaction; var fileStore; if (db.objectStoreNames.contains(IDBFS_DB_STORE_NAME)) { fileStore = transaction.objectStore(IDBFS_DB_STORE_NAME); } else { fileStore = db.createObjectStore(IDBFS_DB_STORE_NAME); } if (!fileStore.indexNames.contains('timestamp')) { fileStore.createIndex('timestamp', 'timestamp', { unique: false }); } }; req.onsuccess = function() { db = req.result; window.IDBFS_db = db; // browser will sometimes close IndexedDB connection behind the scenes db.onclose = function(ev) { console.log('IndexedDB connection closed unexpectedly!'); window.IDBFS_db = null; } callback(null, db); }; req.onerror = function(e) { callback(this.error); e.preventDefault(); }; }, IDBFS_getRemoteSet__deps: ['IDBFS_getDB'], IDBFS_getRemoteSet: function(callback) { var entries = {}; _IDBFS_getDB(function(err, db) { if (err) return callback(err); try { var transaction = db.transaction([IDBFS_DB_STORE_NAME], 'readonly'); transaction.onerror = function(e) { callback(this.error); e.preventDefault(); }; var store = transaction.objectStore(IDBFS_DB_STORE_NAME); var index = store.index('timestamp'); index.openKeyCursor().onsuccess = function(event) { var cursor = event.target.result; if (!cursor) { return callback(null, { type: 'remote', db: db, entries: entries }); } entries[cursor.primaryKey] = { timestamp: cursor.key }; cursor.continue(); }; } catch (e) { return callback(e); } }); }, IDBFS_loadRemoteEntry: function(store, path, callback) { var req = store.get(path); req.onsuccess = function(event) { callback(null, event.target.result); }; req.onerror = function(e) { callback(this.error); e.preventDefault(); }; }, IDBFS_storeRemoteEntry: function(store, path, entry, callback) { var req = store.put(entry, path); req.onsuccess = function() { callback(null); }; req.onerror = function(e) { callback(this.error); e.preventDefault(); }; }, IDBFS_storeLocalEntry: function(path, entry, callback) { try { // ignore directories from IndexedDB created in older game versions if (CCFS.isFile(entry.mode)) { CCFS.writeFile(path, entry.contents); CCFS.utime(path, entry.timestamp); } } catch (e) { return callback(e); } callback(null); }, IDBFS_reconcile__deps: ['IDBFS_loadRemoteEntry', 'IDBFS_storeLocalEntry'], IDBFS_reconcile: function(src, callback) { var total = 0; var create = []; Object.keys(src.entries).forEach(function (key) { create.push(key); total++; }); if (!total) return callback(null); var errored = false; var completed = 0; var transaction = src.db.transaction([IDBFS_DB_STORE_NAME], 'readwrite'); var store = transaction.objectStore(IDBFS_DB_STORE_NAME); function done(err) { if (err) { if (!done.errored) { done.errored = true; return callback(err); } return; } if (++completed >= total) { return callback(null); } }; transaction.onerror = function(e) { done(this.error); e.preventDefault(); }; // sort paths in ascending order so directory entries are created // before the files inside them create.sort().forEach(function (path) { _IDBFS_loadRemoteEntry(store, path, function (err, entry) { if (err) return done(err); _IDBFS_storeLocalEntry(path, entry, done); }); }); }, //######################################################################################################################## //---------------------------------------------------------Sockets-------------------------------------------------------- //######################################################################################################################## interop_InitSockets: function() { window.SOCKETS = { EBADF:-8,EISCONN:-30,ENOTCONN:-53,EAGAIN:-6,EHOSTUNREACH:-23,EINPROGRESS:-26,EALREADY:-7,ECONNRESET:-15,EINVAL:-28,ECONNREFUSED:-14, sockets: [], }; }, interop_SocketCreate: function() { var sock = { error: null, // Used by interop_SocketWritable recv_queue: [], socket: null, }; SOCKETS.sockets.push(sock); return (SOCKETS.sockets.length - 1) | 0; }, interop_SocketConnect: function(sockFD, raw, port) { var addr = UTF8ToString(raw); var sock = SOCKETS.sockets[sockFD]; if (!sock) return SOCKETS.EBADF; // already connecting or connected var ws = sock.socket; if (ws) { if (ws.readyState === ws.CONNECTING) return SOCKETS.EALREADY; return SOCKETS.EISCONN; } // create the actual websocket object and connect try { var parts = addr.split('/'); var proto = _interop_IsHttpsOnly() ? 'wss://' : 'ws://'; var url = proto + parts[0] + ":" + port + "/" + parts.slice(1).join('/'); ws = new WebSocket(url, 'ClassiCube'); ws.binaryType = 'arraybuffer'; } catch (e) { return SOCKETS.EHOSTUNREACH; } sock.socket = ws; ws.onopen = function() {}; ws.onclose = function() {}; ws.onmessage = function(event) { var data = event.data; if (typeof data === 'string') { var encoder = new TextEncoder(); // should be utf-8 data = encoder.encode(data); // make a typed array from the string } else { assert(data.byteLength !== undefined); // must receive an ArrayBuffer if (data.byteLength == 0) { // An empty ArrayBuffer will emit a pseudo disconnect event // as recv/recvmsg will return zero which indicates that a socket // has performed a shutdown although the connection has not been disconnected yet. return; } else { data = new Uint8Array(data); // make a typed array view on the array buffer } } sock.recv_queue.push(data); }; ws.onerror = function(error) { // The WebSocket spec only allows a 'simple event' to be thrown on error, // so we only really know as much as ECONNREFUSED. sock.error = SOCKETS.ECONNREFUSED; // Used by interop_SocketWritable }; // always "fail" in non-blocking mode return SOCKETS.EINPROGRESS; }, interop_SocketClose: function(sockFD) { var sock = SOCKETS.sockets[sockFD]; if (!sock) return SOCKETS.EBADF; try { sock.socket.close(); } catch (e) { } delete sock.socket; return 0; }, interop_SocketSend: function(sockFD, src, length) { var sock = SOCKETS.sockets[sockFD]; if (!sock) return SOCKETS.EBADF; var ws = sock.socket; if (!ws || ws.readyState === ws.CLOSING || ws.readyState === ws.CLOSED) { return SOCKETS.ENOTCONN; } else if (ws.readyState === ws.CONNECTING) { return SOCKETS.EAGAIN; } // var data = HEAP8.slice(src, src + length); unsupported in IE11 var data = new Uint8Array(length); for (var i = 0; i < length; i++) { data[i] = HEAP8[src + i]; } try { ws.send(data); return length; } catch (e) { return SOCKETS.EINVAL; } }, interop_SocketRecv: function(sockFD, dst, length) { var sock = SOCKETS.sockets[sockFD]; if (!sock) return SOCKETS.EBADF; var packet = sock.recv_queue.shift(); if (!packet) { var ws = sock.socket; if (!ws || ws.readyState == ws.CLOSING || ws.readyState == ws.CLOSED) { return SOCKETS.ENOTCONN; } else { // socket is in a valid state but truly has nothing available return SOCKETS.EAGAIN; } } // packet will be an ArrayBuffer if it's unadulterated, but if it's // requeued TCP data it'll be an ArrayBufferView var packetLength = packet.byteLength || packet.length; var packetOffset = packet.byteOffset || 0; var packetBuffer = packet.buffer || packet; var bytesRead = Math.min(length, packetLength); var msg = new Uint8Array(packetBuffer, packetOffset, bytesRead); // push back any unread data for TCP connections if (bytesRead < packetLength) { var bytesRemaining = packetLength - bytesRead; packet = new Uint8Array(packetBuffer, packetOffset + bytesRead, bytesRemaining); sock.recv_queue.unshift(packet); } HEAPU8.set(msg, dst); return msg.byteLength; }, interop_SocketWritable: function(sockFD, writable) { HEAPU8[writable|0] = 0; var sock = SOCKETS.sockets[sockFD]; if (!sock) return SOCKETS.EBADF; var ws = sock.socket; if (!ws) return SOCKETS.ENOTCONN; if (ws.readyState === ws.OPEN) HEAPU8[writable|0] = 1; return sock.error || 0; }, //######################################################################################################################## //----------------------------------------------------------Window-------------------------------------------------------- //######################################################################################################################## interop_CanvasWidth: function() { return Module['canvas'].width; }, interop_CanvasHeight: function() { return Module['canvas'].height; }, interop_ScreenWidth: function() { return screen.width; }, interop_ScreenHeight: function() { return screen.height; }, interop_IsAndroid: function() { return /Android/i.test(navigator.userAgent); }, interop_IsIOS: function() { // iOS 13 on iPad doesn't identify itself as iPad by default anymore // https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up return /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints && navigator.maxTouchPoints > 2); }, interop_InitContainer: function() { // Create wrapper div if necessary (so input textbox shows in fullscreen on android) var agent = navigator.userAgent; var canvas = Module['canvas']; window.cc_container = document.body; if (/Android/i.test(agent)) { var wrapper = document.createElement("div"); wrapper.id = 'canvas_wrapper'; canvas.parentNode.insertBefore(wrapper, canvas); wrapper.appendChild(canvas); window.cc_container = wrapper; } }, interop_GetContainerID: function() { // For chrome on android, need to make container div fullscreen instead return document.getElementById('canvas_wrapper') ? 1 : 0; }, interop_ForceTouchPageLayout: function() { if (typeof(forceTouchLayout) === 'function') forceTouchLayout(); }, interop_SetPageTitle : function(title) { document.title = UTF8ToString(title); }, interop_AddClipboardListeners: function() { // Copy text, but only if user isn't selecting something else on the webpage // (don't check window.clipboardData here, that's handled in interop_TrySetClipboardText instead) window.addEventListener('copy', function(e) { if (window.getSelection && window.getSelection().toString()) return; _interop_callVoidFunc('Window_RequestClipboardText'); if (!window.cc_copyText) return; if (e.clipboardData) { e.clipboardData.setData('text/plain', window.cc_copyText); e.preventDefault(); } window.cc_copyText = null; }); // Paste text (window.clipboardData is handled in interop_TryGetClipboardText instead) window.addEventListener('paste', function(e) { if (e.clipboardData) { var contents = e.clipboardData.getData('text/plain'); _interop_callStringFunc('Window_GotClipboardText', contents); } }); }, interop_TryGetClipboardText: function() { // For IE11, use window.clipboardData to get the clipboard if (window.clipboardData) { var contents = window.clipboardData.getData('Text'); _interop_callStringFunc('Window_StoreClipboardText', contents); } }, interop_TrySetClipboardText: function(text) { // For IE11, use window.clipboardData to set the clipboard */ // For other browsers, instead use the window.copy events */ if (window.clipboardData) { if (window.getSelection && window.getSelection().toString()) return; window.clipboardData.setData('Text', UTF8ToString(text)); } else { window.cc_copyText = UTF8ToString(text); } }, interop_EnterFullscreen: function() { // emscripten sets css size to screen's base width/height, // except that becomes wrong when device rotates. // Better to just set CSS width/height to always be 100% var canvas = Module['canvas']; canvas.style.width = '100%'; canvas.style.height = '100%'; // By default, pressing Escape will immediately exit fullscreen - which is // quite annoying given that it is also the Menu key. Some browsers allow // 'locking' the Escape key, so that you have to hold down Escape to exit. // NOTE: This ONLY works when the webpage is a https:// one try { navigator.keyboard.lock(["Escape"]); } catch (ex) { } }, // Adjust from document coordinates to element coordinates interop_AdjustXY: function(x, y) { var canvasRect = Module['canvas'].getBoundingClientRect(); HEAP32[x >> 2] = HEAP32[x >> 2] - canvasRect.left; HEAP32[y >> 2] = HEAP32[y >> 2] - canvasRect.top; }, interop_RequestCanvasResize: function() { if (typeof(resizeGameCanvas) === 'function') resizeGameCanvas(); }, interop_SetCursorVisible: function(visible) { Module['canvas'].style['cursor'] = visible ? 'default' : 'none'; }, interop_ShowDialog: function(title, msg) { alert(UTF8ToString(title) + "\n\n" + UTF8ToString(msg)); }, interop_OpenKeyboard: function(text, flags, placeholder) { var elem = window.cc_inputElem; var shown = true; var type = flags & 0xFF; if (!elem) { if (type == 1) { // KEYBOARD_TYPE_NUMBER elem = document.createElement('input'); elem.setAttribute('type', 'text') elem.setAttribute('inputmode', 'decimal'); } else if (type == 3) { // KEYBOARD_TYPE_INTEGER elem = document.createElement('input'); elem.setAttribute('type', 'text') elem.setAttribute('inputmode', 'numeric'); // Fix for older iOS safari where inputmode is unsupported // https://news.ycombinator.com/item?id=22433654 // https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/ elem.setAttribute('pattern', '[0-9]*'); } else { elem = document.createElement('textarea'); } shown = false; } if (flags & 0x100) { elem.setAttribute('enterkeyhint', 'send'); } //elem.setAttribute('style', 'position:absolute; left:0.5%; bottom:1%; margin: 0px; width: 99%; background-color: #080808; border: none; color: white; opacity: 0.7'); elem.setAttribute('style', 'position:absolute; left:0; bottom:0; margin: 0px; width: 100%; background-color: #222222; border: none; color: white;'); elem.setAttribute('placeholder', UTF8ToString(placeholder)); elem.value = UTF8ToString(text); if (!shown) { // stop event propagation, because we don't want the game trying to handle these events elem.addEventListener('touchstart', function(ev) { ev.stopPropagation(); }, false); elem.addEventListener('touchmove', function(ev) { ev.stopPropagation(); }, false); elem.addEventListener('mousedown', function(ev) { ev.stopPropagation(); }, false); elem.addEventListener('mousemove', function(ev) { ev.stopPropagation(); }, false); elem.addEventListener('input', function(ev) { _interop_callStringFunc('Window_OnTextChanged', ev.target.value); }, false); window.cc_inputElem = elem; window.cc_divElem = document.createElement('div'); window.cc_divElem.setAttribute('style', 'position:absolute; left:0; top:0; width:100%; height:100%; background-color: black; opacity:0.4; resize:none; pointer-events:none;'); window.cc_container.appendChild(window.cc_divElem); window.cc_container.appendChild(elem); } // force on-screen keyboard to be shown elem.focus(); elem.click(); }, interop_SetKeyboardText: function(text) { if (!window.cc_inputElem) return; var str = UTF8ToString(text); var cur = window.cc_inputElem.value; // when pressing 'Go' on the on-screen keyboard, some web browsers add \n to value if (cur.length && cur[cur.length - 1] == '\n') { cur = cur.substring(0, cur.length - 1); } if (str != cur) window.cc_inputElem.value = str; }, interop_CloseKeyboard: function() { if (!window.cc_inputElem) return; window.cc_container.removeChild(window.cc_divElem); window.cc_container.removeChild(window.cc_inputElem); window.cc_divElem = null; window.cc_inputElem = null; }, interop_OpenFileDialog: function(filter, action, folder) { var elem = window.cc_uploadElem; var root = UTF8ToString(folder); if (!elem) { elem = document.createElement('input'); elem.setAttribute('type', 'file'); elem.setAttribute('style', 'display: none'); elem.accept = UTF8ToString(filter); elem.addEventListener('change', function(ev) { var files = ev.target.files; for (var i = 0; i < files.length; i++) { var reader = new FileReader(); var name = files[i].name; reader.onload = function(e) { var data = new Uint8Array(e.target.result); var path = root + '/' + name; CCFS.writeFile(path, data); _interop_callStringFunc('Window_OnFileUploaded', path); if (action == 0) CCFS.unlink(path); // OFD_UPLOAD_DELETE if (action == 1) _interop_SaveNode(path); // OFD_UPLOAD_PERSIST }; reader.readAsArrayBuffer(files[i]); } window.cc_container.removeChild(window.cc_uploadElem); window.cc_uploadElem = null; }, false); window.cc_uploadElem = elem; window.cc_container.appendChild(elem); } elem.click(); }, //######################################################################################################################## //--------------------------------------------------------GLContext------------------------------------------------------- //######################################################################################################################### interop_GetGpuRenderer : function(buffer, len) { var dbg = GLctx.getExtension('WEBGL_debug_renderer_info'); var str = dbg ? GLctx.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ""; stringToUTF8(str, buffer, len); }, //######################################################################################################################## //---------------------------------------------------------Sockets-------------------------------------------------------- //######################################################################################################################## interop_AudioLog: function(err) { console.log(err); window.AUDIO.errors.push(''+err); return window.AUDIO.errors.length|0; }, interop_InitAudio: function() { window.AUDIO = window.AUDIO || { context: null, sources: [], buffers: {}, errors: [], seen: {}, }; if (window.AUDIO.context) return 0; try { if (window.AudioContext) { AUDIO.context = new window.AudioContext(); } else { AUDIO.context = new window.webkitAudioContext(); } return 0; } catch (err) { return _interop_AudioLog(err) } }, interop_InitAudio__deps: ['interop_AudioLog'], interop_AudioCreate: function() { var src = { source: null, gain: AUDIO.context.createGain(), playing: false, }; AUDIO.sources.push(src); return AUDIO.sources.length|0; // NOTE: 0 is used by Audio.c for "no source" }, interop_AudioClose: function(ctxID) { var src = AUDIO.sources[ctxID - 1|0]; if (src.source) src.source.stop(); AUDIO.sources[ctxID - 1|0] = null; }, interop_AudioPoll: function(ctxID, inUse) { var src = AUDIO.sources[ctxID - 1|0]; HEAP32[inUse >> 2] = src.playing; // only 1 buffer return 0; }, interop_AudioVolume: function(ctxID, volume) { var src = AUDIO.sources[ctxID - 1|0]; src.gain.gain.value = volume / 100; }, interop_AudioPlay: function(ctxID, sndID, rate) { var src = AUDIO.sources[ctxID - 1|0]; var name = UTF8ToString(sndID); // do we need to download this file? if (!AUDIO.seen.hasOwnProperty(name)) { AUDIO.seen[name] = true; _interop_AudioDownload(name); return 0; } // still downloading or failed to download this file var buffer = AUDIO.buffers[name]; if (!buffer) return 0; try { // AudioBufferSourceNode only allows the buffer property // to be assigned *ONCE* (throws InvalidStateError next time) // MDN says that these nodes are very inexpensive to create though // https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode src.source = AUDIO.context.createBufferSource(); src.source.buffer = buffer; src.source.playbackRate.value = rate / 100; // source -> gain -> output src.source.connect(src.gain); src.gain.connect(AUDIO.context.destination); src.source.start(); return 0; } catch (err) { return _interop_AudioLog(err) } }, interop_AudioPlay__deps: ['interop_AudioDownload'], interop_AudioDownload: function(name) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/static/sounds/' + name + '.wav', true); xhr.responseType = 'arraybuffer'; xhr.onload = function() { var data = xhr.response; AUDIO.context.decodeAudioData(data, function(buffer) { AUDIO.buffers[name] = buffer; }); }; xhr.send(); }, interop_AudioDescribe: function(errCode, buffer, bufferLen) { if (errCode > AUDIO.errors.length) return 0; var str = AUDIO.errors[errCode - 1]; return stringToUTF8(str, buffer, bufferLen); }, //######################################################################################################################## //-----------------------------------------------------------Font--------------------------------------------------------- //######################################################################################################################## interop_SetFont: function(fontStr, size, flags) { if (!window.FONT_CANVAS) { window.FONT_CANVAS = document.createElement('canvas'); window.FONT_CONTEXT = window.FONT_CANVAS.getContext('2d'); } var prefix = ''; if (flags & 1) prefix += 'Bold '; size += 4; // adjust font size so text appears more like FreeType var font = UTF8ToString(fontStr); var ctx = window.FONT_CONTEXT; ctx.font = prefix + size + 'px ' + font; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; return ctx; }, interop_TextWidth: function(textStr, textLen) { var text = UTF8ArrayToString(HEAPU8, textStr, textLen); var ctx = window.FONT_CONTEXT; var data = ctx.measureText(text); return data.width; }, interop_TextDraw: function(textStr, textLen, bmp, dstX, dstY, shadow, hexStr) { var text = UTF8ArrayToString(HEAPU8, textStr, textLen); var hex = UTF8ArrayToString(HEAPU8, hexStr, 7); var ctx = window.FONT_CONTEXT; // resize canvas if necessary so text fits var data = ctx.measureText(text); var text_width = Math.ceil(data.width)|0; if (text_width > ctx.canvas.width) { var font = ctx.font; ctx.canvas.width = text_width; // resizing canvas also resets the properties of CanvasRenderingContext2D ctx.font = font; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; } var text_offset = 0.0; ctx.fillStyle = hex; if (shadow) { text_offset = 1.3; } ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.fillText(text, text_offset, text_offset); bmp = bmp|0; dstX = dstX|0; dstY = dstY|0; var dst_pixels = HEAP32[(bmp + 0|0)>>2] + (dstX << 2); var dst_width = HEAP32[(bmp + 4|0)>>2]; var dst_height = HEAP32[(bmp + 8|0)>>2]; // TODO not all of it var src = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); var src_pixels = src.data; var src_width = src.width|0; var src_height = src.height|0; var img_width = Math.min(src_width, dst_width); var img_height = Math.min(src_height, dst_height); for (var y = 0; y < img_height; y++) { var yy = y + dstY; if (yy < 0 || yy >= dst_height) continue; var src_row = (y *(src_width << 2))|0; var dst_row = dst_pixels + (yy*(dst_width << 2))|0; for (var x = 0; x < img_width; x++) { var xx = x + dstX; if (xx < 0 || xx >= dst_width) continue; var I = src_pixels[src_row + (x<<2)+3], invI = 255 - I|0; HEAPU8[dst_row + (x<<2)+0] = ((src_pixels[src_row + (x<<2)+0] * I) >> 8) + ((HEAPU8[dst_row + (x<<2)+0] * invI) >> 8); HEAPU8[dst_row + (x<<2)+1] = ((src_pixels[src_row + (x<<2)+1] * I) >> 8) + ((HEAPU8[dst_row + (x<<2)+1] * invI) >> 8); HEAPU8[dst_row + (x<<2)+2] = ((src_pixels[src_row + (x<<2)+2] * I) >> 8) + ((HEAPU8[dst_row + (x<<2)+2] * invI) >> 8); HEAPU8[dst_row + (x<<2)+3] = I + ((HEAPU8[dst_row + (x<<2)+3] * invI) >> 8); } } return data.width; }, //######################################################################################################################## //------------------------------------------------------------FS---------------------------------------------------------- //######################################################################################################################## interop_FS_Init: function() { if (window.CCFS) return; window.MEMFS={ createNode:function(path) { var node = CCFS.createNode(path); node.usedBytes = 0; // The actual number of bytes used in the typed array, as opposed to contents.length which gives the whole capacity. // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. node.contents = null; node.timestamp = Date.now(); return node; }, getFileDataAsTypedArray:function(node) { if (!node.contents) return new Uint8Array; if (node.contents.subarray) return node.contents.subarray(0, node.usedBytes); // Make sure to not return excess unused bytes. return new Uint8Array(node.contents); }, expandFileStorage:function(node, newCapacity) { var prevCapacity = node.contents ? node.contents.length : 0; if (prevCapacity >= newCapacity) return; // No need to expand, the storage was already large enough. // Don't expand strictly to the given requested limit if it's only a very small increase, but instead geometrically grow capacity. // For small filesizes (<1MB), perform size*2 geometric increase, but for large sizes, do a much more conservative size*1.125 increase to // avoid overshooting the allocation cap by a very large margin. var CAPACITY_DOUBLING_MAX = 1024 * 1024; newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) | 0); if (prevCapacity != 0) newCapacity = Math.max(newCapacity, 256); // At minimum allocate 256b for each file when expanding. var oldContents = node.contents; node.contents = new Uint8Array(newCapacity); // Allocate new storage. if (node.usedBytes > 0) node.contents.set(oldContents.subarray(0, node.usedBytes), 0); // Copy old data over to the new storage. return; }, clearFileStorage:function(node) { node.contents = null; // Fully decommit when requesting a resize to zero. node.usedBytes = 0; }, stream_read:function(stream, buffer, offset, length, position) { var contents = stream.node.contents; if (position >= stream.node.usedBytes) return 0; var size = Math.min(stream.node.usedBytes - position, length); assert(size >= 0); if (size > 8 && contents.subarray) { // non-trivial, and typed array buffer.set(contents.subarray(position, position + size), offset); } else { for (var i = 0; i < size; i++) buffer[offset + i] = contents[position + i]; } return size; }, stream_write:function(stream, buffer, offset, length, position, canOwn) { if (!length) return 0; var node = stream.node; var chunk = buffer.subarray(offset, offset + length); node.timestamp = Date.now(); if (canOwn) { // NOTE: buffer cannot be a part of the memory buffer (i.e. HEAP8) // - don't want to hold on to references of the memory Buffer, // as they may get invalidated. assert(position === 0, 'canOwn must imply no weird position inside the file'); node.contents = chunk; node.usedBytes = length; } else if (node.usedBytes === 0 && position === 0) { // First write to an empty file, do a fast set since don't need to care about old data node.contents = new Uint8Array(chunk); node.usedBytes = length; } else if (position + length <= node.usedBytes) { // Writing to an already allocated and used subrange of the file node.contents.set(chunk, position); } else { // Appending to an existing file and we need to reallocate MEMFS.expandFileStorage(node, position+length); node.contents.set(chunk, position); node.usedBytes = Math.max(node.usedBytes, position+length); } return length; } }; window.CCFS={ streams:[],entries:{},currentPath:"/",ErrnoError:null, resolvePath:function(path) { if (path.charAt(0) !== '/') { path = CCFS.currentPath + '/' + path; } return path; }, lookupPath:function(path) { path = CCFS.resolvePath(path); var node = CCFS.entries[path]; if (!node) throw new CCFS.ErrnoError(2); return { path: path, node: node }; }, createNode:function(path) { var node = { path: path }; CCFS.entries[path] = node; return node; }, MODE_TYPE_FILE:32768, isFile:function(mode) { return (mode & 61440) === CCFS.MODE_TYPE_FILE; }, nextfd:function() { // max 4096 open files for (var fd = 0; fd <= 4096; fd++) { if (!CCFS.streams[fd]) return fd; } throw new CCFS.ErrnoError(24); }, getStream:function(fd) { return CCFS.streams[fd]; }, createStream:function(stream) { var fd = CCFS.nextfd(); stream.fd = fd; CCFS.streams[fd] = stream; return stream; }, readdir:function(path) { path = CCFS.resolvePath(path) + '/'; // all entries starting with given directory var entries = []; for (var entry in CCFS.entries) { if (entry.indexOf(path) !== 0) continue; entries.push(entry); } return entries; }, unlink:function(path) { var lookup = CCFS.lookupPath(path); delete CCFS.entries[lookup.path]; }, utime:function(path, mtime) { var lookup = CCFS.lookupPath(path); var node = lookup.node; node.timestamp = mtime; }, open:function(path, flags) { path = CCFS.resolvePath(path); var node = CCFS.entries[path]; // perhaps we need to create the node var created = false; if ((flags & 64)) { if (node) { // if O_CREAT and O_EXCL are set, error out if the node already exists if ((flags & 128)) { throw new CCFS.ErrnoError(17); } } else { // node doesn't exist, try to create it node = MEMFS.createNode(path); created = true; } } if (!node) { throw new CCFS.ErrnoError(2); } // do truncation if necessary if ((flags & 512)) { MEMFS.clearFileStorage(node); node.timestamp = Date.now(); } // we've already handled these, don't pass down to the underlying vfs flags &= ~(128 | 512); // register the stream with the filesystem var stream = CCFS.createStream({ node: node, path: path, flags: flags, position: 0 }); return stream; }, close:function(stream) { if (CCFS.isClosed(stream)) { throw new CCFS.ErrnoError(9); } CCFS.streams[stream.fd] = null; stream.fd = null; }, isClosed:function(stream) { return stream.fd === null; }, llseek:function(stream, offset, whence) { if (CCFS.isClosed(stream)) { throw new CCFS.ErrnoError(9); } var position = offset; if (whence === 0) { // SEEK_SET // beginning of file, no need to add anything } else if (whence === 1) { // SEEK_CUR position += stream.position; } else if (whence === 2) { // SEEK_END position += stream.node.usedBytes; } if (position < 0) { throw new CCFS.ErrnoError(22); } stream.position = position; return stream.position; }, read:function(stream, buffer, offset, length) { if (length < 0) { throw new CCFS.ErrnoError(22); } if (CCFS.isClosed(stream)) { throw new CCFS.ErrnoError(9); } if ((stream.flags & 2097155) === 1) { throw new CCFS.ErrnoError(9); } var position = stream.position; var bytesRead = MEMFS.stream_read(stream, buffer, offset, length, position); stream.position += bytesRead; return bytesRead; }, write:function(stream, buffer, offset, length, canOwn) { if (length < 0) { throw new CCFS.ErrnoError(22); } if (CCFS.isClosed(stream)) { throw new CCFS.ErrnoError(9); } if ((stream.flags & 2097155) === 0) { throw new CCFS.ErrnoError(9); } if (stream.flags & 1024) { // seek to the end before writing in append mode CCFS.llseek(stream, 0, 2); } var position = stream.position; var bytesWritten = MEMFS.stream_write(stream, buffer, offset, length, position, canOwn); stream.position += bytesWritten; return bytesWritten; }, readFile:function(path, opts) { opts = opts || {}; opts.encoding = opts.encoding || 'binary'; var ret; var stream = CCFS.open(path, 0); // O_RDONLY var length = stream.node.usedBytes; var buf = new Uint8Array(length); CCFS.read(stream, buf, 0, length); if (opts.encoding === 'utf8') { ret = UTF8ArrayToString(buf, 0); } else if (opts.encoding === 'binary') { ret = buf; } else { throw new Error('Invalid encoding type "' + opts.encoding + '"'); } CCFS.close(stream); return ret; }, writeFile:function(path, data) { var stream = CCFS.open(path, 577); // O_WRONLY | O_CREAT | O_TRUNC if (typeof data === 'string') { var buf = new Uint8Array(lengthBytesUTF8(data)+1); var actualNumBytes = stringToUTF8Array(data, buf, 0, buf.length); CCFS.write(stream, buf, 0, actualNumBytes, true); } else if (ArrayBuffer.isView(data)) { CCFS.write(stream, data, 0, data.byteLength, true); } else { throw new Error('Unsupported data type'); } CCFS.close(stream); }, chdir:function(path) { CCFS.currentPath = CCFS.resolvePath(path); }, ensureErrnoError:function() { CCFS.ErrnoError = function ErrnoError(errno, node) { this.node = node; this.errno = errno; }; CCFS.ErrnoError.prototype = new Error(); CCFS.ErrnoError.prototype.constructor = CCFS.ErrnoError; }}; CCFS.ensureErrnoError(); }, });