summary refs log tree commit diff
path: root/src/interop_web.js
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/interop_web.js
initial commit
Diffstat (limited to 'src/interop_web.js')
-rw-r--r--src/interop_web.js1448
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();
+  },
+});