diff options
author | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
---|---|---|
committer | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
commit | abef6da56913f1c55528103e60a50451a39628b1 (patch) | |
tree | b3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/interop_web.js |
initial commit
Diffstat (limited to 'src/interop_web.js')
-rw-r--r-- | src/interop_web.js | 1448 |
1 files changed, 1448 insertions, 0 deletions
diff --git a/src/interop_web.js b/src/interop_web.js new file mode 100644 index 0000000..c2903a9 --- /dev/null +++ b/src/interop_web.js @@ -0,0 +1,1448 @@ +// 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(); + }, +}); |