+ !!( + !ReduxStore?.getState?.()?.scratchGui?.mode.isPlayerOnly && + ReduxStore?.getState?.()?.scratchGui?.mode.hasEverEnteredEditor + ); + /* eslint-enable no-undef */ + // Wait for some extensions + const waiting = new Map(), + registerOnLoad = []; + vm.on("EXTENSION_ADDED", (extension) => { + if (extension.id === extId) { + // Load anything that needs loaded when our extension is added + let fn = () => {}; + while ((fn = registerOnLoad.shift())) { + fn(inEditor()); + } + vm.extensionManager.refreshBlocks(); + } else if (waiting.has(extension.id)) { + // Check if this is a waiting extension and if it is remove it from the waitlist + // and run the function 300ms later (to allow for the refresh to take effect correctly) + const fn = waiting.get(extension.id); + waiting.delete(extension.id); + setTimeout(() => { + fn(inEditor()); + vm.extensionManager.refreshBlocks(); + }, 300); + } + }); + return (extensionId, callback) => { + // If the extension is already loaded then just wait for us to load + // then run the function. + if (vm.extensionManager.isExtensionLoaded(extensionId)) + registerOnLoad.push(callback); + // Otherwise set it to be waiting with the callback + else waiting.set(extensionId, callback); + }; + })(); + modExtension( + "cst1229zip", + () => (runtime.ext_cst1229zip._showArrayBufferOption = true) + ); + modExtension( + "filesExpanded", + () => (runtime.ext_filesexpanded._showUnsafeOptions = true) + ); + + // This is probably not a good implementation but IDC it does what I need. + class View { + constructor(arrayBuffer) { + // Make sure this is a valid argument and arraybuffer + if (!arrayBuffer) throw new Error("View takes 1 arguments but found 0"); + if (!(arrayBuffer instanceof ArrayBuffer)) + throw new TypeError( + `TypeError: The first argument must be ArrayBuffer. Received type ${typeof arrayBuffer}` + ); + this.buffer = new Buffer(arrayBuffer); + // Some info about the Buffer / View + this.length = Number(this.buffer.length); + this.littleEndian = false; + this.byteOffset = 0; + } + // Utils + _checkIdx(idx) { + // Make sure the index is greater than zero but not the length + if (idx + this.byteOffset > this.length - 1 || idx < 0) + throw new Error( + `Index (${idx}) is out of bounds of the view (${this.length})` + ); + } + get arrayBuffer() { + // Get the arrayBuffer + return this.buffer.slice(this.byteOffset, this.byteOffset + this.length) + .buffer; + } + // Some management functions + copy(target, ...args) { + // Allocate / get the buffer + target = target || Buffer.allocUnsafe(this.length); + if (target instanceof View) target = target.buffer; + // Run the copy and return a View + this.buffer.copy(target, ...args); + return new View(target.buffer); + } + fill(byte) { + // Just a map lol + this.buffer.fill(byte); + } + concat(other) { + // Allocate a buffer thats the length of both buffers + const buff = Buffer.allocUnsafe(this.length + other.length); + // Copy the buffers into the new buffer + this.buffer.copy(buff, 0, 0, this.length); + other.copy(buff, this.length, 0, other.length); + // Return a view + return new View(buff); + } + // Data manipulation + getInt8(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + return this.buffer.readInt8(idx); + } + setInt8(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + return this.buffer.writeInt8(val, idx); + } + getInt16(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.readInt16LE(idx); + else return this.buffer.readInt16BE(idx); + } + setInt16(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.writeInt16LE(val, idx); + else return this.buffer.writeInt16BE(val, idx); + } + getInt32(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.readInt32LE(idx); + else return this.buffer.readInt32BE(idx); + } + setInt32(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.writeInt32LE(val, idx); + else return this.buffer.writeInt32BE(val, idx); + } + getUint8(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + return this.buffer.readUint8(idx); + } + setUint8(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + return this.buffer.writeUint8(val, idx); + } + getUint16(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.readUint16LE(idx); + else return this.buffer.readUint16BE(idx); + } + setUint16(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.writeUint16LE(val, idx); + else return this.buffer.writeUint16BE(val, idx); + } + getUint32(idx, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.readUint32LE(idx); + else return this.buffer.readUint32BE(idx); + } + setUint32(idx, val, littleEndian = this.littleEndian) { + this._checkIdx(idx), (idx += this.byteOffset); + if (littleEndian) return this.buffer.writeUint32LE(val, idx); + else return this.buffer.writeUint32BE(val, idx); + } + } + + class extension { + constructor() { + // The last block error + this.lastErr = ""; + // All the views + this.views = new Map(); + // Current view + this.view_name = ""; + this.view = this.emptyView; + // Access methods (LE, BE, and the type) + this.accessMethod = "Uint8"; + this.littleEndian = false; + // Encoders and decoders for the string option + this.textDecoder = new TextDecoder(); + this.textEncoder = new TextEncoder(); + } + // Some exports for any extension that wants them + static exports = { + Buffer, + Base64, + View, + _swap8, + _swap16, + _swap32, + TypedArray, + ArrayBuffer, + }; + getInfo() { + return { + id: extId, + name: "Buffers", + color1: "#e855e3", + color2: "#e43ade", + color3: "#e43ade", + color4: "#e43ade", + blocks: [ + { + blockType: BlockType.REPORTER, + func: "getAttr", + opcode: "getAttr1", + text: "[ATTR]", + arguments: { + ATTR: { type: ArgumentType.STRING, menu: "attr_type" }, + }, + allowDropAnywhere: true, + }, + "---", + { + blockType: BlockType.BOOLEAN, + opcode: "hasView", + text: "buffer [NAME] exists?", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my new buffer", + }, + }, + }, + "---", + { + blockType: BlockType.COMMAND, + opcode: "remove", + text: "delete buffer [NAME]", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my buffer" }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "create", + text: "new buffer [NAME] from [OPT] [DATA]", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my buffer" }, + OPT: { type: ArgumentType.STRING, menu: "creation_type" }, + DATA: { + type: ArgumentType.STRING, + defaultValue: "Hello, World~", + }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "use", + text: "use buffer [NAME]", + arguments: { + NAME: { type: ArgumentType.STRING, defaultValue: "my buffer" }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "setAccessMethod", + text: "set access method to [SIGNED][TYPE][SIZE]", + arguments: { + SIGNED: { type: ArgumentType.STRING, menu: "is_signed" }, + TYPE: { type: ArgumentType.STRING, menu: "number_type" }, + SIZE: { type: ArgumentType.STRING, menu: "block_size" }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "setLittleEndian", + text: "enable little endian: [LITTLE_ENDIAN]?", + arguments: { + LITTLE_ENDIAN: { type: ArgumentType.BOOLEAN }, + }, + }, + "---", + { + blockType: BlockType.COMMAND, + opcode: "setAtIdx", + text: "set byte [IDX] to [NUM]", + arguments: { + IDX: { type: ArgumentType.NUMBER, defaultValue: 1 }, + NUM: { type: ArgumentType.NUMBER, defaultValue: 5 }, + }, + }, + { + blockType: BlockType.REPORTER, + opcode: "getAtIdx", + text: "get byte [IDX]", + arguments: { + IDX: { type: ArgumentType.NUMBER, defaultValue: 1 }, + }, + }, + { + blockType: BlockType.REPORTER, + func: "getAttr", + opcode: "getAttr2", + text: "current buffer as [ATTR]", + arguments: { + ATTR: { type: ArgumentType.STRING, menu: "attr_type2" }, + }, + allowDropAnywhere: true, + }, + "---", + { + blockType: BlockType.COMMAND, + opcode: "sliceIntoView", + text: "slice [START]-[END] into [NAME]", + arguments: { + START: { type: ArgumentType.NUMBER, defaultValue: 1 }, + END: { type: ArgumentType.NUMBER, defaultValue: 5 }, + NAME: { + type: ArgumentType.STRING, + defaultValue: "my new buffer", + }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "mergeViews", + text: "merge [NAME1] and [NAME2] into buffer [NAME3]", + arguments: { + NAME1: { type: ArgumentType.STRING, defaultValue: "my buffer" }, + NAME2: { + type: ArgumentType.STRING, + defaultValue: "my other buffer", + }, + NAME3: { + type: ArgumentType.STRING, + defaultValue: "my new buffer", + }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "cloneView", + text: "clone buffer into [NAME]", + arguments: { + NAME: { + type: ArgumentType.STRING, + defaultValue: "my new buffer", + }, + }, + }, + { + blockType: BlockType.COMMAND, + opcode: "fillView", + text: "fill buffer with [BYTE]", + arguments: { + BYTE: { type: ArgumentType.STRING, defaultValue: 0 }, + }, + }, + "---", + { + blockType: BlockType.REPORTER, + opcode: "swapEndians", + text: "swap endian for u[SIZE] [NUM]", + arguments: { + SIZE: { type: ArgumentType.STRING, menu: "block_size" }, + NUM: { type: ArgumentType.NUMBER, defaultValue: 0 }, + }, + }, + ], + menus: { + number_type: { + // Removed "float" because its a pain to support, and I do not see a use for it + items: ["int" /*, 'float'*/], + acceptReporters: true, + }, + is_signed: { + items: ["signed", "unsigned"], + acceptReporters: true, + }, + block_size: { + // Remove "64" because only floats and bigint use it + items: ["8", "16", "32" /*, '64'*/], + acceptReporters: false, + }, + creation_type: { + // Removed "array" becayse arrayBuffer exists (and its better) + items: [ + "text", + "length", + /*'array',*/ "base64", + "url", + "arrayBuffer", + ], + acceptReporters: true, + }, + attr_type: { + items: [ + { text: "last block error", value: "error" }, + { text: "current buffer name", value: "name" }, + { text: "current buffer length", value: "length" }, + { text: "current access method", value: "access" }, + { text: "is little endian enabled?", value: "le" }, + ], + acceptReporters: true, + }, + attr_type2: { + items: [ + { text: "text", value: "as_text" }, + // Removed for now due to arrayBuffer existing + /*{text: 'array', value: 'as_array'},*/ + { text: "base64", value: "as_base64" }, + { text: "arrayBuffer", value: "as_arrayBuffer" }, + ], + acceptReporters: true, + }, + }, + }; + } + // Util functions + get emptyView() { + // Return an empty view + return new View(new ArrayBuffer(0)); + } + _CToSWO(str) { + if (typeof str === "object") return str; + return Cast.toString(str); + } + _toJSON(str) { + // If this is already + if (typeof str === "object") return str; + try { + return JSON.parse(str); + } catch { + return false; + } + } + lastBlockError() { + // Return the last block error + return this.lastErr; + } + // Some current data for the user + hasView({ NAME }) { + NAME = Cast.toString(NAME); + if (this.views.has(NAME)) return true; + return false; + } + // Empty opcode functions + getAttr1() {} + getAttr2() {} + // Shared getAttr function + getAttr({ ATTR }) { + ATTR = Cast.toString(ATTR); + switch (ATTR) { + // Some output options + case "as_text": + return this.view.buffer.toString("utf8"); + case "as_array": + return JSON.stringify(this.view.toJSON()); + case "as_base64": + return Base64.fromByteArray(this.view.buffer); + case "as_arrayBuffer": + return this.view.arrayBuffer; + // Real attrs + case "name": + return this.view_name; + case "length": + return this.view.length; + case "access": + return this.accessMethod; + case "le": + return this.littleEndian; + case "error": + return this.lastErr; + default: + return ""; + } + } + // Management + setLittleEndian({ LITTLE_ENDIAN }) { + this.littleEndian = Cast.toBoolean(LITTLE_ENDIAN); + } + _remove(name) { + this.views.get(name).fill(0); + return this.views.delete(name); + } + remove({ NAME }) { + // Remove the view from the map + NAME = Cast.toString(NAME); + return this.views.delete(NAME); + } + async create({ NAME, OPT, DATA }) { + NAME = Cast.toString(NAME); + OPT = Cast.toString(OPT); + let buff; + // Remove the view from the map to temporarilly save memory + this.views.delete(NAME); + switch (OPT) { + case "length": + DATA = Cast.toNumber(DATA); + // Create the ArrayBuffer based on a number + // which makes it that length (filled with 0's) + buff = new ArrayBuffer(DATA); + break; + case "array": + // If the user wants to use slow and bad array they can :shrug: + DATA = this._toJSON(this._CToSWO(DATA)); + if (!DATA || !Array.isArray(DATA)) { + this.lastErr = "Invalid array json."; + return false; + } + // Create the ArrayBuffer + buff = new ArrayBuffer(DATA); + break; + case "base64": + DATA = Cast.toString(DATA); + // Decode the base64 and get the ArrayBuffer + buff = Base64.toByteArray(DATA).buffer; + break; + case "url": + DATA = Cast.toString(DATA); + // Fetch the URL + if (!(await Scratch.canFetch(DATA))) return false; + buff = await Scratch.fetch(DATA); + if (!buff.ok) { + this.lastErr = "Failed to fetch"; + return false; + } + // Get the ArrayBuffer + buff = await buff.arrayBuffer(); + break; + case "text": + DATA = Cast.toString(DATA); + buff = this.textEncoder.encode(DATA).buffer; + break; + case "arrayBuffer": + // Make sure its actually an ArrayBuffer and not some random object + if (!(DATA instanceof ArrayBuffer)) { + this.lastErr = `Expected arrayBuffer got ${typeof DATA}:${DATA.constructor.name}`; + return false; + } + buff = DATA; + break; + default: + this.lastErr = "Unkown creation type"; + return false; + } + // Add the view + this.views.set(NAME, new View(buff)); + // Clear any errors + this.lastErr = ""; + // If the view is the same as the one we just created automatically update the view + if (this.view_name === NAME) return this.use({ NAME }); + return true; + } + use({ NAME }) { + NAME = Cast.toString(NAME); + // Check if the view exists + if (!this.views.has(NAME)) { + // Make the view empty to save memory + this.view = this.emptyView; + this.view_name = ""; + this.lastErr = "Could not find Buffer"; + return false; + } + // Update the current view + this.view_name = NAME; + this.view = this.views.get(NAME); + return true; + } + setAccessMethod({ SIGNED, TYPE, SIZE }) { + SIGNED = Cast.toString(SIGNED).toLowerCase() === "signed"; + SIZE = Cast.toNumber(SIZE); + TYPE = + Cast.toString(TYPE).toLowerCase() === "int" + ? !SIGNED && SIZE <= 32 + ? "Uint" + : "Int" + : "Float"; + this.accessMethod = `${TYPE}${SIZE}`; + } + // Usage + setAtIdx({ IDX, NUM, LITTLE_ENDIAN }) { + if (!this.view) return false; + IDX = clamp(Cast.toNumber(IDX), 1, this.view.byteLength); + NUM = Cast.toNumber(NUM); + try { + // Set the value + this.view[`set${this.accessMethod}`](IDX - 1, NUM, this.littleEndian); + return true; + } catch (err) { + // Catch the error while setting + this.lastErr = err.message; + return false; + } + } + getAtIdx({ IDX }) { + if (!this.view) return ""; + IDX = clamp(Cast.toNumber(IDX), 1, this.view.length); + try { + // Get the value + return this.view[`get${this.accessMethod}`](IDX - 1, this.littleEndian); + } catch (err) { + // Catch the error while getting + this.lastErr = err.message; + return ""; + } + } + sliceIntoView({ NAME, START, END }) { + if (!this.view) return false; + END = Cast.toNumber(END); + // Clamp the start and end values to be in a valid range (prevents errors) + START = clamp(Cast.toNumber(START), 1, this.view.length) - 1; + // If end is 0 then just treat it like passing 1 argument to slice on an array + if (END !== 0) END = clamp(END, START + 1, this.view.length) - 1; + else END = this.view.length - 1; + // Get the size + const size = END - START + 1; + if (size < 1) { + this.lastErr = "Invalid size"; + return false; + } + // Create the output view + const newBuff = new View(new ArrayBuffer(size)); + for (let i = START; i <= END; i++) { + // Populate the view + newBuff[`set${this.accessMethod}`]( + i - START, + this.view[`get${this.accessMethod}`](i) + ); + } + // Set the buff into the output view in the map + this.views.set(Cast.toString(NAME), newBuff); + if (NAME === this.view_name) this.use({ NAME: this.view_name }); + return true; + } + mergeViews({ NAME1, NAME2, NAME3 }) { + NAME1 = Cast.toString(NAME1); + NAME2 = Cast.toString(NAME2); + NAME3 = Cast.toString(NAME3); + // Make sure the 2 views exist + if (!this.views.has(NAME1) || !this.views.has(NAME2)) { + this.lastError = `Missing a view, cannot merge "${NAME1}" and "${NAME2}".`; + return false; + } + // Get the views + NAME1 = this.views.get(NAME1); + NAME2 = this.views.get(NAME2); + // Concat the 2 views + this.views.set(NAME3, NAME1.concat(NAME2)); + if (NAME3 === this.view_name) this.use({ NAME: this.view_name }); + return true; + } + cloneView({ NAME }) { + if (!this.view) return false; + // Clone the view and save it + NAME = Cast.toString(NAME); + this.views.set(NAME, this.view.copy()); + if (NAME === this.view_name) this.use({ NAME: this.view_name }); + return true; + } + fillView({ BYTE }) { + if (!this.view) return false; + // Fill the current view with the byte specified + // If this is a string grab the first letter, it will be converted to a number by the fill function + if (typeof BYTE !== "string") BYTE = Cast.toNumber(BYTE); + else BYTE = BYTE[0] || ""; + this.view.fill(BYTE); + return true; + } + // Some utilitie functions for users + swapEndians({ SIZE, NUM }) { + SIZE = Cast.toString(SIZE); + NUM = Cast.toNumber(NUM); + switch (SIZE) { + // Swap64 was going to be added, but it is not needed as we are not supporting floats / bigint + case 32: + return _swap32(NUM); + case 16: + return _swap16(NUM); + default: + case 8: + return _swap8(NUM); + } + } + isLittleEndianOnForUser() { + return pcLittleEndian; + } + } + + // Register and expose the class + Scratch.extensions.register((runtime[`ext_${extId}`] = new extension())); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 9027c00ac9..79e8e27997 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -96,5 +96,6 @@ "itchio", "gamejolt", "obviousAlexC/newgroundsIO", + "Miyo/Buffer", "Lily/McUtils" // McUtils should always be the last item. ]