From f40d494b370d60fd4c2f3c2696f6291ae0b012d5 Mon Sep 17 00:00:00 2001 From: CST1229 <68464103+CST1229@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:29:57 +0200 Subject: [PATCH 001/196] DT/cameracontrols: a few fixes (#959) This makes the following changes to Camera Controls: - Fix touching mouse-pointer blocks (#349) - Remove speech bubble bounds (#743, #741) - Reset camera when the project is loaded - Change a bunch of `let`s to `const`s in the code Debug project (sb3 renamed to zip): [Camera test.zip](https://github.com/TurboWarp/extensions/files/12446932/Camera.test.zip) --- extensions/DT/cameracontrols.js | 208 +++++++++++++++++++++++++++++--- 1 file changed, 189 insertions(+), 19 deletions(-) diff --git a/extensions/DT/cameracontrols.js b/extensions/DT/cameracontrols.js index fb7944affc..ebb9960994 100644 --- a/extensions/DT/cameracontrols.js +++ b/extensions/DT/cameracontrols.js @@ -36,10 +36,10 @@ rot = -cameraDirection + 90 ) { rot = (rot / 180) * Math.PI; - let s = Math.sin(rot) * scale; - let c = Math.cos(rot) * scale; - let w = vm.runtime.stageWidth / 2; - let h = vm.runtime.stageHeight / 2; + const s = Math.sin(rot) * scale; + const c = Math.cos(rot) * scale; + const w = vm.runtime.stageWidth / 2; + const h = vm.runtime.stageHeight / 2; vm.renderer._projection = [ c / w, -s / h, @@ -61,19 +61,174 @@ vm.renderer.dirty = true; } + function updateCameraBG(color = cameraBG) { + const rgb = Scratch.Cast.toRgbColorList(color); + Scratch.vm.renderer.setBackgroundColor( + rgb[0] / 255, + rgb[1] / 255, + rgb[2] / 255 + ); + } + // tell resize to update camera as well vm.runtime.on("STAGE_SIZE_CHANGED", (_) => updateCamera()); - // fix mouse positions - let oldSX = vm.runtime.ioDevices.mouse.getScratchX; - let oldSY = vm.runtime.ioDevices.mouse.getScratchY; + vm.runtime.on("PROJECT_LOADED", (_) => { + cameraX = 0; + cameraY = 0; + cameraZoom = 100; + cameraDirection = 90; + cameraBG = "#ffffff"; + updateCamera(); + updateCameraBG(); + }); + function _translateX(x, fromTopLeft = false, multiplier = 1, doZoom = true) { + const w = fromTopLeft ? vm.runtime.stageWidth / 2 : 0; + return (x - w) / (doZoom ? cameraZoom / 100 : 1) + w + cameraX * multiplier; + } + + function _translateY(y, fromTopLeft = false, multiplier = 1, doZoom = true) { + const h = fromTopLeft ? vm.runtime.stageHeight / 2 : 0; + return (y - h) / (doZoom ? cameraZoom / 100 : 1) + h + cameraY * multiplier; + } + + function rotate(cx, cy, x, y, radians) { + const cos = Math.cos(radians), + sin = Math.sin(radians), + nx = cos * (x - cx) + sin * (y - cy) + cx, + ny = cos * (y - cy) - sin * (x - cx) + cy; + return [nx, ny]; + } + + // rotation hell + function translateX( + x, + fromTopLeft = false, + xMult = 1, + doZoom = true, + y = 0, + yMult = xMult + ) { + if ((cameraDirection - 90) % 360 === 0 || !doZoom) { + return _translateX(x, fromTopLeft, xMult, doZoom); + } else { + const w = fromTopLeft ? vm.runtime.stageWidth / 2 : 0; + const h = fromTopLeft ? vm.runtime.stageHeight / 2 : 0; + const rotated = rotate( + cameraX + w, + cameraY + h, + _translateX(x, fromTopLeft, xMult, doZoom), + _translateY(y, fromTopLeft, yMult, doZoom), + ((-cameraDirection + 90) / 180) * Math.PI + ); + return rotated[0]; + } + } + function translateY( + y, + fromTopLeft = false, + yMult = 1, + doZoom = true, + x = 0, + xMult = yMult + ) { + if ((cameraDirection - 90) % 360 === 0 || !doZoom) { + return _translateY(y, fromTopLeft, yMult, doZoom); + } else { + const w = fromTopLeft ? vm.runtime.stageWidth / 2 : 0; + const h = fromTopLeft ? vm.runtime.stageHeight / 2 : 0; + const rotated = rotate( + cameraX + w, + cameraY + h, + _translateX(x, fromTopLeft, xMult, doZoom), + _translateY(y, fromTopLeft, yMult, doZoom), + ((-cameraDirection + 90) / 180) * Math.PI + ); + return rotated[1]; + } + } + + // fix mouse positions + const oldSX = vm.runtime.ioDevices.mouse.getScratchX; + const oldSY = vm.runtime.ioDevices.mouse.getScratchY; vm.runtime.ioDevices.mouse.getScratchX = function (...a) { - return ((oldSX.apply(this, a) + cameraX) / cameraZoom) * 100; + return translateX( + oldSX.apply(this, a), + false, + 1, + true, + oldSY.apply(this, a), + 1 + ); }; vm.runtime.ioDevices.mouse.getScratchY = function (...a) { - return ((oldSY.apply(this, a) + cameraY) / cameraZoom) * 100; + return translateY( + oldSY.apply(this, a), + false, + 1, + true, + oldSX.apply(this, a), + 1 + ); }; + const oldCX = vm.runtime.ioDevices.mouse.getClientX; + const oldCY = vm.runtime.ioDevices.mouse.getClientY; + vm.runtime.ioDevices.mouse.getClientX = function (...a) { + return translateX( + oldCX.apply(this, a), + true, + 1, + true, + oldCY.apply(this, a), + -1 + ); + }; + vm.runtime.ioDevices.mouse.getClientY = function (...a) { + return translateY( + oldCY.apply(this, a), + true, + -1, + true, + oldCX.apply(this, a), + 1 + ); + }; + + const oldPick = vm.renderer.pick; + vm.renderer.pick = function (x, y) { + return oldPick.call( + this, + translateX(x, true, 1, true, y, -1), + translateY(y, true, -1, true, x, 1) + ); + }; + + const oldExtract = vm.renderer.extractDrawableScreenSpace; + vm.renderer.extractDrawableScreenSpace = function (...args) { + const extracted = oldExtract.apply(this, args); + extracted.x = translateX(extracted.x, false, -1, false, extracted.y, 1); + extracted.y = translateY(extracted.y, false, 1, false, extracted.x, -1); + return extracted; + }; + + // @ts-expect-error + if (vm.runtime.ext_scratch3_looks) { + // @ts-expect-error + const oldPosBubble = vm.runtime.ext_scratch3_looks._positionBubble; + // @ts-expect-error + vm.runtime.ext_scratch3_looks._positionBubble = function (target) { + // it's harder to limit speech bubbles to the camera region... + // it's easier to just remove speech bubble bounds entirely + const oldGetNativeSize = this.runtime.renderer.getNativeSize; + this.runtime.renderer.getNativeSize = () => [Infinity, Infinity]; + try { + return oldPosBubble.call(this, target); + } finally { + this.runtime.renderer.getNativeSize = oldGetNativeSize; + } + }; + } class Camera { getInfo() { @@ -240,6 +395,19 @@ blockType: Scratch.BlockType.REPORTER, text: "camera direction", }, + /* + // debugging blocks + { + opcode: "getCX", + blockType: Scratch.BlockType.REPORTER, + text: "client x", + }, + { + opcode: "getCY", + blockType: Scratch.BlockType.REPORTER, + text: "client y", + }, + */ "---", { opcode: "changeZoom", @@ -294,8 +462,15 @@ }; } + getCX() { + return vm.runtime.ioDevices.mouse.getClientX(); + } + getCY() { + return vm.runtime.ioDevices.mouse.getClientY(); + } + getSprites() { - let sprites = []; + const sprites = []; Scratch.vm.runtime.targets.forEach((e) => { if (e.isOriginal && !e.isStage) sprites.push(e.sprite.name); }); @@ -369,19 +544,14 @@ return cameraDirection; } setCol(args, util) { - const rgb = Scratch.Cast.toRgbColorList(args.val); - Scratch.vm.renderer.setBackgroundColor( - rgb[0] / 255, - rgb[1] / 255, - rgb[2] / 255 - ); cameraBG = args.val; + updateCameraBG(); } getCol() { return cameraBG; } moveSteps(args) { - let dir = ((-cameraDirection + 90) * Math.PI) / 180; + const dir = ((-cameraDirection + 90) * Math.PI) / 180; cameraX += args.val * Math.cos(dir); cameraY += args.val * Math.sin(dir); updateCamera(); @@ -400,8 +570,8 @@ const target = Scratch.Cast.toString(args.sprite); const sprite = vm.runtime.getSpriteTargetByName(target); if (!sprite) return; - let targetX = sprite.x; - let targetY = sprite.y; + const targetX = sprite.x; + const targetY = sprite.y; const dx = targetX - cameraX; const dy = targetY - cameraY; cameraDirection = 90 - this.radToDeg(Math.atan2(dy, dx)); From e82370378c49ef152eb3b1ba11f8e9df81b5d385 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Mon, 28 Aug 2023 13:30:09 -0500 Subject: [PATCH 002/196] Add image for iframe (#975) --- images/README.md | 3 +++ images/iframe.svg | 1 + 2 files changed, 4 insertions(+) create mode 100644 images/iframe.svg diff --git a/images/README.md b/images/README.md index e88d5d66ac..a67e0ceca9 100644 --- a/images/README.md +++ b/images/README.md @@ -284,3 +284,6 @@ All images in this folder are licensed under the [GNU General Public License ver ## Lily/SoundExpanded.svg - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694410464 + +## iframe.svg + - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694716263 diff --git a/images/iframe.svg b/images/iframe.svg new file mode 100644 index 0000000000..b27fe6b2c8 --- /dev/null +++ b/images/iframe.svg @@ -0,0 +1 @@ + \ No newline at end of file From a45b9df57c40c2caaba9a40a4181f6f56251a8f9 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Mon, 28 Aug 2023 15:44:36 -0500 Subject: [PATCH 003/196] Boolean monitor preparations (#978) --- extensions/Lily/ClonesPlus.js | 2 ++ extensions/Lily/lmsutils.js | 2 ++ extensions/Longboost/color_channels.js | 2 ++ extensions/NOname-awa/math-and-string.js | 2 ++ extensions/NOname-awa/more-comparisons.js | 2 ++ extensions/Xeltalliv/clippingblending.js | 1 + extensions/box2d.js | 1 + extensions/gamejolt.js | 1 + extensions/lab/text.js | 2 ++ extensions/mdwalters/notifications.js | 1 + extensions/obviousAlexC/SensingPlus.js | 3 ++- extensions/obviousAlexC/newgroundsIO.js | 1 + extensions/rixxyx.js | 2 ++ extensions/true-fantom/couplers.js | 2 ++ extensions/turboloader/audiostream.js | 2 ++ extensions/utilities.js | 2 ++ 16 files changed, 27 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/ClonesPlus.js b/extensions/Lily/ClonesPlus.js index 009e997692..cfd7b901f7 100644 --- a/extensions/Lily/ClonesPlus.js +++ b/extensions/Lily/ClonesPlus.js @@ -109,6 +109,7 @@ blockType: Scratch.BlockType.BOOLEAN, text: "touching main sprite?", filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, "---", @@ -316,6 +317,7 @@ blockType: Scratch.BlockType.BOOLEAN, text: "is clone?", filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, "---", diff --git a/extensions/Lily/lmsutils.js b/extensions/Lily/lmsutils.js index f5467b6058..1186455c1a 100644 --- a/extensions/Lily/lmsutils.js +++ b/extensions/Lily/lmsutils.js @@ -141,12 +141,14 @@ blockType: Scratch.BlockType.BOOLEAN, text: "is clone?", filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, { opcode: "spriteClicked", blockType: Scratch.BlockType.BOOLEAN, text: "sprite clicked?", filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, "---", diff --git a/extensions/Longboost/color_channels.js b/extensions/Longboost/color_channels.js index 0d0c6e2523..d3484380a4 100644 --- a/extensions/Longboost/color_channels.js +++ b/extensions/Longboost/color_channels.js @@ -24,12 +24,14 @@ opcode: "true", blockType: Scratch.BlockType.BOOLEAN, text: "true", + disableMonitor: true, }, { opcode: "false", blockType: Scratch.BlockType.BOOLEAN, text: "false", hideFromPalette: true, + disableMonitor: true, }, { opcode: "enabledCheck", diff --git a/extensions/NOname-awa/math-and-string.js b/extensions/NOname-awa/math-and-string.js index 2793a40fc8..95eebf7554 100644 --- a/extensions/NOname-awa/math-and-string.js +++ b/extensions/NOname-awa/math-and-string.js @@ -713,11 +713,13 @@ opcode: "true", blockType: Scratch.BlockType.BOOLEAN, text: "true", + disableMonitor: true, }, { opcode: "false", blockType: Scratch.BlockType.BOOLEAN, text: "false", + disableMonitor: true, }, { opcode: "new_line", diff --git a/extensions/NOname-awa/more-comparisons.js b/extensions/NOname-awa/more-comparisons.js index 19a08b5e6b..ce1d967b26 100644 --- a/extensions/NOname-awa/more-comparisons.js +++ b/extensions/NOname-awa/more-comparisons.js @@ -21,12 +21,14 @@ blockType: Scratch.BlockType.BOOLEAN, text: "true", arguments: {}, + disableMonitor: true, }, { opcode: "false", blockType: Scratch.BlockType.BOOLEAN, text: "false", arguments: {}, + disableMonitor: true, }, { opcode: "boolean", diff --git a/extensions/Xeltalliv/clippingblending.js b/extensions/Xeltalliv/clippingblending.js index 3b79908c01..8f02e2cba7 100644 --- a/extensions/Xeltalliv/clippingblending.js +++ b/extensions/Xeltalliv/clippingblending.js @@ -370,6 +370,7 @@ text: "is additive blending on?", filter: [Scratch.TargetType.SPRITE], hideFromPalette: true, + disableMonitor: true, }, ], menus: { diff --git a/extensions/box2d.js b/extensions/box2d.js index 42df1ab034..bacc801e66 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12823,6 +12823,7 @@ }), blockType: BlockType.BOOLEAN, filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, "---", diff --git a/extensions/gamejolt.js b/extensions/gamejolt.js index 6c35c349bb..d0f689df7d 100644 --- a/extensions/gamejolt.js +++ b/extensions/gamejolt.js @@ -1407,6 +1407,7 @@ blockIconURI: icons.main, blockType: Scratch.BlockType.BOOLEAN, text: "Session?", + disableMonitor: true, }, { blockType: Scratch.BlockType.LABEL, diff --git a/extensions/lab/text.js b/extensions/lab/text.js index 037573038d..5793d78c90 100644 --- a/extensions/lab/text.js +++ b/extensions/lab/text.js @@ -775,6 +775,7 @@ blockType: Scratch.BlockType.BOOLEAN, text: "is animating?", hideFromPalette: compatibilityMode, + disableMonitor: true, }, "---", { @@ -852,6 +853,7 @@ blockType: Scratch.BlockType.BOOLEAN, text: "is showing text?", hideFromPalette: compatibilityMode, + disableMonitor: true, }, { opcode: "getDisplayedText", diff --git a/extensions/mdwalters/notifications.js b/extensions/mdwalters/notifications.js index e498ff7af4..8f2491c797 100644 --- a/extensions/mdwalters/notifications.js +++ b/extensions/mdwalters/notifications.js @@ -54,6 +54,7 @@ opcode: "hasPermission", blockType: Scratch.BlockType.BOOLEAN, text: "has notification permission", + disableMonitor: true, }, { opcode: "showNotification", diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index e953733a42..f8ace8ca6f 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -390,6 +390,7 @@ blockIconURI: touchIco, filter: [Scratch.TargetType.SPRITE], arguments: {}, + disableMonitor: true, }, { opcode: "touchingSpecificFinger", @@ -563,6 +564,7 @@ text: "Hidden?", blockIconURI: effectIco, filter: [Scratch.TargetType.SPRITE], + disableMonitor: true, }, { opcode: "getRotationStyle", @@ -606,7 +608,6 @@ blockIconURI: packagedIco, blockType: Scratch.BlockType.BOOLEAN, text: "Is Packaged?", - disableMonitor: false, }, "---", { diff --git a/extensions/obviousAlexC/newgroundsIO.js b/extensions/obviousAlexC/newgroundsIO.js index 286abf8dc6..4dabac4c20 100644 --- a/extensions/obviousAlexC/newgroundsIO.js +++ b/extensions/obviousAlexC/newgroundsIO.js @@ -9238,6 +9238,7 @@ opcode: "loggedIn", blockType: Scratch.BlockType.BOOLEAN, text: "Logged In?", + disableMonitor: true, }, { opcode: "version", diff --git a/extensions/rixxyx.js b/extensions/rixxyx.js index 73b69e2058..4487e5cd70 100644 --- a/extensions/rixxyx.js +++ b/extensions/rixxyx.js @@ -54,12 +54,14 @@ blockType: Scratch.BlockType.BOOLEAN, text: "true", arguments: {}, + disableMonitor: true, }, { opcode: "returnFalse", blockType: Scratch.BlockType.BOOLEAN, text: "false", arguments: {}, + disableMonitor: true, }, { opcode: "ifElseString", diff --git a/extensions/true-fantom/couplers.js b/extensions/true-fantom/couplers.js index 13dcd9bfa5..702bafe878 100644 --- a/extensions/true-fantom/couplers.js +++ b/extensions/true-fantom/couplers.js @@ -78,12 +78,14 @@ blockType: Scratch.BlockType.BOOLEAN, text: "true", hideFromPalette: true, + disableMonitor: true, }, { opcode: "false_block", blockType: Scratch.BlockType.BOOLEAN, text: "false", hideFromPalette: true, + disableMonitor: true, }, "---", diff --git a/extensions/turboloader/audiostream.js b/extensions/turboloader/audiostream.js index 2afeba53ff..5fe176e307 100644 --- a/extensions/turboloader/audiostream.js +++ b/extensions/turboloader/audiostream.js @@ -112,12 +112,14 @@ blockType: Scratch.BlockType.BOOLEAN, text: "has stopped", arguments: {}, + disableMonitor: true, }, { opcode: "am_isPaused", blockType: Scratch.BlockType.BOOLEAN, text: "is paused", arguments: {}, + disableMonitor: true, }, "---", { diff --git a/extensions/utilities.js b/extensions/utilities.js index a57a333db7..6657951820 100644 --- a/extensions/utilities.js +++ b/extensions/utilities.js @@ -103,11 +103,13 @@ opcode: "trueBlock", blockType: Scratch.BlockType.BOOLEAN, text: "true", + disableMonitor: true, }, { opcode: "falseBlock", blockType: Scratch.BlockType.BOOLEAN, text: "false", + disableMonitor: true, }, { opcode: "exponent", From 70182ae457dfb668ef4a39760a8031f2bfd29b5c Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Mon, 28 Aug 2023 15:51:48 -0500 Subject: [PATCH 004/196] Remove old plugin runner compatibility stuff (#979) This all has become more and more out of date. Plugin loaders are largely irrelevant now, and we don't care to support them here. --- extensions/obviousAlexC/SensingPlus.js | 52 ------------------------- extensions/obviousAlexC/newgroundsIO.js | 34 ---------------- extensions/penplus.js | 43 -------------------- 3 files changed, 129 deletions(-) diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index f8ace8ca6f..0cf92b63eb 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -129,58 +129,6 @@ } }; - if (!Scratch) { - Scratch = { - TargetType: { - SPRITE: "sprite", - STAGE: "stage", - }, - BlockType: { - COMMAND: "command", - REPORTER: "reporter", - BOOLEAN: "Boolean", - HAT: "hat", - }, - ArgumentType: { - STRING: "string", - NUMBER: "number", - COLOR: "color", - ANGLE: "angle", - BOOLEAN: "Boolean", - MATRIX: "matrix", - NOTE: "note", - }, - Cast: { - toNumber: (input) => { - return Number(input); - }, - - toString: (input) => { - return String(input); - }, - - toBoolean: (input) => { - return Boolean(input); - }, - }, - vm: window.vm, - extensions: { - unsandboxed: true, - register: (object) => { - const serviceName = - vm.extensionManager._registerInternalExtension(object); - vm.extensionManager._loadedExtensions.set( - object.getInfo().id, - serviceName - ); - }, - }, - }; - if (!Scratch.vm) { - throw new Error("The VM does not exist"); - } - } - const isPackaged = Scratch.vm.runtime.isPackaged; const vm = Scratch.vm; diff --git a/extensions/obviousAlexC/newgroundsIO.js b/extensions/obviousAlexC/newgroundsIO.js index 4dabac4c20..8696825fb0 100644 --- a/extensions/obviousAlexC/newgroundsIO.js +++ b/extensions/obviousAlexC/newgroundsIO.js @@ -9030,40 +9030,6 @@ let menuIco = "data:image/png;base64,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XYQ6wi3CIsEYV6pUKVOT999/v7/kkkt8uXLl3tD71YVcnX37CiO22mqrFVdeeaVftGiRMQWaMmWKHzt2rDFn11139a1atfI//fSTMfXHH3/0d999tzFp7733NscMGwdTq1atylgMEnKKcTDtQuET4U/NwlWKb0zv63UUgT0QpgsthW2EXCMkbZQm16o777zT//333xGWrZGmxx57zN9zzz1+8eLFvkGDBr5///7mbA0ZMsTXqVPHy1v23bp187NmzfIKfewz7N7OO+/Mc98j5NRkrSR8LhizrrrqKj9mzBjfpEkTUyW8H0aZMmV8hQoVluizF/R6fyFXiEG9onTp0v/17NnT7FRAqHicrMqVK5vDhbocNGiQP/roo+05sYEwFE8TPPPMM75du3b+ySef9A888ADjQABOliWniGB6uWCMgXEYcVQH6qJSpUrGQCQOpl188cV+8uTJvkaNGmROXhFyQfJIGpwufK3A2v/888/GMJ7js88+82effbZX/BadeDDxn3/+8UjlHnvs4bt27ervuOMOf9JJJ9nzbrPNNuZ1fvDBBxYy6LxpAhM8Z4j83EAhKlEnnHCCGXEI15kZOHDgQAsJXnnlFf/777+bGjnxxBM5HieGAVufVFHoJW3xu+ya79ixozEMxjz11FNms0Jq3lCxYkVTj6jSUaNGma077LDDzBHDw0TSUJFnnHEGWoik8wVCzqnJmUL0odDzCxcuNMYlIoLTc889NzjnJQFvdF0TOce6wttiWAGDj4a4+eabzbVHYrDV+jwudtxxRzseiWQy4qTMnz/fQgUmaK1atQgVmJi3CVsKOUVHC38I0QdCdXz//fcRFq0hvC68s3fffdf369fPd+/e3R4scs5PAhn28gIPiLOTbSLd1lmStIB47JFHHjFHYvjw4Wa3mHyxzlU8cAzepUIcT9jQuHFjkgswnJDnG+ESgXgw56iZQCnDHkTemKnKIK5B5RDDPfzwwzYg6P5gQPgbUUHYuoXCZwLx3WMCFYSzBCZGFYH0GFWGkqobJsXBwktyQpYz0OQb8Rih2bNnRwPrYgInBK+5m0CaLOfitoCuFRh4u3EMeO/evc2GzZs3z997771mnBXIFnpAcnqPP/64GfeLLrrI7B0zNTDscmZWi6lk05HmucJHwnDhPqGj0EioIewuUH7A1hbFVGb++cJXslGr+W68w4BgHk7TgQceWOheUwDPz30y6bg+5iMlrbE+jd6tQvc1/66hatWquWbNmrmRI0e6mTNnWjknlmTAneyAk0qxEsivv/7qxDAnt9vJPjp5pG7OnDlOKtfKJb/88ouTHXFyGNzy5ctX65owlfwfA4a0LhDmCORA5wm/CGTlfxe4gT2F6zQhzpPrvs1tt93m6tat6/TaiWd2/QEDBrgnnnjCvo/3UiDUIUXTtwRCm6kCNm2DoLMF1EN0BqL+irIPBKRTp061mY4HhsoiVoolpAAPFWeHoHb8+PH++eef98RZxEmUTbBH2Jmtt94aSYVJMBTGUe/rKXCPU+RMrLzhhhssTAmIa2PXiMNitUISEPrApC4C6hBp3+BoV2GyEO8BEwLG3nXXXea0kD568cUXLXQIE0wLkrrxKJap7733ngXFPXr0MI8QhmoSFZCZJzYbMWJENKjmXMIUmE8tLd49RhCoQSSL1gvaEc4VKgjrwonKCqGiKc3QhhDvoZMCaSFWiiVChddff91feOGF/vLLLzebSTmFYBbnAe8UhjH48QhmU7Tl+ttuu63lR3HRg3IL52N7ySdGnKNEWCwQo9YT9hOofGQ0bFkfNo4HOFm4SThMSNt7knpzY8aMcXIGzA6KGda3sXjxYnfqqac6eXtOkumkVs3maOBdmTJlrDVgl112cbvttpuTB+gUftj/HCfpcZI816tXLyemuC5dupi9VZzmJG1O4Yjr06eP0yRwK1Yk7CRYIrwt9BM+EFCNGwXhNfURmJHxZmpKwKZgryAq46effrqFDqjMFi1a2DGkykgnkZUnQ0G8df311/tzzjnHCpf777+/2TdKKBUqVPD77LOP/d+yZUtTwUgfQJVSatlhhx3Wuo8QYBAMO1PIxQR4sYlMw/ECZfgCId7Dp4VOnTrZwNLWQFY9cM9JjwVqjHAiqOdBqDxJi6WbcDTo7SCDQZaD+hcxI8E0REYDZsPgJA4TojdFaCvsKGxURKzUWfhRiPfwxQJJWWI+KOyIYONI5nKMAmU/bNiwyCeFacmSJV6uvUndaaedZpKJ7UNq33nnHX/KKadErxMHeKB4np0E8pXrM6zKOPEwZC6eEYLu3IzhgAMOsBxfLOFckAvkGCSPkkk8oqjJcZ07d/aKBY35VKt5HamBJQKFXmJQnKsN1jtMRDgcxwkfCgSb8QagRJCTYbYI9Ye0UDGGyHWi9jgmEeOQLEpI9LTgNRIH0t9BBoY+kNjvwl6iMsns6zX1Q6RsvVI2ZgxBJekb3GHyhVmZlcuXL7fMCV4lWYuBAwdGvUcAkd3YfXcyW4VJ9tA8SDxPSahT7OY6duzoFJ9ZY2qYxEjXoUMHJxXsFGbgcSJpB635dOOifYTvhbVmbiaBZ0k2g0bZoUOH+gkTJlhbAEnqatWq2TFimp8xY0ZEzv5PVNnJlnANgujAmQnAa4UJ1rCK40JVGiL7ouMJrO8QNiq7BtUW/hIKDUamgUojMc3A0sKG2qMTmH4OgmeOIevx2muvmd2bM2eOeZ44JTA8llmA91DBl156qXmchBp0ZJHUhqhuk1XRsTTu0lK+URFV6Wi5Jls45JBDLCtCEw4FTPoQqRRQTcbG4XjAVCoGMJb3+Oz44483TzL2ejCN86n7SQ0bo2A655NWg0h70XWm4+ntZMXNeqNsiHt74VEhqx6XvEqnGMyJKZY1oVpAlkPqz2wd2RRsmaTEFlYA/l+0aJFlWLCPkYUkUbvGypmqVataNoWsyvTp090bb7zhNDlcnTp1nNSr0yRxiu9IINBdTDs8oQHMXKeUDcbdLLDKJGNUqlQpV65cOWMIA11QUBAdbBwMmMUxlHckaZYSkxfopCqdpMyYwDGcB7NY0vTHH39EGUspCMbyl9IQZSAF6cZ8vo/r8v1cm3PkifLlFD2RPEpB9D0S160zyjTjuB4Fy6vsVQmIgcYjZKZTgyMvKW1lg86gxgIpQpqojym+swEmx0huUurRtW/f3hiJpxmPuDaM5Ry+g/oeDOR6YcbymmvzOd+LzRRReadIi+OyTijTjCN+I+A+z14Vg5jdNWvWdM2bN3f16tVzckCMKbJRliRORAw8oQEDz6Ay0Lj648aNcxMnTnSlS5d2cjoMO+zAwqD0KLg+Eogkomq5/mWXXYb0UmGn1TCn17ElI3oMRwqFDH8qIIlLDyIOAZ1SFClpTaAdAOeC1rXw4olUiTIOWRK6hfFCzzvvPKun4SGSl8ThSOeakjYr4NK+cN111wUeLGqzlYDdIyfLEugDBIJIZgmBZUZtfqYljt4M1mpz8ykR5ZSTTjrJtWnTxiTqrbfestYFMc/sUZiwWxo0V6tWLSu9pEO0M3Tu3NmcDRwabBbfh92irFOxYkUD//M5n3FvOD1IPTYUacM5efTRR620wz1gYwlNZH9XAfEWIHlU9ynzEBr9KtD/8pXAOnZsIwXWYktophlHQpnGl1r2KgmR3Tj22GMtG8HgDBs2zJiGs4ATkog4Z8iQIeaApEvz5s1zkmL39ttvG+Owf9gzJggqFlUIM7CvMAsvEuahWmEm9/Xmm2+aqgyI6zRu3DjahxLYW3phsIfYXf7nPYq/YjixBr0u7wlPCnimVEzSokwzDrUwVqCLKi4xQ5EcJIyBe/XVV9348ePNy9MDRY5KTAyq1JRJD9dKl6ZMmeKkko0xzz77rEkc9orvxxlhoPEsAyeE97CZNBsBGAODw8TEQ2vccsstTvGlMR8mBzaX87guE+eLL75wEyZMoNhLeYn+FtZ4Pyys12YhVqx8KRSyXwE0O33r1q2t/H/qqadacBvvuKJAIB2vMpAKaTCtYYiSDb0riewb70s1Wu2O3hSS0axtk5q0QJ2UWfieCOCxx5LmaKtDPOIzbKsmrPWLavJhD/oK7Hey3gj9hR4v9FC6OXsoeuupfQUpqeICJ4P+e1QPgwszkg1WLJHP5H6YADgp6RLVBLI2pNh4tvC9kRLD+SmKuF/KSFTvZSOxh8S+KfelZLpbFn+9XeSvEW4420JgJ3DNycBj5EtCqKCpU6eaozFq1Ci77qRJkyzTwV4h9Dei7lBx2DC+j3M0XnY+mRbChdGjR7vatWu7/fajnyd1wj7TA9qgQQOLGflOVCOEPUOdEnui1hMR6pQxwdGS+txczhNd0nSD4bysc6Lkgfdksw/1gZuPiuT/4P10gMdGLYxrxPs8DL4DCWAlK6qMfCUVAs5HOmhxp0+f7q3ITLe+lHSkNZZQ2fR2hp+PZ6aSkCqhgkmS61x6LmnAXedESWe+EH2IkgAmsDKH1jrUbHGZnwgwlqajkjAOopAbTlxzn126dEn5uqh6lktLO+H1sA5+na4bQD+T6iJ2KTRAxQUD0L59e8vW071MN1a844oD2hMo1+B0BNWA4hJ2lupE+PqsKCJhwGY02FHsIt+TyB7jACnU4VxW6hSpuzMVDtCSRkMQWLOPU4aofPnyFpDjZrds2dKNGDHC3sc+EANKKs2dx64ELjtxFrEZVXLccWwcecgAxF59+/Y123vxxRc71gPIabLragytOk4YIHUazW1iw/gOYrt4+c7nnnvOtWvXLhoqSA1HA/ggHiSoJx4kyCcUig32qbIrpFilWK+rLkEbY1Zzn+x6w6ZpGW8IAkgdhU/cczaoCbw4uolZDMjs5TMxxNJXJH0Vk3nFYbbqh/QZ/Sh0MyvAN5XLzOYYmoRoycO7CwiJoGGoWbNmhbqluQ42kup5QAqqbUsL7NzEiROLakkvBJ4LG0snGl62GGnqlvf0Ob06SROqJdWluP+9hTYCecqskB7SnXUWS96crdQh3cRsbdu2rXmIfI7kIQmkpvD6glnOjJaKdZUrV3ZitgXWL774onm306ZNs/PwQAmO+Yv0kiA47rjj7PyAkCQ8SMVeJi3QjBkz3NVXX23aAM0ge1koq1IUifdWnkIjcB7aQu8hZTOEoQJhQlwqiaqkAkwJp4mQVWPKoL/zzjv2kLjgP/zwg7njpK5gSjr09NNPm3pE9QUE81BtAdMpyIbVG2oZxgCYFqg3si8wm5ITf5s0aWLhAPU/JhSMCFQ2kw3m870wLAGhtV4W6Gkhn5nwwOIyLthTkjUAWa10QwwSawUqVapktTUS0NgnGMcgJqNg0GAKEkmai66tMOPSobBkwziYTCmK92fNmmXM6d69u30HTIN5QUotyGEG+Uvs6Ny5c01ydT80DMMwirJZSX+xew4L8rLSLxkPpMZo3MFukfHgPdJFeGrJCNvHVhTHHHOMpanYgglvD/sS+x2ZBCWfZIRdxpZik+m8FvM5j3aPlDVXutIC01iJwu6taUsatggVwuxMlzhXzxxVM0F/STLiWNQqmQ255pZpIckcXCNbxL0mIz5HNZNVQg1HnoObSlkNpDOCBNcwrYFQLPVIGwKuNzecDnE8bjUqD/ceYgLEc8vDxAShfjZ27Fhj2vDhw50C+sin2SGYwr0mItQl9TwakChPMaGw3aJijWlRxBqA0UKJ1ONNN91kbjkpIQ36WgnaRGAJFK49G7ewFIr32GUIVZgu0coXe/1MAnd+wIABkW9bm+jxlF22Z2cMJG2B6qa8k7LPkQqXKb/fLzQU0poVSMSRRx5p7W4QSV+8QYLVQYMG2ewvSq1ALEKkbkbyGGMP0UiUrsrVuNmMzyahAlF/iYj7ZvFkjx49XKdOnSzwj6jKwuX+EhL1NXYFCHoHU4Y8QX/NNdd42RfrLuY9glrSQwTJ1LlefvnlZEuZDMxG1mYjXayD4zUzVcxfM4XTIKlaSzLH+55MgWAarZIK4aTQgEtCXOeyoD9lKmrKssUgG8mk7u1ICghI+/fv73r27GnSQvwD4friFvfr18+q0A8/TOE3OeHuU10mQJVnaVKDY0LXV7pEoIsrnk0iNCgqRAkIbUMaraCgAKeELTpSpmSMw/KzK3ly1y1EDGjr1q0tM8H6aYJZVEeVKpjINT0fAKZ17drV3Xrrra5Vq1YJVR4PJjfeVa9e3Ur+1Nsg1GagfpMRjgzZDmIm1DRBMsgmwbRkqjKWmEhiHBqNhqKMEK4R65rXUgexQH1RT6KPHxUYENlw2uxYd8ZxGGTiKlREQGKI5R1jrwmopbFjD+qVhRh8D+/T/x/OIyYitrlg5Q7fz37OoCjVXFKwByXxWSrEOEScpT+FY4SMEJuNsZ3SWjcXBkVLliNRDCSoDG4IL/Dqq682Wxc+vkaNGl6xlR0HcQ5bBMYWSgm62XSN5DH7kJCEDT4jCZyKR8kurEwKtovCG40EulnFjTfeWGhiholnpd8EGw9zly1bFmw2QBs72aiMEPLOJpdr3VwAmEZTKFn2gJAOxUyWrcA1jj0H95f9hwMm85cHYaE8a9KQKgqcHMODUcti35HgfLLpODWpENdlAuEgscUg+5+E7yXTYDzoRUlETEB2iUVjMNnPOuusYJUr1YCM/QgGq0njLrhncFF7MIYWANaeMcuQJGYccUq88wKgAnkIzkHl4VnxAxF0Gbdt29YWKzIBmJ1sSg2zg3NZLkVxMl1ihuPVhu8j00Cy2QUiEdEhxrNTxEUT0fQUOZe2/eTZhBQJaaMjGW+n0M0x45m59G3wPwxkbxDazahz4apzHLaEhYWJWvDosApaylGH1McALjtEXpItMUIPZ2CWYjvTJSYVC/7D18o0qNcls73kVpl02HX6KqkzavxIarBgpMRERMzO5NG9ksOAIahHVBAxEdKHRIQZxL6NOCq0wSFFgVMRBu9hfyiOosqQLhybBQsWmCpEnYQlDfAae1gcCpYPh6+XSfA8/DJHIvsWS0xWsj86l4wA2q3EhK6lPXqtmwuAFLARDBl7xD72c/ovCEI7dOhQZJ8IzGBnWLL2LMxgn/1YKQsAo8PV6lQJqUaFx5tAmQJpvEmTJkW+sWhCA0QqHWxEl5HNbeA+ezWudXNhoHbQ59i42M9wLmBosoFCxcK0VAeT49iaKVClySjYNYjtNFBNOCZsUBrvupkC2SG0RqrE3suaoJiiXkJ6ubsExFLgldgqbJj+jws8KLqJkTyYEO+YeMChqVmzprWhs7Ya5yM2ZIgHJgM7KxRFeKKoZySAEAJPFYcgsL3ZANfGuUqkJnGMsH3Y5iDlh1+gc1GTbMpdYsK+9YYRbdq0sQXyep0QqB82OEvG4DCQGgJUJBXgjS5dutRc/6KuweZpHFsULVy40O4dR4kJQgCe6v0VF/xcWCIVDtNouiUMYI0f29Lj3UbiViouGemKIyf5bNmyZa00QTlFrxMC1x23PnBMYDj759MLyYxCSsLH01GM7cNbxHVmX342+KRLikEOHxsG0sMvPKVCzPpghuOlUmHm/HjXzQR4Znblw9mIRzCUZwtCABykiIaisMhGPhkhYokhfMHAgQOLdCwIC9iwjJtBmmgPwMXHvmBnnnjiiagnh4pEstj8MywBpKNos+Phg/fC4LpMEFRguhSbKssG8KCxpYmISUTTLSEA2SUSDVQQdO4kYU27WDEo1ihywRUkhjW4lukmUZyIWMFJfYvKNI08NKiydo1sPlss1a9f3xZVQHRHUTWgWUcDau9BeihrLWCbCs3EyLv/Jzq8rrjiCmvMSZdov6PqrfGLvJNZYpxIqnPviYhxJMkuTWTdYKwblzZA2voLxS5VxI6UlRck9ta3OHjwYCvNxGMeXU7cDIPO8WTxg74OFgTSlTt58mTrT9SMtzIMJQx2Jw8T5+pB7AE5Lkx8B4vj6ehKl/guxYhZrQYceuih1r0s5yTyTmJi8rCqaOjQofw/Xm/Rkl3sGRVP4r6RWlpNOUSutK2dZlBjid5BGAezuHFKL0HvI4QUygExqUSSkDh5U7Sh2ecBKSQwyaZVLfwZTGzYsKENDDM7GcF83XN0STB/mXRMnmxJGz2XN9xwg2mEVIjqPdpIdpeZxM+KlagUHy9++EID8TdbTMgzsxnfokWLQmoMRsnLM0mR3jbJoEGGWR6WGhhOXyODB4PoAkaFcnxAMJZJQL0tPMioH8r7RRUlqbOxfzLNqE2bNnXy2gzdunUzBmaDeH4mVKNGjdbSEjzD7NmzreeTJdJyxgz0WmqssBFUj9+3gzNM5Ckn4uKSioIIYsn26ybNIyJbz3ukbHgPB2TkyJFWawucEVJjBJnsC8lrUljsJUmphd9S4z2uRekH4x30SwIy5tTSiird4EFyPY7HoZIUrJUmyzR4XpZB4wnHI5wQSleUkPDO8bhxRsRsVNEQIfUqa5rEFLpK6mkFdS+CRpKjBMu4tfxyEzGYnIzoIgfdlG3UidcEg3g4/pKx4CE4BmaxMbWk2fOzyjwM2QbcZX7gjoCe43Cb2WaQ41Ihcpt4sSRtqd9lcilWLHgu0nI8RyJinxa8bdJ+NOBGQiJsADvuZf3XlFnMMZHBJcOxaNEiSxgzyAS4xHjEYTomCjaXIXZiRYzUiC32I6sidWmfEw6wBzLuMdKKNBIGkO8MKuBkOWgCSiWtFSauSaUdqWVww/eVKXBdKuhMxmSEFkBTMGaEOJI6JO1dgQ1rsk5I3SnCz4g8G1fTaQVYfB+uBATgPX6NkMIngLmxs58qNDUpPmcCsNE1ATuDwqJ3ZisSDuNSAXEay59Qt9kMsgNJQ7L5znj3EoDP2dyUiaxJW6Dz2WmJDvCMUmHLWpjwcdnmiIUIKf3Uo+xbdLM0PEq8yFiioYi4BmcHjxUHh/9x+ekI0yBFjiyaVq5cad9DiBHrrWaS8GrZhY+eyKLuj/vgnqSd/tH/FEhJIv9sH2aQiholmMcuQR0EfpUj5349MEeJjWfwHtmcLSuubarTm0AObyhrixc3MkLVBKWxPOUpT3nKU57ylKc85SlPecpTnvKUpzzlKU95ylOecomc+x/Y4lekcFolLwAAAABJRU5ErkJggg=="; - if (!Scratch) { - Scratch = { - BlockType: { - COMMAND: "command", - REPORTER: "reporter", - BOOLEAN: "Boolean", - HAT: "hat", - }, - ArgumentType: { - STRING: "string", - NUMBER: "number", - COLOR: "color", - ANGLE: "angle", - BOOLEAN: "Boolean", - MATRIX: "matrix", - NOTE: "note", - }, - vm: window.vm, - extensions: { - unsandboxed: true, - register: (object) => { - const serviceName = - vm.extensionManager._registerInternalExtension(object); - vm.extensionManager._loadedExtensions.set( - object.getInfo().id, - serviceName - ); - }, - }, - }; - if (!Scratch.vm) { - throw new Error("The VM does not exist"); - } - } const vm = Scratch.vm; const runtime = vm.runtime; diff --git a/extensions/penplus.js b/extensions/penplus.js index 2da667498c..fb11d2de3c 100644 --- a/extensions/penplus.js +++ b/extensions/penplus.js @@ -26,49 +26,6 @@ Other various small fixes (function (Scratch) { "use strict"; - // This is for compatibility with plugin loaders that don't implement window.Scratch. - // This is a one-time exception. Similar code like this WILL NOT be accepted in new extensions without - // significant justification. - if (!Scratch) { - Scratch = { - // @ts-expect-error - BlockType: { - COMMAND: "command", - REPORTER: "reporter", - BOOLEAN: "Boolean", - HAT: "hat", - }, - // @ts-expect-error - ArgumentType: { - STRING: "string", - NUMBER: "number", - COLOR: "color", - ANGLE: "angle", - BOOLEAN: "Boolean", - MATRIX: "matrix", - NOTE: "note", - }, - // @ts-expect-error - vm: window.vm, - extensions: { - unsandboxed: true, - register: (object) => { - // @ts-expect-error - const serviceName = - vm.extensionManager._registerInternalExtension(object); - // @ts-expect-error - vm.extensionManager._loadedExtensions.set( - object.getInfo().id, - serviceName - ); - }, - }, - }; - if (!Scratch.vm) { - throw new Error("The VM does not exist"); - } - } - if (!Scratch.extensions.unsandboxed) { throw new Error("Pen+ must be run unsandboxed"); } From da211fefc7a87f6f7590112853d77bbb8a8f3dd9 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Mon, 28 Aug 2023 15:57:30 -0500 Subject: [PATCH 005/196] files: various improvements (#976) - Use the new API for stage overlays instead of overlaying the entire page - At least the latest 2 versions of all major desktop browsers now support the input cancel event, so we can finally surface the secret option to only show a file picker and not the custom modal - If oncancel is detected to not be supported, it will instead just always show the modal to avoid stalling the script if someone cancels the prompt - Close file modal when project stopped (as you are now able to actually press the stop button) --- extensions/files.js | 52 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index c4212b34c8..686906f150 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -22,6 +22,20 @@ const AS_TEXT = "text"; const AS_DATA_URL = "url"; + /** + * @param {HTMLInputElement} input + * @returns {boolean} + */ + const isCancelEventSupported = (input) => { + if ("oncancel" in input) { + // Chrome 113+, Safari 16.4+ + return true; + } + // Firefox is weird. cancel is supported since Firefox 91, but oncancel doesn't exist. + // Firefox 91 is from August 2021. That's old enough to not care about previous versions. + return navigator.userAgent.includes("Firefox"); + }; + /** * @param {string} accept See MODE_ constants above * @param {string} as See AS_ constants above @@ -33,14 +47,15 @@ // so we have to show our own UI anyways. We may as well use this to implement some nice features // that native file pickers don't have: // - Easy drag+drop - // - Reliable cancel button (input cancel event is still basically nonexistent) + // - Reliable cancel button (input cancel event is still not perfect) // This is important so we can make this just a reporter instead of a command+hat block. - // Without an interface, the script would be stalled if the prompt was just cancelled. + // Without an interface, the script would be stalled if the prompt was cancelled. /** @param {string} text */ const callback = (text) => { _resolve(text); - outer.remove(); + Scratch.vm.renderer.removeOverlay(outer); + Scratch.vm.runtime.off("PROJECT_STOP_ALL", handleProjectStopped); document.body.removeEventListener("keydown", handleKeyDown); }; @@ -80,21 +95,22 @@ capture: true, }); + const handleProjectStopped = () => { + callback(""); + }; + Scratch.vm.runtime.on("PROJECT_STOP_ALL", handleProjectStopped); + const INITIAL_BORDER_COLOR = "#888"; const DROPPING_BORDER_COLOR = "#03a9fc"; const outer = document.createElement("div"); - outer.className = "extension-content"; - outer.style.position = "fixed"; - outer.style.top = "0"; - outer.style.left = "0"; + outer.style.pointerEvents = "auto"; outer.style.width = "100%"; outer.style.height = "100%"; outer.style.display = "flex"; outer.style.alignItems = "center"; outer.style.justifyContent = "center"; outer.style.background = "rgba(0, 0, 0, 0.5)"; - outer.style.zIndex = "20000"; outer.style.color = "black"; outer.style.colorScheme = "light"; outer.addEventListener("dragover", (e) => { @@ -158,7 +174,19 @@ subtitle.textContent = `Accepted formats: ${formattedAccept}`; modal.appendChild(subtitle); - document.body.appendChild(outer); + // To avoid the script getting stalled forever, if cancel isn't supported, we'll just forcibly + // show our modal. + if ( + openFileSelectorMode === MODE_ONLY_SELECTOR && + !isCancelEventSupported(input) + ) { + openFileSelectorMode = MODE_IMMEDIATELY_SHOW_SELECTOR; + } + + if (openFileSelectorMode !== MODE_ONLY_SELECTOR) { + const overlay = Scratch.vm.renderer.addOverlay(outer, "scale"); + overlay.container.style.zIndex = "100"; + } if ( openFileSelectorMode === MODE_IMMEDIATELY_SHOW_SELECTOR || @@ -172,7 +200,6 @@ input.addEventListener("cancel", () => { callback(""); }); - outer.remove(); } }); @@ -358,6 +385,11 @@ text: "open selector immediately", value: MODE_IMMEDIATELY_SHOW_SELECTOR, }, + { + // Will not work if the browser doesn't think we are responding to a click event. + text: "only show selector (unreliable)", + value: MODE_ONLY_SELECTOR, + }, ], }, }, From ca2a9dc222b39aed9bfc0fd55f652fe04f358271 Mon Sep 17 00:00:00 2001 From: pumpkin <113677241+pumpkinhasapatch@users.noreply.github.com> Date: Tue, 29 Aug 2023 06:58:03 +1000 Subject: [PATCH 006/196] Lily/HackedBlocks: various tweaks (#969) --- extensions/Lily/HackedBlocks.js | 35 ++++++++++++++++---------------- images/Lily/HackedBlocks.png | Bin 0 -> 9544 bytes 2 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 images/Lily/HackedBlocks.png diff --git a/extensions/Lily/HackedBlocks.js b/extensions/Lily/HackedBlocks.js index 771e0bf219..f48bfe6ccf 100644 --- a/extensions/Lily/HackedBlocks.js +++ b/extensions/Lily/HackedBlocks.js @@ -1,7 +1,8 @@ -// Name: Hacked Block Collection +// Name: Hidden Block Collection // ID: lmsHackedBlocks -// Description: Various modified vanilla blocks. +// Description: Various "hacked blocks" that work in Scratch but are not visible in the palette. // By: LilyMakesThings +// By: pumpkinhasapatch (function (Scratch) { "use strict"; @@ -10,11 +11,13 @@ getInfo() { return { id: "lmsHackedBlocks", - name: "Hacked Block Collection", + name: "Hidden Blocks", + docsURI: "https://en.scratch-wiki.info/wiki/Hidden_Blocks#Events", blocks: [ + // Use the sensing_touchingobjectmenu instead of event_ to also list sprites, since the block supports it { blockType: Scratch.BlockType.XML, - xml: '', + xml: '', }, "---", { @@ -26,38 +29,34 @@ xml: '', }, "---", + // Counting blocks that function similarly to variables { blockType: Scratch.BlockType.XML, - xml: 'enter', - }, - { - blockType: Scratch.BlockType.XML, - xml: 'Stage', - }, - { - blockType: Scratch.BlockType.XML, - xml: '#ffffff', + xml: '', }, { blockType: Scratch.BlockType.XML, - xml: '#ffffff#ffffff', + xml: '', }, { blockType: Scratch.BlockType.XML, - xml: '', + xml: '', }, "---", { blockType: Scratch.BlockType.XML, - xml: '', + xml: '60', }, + "---", { blockType: Scratch.BlockType.XML, - xml: '', + xml: '', }, + // Dot matrix input from the micro:bit extension + // Returns a 5x5 binary grid of pixels depending on what was drawn. White pixels are 1 and green pixels are 0 { blockType: Scratch.BlockType.XML, - xml: '', + xml: '1111110101001000010001110', }, ], }; diff --git a/images/Lily/HackedBlocks.png b/images/Lily/HackedBlocks.png new file mode 100644 index 0000000000000000000000000000000000000000..a89998c0ddd9486430cb828462926c3d00ee1451 GIT binary patch literal 9544 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 Date: Wed, 30 Aug 2023 05:06:59 +0100 Subject: [PATCH 007/196] Add Lily/Video extension (#840) --- extensions/Lily/Video.js | 491 +++++++++++++++++++++++++++++++++++++ extensions/extensions.json | 1 + images/Lily/Video.svg | 1 + images/README.md | 3 + website/dango.mp4 | Bin 0 -> 107326 bytes 5 files changed, 496 insertions(+) create mode 100644 extensions/Lily/Video.js create mode 100644 images/Lily/Video.svg create mode 100644 website/dango.mp4 diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js new file mode 100644 index 0000000000..307e89f2ec --- /dev/null +++ b/extensions/Lily/Video.js @@ -0,0 +1,491 @@ +// Name: Video +// ID: lmsVideo +// Description: Play videos from URLs. +// By: LilyMakesThings + +// Attribution is not required, but greatly appreciated. + +(function (Scratch) { + "use strict"; + + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = vm.renderer; + const Cast = Scratch.Cast; + + const BitmapSkin = runtime.renderer.exports.BitmapSkin; + class VideoSkin extends BitmapSkin { + constructor(id, renderer, videoName, videoSrc) { + super(id, renderer); + + /** @type {string} */ + this.videoName = videoName; + + /** @type {string} */ + this.videoSrc = videoSrc; + + this.videoError = false; + + this.readyPromise = new Promise((resolve) => { + this.readyCallback = resolve; + }); + + this.videoElement = document.createElement("video"); + // Need to set non-zero dimensions, otherwise scratch-render thinks this is an empty image + this.videoElement.width = 1; + this.videoElement.height = 1; + this.videoElement.crossOrigin = "anonymous"; + this.videoElement.onloadeddata = () => { + // First frame loaded + this.readyCallback(); + this.markVideoDirty(); + }; + this.videoElement.onerror = () => { + this.videoError = true; + this.readyCallback(); + this.markVideoDirty(); + }; + this.videoElement.src = videoSrc; + this.videoElement.currentTime = 0; + + this.videoDirty = true; + + this.reuploadVideo(); + } + + reuploadVideo() { + this.videoDirty = false; + if (this.videoError) { + // Draw an image that looks similar to Scratch's normal costume loading errors + const canvas = document.createElement("canvas"); + canvas.width = this.videoElement.videoWidth || 128; + canvas.height = this.videoElement.videoHeight || 128; + const ctx = canvas.getContext("2d"); + + if (ctx) { + ctx.fillStyle = "#cccccc"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const fontSize = Math.min(canvas.width, canvas.height); + ctx.fillStyle = "#000000"; + ctx.font = `${fontSize}px serif`; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; + ctx.fillText("?", canvas.width / 2, canvas.height / 2); + } else { + // guess we can't draw the error then + } + + this.setBitmap(canvas); + } else { + this.setBitmap(this.videoElement); + } + } + + markVideoDirty() { + this.videoDirty = true; + this.emitWasAltered(); + } + + get size() { + if (this.videoDirty) { + this.reuploadVideo(); + } + return super.size; + } + + getTexture(scale) { + if (this.videoDirty) { + this.reuploadVideo(); + } + return super.getTexture(scale); + } + + dispose() { + super.dispose(); + this.videoElement.pause(); + } + } + + class Video { + constructor() { + /** @type {Record} */ + this.videos = Object.create(null); + + runtime.on("PROJECT_STOP_ALL", () => this.resetEverything()); + runtime.on("PROJECT_START", () => this.resetEverything()); + + runtime.on("BEFORE_EXECUTE", () => { + for (const skin of renderer._allSkins) { + if (skin instanceof VideoSkin && !skin.videoElement.paused) { + skin.markVideoDirty(); + } + } + }); + } + + getInfo() { + return { + id: "lmsVideo", + color1: "#557882", + name: "Video", + blocks: [ + { + opcode: "loadVideoURL", + blockType: Scratch.BlockType.COMMAND, + text: "load video from URL [URL] as [NAME]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.mp4", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "deleteVideoURL", + blockType: Scratch.BlockType.COMMAND, + text: "delete video [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "getLoadedVideos", + blockType: Scratch.BlockType.REPORTER, + text: "loaded videos", + }, + "---", + { + opcode: "showVideo", + blockType: Scratch.BlockType.COMMAND, + text: "show video [NAME] on [TARGET]", + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targets", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "stopShowingVideo", + blockType: Scratch.BlockType.COMMAND, + text: "stop showing video on [TARGET]", + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targets", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "getCurrentVideo", + blockType: Scratch.BlockType.REPORTER, + text: "current video on [TARGET]", + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targets", + }, + }, + }, + "---", + { + opcode: "startVideo", + blockType: Scratch.BlockType.COMMAND, + text: "start video [NAME] at [DURATION] seconds", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + DURATION: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "getAttribute", + blockType: Scratch.BlockType.REPORTER, + text: "[ATTRIBUTE] of video [NAME]", + arguments: { + ATTRIBUTE: { + type: Scratch.ArgumentType.STRING, + menu: "attribute", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + "---", + { + opcode: "pause", + blockType: Scratch.BlockType.COMMAND, + text: "pause video [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "resume", + blockType: Scratch.BlockType.COMMAND, + text: "resume video [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + }, + }, + { + opcode: "getState", + blockType: Scratch.BlockType.BOOLEAN, + text: "video [NAME] is [STATE]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "state", + }, + }, + }, + "---", + { + opcode: "setVolume", + blockType: Scratch.BlockType.COMMAND, + text: "set volume of video [NAME] to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "my video", + }, + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + ], + menus: { + targets: { + acceptReporters: true, + items: "_getTargets", + }, + state: { + acceptReporters: true, + items: ["playing", "paused"], + }, + attribute: { + acceptReporters: false, + items: ["current time", "duration", "volume", "width", "height"], + }, + }, + }; + } + + resetEverything() { + for (const { videoElement } of Object.values(this.videos)) { + videoElement.pause(); + videoElement.currentTime = 0; + } + + for (const target of runtime.targets) { + const drawable = renderer._allDrawables[target.drawableID]; + if (drawable.skin instanceof VideoSkin) { + target.setCostume(target.currentCostume); + } + } + } + + async loadVideoURL(args) { + // Always delete the old video with the same name, if it exists. + this.deleteVideoURL(args); + + const videoName = Cast.toString(args.NAME); + const url = Cast.toString(args.URL); + + if ( + url.startsWith("https://www.youtube.com/") || + url.startsWith("https://youtube.com/") + ) { + alert( + [ + "The video extension does not support YouTube links.", + "You can use the Iframe extension instead.", + ].join("\n\n") + ); + return; + } + + if (!(await Scratch.canFetch(url))) return; + + const skinId = renderer._nextSkinId++; + const skin = new VideoSkin(skinId, renderer, videoName, url); + renderer._allSkins[skinId] = skin; + this.videos[videoName] = skin; + + return skin.readyPromise; + } + + deleteVideoURL(args) { + const videoName = Cast.toString(args.NAME); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; + + for (const target of runtime.targets) { + const drawable = renderer._allDrawables[target.drawableID]; + if (drawable && drawable.skin === videoSkin) { + target.setCostume(target.currentCostume); + } + } + + renderer.destroySkin(videoSkin.id); + Reflect.deleteProperty(this.videos, videoName); + } + + getLoadedVideos() { + return JSON.stringify(Object.keys(this.videos)); + } + + showVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const videoName = Cast.toString(args.NAME); + const target = this._getTargetFromMenu(targetName, util); + const videoSkin = this.videos[videoName]; + if (!target || !videoSkin) return; + + vm.renderer.updateDrawableSkinId(target.drawableID, videoSkin._id); + } + + stopShowingVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + + target.setCostume(target.currentCostume); + } + + getCurrentVideo(args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + + const drawable = renderer._allDrawables[target.drawableID]; + const skin = drawable && drawable.skin; + return skin instanceof VideoSkin ? skin.videoName : ""; + } + + startVideo(args) { + const videoName = Cast.toString(args.NAME); + const duration = Cast.toNumber(args.DURATION); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; + + videoSkin.videoElement.play(); + videoSkin.videoElement.currentTime = duration; + videoSkin.markVideoDirty(); + } + + getAttribute(args) { + const videoName = Cast.toString(args.NAME); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return 0; + + switch (args.ATTRIBUTE) { + case "current time": + return videoSkin.videoElement.currentTime; + case "duration": + return videoSkin.videoElement.duration; + case "volume": + return videoSkin.videoElement.volume * 100; + case "width": + return videoSkin.size[0]; + case "height": + return videoSkin.size[1]; + default: + return 0; + } + } + + pause(args) { + const videoName = Cast.toString(args.NAME); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; + + videoSkin.videoElement.pause(); + videoSkin.markVideoDirty(); + } + + resume(args) { + const videoName = Cast.toString(args.NAME); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; + + videoSkin.videoElement.play(); + videoSkin.markVideoDirty(); + } + + getState(args) { + const videoName = Cast.toString(args.NAME); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return args.STATE === "paused"; + + return args.STATE == "playing" + ? !videoSkin.videoElement.paused + : videoSkin.videoElement.paused; + } + + setVolume(args) { + const videoName = Cast.toString(args.NAME); + const value = Cast.toNumber(args.VALUE); + const videoSkin = this.videos[videoName]; + if (!videoSkin) return; + + videoSkin.videoElement.volume = value / 100; + } + + /** @returns {VM.Target|undefined} */ + _getTargetFromMenu(targetName, util) { + if (targetName === "_myself_") return util.target; + if (targetName === "_stage_") return runtime.getTargetForStage(); + return Scratch.vm.runtime.getSpriteTargetByName(targetName); + } + + _getTargets() { + let spriteNames = [ + { text: "myself", value: "_myself_" }, + { text: "Stage", value: "_stage_" }, + ]; + const targets = Scratch.vm.runtime.targets + .filter((target) => target.isOriginal && !target.isStage) + .map((target) => target.getName()); + spriteNames = spriteNames.concat(targets); + return spriteNames; + } + } + + Scratch.extensions.register(new Video()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 2467cede1b..2781ff7955 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -15,6 +15,7 @@ "Skyhigh173/bigint", "utilities", "sound", + "Lily/Video", "iframe", "Xeltalliv/clippingblending", "clipboard", diff --git a/images/Lily/Video.svg b/images/Lily/Video.svg new file mode 100644 index 0000000000..bd9a2f932b --- /dev/null +++ b/images/Lily/Video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index a67e0ceca9..069ffb31ca 100644 --- a/images/README.md +++ b/images/README.md @@ -287,3 +287,6 @@ All images in this folder are licensed under the [GNU General Public License ver ## iframe.svg - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694716263 + +## Lily/Video.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 diff --git a/website/dango.mp4 b/website/dango.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..f5dbfd4738f45e2e1b3a4d9cbe0e91efb3991760 GIT binary patch literal 107326 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 z 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%0DLQyG5(Jo5Xpz^_D2GG`>MqF@A^kT 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 KQ2`kP3H5(3t(AuW literal 0 HcmV?d00001 From 035d8d2a98e4fece6fdcdf8a81d6722d13ce58a4 Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 02:15:54 -0400 Subject: [PATCH 008/196] Skyhigh173/json: reword replace item block for consistency (#984) --- extensions/Skyhigh173/json.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Skyhigh173/json.js b/extensions/Skyhigh173/json.js index 8fe25e8f4c..5abb20245c 100644 --- a/extensions/Skyhigh173/json.js +++ b/extensions/Skyhigh173/json.js @@ -239,7 +239,7 @@ { opcode: "json_array_set", blockType: Scratch.BlockType.REPORTER, - text: "replace item [pos] of [json] to [item]", + text: "replace item [pos] of [json] with [item]", arguments: { item: { type: Scratch.ArgumentType.STRING, From d64b688ba212a41f263cb997f8d92d4f62b94abf Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 03:14:12 -0400 Subject: [PATCH 009/196] runtime-options: rename some blocks for consistency (#985) --- extensions/runtime-options.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/runtime-options.js b/extensions/runtime-options.js index 9b3aec504e..6e5134ad73 100644 --- a/extensions/runtime-options.js +++ b/extensions/runtime-options.js @@ -28,7 +28,7 @@ blocks: [ { opcode: "getEnabled", - text: "is [thing] enabled?", + text: "[thing] enabled?", blockType: Scratch.BlockType.BOOLEAN, arguments: { thing: { @@ -60,7 +60,7 @@ { opcode: "getFramerate", - text: "get framerate limit", + text: "framerate limit", blockType: Scratch.BlockType.REPORTER, }, { @@ -79,12 +79,12 @@ { opcode: "getCloneLimit", - text: "get clone limit", + text: "clone limit", blockType: Scratch.BlockType.REPORTER, }, { opcode: "setCloneLimit", - text: "set clone limit [limit]", + text: "set clone limit to [limit]", blockType: Scratch.BlockType.COMMAND, arguments: { limit: { @@ -99,7 +99,7 @@ { opcode: "getDimension", - text: "get stage [dimension]", + text: "stage [dimension]", blockType: Scratch.BlockType.REPORTER, arguments: { dimension: { From ac086397d26e6db4fcd1e6f9dd94cb75ddee930a Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:02:51 -0400 Subject: [PATCH 010/196] TheShovel/ShovelUtils: rename blocks for consistency (#986) --- extensions/TheShovel/ShovelUtils.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/extensions/TheShovel/ShovelUtils.js b/extensions/TheShovel/ShovelUtils.js index 2d702595c3..39ab81254f 100644 --- a/extensions/TheShovel/ShovelUtils.js +++ b/extensions/TheShovel/ShovelUtils.js @@ -38,7 +38,7 @@ { opcode: "importImage", blockType: Scratch.BlockType.COMMAND, - text: "Import image from [TEXT] name [NAME]", + text: "import image from [TEXT] name [NAME]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -53,7 +53,7 @@ { opcode: "getlist", blockType: Scratch.BlockType.REPORTER, - text: "Get list [TEXT]", + text: "get list [TEXT] as array", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -64,7 +64,7 @@ { opcode: "setlist", blockType: Scratch.BlockType.COMMAND, - text: "Set list [NAME] to [TEXT]", + text: "set list [NAME] to [TEXT]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -79,7 +79,7 @@ { opcode: "importSprite", blockType: Scratch.BlockType.COMMAND, - text: "Import sprite from [TEXT]", + text: "import sprite from [TEXT]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -90,7 +90,7 @@ { opcode: "importSound", blockType: Scratch.BlockType.COMMAND, - text: "Import sound from [TEXT] name [NAME]", + text: "import sound from [TEXT] name [NAME]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -105,7 +105,7 @@ { opcode: "importProject", blockType: Scratch.BlockType.COMMAND, - text: "Import project from [TEXT]", + text: "import project from [TEXT]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -117,7 +117,7 @@ { opcode: "loadExtension", blockType: Scratch.BlockType.COMMAND, - text: "Load extension from [TEXT]", + text: "load extension from [TEXT]", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -129,7 +129,7 @@ { opcode: "restartProject", blockType: Scratch.BlockType.COMMAND, - text: "Restart project", + text: "restart project", arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -140,7 +140,7 @@ { opcode: "deleteSprite", blockType: Scratch.BlockType.COMMAND, - text: "Delete sprite [SPRITE]", + text: "delete sprite [SPRITE]", arguments: { SPRITE: { type: Scratch.ArgumentType.STRING, @@ -151,7 +151,7 @@ { opcode: "deleteImage", blockType: Scratch.BlockType.COMMAND, - text: "Delete costume [COSNAME] in [SPRITE]", + text: "delete costume [COSNAME] in [SPRITE]", arguments: { COSNAME: { type: Scratch.ArgumentType.STRING, @@ -166,7 +166,7 @@ { opcode: "setedtarget", blockType: Scratch.BlockType.COMMAND, - text: "Set editing target to [NAME]", + text: "set editing target to [NAME]", arguments: { NAME: { type: Scratch.ArgumentType.STRING, @@ -178,7 +178,7 @@ { opcode: "brightnessByColor", blockType: Scratch.BlockType.REPORTER, - text: "Get brightness of [color]", + text: "brightness of [color]", arguments: { color: { type: Scratch.ArgumentType.STRING, @@ -190,12 +190,12 @@ { opcode: "getAllSprites", blockType: Scratch.BlockType.REPORTER, - text: "get all sprites", + text: "all sprites", }, { opcode: "getfps", blockType: Scratch.BlockType.REPORTER, - text: "Fps", + text: "fps", }, ], }; From 7af22a9a997e1738dd98e8a431a61e16f0c14a2e Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:03:33 -0400 Subject: [PATCH 011/196] gamepad: rename blocks for consistency (#987) --- extensions/gamepad.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/gamepad.js b/extensions/gamepad.js index 01651ca31e..ada40f8604 100644 --- a/extensions/gamepad.js +++ b/extensions/gamepad.js @@ -84,7 +84,7 @@ { opcode: "gamepadConnected", blockType: Scratch.BlockType.BOOLEAN, - text: "is gamepad [pad] connected?", + text: "gamepad [pad] connected?", arguments: { pad: { type: Scratch.ArgumentType.NUMBER, From f708efa19f4606ec67691226c834a9ca872cb862 Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:03:52 -0400 Subject: [PATCH 012/196] pointerlock: rename blocks for consistency (#990) --- extensions/pointerlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/pointerlock.js b/extensions/pointerlock.js index dd73872c9c..f106e6454c 100644 --- a/extensions/pointerlock.js +++ b/extensions/pointerlock.js @@ -125,7 +125,7 @@ { opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, - text: "is pointer locked?", + text: "pointer locked?", }, ], menus: { From 0eaf9a2ecc9c9b86256894c1a011de82f6d5899a Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:04:18 -0400 Subject: [PATCH 013/196] text: rename blocks for consistency (#989) --- extensions/text.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/text.js b/extensions/text.js index 8f07967819..55f0b5de96 100644 --- a/extensions/text.js +++ b/extensions/text.js @@ -163,7 +163,7 @@ { opcode: "unicodeof", blockType: Scratch.BlockType.REPORTER, - text: "Unicode of [STRING]", + text: "unicode of [STRING]", arguments: { STRING: { type: Scratch.ArgumentType.STRING, @@ -174,7 +174,7 @@ { opcode: "unicodefrom", blockType: Scratch.BlockType.REPORTER, - text: "Unicode [NUM] as letter", + text: "unicode [NUM] as letter", arguments: { NUM: { type: Scratch.ArgumentType.NUMBER, From b1edc999b1e837cfce8d07cd2782ca2ce36bbb00 Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:05:19 -0400 Subject: [PATCH 014/196] box2d: rename blocks for consistency (#988) --- extensions/box2d.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/box2d.js b/extensions/box2d.js index bacc801e66..6f0e498058 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12522,7 +12522,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setStage", - default: "setup stage [stageType]", + default: "set stage boundaries to [stageType]", description: "Set the stage type", }), arguments: { @@ -12802,7 +12802,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setStatic", - default: "set fixed [static]", + default: "set fixed to [static]", description: "Sets whether this block is static or dynamic", }), arguments: { @@ -12833,7 +12833,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setDensity", - default: "set density [density]", + default: "set density to [density]", description: "Set the density of the object", }), arguments: { @@ -12880,7 +12880,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setFriction", - default: "set friction [friction]", + default: "set friction to [friction]", description: "Set the friction of the object", }), arguments: { @@ -12927,7 +12927,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setRestitution", - default: "set bounce [restitution]", + default: "set bounce to [restitution]", description: "Set the bounce of the object", }), arguments: { @@ -13024,7 +13024,7 @@ opcode: "getTouching", text: formatMessage({ id: "griffpatch.getTouching", - default: "touching [where]", + default: "list sprites touching [where]", description: "get the name of any sprites we are touching", }), blockType: BlockType.REPORTER, @@ -13047,7 +13047,7 @@ blockType: BlockType.COMMAND, text: formatMessage({ id: "griffpatch.setScroll", - default: "set scroll x: [ox] y: [oy]", + default: "set scroll to x: [ox] y: [oy]", description: "Sets whether this block is static or dynamic", }), arguments: { From 16f75ebd2eaa6e355f1c45144c36bea1cc8868de Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 30 Aug 2023 13:34:22 -0500 Subject: [PATCH 015/196] Lily/Video: add more warnings about YouTube (#993) --- extensions/Lily/Video.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 307e89f2ec..966dd56873 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -130,6 +130,14 @@ color1: "#557882", name: "Video", blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: "Only direct downloads will work", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Use Iframe extension for YouTube", + }, { opcode: "loadVideoURL", blockType: Scratch.BlockType.COMMAND, @@ -329,7 +337,8 @@ if ( url.startsWith("https://www.youtube.com/") || - url.startsWith("https://youtube.com/") + url.startsWith("https://youtube.com/") || + url.startsWith("https://youtu.be/") ) { alert( [ From 5fe470e19562490d5b18414fef8836ea6623d805 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 30 Aug 2023 13:40:47 -0500 Subject: [PATCH 016/196] local-storage: wait a few seconds between each namespace warning (#994) So if someone does this in a loop, for example, they wont be spammed with alerts forever Which matters especially in the desktop app as it won't give you an option to disable alerts for the site --- extensions/local-storage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/local-storage.js b/extensions/local-storage.js index 2689b0e348..5e18ebc322 100644 --- a/extensions/local-storage.js +++ b/extensions/local-storage.js @@ -13,12 +13,15 @@ let namespace = ""; const getFullStorageKey = () => `${PREFIX}${namespace}`; + let lastNamespaceWarning = 0; + const validNamespace = () => { const valid = !!namespace; - if (!valid) { + if (!valid && Date.now() - lastNamespaceWarning > 3000) { alert( 'Local Storage extension: project must run the "set storage namespace ID" block before it can use other blocks' ); + lastNamespaceWarning = Date.now(); } return valid; }; From 7aa93dfea13194cb66260ead9684a69811fab8a3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 3 Sep 2023 07:31:41 +0100 Subject: [PATCH 017/196] Lily/McUtils: Fix place order (#1003) --- extensions/Lily/McUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/Lily/McUtils.js b/extensions/Lily/McUtils.js index 9e4d80758e..889c32e80b 100644 --- a/extensions/Lily/McUtils.js +++ b/extensions/Lily/McUtils.js @@ -105,7 +105,8 @@ } placeOrder(args, util) { - if (args.INPUT.includes("ice cream")) { + const text = Scratch.Cast.toString(args.INPUT); + if (text.includes("ice cream")) { return false; } else { return args.INPUT; From b4da19c7691f3963e18bba579873f1d915b253c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Sep 2023 23:04:24 -0500 Subject: [PATCH 018/196] build(deps): bump @turbowarp/types from `0caaca7` to `313772e` (#1005) --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1abf464aa..6ec2c80788 100644 --- a/package-lock.json +++ b/package-lock.json @@ -137,8 +137,8 @@ "integrity": "sha512-DUay/UeKoYht03tfcBfp8+m8RSrOtr7eMxFTXujdUXfoiRM7xnQNy6SutufeFmIOdVZU65w3vstLcV3K+6Mhyg==" }, "@turbowarp/types": { - "version": "git+https://github.com/TurboWarp/types-tw.git#0caaca708be023cf9c3c782fbd7e956ca75e9250", - "from": "git+https://github.com/TurboWarp/types-tw.git#0caaca708be023cf9c3c782fbd7e956ca75e9250" + "version": "git+https://github.com/TurboWarp/types-tw.git#313772e79553e3f2a90586fdf689c4f844fdffb1", + "from": "git+https://github.com/TurboWarp/types-tw.git#313772e79553e3f2a90586fdf689c4f844fdffb1" }, "accepts": { "version": "1.3.8", From 4942e25a9bd1f59caf1f3137ad02916adf1dc41b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Sep 2023 23:07:03 -0500 Subject: [PATCH 019/196] build(deps-dev): bump prettier from 3.0.2 to 3.0.3 (#1006) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ec2c80788..a3f4af75d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1169,9 +1169,9 @@ "dev": true }, "prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true }, "proxy-addr": { diff --git a/package.json b/package.json index 6a71725154..be21be71d8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "eslint": "^8.48.0", - "prettier": "^3.0.2" + "prettier": "^3.0.3" }, "private": true } From 5c28a7da640acd4ae0c0de8f75e4f478dbf9a9c9 Mon Sep 17 00:00:00 2001 From: ferny <116464667+fernyrepos@users.noreply.github.com> Date: Tue, 5 Sep 2023 22:25:25 -0400 Subject: [PATCH 020/196] Skyhigh173/json: rename blocks for consistency (#1013) - made booleans internally consistent with missing question marks - removed "get" where applicable - made some blocks be more internally consistent when it comes to value order, like putting the array before the new value - removed unnecessary words like "content" in the list block --- extensions/Skyhigh173/json.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/Skyhigh173/json.js b/extensions/Skyhigh173/json.js index 5abb20245c..3b0065c317 100644 --- a/extensions/Skyhigh173/json.js +++ b/extensions/Skyhigh173/json.js @@ -30,7 +30,7 @@ { opcode: "json_is_valid", blockType: Scratch.BlockType.BOOLEAN, - text: "is JSON [json] valid", + text: "is JSON [json] valid?", arguments: { json: { type: Scratch.ArgumentType.STRING, @@ -41,7 +41,7 @@ { opcode: "json_is", blockType: Scratch.BlockType.BOOLEAN, - text: "is [json] [types]", + text: "is [json] [types]?", arguments: { json: { type: Scratch.ArgumentType.STRING, @@ -58,7 +58,7 @@ { opcode: "json_get_all", blockType: Scratch.BlockType.REPORTER, - text: "get all [Stype] of [json]", + text: "all [Stype] of [json]", arguments: { Stype: { type: Scratch.ArgumentType.STRING, @@ -148,7 +148,7 @@ { opcode: "json_get", blockType: Scratch.BlockType.REPORTER, - text: "get [item] in [json]", + text: "value of [item] in [json]", arguments: { item: { type: Scratch.ArgumentType.STRING, @@ -163,7 +163,7 @@ { opcode: "json_set", blockType: Scratch.BlockType.REPORTER, - text: "set [item] to [value] in [json]", + text: "set [item] in [json] to [value]", arguments: { item: { type: Scratch.ArgumentType.STRING, @@ -336,7 +336,7 @@ { opcode: "json_array_fromto", blockType: Scratch.BlockType.REPORTER, - text: "array [json] from item [item] to [item2]", + text: "items [item] to [item2] of array [json]", arguments: { json: { type: Scratch.ArgumentType.STRING, @@ -396,7 +396,7 @@ { opcode: "json_array_filter", blockType: Scratch.BlockType.REPORTER, - text: "get all value with key [key] in array [json]", + text: "get all values with key [key] in array [json]", arguments: { key: { type: Scratch.ArgumentType.STRING, @@ -487,7 +487,7 @@ { opcode: "json_vm_setlist", blockType: Scratch.BlockType.COMMAND, - text: "set list [list] to content [json]", + text: "set list [list] to [json]", arguments: { list: { type: Scratch.ArgumentType.STRING, From b67ed4acaba07b49ce2f517d2fed8d53c504105f Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 8 Sep 2023 03:33:44 +0100 Subject: [PATCH 021/196] Lily/Video: Make the disclaimer more visually appealing (#1021) https://github.com/TurboWarp/extensions/assets/127533508/07a2bbbe-5c0d-4bde-b181-6aea2b06dfba --- extensions/Lily/Video.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/extensions/Lily/Video.js b/extensions/Lily/Video.js index 966dd56873..828575d5e8 100644 --- a/extensions/Lily/Video.js +++ b/extensions/Lily/Video.js @@ -131,12 +131,8 @@ name: "Video", blocks: [ { - blockType: Scratch.BlockType.LABEL, - text: "Only direct downloads will work", - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Use Iframe extension for YouTube", + blockType: Scratch.BlockType.XML, + xml: "
  • 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 literal 0 HcmV?d00001 diff --git a/samples/Additive Fire.sb3 b/samples/Additive Fire.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..6f1f8c3ce31cdb8ab37d94498d75187cabf65986 GIT binary patch literal 6905 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 literal 0 HcmV?d00001 diff --git a/samples/Clipping Sprites and Pen.sb3 b/samples/Clipping Sprites and Pen.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..00643bbb9678a87afd023f6ff0ee7946b5f5ffd6 GIT binary patch literal 13188 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 literal 0 HcmV?d00001 From 6983072861d5e85d3e52fe15a7d16230f905b8df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:27:41 -0500 Subject: [PATCH 031/196] build(deps-dev): bump eslint from 8.50.0 to 8.51.0 (#1087) --- package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0560ac955a..6bfee972f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", - "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true }, "@eslint/eslintrc": { @@ -60,9 +60,9 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true }, "@humanwhocodes/config-array": { @@ -426,15 +426,15 @@ "dev": true }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -691,12 +691,12 @@ } }, "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "requires": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } @@ -767,9 +767,9 @@ } }, "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -946,9 +946,9 @@ "dev": true }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "requires": { "json-buffer": "3.0.1" diff --git a/package.json b/package.json index 6570a3d0d9..7320ca09ce 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "markdown-it": "^13.0.2" }, "devDependencies": { - "eslint": "^8.50.0", + "eslint": "^8.51.0", "prettier": "^3.0.3" }, "private": true From 321333cb4628c4b318503e71eb9308ad73540d9b Mon Sep 17 00:00:00 2001 From: !Ryan <100989385+softedco@users.noreply.github.com> Date: Mon, 9 Oct 2023 23:35:49 +0600 Subject: [PATCH 032/196] itchio: updates (#1069) - replaced indents with labels - added blocks related to sub products not mentioned in the original documentation for some reason - some undefined checking refactored --- extensions/itchio.js | 105 ++++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/extensions/itchio.js b/extensions/itchio.js index 96404435fd..913712d65a 100644 --- a/extensions/itchio.js +++ b/extensions/itchio.js @@ -1,7 +1,7 @@ // Name: itch.io // ID: itch // Description: Blocks that interact with the itch.io website. Unofficial. -// By: softed +// By: softed ((Scratch) => { "use strict"; @@ -14,8 +14,8 @@ const getGameData = (user, game, secret, onComplete) => { if (!user || !game) { let callback = { errors: [] }; - if (!user) callback.errors.push("missing user argument"); - if (!game) callback.errors.push("missing game argument"); + if (!user) callback.errors.push("user argument not found"); + if (!game) callback.errors.push("game argument not found"); return onComplete(callback); } const url = @@ -37,16 +37,10 @@ }); }; - /** - * Used for storing the response object - */ let data = {}; - /** - * Used for returning standardized error messages - */ let err = () => { - if (!data.errors) return "Error."; + if (!data.errors) return "Error: Data not found."; let output = data.errors[1] ? "Errors: " : "Error: "; output += data.errors[0].charAt(0).toUpperCase() + data.errors[0].slice(1); for (let i = 1; true; i++) @@ -54,16 +48,12 @@ else return output + "."; }; - /** + /*! * Icon by itch.io */ let icon = "data:image/png;base64,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"; - /** - * Mostly visual stuff for Scratch GUI - * The loader only uses getInfo().id and other methods - */ class Itch { getInfo() { return { @@ -75,6 +65,10 @@ color2: "#222222", color3: "#FA5C5C", blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: "Window", + }, { opcode: "openItchWindow", blockType: Scratch.BlockType.COMMAND, @@ -98,7 +92,10 @@ }, }, }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "Data", + }, { opcode: "getGameData", blockType: Scratch.BlockType.COMMAND, @@ -160,7 +157,10 @@ blockType: Scratch.BlockType.BOOLEAN, text: "Game data?", }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "Rewards", + }, { opcode: "returnGameRewards", blockType: Scratch.BlockType.REPORTER, @@ -178,16 +178,49 @@ }, }, { - opcode: "rewardsLenght", + opcode: "rewardsLenght", // fixing this typo would break projects blockType: Scratch.BlockType.REPORTER, - text: "Return rewards list lenght", + text: "Return rewards list length", }, { opcode: "rewardsBool", blockType: Scratch.BlockType.BOOLEAN, text: "Rewards?", }, - "---", + { + blockType: Scratch.BlockType.LABEL, + text: "Sub products", + }, + { + opcode: "returnGameSubProducts", + blockType: Scratch.BlockType.REPORTER, + text: "Return game sub products [subProducts] by index:[index]", + arguments: { + subProducts: { + type: Scratch.ArgumentType.STRING, + menu: "subProductsMenu", + defaultValue: "id", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "subProductsLength", + blockType: Scratch.BlockType.REPORTER, + text: "Return sub products list length", + }, + { + opcode: "subProductsBool", + blockType: Scratch.BlockType.BOOLEAN, + text: "Sub products?", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Sale", + }, { opcode: "returnGameSale", blockType: Scratch.BlockType.REPORTER, @@ -226,6 +259,13 @@ { text: "available", value: "available" }, ], }, + subProductsMenu: { + items: [ + { text: "id", value: "id" }, + { text: "name", value: "name" }, + { text: "price", value: "price" }, + ], + }, saleMenu: { items: [ { text: "id", value: "id" }, @@ -255,7 +295,7 @@ getGameData({ user, game, secret }) { return new Promise((resolve) => { getGameData(user, game, secret, (response) => { - data = response; + data = response || {}; resolve(); }); }); @@ -263,13 +303,13 @@ getGameDataJson({ user, game, secret }) { return new Promise((resolve) => { getGameData(user, game, secret, (response) => { - data = response; + data = response || {}; resolve(JSON.stringify(response)); }); }); } returnGameDataJson() { - return JSON.stringify(data) || err(); + return JSON.stringify(data); } returnGameData(game) { return data[game.data] || err(); @@ -278,20 +318,25 @@ return data.id ? true : false; } returnGameRewards({ index, rewards }) { - try { - return data.rewards[index][rewards]; - } catch (e) { - return err(); - } + return data.rewards?.[index]?.[rewards] || err(); } rewardsLenght() { - return data.rewards ? data.rewards.lenght : 0; + return data.rewards?.length || 0; } rewardsBool() { return data.rewards ? true : false; } + returnGameSubProducts({ subProducts, index }) { + data.sub_products?.[index]?.[subProducts] || err(); + } + subProductsLength() { + return data.sub_products?.length || 0; + } + subProductsBool() { + return data.sub_products ? true : false; + } returnGameSale({ sale }) { - return data.sale ? data.sale[sale] : err(); + return data.sale?.[sale] || err(); } saleBool() { return data.sale ? true : false; From 483fbf3eeb1ae80032a1a4da8be129f14c30b4f3 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 15 Oct 2023 19:10:43 -0500 Subject: [PATCH 033/196] Reset various extensions when runtime disposed (#1100) --- extensions/CST1229/images.js | 4 ++ extensions/CST1229/zip.js | 4 ++ extensions/DT/cameracontrols.js | 2 +- extensions/Longboost/color_channels.js | 5 ++ extensions/NexusKitten/controlcontrols.js | 10 ++++ extensions/TheShovel/CanvasEffects.js | 40 ++++++++------- extensions/TheShovel/CustomStyles.js | 61 +++++++++++++---------- extensions/cursor.js | 7 +++ extensions/godslayerakp/http.js | 12 +++-- extensions/iframe.js | 20 +++++--- extensions/local-storage.js | 4 ++ extensions/mdwalters/notifications.js | 6 +++ extensions/vercte/dictionaries.js | 5 ++ 13 files changed, 124 insertions(+), 56 deletions(-) diff --git a/extensions/CST1229/images.js b/extensions/CST1229/images.js index 7524b64dea..4f1691ba86 100644 --- a/extensions/CST1229/images.js +++ b/extensions/CST1229/images.js @@ -19,6 +19,10 @@ this.createdImages = new Set(); this.validImages = new Set(); + + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this.deleteAllImages(); + }); } getInfo() { diff --git a/extensions/CST1229/zip.js b/extensions/CST1229/zip.js index a362fd17d7..a80062b1ac 100644 --- a/extensions/CST1229/zip.js +++ b/extensions/CST1229/zip.js @@ -18,6 +18,10 @@ // jszip has its own "go to directory" system, but it sucks // implement our own instead this.zipPath = null; + + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this.close(); + }); } getInfo() { diff --git a/extensions/DT/cameracontrols.js b/extensions/DT/cameracontrols.js index ebb9960994..6d577b51d8 100644 --- a/extensions/DT/cameracontrols.js +++ b/extensions/DT/cameracontrols.js @@ -73,7 +73,7 @@ // tell resize to update camera as well vm.runtime.on("STAGE_SIZE_CHANGED", (_) => updateCamera()); - vm.runtime.on("PROJECT_LOADED", (_) => { + vm.runtime.on("RUNTIME_DISPOSED", (_) => { cameraX = 0; cameraY = 0; cameraZoom = 100; diff --git a/extensions/Longboost/color_channels.js b/extensions/Longboost/color_channels.js index d3484380a4..cface39fa9 100644 --- a/extensions/Longboost/color_channels.js +++ b/extensions/Longboost/color_channels.js @@ -8,6 +8,11 @@ const gl = renderer._gl; let channel_array = [true, true, true, true]; class LBdrawtest { + constructor() { + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this.clearEffects(); + }); + } getInfo() { return { id: "lbdrawtest", diff --git a/extensions/NexusKitten/controlcontrols.js b/extensions/NexusKitten/controlcontrols.js index 3a4910012e..e4d7351e0b 100644 --- a/extensions/NexusKitten/controlcontrols.js +++ b/extensions/NexusKitten/controlcontrols.js @@ -43,6 +43,16 @@ }; class controlcontrols { + constructor() { + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + getButtons(); + for (const button of [fullScreen, greenFlag, pauseButton, stopButton]) { + if (button) { + button.style.display = "block"; + } + } + }); + } getInfo() { return { id: "nkcontrols", diff --git a/extensions/TheShovel/CanvasEffects.js b/extensions/TheShovel/CanvasEffects.js index e14c905bf5..4bbd31ef46 100644 --- a/extensions/TheShovel/CanvasEffects.js +++ b/extensions/TheShovel/CanvasEffects.js @@ -60,6 +60,28 @@ let invert = 0; let resizeMode = "default"; + const resetStyles = () => { + borderRadius = 0; + rotation = 0; + offsetY = 0; + offsetX = 0; + skewY = 0; + skewX = 0; + scale = 100; + transparency = 0; + sepia = 0; + blur = 0; + contrast = 100; + saturation = 100; + color = 0; + brightness = 100; + invert = 0; + resizeMode = "default"; + updateStyle(); + }; + + Scratch.vm.runtime.on("RUNTIME_DISPOSED", resetStyles); + class CanvasEffects { getInfo() { return { @@ -245,23 +267,7 @@ updateStyle(); } cleareffects() { - borderRadius = 0; - rotation = 0; - offsetY = 0; - offsetX = 0; - skewY = 0; - skewX = 0; - scale = 100; - transparency = 0; - sepia = 0; - blur = 0; - contrast = 100; - saturation = 100; - color = 0; - brightness = 100; - invert = 0; - resizeMode = "default"; - updateStyle(); + resetStyles(); } setrendermode({ EFFECT }) { resizeMode = EFFECT; diff --git a/extensions/TheShovel/CustomStyles.js b/extensions/TheShovel/CustomStyles.js index fda63171b0..2a8b8f2fdb 100644 --- a/extensions/TheShovel/CustomStyles.js +++ b/extensions/TheShovel/CustomStyles.js @@ -197,6 +197,37 @@ stylesheet.textContent = css; }; + const resetStyles = () => { + monitorText = ""; + monitorBorder = ""; + monitorBackgroundColor = ""; + variableValueBackground = ""; + variableValueTextColor = ""; + listFooterBackground = ""; + listHeaderBackground = ""; + listValueText = ""; + listValueBackground = ""; + variableValueRoundness = -1; + listValueRoundness = -1; + monitorBackgroundRoundness = -1; + monitorBackgroundBorderWidth = -1; + allowScrolling = ""; + askBackground = ""; + askBackgroundRoundness = -1; + askBackgroundBorderWidth = -1; + askButtonBackground = ""; + askButtonRoundness = -1; + askInputBackground = ""; + askInputRoundness = -1; + askInputBorderWidth = -1; + askBoxIcon = ""; + askInputText = ""; + askButtonImage = ""; + askInputBorder = ""; + + applyCSS(); + }; + const getMonitorRoot = (id) => { const allMonitors = document.querySelectorAll(monitorRoot); for (const monitor of allMonitors) { @@ -271,6 +302,8 @@ console.error("Invalid color", color); }; + Scratch.vm.runtime.on("RUNTIME_DISPOSED", resetStyles); + class MonitorStyles { getInfo() { return { @@ -658,33 +691,7 @@ } clearCSS() { - monitorText = ""; - monitorBorder = ""; - monitorBackgroundColor = ""; - variableValueBackground = ""; - variableValueTextColor = ""; - listFooterBackground = ""; - listHeaderBackground = ""; - listValueText = ""; - listValueBackground = ""; - variableValueRoundness = -1; - listValueRoundness = -1; - monitorBackgroundRoundness = -1; - monitorBackgroundBorderWidth = -1; - allowScrolling = ""; - askBackground = ""; - askBackgroundRoundness = -1; - askBackgroundBorderWidth = -1; - askButtonBackground = ""; - askButtonRoundness = -1; - askInputBackground = ""; - askInputRoundness = -1; - askInputBorderWidth = -1; - askBoxIcon = ""; - askInputText = ""; - askButtonImage = ""; - askInputBorder = ""; - applyCSS(); + resetStyles(); } getValue(args) { diff --git a/extensions/cursor.js b/extensions/cursor.js index 3fdf7c47ca..ae478f76aa 100644 --- a/extensions/cursor.js +++ b/extensions/cursor.js @@ -178,6 +178,13 @@ ]; class MouseCursor { + constructor() { + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this.setCur({ + cur: "default", + }); + }); + } getInfo() { return { id: "MouseCursor", diff --git a/extensions/godslayerakp/http.js b/extensions/godslayerakp/http.js index e08232cdff..89ea542c7c 100644 --- a/extensions/godslayerakp/http.js +++ b/extensions/godslayerakp/http.js @@ -211,8 +211,8 @@ return defaultRequest; } - static get defualtResponse() { - const defualtResponse = { + static get defaultResponse() { + const defaultResponse = { text: "", status: "", statusText: "", @@ -221,7 +221,7 @@ url: "", }; - return defualtResponse; + return defaultResponse; } /** @@ -230,6 +230,10 @@ constructor() { this.clearAll(); this.showingExtra = false; + + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this.clearAll(); + }); } getInfo() { return { @@ -548,7 +552,7 @@ clearAll() { this.request = WebRequests.defaultRequest; - this.response = WebRequests.defualtResponse; + this.response = WebRequests.defaultResponse; } /* ------- DATA READING -------- */ diff --git a/extensions/iframe.js b/extensions/iframe.js index 4e84d9edf3..29224ef9d6 100644 --- a/extensions/iframe.js +++ b/extensions/iframe.js @@ -107,8 +107,18 @@ updateFrameAttributes(); }; + const closeFrame = () => { + if (iframe) { + Scratch.renderer.removeOverlay(iframe); + iframe = null; + overlay = null; + } + }; + Scratch.vm.on("STAGE_SIZE_CHANGED", updateFrameAttributes); + Scratch.vm.runtime.on("RUNTIME_DISPOSED", closeFrame); + class IframeExtension { getInfo() { return { @@ -259,14 +269,14 @@ } async display({ URL }) { - this.close(); + closeFrame(); if (await Scratch.canEmbed(URL)) { createFrame(Scratch.Cast.toString(URL)); } } async displayHTML({ HTML }) { - this.close(); + closeFrame(); const url = `data:text/html;,${encodeURIComponent( Scratch.Cast.toString(HTML) )}`; @@ -288,11 +298,7 @@ } close() { - if (iframe) { - Scratch.renderer.removeOverlay(iframe); - iframe = null; - overlay = null; - } + closeFrame(); } get({ MENU }) { diff --git a/extensions/local-storage.js b/extensions/local-storage.js index 5e18ebc322..6d93321696 100644 --- a/extensions/local-storage.js +++ b/extensions/local-storage.js @@ -82,6 +82,10 @@ } }); + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + namespace = ""; + }); + class LocalStorage { getInfo() { return { diff --git a/extensions/mdwalters/notifications.js b/extensions/mdwalters/notifications.js index 8f2491c797..524d9ae63a 100644 --- a/extensions/mdwalters/notifications.js +++ b/extensions/mdwalters/notifications.js @@ -40,6 +40,11 @@ }; class Notifications { + constructor() { + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + this._closeNotification(); + }); + } getInfo() { return { id: "mdwaltersnotifications", @@ -118,6 +123,7 @@ async _closeNotification() { if (notification) { notification.close(); + notification = null; } const registration = await getServiceWorkerRegistration(); diff --git a/extensions/vercte/dictionaries.js b/extensions/vercte/dictionaries.js index 401ba550a3..fc284db78f 100644 --- a/extensions/vercte/dictionaries.js +++ b/extensions/vercte/dictionaries.js @@ -6,6 +6,11 @@ (function (Scratch) { "use strict"; let dictionaries = new Map(); + + Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { + dictionaries.clear(); + }); + class DictionaryExtension { getInfo() { return { From 777904b89cd3c62bb26f5a9eb95804f26952dbcd Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 15 Oct 2023 19:29:56 -0500 Subject: [PATCH 034/196] Lily/MoreEvents: Add before project saves & after project saves events (#1086) --- extensions/Lily/MoreEvents.js | 108 ++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/extensions/Lily/MoreEvents.js b/extensions/Lily/MoreEvents.js index cd1f329b65..d0d40dbe65 100644 --- a/extensions/Lily/MoreEvents.js +++ b/extensions/Lily/MoreEvents.js @@ -79,6 +79,99 @@ var lastValues = {}; var runTimer = 0; + const MAX_BEFORE_SAVE_MS = 3000; + + const beforeSave = () => + new Promise((resolve) => { + const threads = vm.runtime.startHats("lmsMoreEvents_beforeSave"); + + if (threads.length === 0) { + resolve(); + return; + } + + const startTime = performance.now(); + const checkThreadStatus = () => { + if ( + performance.now() - startTime > MAX_BEFORE_SAVE_MS || + threads.every((thread) => !vm.runtime.isActiveThread(thread)) + ) { + vm.runtime.off("AFTER_EXECUTE", checkThreadStatus); + resolve(); + } + }; + + vm.runtime.on("AFTER_EXECUTE", checkThreadStatus); + }); + + const afterSave = () => { + // Wait until the next frame actually starts so that the actual file + // saving routine has a chance to finish before we starting running blocks. + vm.runtime.once("BEFORE_EXECUTE", () => { + vm.runtime.startHats("lmsMoreEvents_afterSave"); + }); + }; + + const originalSaveProjectSb3 = vm.saveProjectSb3; + vm.saveProjectSb3 = async function (...args) { + await beforeSave(); + const result = await originalSaveProjectSb3.apply(this, args); + afterSave(); + return result; + }; + + const originalSaveProjectSb3Stream = vm.saveProjectSb3Stream; + vm.saveProjectSb3Stream = function (...args) { + // This is complicated because we need to return a stream object syncronously... + + let realStream = null; + const queuedCalls = []; + + const whenStreamReady = (methodName, args) => { + if (realStream) { + return realStream[methodName].apply(realStream, args); + } else { + return new Promise((resolve) => { + queuedCalls.push({ + resolve, + methodName, + args, + }); + }); + } + }; + + const streamWrapper = { + on: (...args) => void whenStreamReady("on", args), + pause: (...args) => void whenStreamReady("pause", args), + resume: (...args) => void whenStreamReady("resume", args), + accumulate: (...args) => whenStreamReady("accumulate", args), + }; + + beforeSave().then(() => { + realStream = originalSaveProjectSb3Stream.apply(this, args); + + realStream.on("end", () => { + // Not sure how JSZip handles errors here, so we'll make sure not to break anything if + // afterSave somehow throws + try { + afterSave(); + } catch (e) { + console.error(e); + } + }); + + for (const queued of queuedCalls) { + queued.resolve( + realStream[queued.methodName].apply(realStream, queued.args) + ); + } + queuedCalls.length = 0; + }); + + return streamWrapper; + }; + class MoreEvents { constructor() { // Stop Sign Clicked contributed by @CST1229 @@ -354,6 +447,21 @@ blockType: Scratch.BlockType.XML, xml: '', }, + "---", + { + blockType: Scratch.BlockType.EVENT, + opcode: "beforeSave", + text: "before project saves", + shouldRestartExistingThreads: true, + isEdgeActivated: false, + }, + { + blockType: Scratch.BlockType.EVENT, + opcode: "afterSave", + text: "after project saves", + shouldRestartExistingThreads: true, + isEdgeActivated: false, + }, ], menus: { // Targets have acceptReporters: true From 2640935fb3176b5216af1c6a17ee8dee03c1ca42 Mon Sep 17 00:00:00 2001 From: SamuelLouf <104774504+samuellouf@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:58:30 +0100 Subject: [PATCH 035/196] Lily/lmsutils: Add Microsoft Edge detection (#1115) --- extensions/Lily/lmsutils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/Lily/lmsutils.js b/extensions/Lily/lmsutils.js index 1186455c1a..80d52a8815 100644 --- a/extensions/Lily/lmsutils.js +++ b/extensions/Lily/lmsutils.js @@ -1341,6 +1341,7 @@ return "Other"; } if (args.DROPDOWN === "browser") { + if (user.includes("Edg")) return "Edge"; if (user.includes("Chrome")) return "Chrome"; if (user.includes("MSIE")) return "Internet Explorer"; if (user.includes("Firefox")) return "Firefox"; From dd30b153ec81bee06f89157b35d4ffbd05224e4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:59:02 -0500 Subject: [PATCH 036/196] build(deps-dev): bump eslint from 8.51.0 to 8.52.0 (#1109) --- package-lock.json | 37 ++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6bfee972f3..e33ce1f6ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,18 +60,18 @@ } }, "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -100,9 +100,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@nodelib/fs.scandir": { @@ -140,6 +140,12 @@ "version": "git+https://github.com/TurboWarp/types-tw.git#cfaa5d3ce5dc96f16f7da9054325a4f1ed457662", "from": "git+https://github.com/TurboWarp/types-tw.git#cfaa5d3ce5dc96f16f7da9054325a4f1ed457662" }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -426,18 +432,19 @@ "dev": true }, "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", diff --git a/package.json b/package.json index 7320ca09ce..36b36b41d4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "markdown-it": "^13.0.2" }, "devDependencies": { - "eslint": "^8.51.0", + "eslint": "^8.52.0", "prettier": "^3.0.3" }, "private": true From e9a5fb88a4c7d747d63e420d00a9bdd8d9d7d911 Mon Sep 17 00:00:00 2001 From: CST1229 <68464103+CST1229@users.noreply.github.com> Date: Mon, 30 Oct 2023 21:56:23 +0100 Subject: [PATCH 037/196] utilities: fix some blocks breaking if specific costumes exist (#1119) Resolves [#Clamp bug](https://discord.com/channels/837024174865776680/1168243452899774494) For some weird, inexplicable reason that I do not know why it happens, ArgumentType.NUMBER arguments are auto-casted to number, but _only_ if a costume with that name does not exist. This breaks a few math-related blocks in Utilities under specific circumstances (for example, `clamp 30 between 25 and 100` returns `25` if costumes named 25 and 100 exist). This PR fixes that, by casting the arguments to numbers in `clamp` and using Scratch.Cast.compare on `isLessOrEqual` and `isMoreOrEqual`. `exponent`'s arguments are also casted now just to be safe. --- extensions/utilities.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/extensions/utilities.js b/extensions/utilities.js index 6657951820..dd156b4057 100644 --- a/extensions/utilities.js +++ b/extensions/utilities.js @@ -279,11 +279,11 @@ } isLessOrEqual({ A, B }) { - return A <= B; + return Scratch.Cast.compare(A, B) <= 0; } isMoreOrEqual({ A, B }) { - return A >= B; + return Scratch.Cast.compare(A, B) >= 0; } trueBlock() { @@ -295,6 +295,8 @@ } exponent({ A, B }) { + A = Scratch.Cast.toNumber(A); + B = Scratch.Cast.toNumber(B); return Math.pow(A, B); } @@ -311,10 +313,13 @@ } clamp({ INPUT, MIN, MAX }) { + INPUT = Scratch.Cast.toNumber(INPUT); + MIN = Scratch.Cast.toNumber(MIN); + MAX = Scratch.Cast.toNumber(MAX); if (MIN > MAX) { - return Scratch.Cast.toNumber(Math.min(Math.max(INPUT, MAX), MIN)); + return Math.min(Math.max(INPUT, MAX), MIN); } else { - return Scratch.Cast.toNumber(Math.min(Math.max(INPUT, MIN), MAX)); + return Math.min(Math.max(INPUT, MIN), MAX); } } From 396de07d06ffb789c67369ecb5d43854ea56b61c Mon Sep 17 00:00:00 2001 From: !Ryan <100989385+softedco@users.noreply.github.com> Date: Wed, 1 Nov 2023 07:15:51 +0600 Subject: [PATCH 038/196] gamejolt: major update (#1106) ### Changes - Added docs. - Replaced some blocks with more efficient ones, replaced blocks marked as deprecated. - Added batch blocks. There isn't any way to independently test them without a game so we'll need people that are devs on GJ, here are the [api docs](https://gamejolt.com/game-api/doc), they cover what fields and params namespaces have, and the extension got rest of the URL construction covered. - Added debug mode and debug blocks, debug mode solves error messages being interpreted as a valid response, on by default to prevent significant block behaviour change. - Unwrapped some resolves to put the error message into `err.last` for the `Last API error` block to work properly, solves people improperly using non-returning blocks and telling me that the extension is not working instead of opening the console to see the errors. Thought of doing this with alerts but that would significantly change most existing projects' behaviour. - Changed text on blocks and some error messages to be more readable. - Changed GameJolt to Game Jolt because apparently Game Jolt with a space is their actual name, didn't check if this breaks something because it shouldn't (IDs and ID-like stuff untouched) ### And most importantly - The `bool` object is now just `trueStr`. ### Wrapping up Lint will probably complain about `data.stuff = undefined;`. It is there to make blocks not skip errors. ![scratchblocks (2)](https://github.com/TurboWarp/extensions/assets/100989385/9b28ec3a-1d30-4d09-898d-7091a40d9320) Should be it. --------- Co-authored-by: Muffin --- docs/gamejolt.md | 502 ++++ extensions/gamejolt.js | 5224 +++++++++++++++++++++++----------------- 2 files changed, 3498 insertions(+), 2228 deletions(-) create mode 100644 docs/gamejolt.md diff --git a/docs/gamejolt.md b/docs/gamejolt.md new file mode 100644 index 0000000000..df27bf184e --- /dev/null +++ b/docs/gamejolt.md @@ -0,0 +1,502 @@ +# Game Jolt API +This extension allows you to easily implement the Game Jolt API using a public domain library. +## Blocks +Blocks that the extension uses to send requests to the Game Jolt API. + +```scratch + +``` +Checks to see if the URL is the Game Jolt website. +### Session Blocks +Operating on the game's session. + +```scratch +Set game ID to (0) and private key [private key] :: #2F7F6F +``` +This block is required for all requests to work. + +--- +```scratch +[Open v] session :: #2F7F6F +``` +Opens/closes a game session. +- You must ping the session to keep it open and you must close it when you're done with it. + +When you login the session is opened automatically. + +--- +```scratch +Ping session :: #2F7F6F +``` +Pings an open session. +- If the session hasn't been pinged within 120 seconds, the system will close the session and you will have to open another one. +- It's recommended that you ping about every 30 seconds or so to keep the system from clearing out your session. + +When the session is opened it is pinged every 30 seconds automatically. +- You can ping it manually to update the session status. + +--- +```scratch +Set session status to [active v] :: #2F7F6F +``` +Sets the session status to active/idle. +- Ping the session to update it's status. + +--- +```scratch + +``` +Checks to see if there is an open session for the user. +- Can be used to see if a particular user account is active in the game. +### User Blocks +Login, logout and fetch users. + +```scratch +Login with [username] and [private token] :: #2F7F6F +``` +This block is required for all user based requests to work. + +Requires to not be logged in. +- When logged in on the Game Jolt website and the game is played on Game Jolt, the user is logged in automatically. + +--- +```scratch +Login automatically :: #2F7F6F +``` +Does automatic login after logout. + +Requires to not be logged in. +- Requires to be logged in on the Game Jolt website and for the game to be played on Game Jolt. + +--- +```scratch +Autologin available? :: #2F7F6F +``` +Checks to see if the user is logged in on the Game Jolt website and the game is played on Game Jolt. + +--- +```scratch +Logout :: #2F7F6F +``` +Logs out the user, the game session is then closed. + +Requires to be logged in. + +--- +```scratch + +``` +Checks to see if the user is logged in. + +--- +```scratch +Logged in user's username :: #2F7F6F +``` +Returns the logged in user's username. + +Requires to be logged in. + +--- +```scratch +Fetch user's [username] by [username v] :: #2F7F6F +``` +Fetches user data based on the user's username or the ID + +--- +```scratch +Fetch logged in user :: #2F7F6F +``` +Fetches logged in user data. + +Requires to be logged in. + +--- +```scratch +(Fetched user's [ID v] :: #2F7F6F) +``` +Returns fetched user's data by passed key. + +--- +```scratch +(Fetched user's data in JSON :: #2F7F6F) +``` +Returns fetched user's data in JSON. + +--- +```scratch +Fetch user's friend IDs :: #2F7F6F +``` +Fetches user's friend IDs. + +Requires to be logged in. + +--- +```scratch +(Fetched user's friend ID at index (0) :: #2F7F6F) +``` +Returns fetched user's friend ID at passed index. + +--- +```scratch +(Fetched user's friend IDs in JSON :: #2F7F6F) +``` +Returns fetched user's friend IDs in JSON. +### Trophy Blocks +Achieve, remove and fetch trophies. + +```scratch +Achieve trophy of ID (0) :: #2F7F6F +``` +Achieves trophy of passed ID. + +Requires to be logged in and for the trophy to not be achieved. + +--- +```scratch +Remove trophy of ID (0) :: #2F7F6F +``` +Removes trophy of passed ID. + +Requires to be logged in and for the trophy to be achieved. + +--- +```scratch +Fetch trophy of ID (0) :: #2F7F6F +``` +Fetches trophy of passed ID. + +Requires to be logged in. + +--- +```scratch +Fetch [all v] trophies :: #2F7F6F +``` +Fetches game trophies: +- All - fetches all trophies. +- All achieved - fetches all achieved trophies. +- All unachieved - fetches all unachieved trophies. + +Requires to be logged in. + +--- +```scratch +(Fetched trophy [ID v] at index (0) :: #2F7F6F) +``` +Returns fetched trophy data at passed index by passed key. + +--- +```scratch +(Fetched trophies in JSON :: #2F7F6F) +``` +Returns fetched trophy data in JSON +### Score Blocks +```scratch +Add score (1) in table of ID (0) with text [1 point] and comment [optional] :: #2F7F6F +``` +Adds a score in table of an ID with a text and an optional comment. +- Score, table ID, text and optional comment are passed. + +Requires to be logged in. + +--- +```scratch +Add [guest] score (1) in table of ID (0) with text [1 point] and comment [optional] :: #2F7F6F +``` +Adds a score in table of an ID with text and optional comment for the a guest. +- Score, table ID, text, optional comment and guest's username are passed. + +--- +```scratch +Fetch (1) [global v] score/s in table of ID (0) :: #2F7F6F +``` +Fetches global/user scores in table of an ID. +- Limit, global/user option and table ID are passed. + +Requires to be logged in. + +--- +```scratch +Fetch (1) [global v] score/s [better v] than (1) in table of ID (0) :: #2F7F6F +``` +Fetches global/user scores better/worse than a value in table of an ID. +- Limit, global/user option, better/worse option, a value and table ID are passed. + +Requires to be logged in. + +--- +```scratch +Fetch (1) [guest] score/s in table of ID (0) :: #2F7F6F +``` +Fetches guest's scores in table of an ID. +- Limit, guest's username and table ID are passed. + +--- +```scratch +Fetch (1) [guest] score/s [better v] than (1) in table of ID (0) :: #2F7F6F +``` +Fetched quest's scores better/worse than a value in table of an ID. +- Limit, guest's username, better/worse option, a value and a table ID are passed. + +--- +```scratch +(Fetched score [value v] at index (0) :: #2F7F6F) +``` +Returns fetched score data at passed index by passed key. + +--- +```scratch +(Fetched score data in JSON :: #2F7F6F) +``` +Returns fetched score data in JSON. + +--- +```scratch +(Fetched rank of (1) in table of ID (0) :: #2F7F6F) +``` +Fetches and returns a rank of passed value in table of passed ID. + +--- +```scratch +Fetch score tables :: #2F7F6F +``` +Fetches score tables. + +--- +```scratch +(Fetched table [ID v] at index (0) :: #2F7F6F) +``` +Returns fetched table data at passed index by passed key. + +--- +```scratch +(Fetched tables in JSON :: #2F7F6F) +``` +Returns fetched tables in JSON. +### Data Storage Blocks +Operate on Game Jolt's cloud variables. + +```scratch +Set [global v] data at [key] to [data] :: #2F7F6F +``` +Sets global/user data at passed key to passed data. + +User option requires to be logged in. + +--- +```scratch +(Fetched [global v] data at [key] :: #2F7F6F) +``` +Fetches and returns global/user data at passed key. + +User option requires to be logged in. + +--- +```scratch +Update [global v] data at [key] by [adding v](1) :: #2F7F6F +``` +Updates global/user data at key by operation with value. +- Global/user option, key, operation and value are passed. + +User option requires to be logged in. + +--- +```scratch +Remove [global v] data at [key] :: #2F7F6F +``` +Removes global/user data at passed key. + +User option requires to be logged in. + +--- +```scratch +Fetch all [global v] keys :: #2F7F6F +``` +Fetches all global/user keys. + +User option requires to be logged in. + +--- +```scratch +Fetch [global v] keys matching with [*] :: #2F7F6F +``` +Fetches global/user keys matching with passed pattern. +- Examples: + - A pattern of `*` matches all keys. + - A pattern of `key*` matches all keys with `key` at the start. + - A pattern of `*key` matches all keys with `key` at the end. + - A pattern of `*key*` matches all keys containing `key`. + +User option requires to be logged in. + +--- +```scratch +(Fetched key at index (0) :: #2F7F6F) +``` +Returns fetched key at passed index. + +--- +```scratch +(Fetched keys in JSON :: #2F7F6F) +``` +Returns fetched keys in JSON. +### Time Blocks +Track server's time. + +```scratch +Fetch server's time :: #2F7F6F +``` +Fetches server's time. + +--- +```scratch +(Fetched server's [timestamp v] :: #2F7F6F) +``` +Returns fetched server's time data by passed key. + +--- +```scratch +(Fetched server's time in JSON :: #2F7F6F) +``` +Returns fetched server's time data in JSON. +### Batch Blocks +Fetch more data per request. + +```scratch +Add [data-store/set] request with [{"key":"key", "data":"data"}] to batch :: #2F7F6F +``` +Adds passed arguments to the batch. +- The batch is an array of sub requests consisting of the namespace and the parameters object. + +--- +```scratch +Clear batch :: #2F7F6F +``` +Clears the batch of all sub requests. + +--- +```scratch +(Batch in JSON :: #2F7F6F) +``` +Returns the batch in JSON. + +--- +```scratch +Fetch batch [sequentially v] :: #2F7F6F +``` +Fetches the batch. +- After the fetch the batch is not cleared. + +You can call the batch request in different ways: +- Sequentially - all sub requests are processed in sequence. +- Sequentially, break on error - all sub requests are processed in sequence, if an error in one of them occurs, the whole request will fail. +- In parallel - all sub requests are processed in parallel, this is the fastest way but the results may vary depending on which request finished first. + +User based sub requests require to be logged in. + +--- +```scratch +(Fetched batch data in JSON :: #2F7F6F) +``` +Returns fetched batch data in JSON. + +### Debug Blocks +Blocks used for debugging. + +```scratch +Turn debug mode [off v] :: #2F7F6F +``` +Turns debug mode on/off. +- When debug mode is off, instead of errors, reporters return an empty string and booleans return false. + +--- +```scratch + +``` +Checks to see if debug mode is on. + +--- +```scratch +(Last API error :: #2F7F6F) +``` +Returns the last API error. + +### Handling Common Errors +Handling commonly encountered errors. + +```scratch +// Error: The game ID you passed in does not point to a valid game. +``` +This error occurs when the game ID you set is invalid. +#### Handling +This error can be avoided by using this block: +```scratch +Set game ID to [0] and private key to [private key] :: #2F7F6F +``` +- Make sure the value matches your game's ID. + +--- +```scratch +// Error: The signature you entered for the request is invalid. +``` +This error occurs when the private key you set is invalid. +#### Handling +This error can be avoided by using this block: +```scratch +Set game ID to [0] and private key to [private key] :: #2F7F6F +``` +- Make sure the value matches your game's private key. + +--- +```scratch +// Error: No user logged in. +``` +This error occurs when no user is logged in. +- The most common cause is that the extension failed to recognize the user. +#### Handling +This error can be avoided with a manual login option. +```scratch +when flag clicked +if > then +ask [login or continue as guest?] and wait +if <(answer) = [login]> then +ask [enter your username] and wait +set [username v] to (answer) +ask [enter your private game token] and wait +set [private game token v] to (answer) +Login with (username :: variables) and (private game token) :: #2F7F6F +end +end +``` + +--- +```scratch +// Error: No such user with the credentials passed in could be found. +``` +This error occurs when manual login failed to recognize the user credentials you passed in. +- It can also occur with autologin when no user is recognized by the extension. +#### Handling +This error can be avoided by modifying the previous example to try again after a failed login attempt. +```scratch +when flag clicked +if > then +ask [login or continue as guest?] and wait +if <(answer) = [login]> then +repeat until +ask [enter your username] and wait +set [username v] to (answer) +ask [enter your private game token] and wait +set [private game token v] to (answer) +Login with (username :: variables) and (private game token) :: #2F7F6F +end +end +end +``` + +--- +```scratch +// Error: Data not found. +// Error: Data at such index not found. +``` +These errors occur when you are trying to access non-existent data. +- Make sure you have previously fetched the data you are trying to access. +- Make sure you have the right index as indexing starts at 0 instead of 1. diff --git a/extensions/gamejolt.js b/extensions/gamejolt.js index d0f689df7d..418e938cc6 100644 --- a/extensions/gamejolt.js +++ b/extensions/gamejolt.js @@ -1,2228 +1,2996 @@ -// Name: GameJolt -// ID: GameJoltAPI -// Description: Blocks that allow games to interact with the GameJolt API. Unofficial. -// By: softed - -((Scratch) => { - "use strict"; - - const md5 = (() => { - /*! - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - - var hexcase = 0; - function hex_md5(a) { - return rstr2hex(rstr_md5(str2rstr_utf8(a))); - } - function hex_hmac_md5(a, b) { - return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a), str2rstr_utf8(b))); - } - function md5_vm_test() { - return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; - } - function rstr_md5(a) { - return binl2rstr(binl_md5(rstr2binl(a), a.length * 8)); - } - function rstr_hmac_md5(c, f) { - var e = rstr2binl(c); - if (e.length > 16) { - e = binl_md5(e, c.length * 8); - } - var a = Array(16), - d = Array(16); - for (var b = 0; b < 16; b++) { - a[b] = e[b] ^ 909522486; - d[b] = e[b] ^ 1549556828; - } - var g = binl_md5(a.concat(rstr2binl(f)), 512 + f.length * 8); - return binl2rstr(binl_md5(d.concat(g), 512 + 128)); - } - function rstr2hex(c) { - try { - hexcase; - } catch (g) { - hexcase = 0; - } - var f = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var b = ""; - var a; - for (var d = 0; d < c.length; d++) { - a = c.charCodeAt(d); - b += f.charAt((a >>> 4) & 15) + f.charAt(a & 15); - } - return b; - } - function str2rstr_utf8(c) { - var b = ""; - var d = -1; - var a, e; - while (++d < c.length) { - a = c.charCodeAt(d); - e = d + 1 < c.length ? c.charCodeAt(d + 1) : 0; - if (55296 <= a && a <= 56319 && 56320 <= e && e <= 57343) { - a = 65536 + ((a & 1023) << 10) + (e & 1023); - d++; - } - if (a <= 127) { - b += String.fromCharCode(a); - } else { - if (a <= 2047) { - b += String.fromCharCode(192 | ((a >>> 6) & 31), 128 | (a & 63)); - } else { - if (a <= 65535) { - b += String.fromCharCode( - 224 | ((a >>> 12) & 15), - 128 | ((a >>> 6) & 63), - 128 | (a & 63) - ); - } else { - if (a <= 2097151) { - b += String.fromCharCode( - 240 | ((a >>> 18) & 7), - 128 | ((a >>> 12) & 63), - 128 | ((a >>> 6) & 63), - 128 | (a & 63) - ); - } - } - } - } - } - return b; - } - function rstr2binl(b) { - var a = Array(b.length >> 2); - for (var c = 0; c < a.length; c++) { - a[c] = 0; - } - // eslint-disable-next-line no-redeclare - for (var c = 0; c < b.length * 8; c += 8) { - a[c >> 5] |= (b.charCodeAt(c / 8) & 255) << c % 32; - } - return a; - } - function binl2rstr(b) { - var a = ""; - for (var c = 0; c < b.length * 32; c += 8) { - a += String.fromCharCode((b[c >> 5] >>> c % 32) & 255); - } - return a; - } - function binl_md5(p, k) { - p[k >> 5] |= 128 << k % 32; - p[(((k + 64) >>> 9) << 4) + 14] = k; - var o = 1732584193; - var n = -271733879; - var m = -1732584194; - var l = 271733878; - for (var g = 0; g < p.length; g += 16) { - var j = o; - var h = n; - var f = m; - var e = l; - o = md5_ff(o, n, m, l, p[g + 0], 7, -680876936); - l = md5_ff(l, o, n, m, p[g + 1], 12, -389564586); - m = md5_ff(m, l, o, n, p[g + 2], 17, 606105819); - n = md5_ff(n, m, l, o, p[g + 3], 22, -1044525330); - o = md5_ff(o, n, m, l, p[g + 4], 7, -176418897); - l = md5_ff(l, o, n, m, p[g + 5], 12, 1200080426); - m = md5_ff(m, l, o, n, p[g + 6], 17, -1473231341); - n = md5_ff(n, m, l, o, p[g + 7], 22, -45705983); - o = md5_ff(o, n, m, l, p[g + 8], 7, 1770035416); - l = md5_ff(l, o, n, m, p[g + 9], 12, -1958414417); - m = md5_ff(m, l, o, n, p[g + 10], 17, -42063); - n = md5_ff(n, m, l, o, p[g + 11], 22, -1990404162); - o = md5_ff(o, n, m, l, p[g + 12], 7, 1804603682); - l = md5_ff(l, o, n, m, p[g + 13], 12, -40341101); - m = md5_ff(m, l, o, n, p[g + 14], 17, -1502002290); - n = md5_ff(n, m, l, o, p[g + 15], 22, 1236535329); - o = md5_gg(o, n, m, l, p[g + 1], 5, -165796510); - l = md5_gg(l, o, n, m, p[g + 6], 9, -1069501632); - m = md5_gg(m, l, o, n, p[g + 11], 14, 643717713); - n = md5_gg(n, m, l, o, p[g + 0], 20, -373897302); - o = md5_gg(o, n, m, l, p[g + 5], 5, -701558691); - l = md5_gg(l, o, n, m, p[g + 10], 9, 38016083); - m = md5_gg(m, l, o, n, p[g + 15], 14, -660478335); - n = md5_gg(n, m, l, o, p[g + 4], 20, -405537848); - o = md5_gg(o, n, m, l, p[g + 9], 5, 568446438); - l = md5_gg(l, o, n, m, p[g + 14], 9, -1019803690); - m = md5_gg(m, l, o, n, p[g + 3], 14, -187363961); - n = md5_gg(n, m, l, o, p[g + 8], 20, 1163531501); - o = md5_gg(o, n, m, l, p[g + 13], 5, -1444681467); - l = md5_gg(l, o, n, m, p[g + 2], 9, -51403784); - m = md5_gg(m, l, o, n, p[g + 7], 14, 1735328473); - n = md5_gg(n, m, l, o, p[g + 12], 20, -1926607734); - o = md5_hh(o, n, m, l, p[g + 5], 4, -378558); - l = md5_hh(l, o, n, m, p[g + 8], 11, -2022574463); - m = md5_hh(m, l, o, n, p[g + 11], 16, 1839030562); - n = md5_hh(n, m, l, o, p[g + 14], 23, -35309556); - o = md5_hh(o, n, m, l, p[g + 1], 4, -1530992060); - l = md5_hh(l, o, n, m, p[g + 4], 11, 1272893353); - m = md5_hh(m, l, o, n, p[g + 7], 16, -155497632); - n = md5_hh(n, m, l, o, p[g + 10], 23, -1094730640); - o = md5_hh(o, n, m, l, p[g + 13], 4, 681279174); - l = md5_hh(l, o, n, m, p[g + 0], 11, -358537222); - m = md5_hh(m, l, o, n, p[g + 3], 16, -722521979); - n = md5_hh(n, m, l, o, p[g + 6], 23, 76029189); - o = md5_hh(o, n, m, l, p[g + 9], 4, -640364487); - l = md5_hh(l, o, n, m, p[g + 12], 11, -421815835); - m = md5_hh(m, l, o, n, p[g + 15], 16, 530742520); - n = md5_hh(n, m, l, o, p[g + 2], 23, -995338651); - o = md5_ii(o, n, m, l, p[g + 0], 6, -198630844); - l = md5_ii(l, o, n, m, p[g + 7], 10, 1126891415); - m = md5_ii(m, l, o, n, p[g + 14], 15, -1416354905); - n = md5_ii(n, m, l, o, p[g + 5], 21, -57434055); - o = md5_ii(o, n, m, l, p[g + 12], 6, 1700485571); - l = md5_ii(l, o, n, m, p[g + 3], 10, -1894986606); - m = md5_ii(m, l, o, n, p[g + 10], 15, -1051523); - n = md5_ii(n, m, l, o, p[g + 1], 21, -2054922799); - o = md5_ii(o, n, m, l, p[g + 8], 6, 1873313359); - l = md5_ii(l, o, n, m, p[g + 15], 10, -30611744); - m = md5_ii(m, l, o, n, p[g + 6], 15, -1560198380); - n = md5_ii(n, m, l, o, p[g + 13], 21, 1309151649); - o = md5_ii(o, n, m, l, p[g + 4], 6, -145523070); - l = md5_ii(l, o, n, m, p[g + 11], 10, -1120210379); - m = md5_ii(m, l, o, n, p[g + 2], 15, 718787259); - n = md5_ii(n, m, l, o, p[g + 9], 21, -343485551); - o = safe_add(o, j); - n = safe_add(n, h); - m = safe_add(m, f); - l = safe_add(l, e); - } - return Array(o, n, m, l); - } - function md5_cmn(h, e, d, c, g, f) { - return safe_add(bit_rol(safe_add(safe_add(e, h), safe_add(c, f)), g), d); - } - function md5_ff(g, f, k, j, e, i, h) { - return md5_cmn((f & k) | (~f & j), g, f, e, i, h); - } - function md5_gg(g, f, k, j, e, i, h) { - return md5_cmn((f & j) | (k & ~j), g, f, e, i, h); - } - function md5_hh(g, f, k, j, e, i, h) { - return md5_cmn(f ^ k ^ j, g, f, e, i, h); - } - function md5_ii(g, f, k, j, e, i, h) { - return md5_cmn(k ^ (f | ~j), g, f, e, i, h); - } - function safe_add(a, d) { - var c = (a & 65535) + (d & 65535); - var b = (a >> 16) + (d >> 16) + (c >> 16); - return (b << 16) | (c & 65535); - } - function bit_rol(a, b) { - return (a << b) | (a >>> (32 - b)); - } - - return hex_md5; - })(); - - const GameJolt = (() => { - /*! - * This is a modified version of https://github.com/MausGames/game-jolt-api-js-library (Public Domain) - */ - var GJAPI = {}; - - GJAPI.err = { - noLogin: "No user logged in.", - login: "User already logged in.", - noFetch: "Fetch request not supported.", - - /** - * @param {string} code - */ - get: (code) => { - return { - success: false, - message: GJAPI.err[code] || code, - }; - }, - }; - - GJAPI.iGameID = 0; - GJAPI.sGameKey = ""; - GJAPI.bAutoLogin = true; - - GJAPI.sAPI = "https://api.gamejolt.com/api/game/v1_2"; - GJAPI.sLogName = "[Game Jolt API]"; - GJAPI.iLogStack = 20; - - GJAPI.asQueryParam = (() => { - var asOutput = {}; - var asList = window.location.search.substring(1).split("&"); - - // loop through all parameters - for (var i = 0; i < asList.length; ++i) { - // separate key from value - var asPair = asList[i].split("="); - - // insert value into map - if (typeof asOutput[asPair[0]] === "undefined") - asOutput[asPair[0]] = asPair[1]; // create new entry - else if (typeof asOutput[asPair[0]] === "string") - asOutput[asPair[0]] = [asOutput[asPair[0]], asPair[1]]; - // extend into array - else asOutput[asPair[0]].push(asPair[1]); // append to array - } - - return asOutput; - })(); - - GJAPI.bOnGJ = window.location.hostname.match(/gamejolt/) ? true : false; - - /** - * Log message and stack trace - * @param {string} sMessage - */ - GJAPI.LogTrace = (sMessage) => { - // prevent flooding - if (!GJAPI.iLogStack) return; - if (!--GJAPI.iLogStack) sMessage = "(╯°□°)╯︵ ┻━┻"; - - console.warn(GJAPI.sLogName + " " + sMessage); - console.trace(); - }; - - // ************** - // Main functions - GJAPI.SEND_FOR_USER = true; - GJAPI.SEND_GENERAL = false; - - /** - * @param {string} sURL - * @param {boolean} bSendUser - * @param {function} pCallback - */ - GJAPI.SendRequest = (sURL, bSendUser, pCallback) => { - // forward call to extended function - GJAPI.SendRequestEx(sURL, bSendUser, "json", "", pCallback); - }; - - /** - * @param {string} sURL - * @param {boolean} bSendUser - * @param {string} sFormat - * @param {string} sBodyData - * @param {function} pCallback - */ - GJAPI.SendRequestEx = (sURL, bSendUser, sFormat, sBodyData, pCallback) => { - // add main URL, game ID and format type - sURL = - GJAPI.sAPI + - encodeURI(sURL) + - (sURL.indexOf("/?") === -1 ? "?" : "&") + - "game_id=" + - GJAPI.iGameID + - "&format=" + - sFormat; - - // add credentials of current user (for user-related operations) - if (GJAPI.bLoggedIn && bSendUser) - sURL += - "&username=" + GJAPI.sUserName + "&user_token=" + GJAPI.sUserToken; - - // generate MD5 signature - sURL += "&signature=" + md5(sURL + GJAPI.sGameKey); - - // send off the request - __CreateAjax(sURL, sBodyData, (sResponse) => { - console.info(GJAPI.sLogName + " <" + sURL + "> " + sResponse); - if (sResponse === "" || typeof pCallback !== "function") return; - - switch (sFormat) { - case "json": - pCallback(JSON.parse(sResponse).response); - break; - - case "dump": - var iLineBreakIndex = sResponse.indexOf("\n"); - var sResult = sResponse.substr(0, iLineBreakIndex - 1); - var sData = sResponse.substr(iLineBreakIndex + 1); - - pCallback({ - success: sResult === "SUCCESS", - data: sData, - }); - break; - - default: - pCallback(sResponse); - } - }); - }; - - // automatically retrieve and log in current user on Game Jolt - GJAPI.bLoggedIn = - GJAPI.bAutoLogin && - GJAPI.asQueryParam["gjapi_username"] && - GJAPI.asQueryParam["gjapi_token"] - ? true - : false; - GJAPI.sUserName = GJAPI.bLoggedIn - ? GJAPI.asQueryParam["gjapi_username"] - : ""; - GJAPI.sUserToken = GJAPI.bLoggedIn ? GJAPI.asQueryParam["gjapi_token"] : ""; - - // send some information to the console - console.info(GJAPI.asQueryParam); - console.info( - GJAPI.sLogName + - (GJAPI.bOnGJ ? " E" : " Not e") + - "mbedded on Game Jolt <" + - window.location.origin + - window.location.pathname + - ">" - ); - console.info( - GJAPI.sLogName + - (GJAPI.bLoggedIn ? " U" : " No u") + - "ser recognized <" + - GJAPI.sUserName + - ">" - ); - if (!window.location.hostname) - console.warn( - GJAPI.sLogName + - " XMLHttpRequest may not work properly on a local environment" - ); - - // ***************** - // Session functions - GJAPI.bSessionActive = true; - - /** - * @param {function} pCallback - */ - GJAPI.SessionOpen = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("SessionOpen() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - // check for already open session - if (GJAPI.iSessionHandle) { - pCallback(); - return; - } - - // send open-session request - GJAPI.SendRequest("/sessions/open/", GJAPI.SEND_FOR_USER, (pResponse) => { - // check for success - if (pResponse.success == "true") { - // add automatic session ping and close - GJAPI.iSessionHandle = window.setInterval(GJAPI.SessionPing, 30000); - window.addEventListener("beforeunload", GJAPI.SessionClose, false); - } - - pCallback(pResponse); - }); - }; - - /** - * Send ping-session request - * @param {function} pCallback - */ - GJAPI.SessionPing = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("SessionPing() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - GJAPI.SendRequest( - "/sessions/ping/?status=" + (GJAPI.bSessionActive ? "active" : "idle"), - GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - /** - * Send close-session request - * @param {function} pCallback - */ - GJAPI.SessionClose = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("SessionClose() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - if (GJAPI.iSessionHandle) { - // remove automatic session ping and close - window.clearInterval(GJAPI.iSessionHandle); - window.removeEventListener("beforeunload", GJAPI.SessionClose); - - GJAPI.iSessionHandle = 0; - } - - GJAPI.SendRequest("/sessions/close/", GJAPI.SEND_FOR_USER, pCallback); - }; - - // automatically start player session - if (GJAPI.bLoggedIn) GJAPI.SessionOpen(); - - // ************** - // User functions - - /** - * Send authentification request - * @param {string} sUserName - * @param {string} sUserToken - * @param {function} pCallback - */ - GJAPI.UserLoginManual = (sUserName, sUserToken, pCallback) => { - if (GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "UserLoginManual(" + - sUserName + - ", " + - sUserToken + - ") failed: user " + - GJAPI.sUserName + - " already logged in" - ); - pCallback(GJAPI.err.get("login")); - return; - } - - GJAPI.SendRequest( - "/users/auth/" + "?username=" + sUserName + "&user_token=" + sUserToken, - GJAPI.SEND_GENERAL, - (pResponse) => { - // check for success - if (pResponse.success == "true") { - // save login properties - GJAPI.bLoggedIn = true; - GJAPI.sUserName = sUserName; - GJAPI.sUserToken = sUserToken; - - // open session - GJAPI.SessionOpen(); - } - - // execute nested callback - if (typeof pCallback === "function") pCallback(pResponse); - } - ); - }; - - /** - * @param {function} pCallback - */ - GJAPI.UserLogout = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("UserLogout() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - // close session - GJAPI.SessionClose(); - - // reset login properties - GJAPI.bLoggedIn = false; - GJAPI.sUserName = ""; - GJAPI.sUserToken = ""; - - // reset trophy cache - GJAPI.abTrophyCache = {}; - pCallback({ success: true }); - }; - - /** - * Send fetch-user request - * @param {number} iUserID - * @param {function} pCallback - */ - GJAPI.UserFetchID = (iUserID, pCallback) => { - GJAPI.SendRequest( - "/users/?user_id=" + iUserID, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * Send fetch-ser request - * @param {string} sUserName - * @param {function} pCallback - */ - GJAPI.UserFetchName = (sUserName, pCallback) => { - GJAPI.SendRequest( - "/users/?username=" + sUserName, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * Send fetch-user request - * @param {function} pCallback - */ - GJAPI.UserFetchCurrent = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("UserFetchCurrent() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - GJAPI.UserFetchName(GJAPI.sUserName, pCallback); - }; - - // **************** - // Trophy functions - GJAPI.abTrophyCache = {}; - - GJAPI.TROPHY_ONLY_ACHIEVED = 1; - GJAPI.TROPHY_ONLY_NOTACHIEVED = -1; - GJAPI.TROPHY_ALL = 0; - - /** - * Send achieve-trophy request - * @param {number} iTrophyID - * @param {function} pCallback - */ - GJAPI.TrophyAchieve = (iTrophyID, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "TrophyAchieve(" + iTrophyID + ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - // check for already achieved trophy - if (GJAPI.abTrophyCache[iTrophyID]) { - pCallback(GJAPI.err.get("Trophy already achieved.")); - return; - } - - GJAPI.SendRequest( - "/trophies/add-achieved/?trophy_id=" + iTrophyID, - GJAPI.SEND_FOR_USER, - function (pResponse) { - // check for success - if (pResponse.success == "true") { - // save status - GJAPI.abTrophyCache[iTrophyID] = true; - } - - // execute nested callback - if (typeof pCallback === "function") pCallback(pResponse); - } - ); - }; - - /** - * Send fetch-trophy request - * @param {number} iAchieved - * @param {function} pCallback - */ - GJAPI.TrophyFetch = (iAchieved, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "TrophyFetch(" + iAchieved + ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - // only trophies with the requested status - var sTrophyData = - iAchieved === GJAPI.TROPHY_ALL - ? "" - : "?achieved=" + - (iAchieved >= GJAPI.TROPHY_ONLY_ACHIEVED ? "true" : "false"); - - GJAPI.SendRequest( - "/trophies/" + sTrophyData, - GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - /** - * Send fetch-trophy request - * @param {number} iTrophyID - * @param {function} pCallback - */ - GJAPI.TrophyFetchSingle = (iTrophyID, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "TrophyFetchSingle(" + iTrophyID + ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - GJAPI.SendRequest( - "/trophies/?trophy_id=" + iTrophyID, - GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - // *************** - // Score functions - GJAPI.SCORE_ONLY_USER = true; - GJAPI.SCORE_ALL = false; - - /** - * Send add-score request - * @param {number} iScoreTableID - * @param {number} iScoreValue - * @param {string} sScoreText - * @param {string} sExtraData - * @param {function} pCallback - */ - GJAPI.ScoreAdd = ( - iScoreTableID, - iScoreValue, - sScoreText, - sExtraData, - pCallback - ) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "ScoreAdd(" + - iScoreTableID + - ", " + - iScoreValue + - ", " + - sScoreText + - ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - GJAPI.ScoreAddGuest( - iScoreTableID, - iScoreValue, - sScoreText, - "", - sExtraData, - pCallback - ); - }; - - /** - * Send add-score request - * @param {number} iScoreTableID - * @param {number} iScoreValue - * @param {string} sScoreText - * @param {string} sGuestName - * @param {string} sExtraData - * @param {function} pCallback - */ - GJAPI.ScoreAddGuest = ( - iScoreTableID, - iScoreValue, - sScoreText, - sGuestName, - sExtraData, - pCallback - ) => { - // use current user data or guest name - var bIsGuest = sGuestName && sGuestName.length ? true : false; - - GJAPI.SendRequest( - "/scores/add/?sort=" + - iScoreValue + - "&score=" + - sScoreText + - (bIsGuest ? "&guest=" + sGuestName : "") + - (iScoreTableID ? "&table_id=" + iScoreTableID : "") + - (sExtraData ? "&extra_data=" + sExtraData : ""), - bIsGuest ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - /** - * Send fetch-score request - * @param {number} iScoreTableID - * @param {boolean} bOnlyUser - * @param {number} iLimit - * @param {function} pCallback - */ - GJAPI.ScoreFetch = (iScoreTableID, bOnlyUser, iLimit, pCallback) => { - if (!GJAPI.bLoggedIn && bOnlyUser) { - GJAPI.LogTrace( - "ScoreFetch(" + - iScoreTableID + - ", " + - bOnlyUser + - ", " + - iLimit + - ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - - // only scores from the current user or all scores - var bFetchAll = bOnlyUser === GJAPI.SCORE_ONLY_USER ? false : true; - - GJAPI.SendRequest( - "/scores/?limit=" + - iLimit + - (iScoreTableID ? "&table_id=" + iScoreTableID : ""), - bFetchAll ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - // ******************** - // Data store functions - GJAPI.DATA_STORE_USER = true; - GJAPI.DATA_STORE_GLOBAL = false; - - /** - * Send set-data request - * @param {number} iStore - * @param {string} sKey - * @param {string} sData - * @param {function} pCallback - */ - GJAPI.DataStoreSet = (iStore, sKey, sData, pCallback) => { - GJAPI.SendRequest( - "/data-store/set/?key=" + sKey + "&data=" + sData, - iStore, - pCallback - ); - }; - - /** - * Send fetch-data request - * @param {number} iStore - * @param {string} sKey - * @param {function} pCallback - */ - GJAPI.DataStoreFetch = (iStore, sKey, pCallback) => { - GJAPI.SendRequest("/data-store/?key=" + sKey, iStore, pCallback); - }; - - /** - * Send update-data request - * @param {number} iStore - * @param {string} sKey - * @param {string} sOperation - * @param {string} sValue - * @param {function} pCallback - */ - GJAPI.DataStoreUpdate = (iStore, sKey, sOperation, sValue, pCallback) => { - GJAPI.SendRequest( - "/data-store/update/?key=" + - sKey + - "&operation=" + - sOperation + - "&value=" + - sValue, - iStore, - pCallback - ); - }; - - /** - * Send remove-data request - * @param {number} iStore - * @param {string} sKey - * @param {function} pCallback - */ - GJAPI.DataStoreRemove = (iStore, sKey, pCallback) => { - // send remove-data request - GJAPI.SendRequest("/data-store/remove/?key=" + sKey, iStore, pCallback); - }; - - /** - * Send get-keys request - * @param {number} iStore - * @param {function} pCallback - */ - GJAPI.DataStoreGetKeys = (iStore, pCallback) => { - GJAPI.SendRequest("/data-store/get-keys/", iStore, pCallback); - }; - - /** - * Create asynchronous request - * @param {string} sUrl - * @param {string} sBodyData - * @param {function} pCallback - */ - function __CreateAjax(sUrl, sBodyData, pCallback) { - if (typeof sBodyData !== "string") sBodyData = ""; - - Scratch.canFetch(sUrl).then((allowed) => { - if (!allowed) { - pCallback(GJAPI.err.get("noFetch")); - return; - } - - // canFetch() checked above - // eslint-disable-next-line no-restricted-syntax - var pRequest = new XMLHttpRequest(); - - // bind callback function - pRequest.onreadystatechange = () => { - if (pRequest.readyState === 4) pCallback(pRequest.responseText); - }; - - // send off the request - if (sBodyData !== "") { - pRequest.open("POST", sUrl); - pRequest.setRequestHeader( - "Content-Type", - "application/x-www-form-urlencoded" - ); - pRequest.send(sBodyData); - } else { - pRequest.open("GET", sUrl); - pRequest.send(); - } - }); - } - - GJAPI.BETTER_THAN = true; - GJAPI.WORSE_THAN = false; - GJAPI.FETCH_USERNAME = true; - GJAPI.FETCH_ID = false; - GJAPI.FETCH_ALL = true; - GJAPI.FETCH_SINGLE = false; - - /** - * @param {function} pCallback - */ - GJAPI.TimeFetch = (pCallback) => { - GJAPI.SendRequest( - "/time/?game_id=" + GJAPI.iGameID, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - /** - * @param {function} pCallback - */ - GJAPI.FriendsFetch = (pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace("FriendsFetch() failed: no user logged in"); - pCallback(GJAPI.err.get("noLogin")); - return; - } - GJAPI.SendRequest( - "/friends/?game_id=" + - GJAPI.iGameID + - "&username=" + - GJAPI.sUserName + - "&user_token=" + - GJAPI.sUserToken, - GJAPI.SEND_FOR_USER, - pCallback - ); - }; - /** - * @param {number} iTrophyID - * @param {function} pCallback - */ - GJAPI.TrophyRemove = (iTrophyID, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "TrophyRemove(" + iTrophyID + ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - // Check if the trophy is not achieved - if (!GJAPI.abTrophyCache[iTrophyID]) { - pCallback(GJAPI.err.get("Trophy already achieved.")); - return; - } - GJAPI.SendRequest( - "/trophies/remove-achieved/?game_id=" + - GJAPI.iGameID + - "&username=" + - GJAPI.sUserName + - "&user_token=" + - GJAPI.sUserToken + - "&trophy_id=" + - iTrophyID, - GJAPI.SEND_FOR_USER, - (pResponse) => { - // Update trophy status if the response succeded - if (pResponse.success == "true") { - GJAPI.abTrophyCache[iTrophyID] = false; - } - if (typeof pCallback == "function") { - pCallback(pResponse); - } - } - ); - }; - - /** - * @param {number} iScoreTableID - * @param {number} iScoreValue - * @param {function} pCallback - */ - GJAPI.ScoreGetRank = (iScoreTableID, iScoreValue, pCallback) => { - GJAPI.SendRequest( - "/scores/get-rank/?game_id=" + - GJAPI.iGameID + - "&sort=" + - iScoreValue + - "&table_id=" + - iScoreTableID, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * @param {function} pCallback - */ - GJAPI.ScoreGetTables = (pCallback) => { - GJAPI.SendRequest( - "/scores/tables/?game_id=" + GJAPI.iGameID, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * @param {function} pCallback - */ - GJAPI.SessionCheck = (pCallback) => { - GJAPI.SendRequest( - "/sessions/check/?game_id=" + - GJAPI.iGameID + - "&username=" + - GJAPI.sUserName + - "&user_token=" + - GJAPI.sUserToken, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * SessionOpen and SessionClose combined - * @param {boolean} bIsOpen - * @param {function} pCallback - */ - GJAPI.SessionSetStatus = (bIsOpen, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "SessionSetStatus(" + bIsOpen + ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - GJAPI.bSessionActive = bIsOpen; - if (bIsOpen) { - if (GJAPI.iSessionHandle) { - pCallback({ success: true }); - return; - } - GJAPI.SendRequest( - "/sessions/open/", - GJAPI.SEND_FOR_USER, - function (pResponse) { - if (pResponse.success == "true") { - GJAPI.iSessionHandle = window.setInterval( - GJAPI.SessionPing, - 30000 - ); - window.addEventListener( - "beforeunload", - GJAPI.SessionClose, - false - ); - } - } - ); - if (typeof pCallback == "function") pCallback({ success: true }); - return; - } - if (GJAPI.iSessionHandle) { - window.clearInterval(GJAPI.iSessionHandle); - window.removeEventListener("beforeunload", GJAPI.SessionClose); - GJAPI.iSessionHandle = 0; - } - GJAPI.SendRequest("/sessions/close/", GJAPI.SEND_FOR_USER, pCallback); - }; - - /** - * UserFetchName and UserFetchID combined - * Use GJAPI.FETCH_USERNAME and GJAPI.FETCH_ID for better code readability - * @param {boolean} bIsUsername - * @param {string} sValue - * @param {function} pCallback - */ - GJAPI.UserFetchComb = (bIsUsername, sValue, pCallback) => { - if (bIsUsername) { - GJAPI.SendRequest( - "/users/?username=" + sValue, - GJAPI.SEND_GENERAL, - pCallback - ); - return; - } - GJAPI.SendRequest( - "/users/?user_id=" + sValue, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * ScoreFetch but with better_than and worse_than parameters - * Use GJAPI.BETTER_THAN and GJAPI.WORSE_THAN for better code readability - * If value is set to 0 it will work like riginal ScoreFetch - * @param {number} iScoreTableID - * @param {boolean} bOnlyUser - * @param {number} iLimit - * @param {boolean} bBetterOrWorse - * @param {number} iValue - * @param {function} pCallback - */ - GJAPI.ScoreFetchEx = ( - iScoreTableID, - bOnlyUser, - iLimit, - bBetterOrWorse, - iValue, - pCallback - ) => { - if (!GJAPI.bLoggedIn && bOnlyUser) { - GJAPI.LogTrace( - "ScoreFetch(" + - iScoreTableID + - ", " + - bOnlyUser + - ", " + - iLimit + - ", " + - bBetterOrWorse + - ", " + - iValue + - ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - var bFetchAll = bOnlyUser == GJAPI.SCORE_ONLY_USER ? false : true; - GJAPI.SendRequest( - "/scores/" + - "?limit=" + - iLimit + - (iScoreTableID ? "&table_id=" + iScoreTableID : "") + - (bBetterOrWorse ? "&better_than=" : "&worse_than=") + - iValue, - bFetchAll ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - /** - * Unused in the extension because of ScoreFetchGuestEx - * @param {number} iScoreTableID - * @param {string} sName - * @param {number} iLimit - * @param {function} pCallback - */ - GJAPI.ScoreFetchGuest = (iScoreTableID, sName, iLimit, pCallback) => { - GJAPI.SendRequest( - "/scores/?limit=" + - iLimit + - (iScoreTableID ? "&table_id=" + iScoreTableID : "") + - "&guest=" + - sName, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * ScoreFetchGuest but with better_than and worse_than parameters - * Use GJAPI.BETTER_THAN and GJAPI.WORSE_THAN for better code readability - * If value is set to 0 it will work like original ScoreFetchGuest - * @param {number} iScoreTableID - * @param {string} sName - * @param {number} iLimit - * @param {boolean} bBetterOrWorse - * @param {number} iValue - * @param {function} pCallback - */ - GJAPI.ScoreFetchGuestEx = ( - iScoreTableID, - sName, - iLimit, - bBetterOrWorse, - iValue, - pCallback - ) => { - GJAPI.SendRequest( - "/scores/?limit=" + - iLimit + - (iScoreTableID ? "&table_id=" + iScoreTableID : "") + - "&guest=" + - sName + - (bBetterOrWorse ? "&better_than=" : "&worse_than=") + - iValue, - GJAPI.SEND_GENERAL, - pCallback - ); - }; - - /** - * TrophyFetch and TrophyFetchSingle combined - * Use GJAPI.FETCH_ALL and GJAPI.FETCH_SINGLE for better code readability - * @param {boolean} bIsAll - * @param {number} iValue - * @param {function} pCallback - */ - GJAPI.TrophyFetchComb = (bIsAll, iValue, pCallback) => { - if (!GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "TrophyFetchComb(" + - bIsAll + - ", " + - iValue + - ") failed: no user logged in" - ); - pCallback(GJAPI.err.get("noLogin")); - return; - } - if (bIsAll) { - var sTrophyData = - iValue === GJAPI.TROPHY_ALL - ? "" - : "?achieved=" + - (iValue >= GJAPI.TROPHY_ONLY_ACHIEVED ? "true" : "false"); - GJAPI.SendRequest( - "/trophies/" + sTrophyData, - GJAPI.SEND_FOR_USER, - pCallback - ); - return; - } - GJAPI.SendRequest( - "/trophies/?trophy_id=" + iValue, - GJAPI.SEND_FOR_USER, - pCallback - ); - }; - - /** - * Modified UserLoginManual to login users automatically if their username and token are detected - * @param {function} pCallback - */ - GJAPI.UserLoginAuto = (pCallback) => { - if (!GJAPI.bOnGJ) { - GJAPI.LogTrace("UserLoginAuto() failed: No username or token detected"); - pCallback(GJAPI.err.get("No username or token detected.")); - return; - } - if (GJAPI.bLoggedIn) { - GJAPI.LogTrace( - "UserLoginAuto() failed: user " + - GJAPI.sUserName + - " already logged in" - ); - pCallback(GJAPI.err.get("login")); - return; - } - GJAPI.SendRequest( - "/users/auth/" + - "?username=" + - GJAPI.asQueryParam["gjapi_username"] + - "&user_token=" + - GJAPI.asQueryParam["gjapi_token"], - GJAPI.SEND_GENERAL, - (pResponse) => { - if (pResponse.success == "true") { - GJAPI.bLoggedIn = true; - GJAPI.sUserName = GJAPI.asQueryParam["gjapi_username"]; - GJAPI.sUserToken = GJAPI.asQueryParam["gjapi_token"]; - GJAPI.SessionOpen(); - } - if (typeof pCallback === "function") { - pCallback(pResponse); - } - } - ); - }; - - /** - * DataStoreGetKeys but with a pattern parameter - * The placeholder character for patterns is * - * @param {number} iStore - * @param {string} sPattern - * @param {function} pCallback - */ - GJAPI.DataStoreGetKeysEx = (iStore, sPattern, pCallback) => { - GJAPI.SendRequest( - "/data-store/get-keys/?pattern=" + sPattern, - iStore, - pCallback - ); - }; - - return GJAPI; - })(); - - /** - * Used for storing API error messages - */ - let err = { - noLogin: "No user logged in.", - noItem: "Item not found.", - noIndex: "Index not found.", - - /** - * Used for returning a standartized error message - * @param {string} code - */ - get: (code) => (err[code] ? "Error: " + err[code] : "Error."), - - /** - * Used for returning a standartized error message - * @param {string} text - */ - show: (text) => "Error: " + text, - }; - - /** - * Used for storing API response objects - */ - let data = {}; - - /** - * Apparently API response object's success property is a string and not a boolean - * That's why there is stuff like 'pResponse.success == bool.f' - */ - const bool = { - t: "true", - f: "false", - }; - - /** - * GameJolt icon by GameJolt - * Other icons by softed - * Can be used outside of this extension - */ - const icons = { - GameJolt: - "data:image/png;base64,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", - main: "data:image/png;base64,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", - user: "data:image/png;base64,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", - trophy: - "data:image/png;base64,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", - score: - "data:image/png;base64,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", - store: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAAAXNSR0IArs4c6QAABVJJREFUeF7t3d1t20AQRWGrIwMpKQXEJaiEpICUFCAdMZBebAT6WcJDz/Lyy6uJ2Z1zz44oKZJOL/4hEETgFNSLVhB4ITQJoggQOipOzRCaA1EECB0Vp2YIzYEoAoSOilMzhOZAFAFCR8WpmWGhl2VZ4EKgi8DpdBpydeiiSxOE7orSuhcChOZBFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIkDoqDg10yb06/kNfQSGCfw9/xy6ltBDmFzUTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k1geqH/vIz9Utz3848hlr/Pv4auU+82ptn5EfqO3oQm9IXA2Dhd8cObJvRtsRy421xMaBP6SiDlgBCa0IR+8KzKLYcD0npATGgCtgpY/aoJoQlNaLcc7wSqJ4x6n3tVx4Q2oU3or5zQQ2/ruQiBlQTafqdw5T5djsAQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQgTahX89vQxt0EQIXAtP/Xw5CE3UNAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANgemF9t12t+NM+equ6k+5E/rO8SfMbTDVAlbXIzShrwRSDjChCU3oBzfpvqzRAWk9ICY0AVsFdA9NwCgBCU1oQj+453XL4YBEHRBCE5rQX/kqx+hHsKrvxdTb5zuU316WoXfKp/+QLAH3KWD1GzqEvnOeHZB9HhBCE/pKIOUAE5rQhPak8J1A9T2gerftGn0EMaFNaBPahDahn702NjpRqx+RTGgT2oQ2oU1oE/o/AsuyDL2lM/qZwmeA/f0YBKa/5SD0MUSs6pLQVSTVmYIAoaeIwSaqCBC6iqQ6UxAg9BQx2EQVAUJXkVRnCgKEniIGm6giQOgqkupMQYDQU8RgE1UEphe6qlF1EPhIoO0zhWJAYAsChN6CqpptBAjdht7CWxAg9BZU1WwjQOg29BbeggCht6CqZhsBQreht/AWBAi9BVU12wgQug29hbcg0Ca0bx+9HWfX1wRUf+1Adb3p3/omNKEvBEYPMKHvPJ6NAqyeMOp97gATmtBXAikHmNCEJvSDZ53lP7zpHvpzD8FHu4UxoU1oE9qEfidwtAk4e78mtAltQpvQJvSzd++6XjWZfkL7ssZn6vj7RwKE5kMUAUJHxakZQnMgigCho+LUDKE5EEWA0FFxaobQHIgiQOioODVDaA5EESB0VJyaITQHoggQOipOzRCaA1EECB0Vp2YIzYEoAoSOilMzhOZAFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIkDoqDg1Q2gORBEgdFScmiE0B6IIEDoqTs0QmgNRBKYXOoq2ZqYh0PbDm9MQsJEoAoSOilMzhOZAFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIlAudBQdzcQSOMV2prFDEiD0IWPPbZrQudkesjNCHzL23KYJnZvtITsj9CFjz22a0LnZHrIzQh8y9tymCZ2b7SE7+wcob7kPY5BclgAAAABJRU5ErkJggg==", - time: "data:image/png;base64,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", - }; - - const docs = - "https://softedco.github.io/GameJolt-API-Scratch-extension/DOCUMENTATION"; - - /** - * Mostly visual stuff for Scratch GUI - * The loader only uses getInfo().id and other methods - */ - class GameJoltAPI { - getInfo() { - return { - id: "GameJoltAPI", - name: "GameJolt API", - color1: "#2F7F6F", - color2: "#2A2731", - color3: "#CCFF00", - menuIconURI: icons.GameJolt, - docsURI: docs, - blocks: [ - { - opcode: "gamejoltBool", - blockIconURI: icons.GameJolt, - blockType: Scratch.BlockType.BOOLEAN, - text: "GameJolt?", - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Session Blocks", - }, - { - opcode: "setGame", - blockIconURI: icons.main, - blockType: Scratch.BlockType.COMMAND, - text: "Set game ID:[ID] and key:[key]", - arguments: { - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - key: { - type: Scratch.ArgumentType.STRING, - defaultValue: "key", - }, - }, - }, - { - opcode: "session", - blockIconURI: icons.main, - blockType: Scratch.BlockType.COMMAND, - text: "[openOrClose] session", - arguments: { - openOrClose: { - type: Scratch.ArgumentType.STRING, - menu: "openOrClose", - - /** - * Default value also has to be a string - * Or else it wouldn't display the menu item correctly - * Even if values match - */ - defaultValue: "true", - }, - }, - }, - { - opcode: "sessionPing", - blockIconURI: icons.main, - blockType: Scratch.BlockType.COMMAND, - text: "Ping session", - }, - { - opcode: "sessionBool", - blockIconURI: icons.main, - blockType: Scratch.BlockType.BOOLEAN, - text: "Session?", - disableMonitor: true, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "User Blocks", - }, - { - opcode: "loginManual", - blockIconURI: icons.user, - blockType: Scratch.BlockType.COMMAND, - text: "Login with username:[username] and token:[token]", - arguments: { - username: { - type: Scratch.ArgumentType.STRING, - defaultValue: "username", - }, - token: { - type: Scratch.ArgumentType.STRING, - defaultValue: "token", - }, - }, - }, - { - opcode: "loginAuto", - blockIconURI: icons.user, - blockType: Scratch.BlockType.COMMAND, - text: "Login automatically", - }, - { - opcode: "loginAutoBool", - blockIconURI: icons.user, - blockType: Scratch.BlockType.BOOLEAN, - text: "Autologin?", - }, - { - opcode: "logout", - blockIconURI: icons.user, - blockType: Scratch.BlockType.COMMAND, - text: "Logout", - }, - { - opcode: "loginBool", - blockIconURI: icons.user, - blockType: Scratch.BlockType.BOOLEAN, - text: "Login?", - }, - { - opcode: "loginUser", - blockIconURI: icons.user, - blockType: Scratch.BlockType.REPORTER, - text: "User logged in as", - }, - { - opcode: "userFetch", - blockIconURI: icons.user, - blockType: Scratch.BlockType.COMMAND, - text: "Fetch user:[usernameOrID] by [fetchType]", - arguments: { - usernameOrID: { - type: Scratch.ArgumentType.STRING, - defaultValue: "username", - }, - fetchType: { - type: Scratch.ArgumentType.STRING, - menu: "fetchTypes", - defaultValue: String(GameJolt.FETCH_USERNAME), - }, - }, - }, - { - opcode: "userFetchCurrent", - blockIconURI: icons.user, - blockType: Scratch.BlockType.COMMAND, - text: "Fetch logged in user", - }, - { - opcode: "returnUserData", - blockIconURI: icons.user, - blockType: Scratch.BlockType.REPORTER, - text: "Return fetched user's [userDataType]", - arguments: { - userDataType: { - type: Scratch.ArgumentType.STRING, - menu: "userDataTypes", - defaultValue: "id", - }, - }, - }, - { - opcode: "friendsFetch", - blockIconURI: icons.user, - blockType: Scratch.BlockType.REPORTER, - text: "Return user's friend ID by index:[index]", - arguments: { - index: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Trophy Blocks", - }, - { - opcode: "trophyAchieve", - blockIconURI: icons.trophy, - blockType: Scratch.BlockType.COMMAND, - text: "Achieve trophy with ID:[ID]", - arguments: { - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "trophyRemove", - blockIconURI: icons.trophy, - blockType: Scratch.BlockType.COMMAND, - text: "Remove trophy with ID:[ID]", - arguments: { - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "trophyFetch", - blockIconURI: icons.trophy, - blockType: Scratch.BlockType.REPORTER, - text: "Fetch trophy [trophyDataType] by [indexOrID]:[value]", - arguments: { - trophyDataType: { - type: Scratch.ArgumentType.STRING, - menu: "trophyDataTypes", - defaultValue: "id", - }, - indexOrID: { - type: Scratch.ArgumentType.STRING, - menu: "indexOrID", - defaultValue: String(GameJolt.FETCH_ALL), - }, - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Score Blocks", - }, - { - opcode: "scoreAdd", - blockIconURI: icons.score, - blockType: Scratch.BlockType.COMMAND, - text: "Add score by ID:[ID] with value:[value] text:[text] and extra data:[extraData]", - arguments: { - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - text: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1 point", - }, - extraData: { - type: Scratch.ArgumentType.STRING, - defaultValue: "optional", - }, - }, - }, - { - opcode: "scoreAddGuest", - blockIconURI: icons.score, - blockType: Scratch.BlockType.COMMAND, - text: "Add score by ID:[ID] with value:[value] text:[text] and extra data:[extraData] as guest:[username]", - arguments: { - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - text: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1 point", - }, - extraData: { - type: Scratch.ArgumentType.STRING, - defaultValue: "optional", - }, - username: { - type: Scratch.ArgumentType.STRING, - defaultValue: "username", - }, - }, - }, - { - opcode: "scoreFetch", - blockIconURI: icons.score, - blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount][globalOrPerUser] score/scores [betterOrWorse] than [value] by ID:[ID]", - arguments: { - amount: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - betterOrWorse: { - type: Scratch.ArgumentType.STRING, - menu: "betterOrWorse", - defaultValue: "true", - }, - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "scoreFetchGuest", - blockIconURI: icons.score, - blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount] guest's [username] score/scores [betterOrWorse] than [value] by ID:[ID]", - arguments: { - amount: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - username: { - type: Scratch.ArgumentType.STRING, - defaultValue: "username", - }, - betterOrWorse: { - type: Scratch.ArgumentType.STRING, - menu: "betterOrWorse", - defaultValue: "true", - }, - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "returnScoreData", - blockIconURI: icons.score, - blockType: Scratch.BlockType.REPORTER, - text: "Return fetched score [scoreDataType] by index:[index]", - arguments: { - scoreDataType: { - type: Scratch.ArgumentType.STRING, - menu: "scoreDataTypes", - defaultValue: "sort", - }, - index: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "scoreGetRank", - blockIconURI: icons.score, - blockType: Scratch.BlockType.REPORTER, - text: "Return rank of [value] by ID:[ID]", - arguments: { - value: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - ID: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - opcode: "scoreGetTables", - blockIconURI: icons.score, - blockType: Scratch.BlockType.REPORTER, - text: "Return table [tableDataType] by index:[index]", - arguments: { - tableDataType: { - type: Scratch.ArgumentType.STRING, - menu: "tableDataTypes", - defaultValue: "id", - }, - index: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Data Storage Blocks", - }, - { - opcode: "dataStoreSet", - blockIconURI: icons.store, - blockType: Scratch.BlockType.COMMAND, - text: "Set [globalOrPerUser] data with key:[key] to data:[data]", - arguments: { - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - key: { - type: Scratch.ArgumentType.STRING, - defaultValue: "key", - }, - data: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data", - }, - }, - }, - { - opcode: "dataStoreFetch", - blockIconURI: icons.store, - blockType: Scratch.BlockType.REPORTER, - text: "Fetch [globalOrPerUser] data with key:[key]", - arguments: { - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - key: { - type: Scratch.ArgumentType.STRING, - defaultValue: "key", - }, - }, - }, - { - opcode: "dataStoreUpdate", - blockIconURI: icons.store, - blockType: Scratch.BlockType.COMMAND, - text: "Update [globalOrPerUser] data with key:[key] by operation:[operationType] with value:[value]", - arguments: { - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - key: { - type: Scratch.ArgumentType.STRING, - defaultValue: "key", - }, - operationType: { - type: Scratch.ArgumentType.STRING, - menu: "operationTypes", - defaultValue: "add", - }, - value: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "dataStoreRemove", - blockIconURI: icons.store, - blockType: Scratch.BlockType.COMMAND, - text: "Remove [globalOrPerUser] data with key:[key]", - arguments: { - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - key: { - type: Scratch.ArgumentType.STRING, - defaultValue: "key", - }, - }, - }, - { - opcode: "dataStoreGetKey", - blockIconURI: icons.store, - blockType: Scratch.BlockType.REPORTER, - text: "Fetch [globalOrPerUser] keys with pattern [pattern] by index:[index]", - arguments: { - globalOrPerUser: { - type: Scratch.ArgumentType.STRING, - menu: "globalOrPerUser", - defaultValue: "false", - }, - pattern: { - type: Scratch.ArgumentType.STRING, - defaultValue: "*", - }, - index: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Time Blocks", - }, - { - opcode: "timeFetch", - blockIconURI: icons.time, - blockType: Scratch.BlockType.REPORTER, - text: "Return server's current [timeType]", - arguments: { - timeType: { - type: Scratch.ArgumentType.STRING, - menu: "timeTypes", - defaultValue: "timestamp", - }, - }, - }, - ], - menus: { - fetchTypes: { - items: [ - { text: "username", value: "true" }, - { text: "ID", value: "" }, - ], - }, - userDataTypes: { - items: [ - { text: "ID", value: "id" }, - { text: "username", value: "username" }, - { text: "developer username", value: "developer_name" }, - { text: "description", value: "developer_description" }, - { text: "status", value: "status" }, - { text: "type", value: "type" }, - { text: "avatar URL", value: "avatar_url" }, - { text: "website", value: "website" }, - { text: "sign up date", value: "signed_up" }, - { text: "sign up timestamp", value: "signed_up_timestamp" }, - { text: "last login", value: "last_logged_in" }, - { - text: "last login timestamp", - value: "last_logged_in_timestamp", - }, - ], - }, - operationTypes: { - items: [ - "add", - "subtract", - "multiply", - "divide", - "append", - "prepend", - ], - }, - scoreDataTypes: { - items: [ - { text: "value", value: "sort" }, - { text: "text", value: "score" }, - { text: "extra data", value: "extra_data" }, - { text: "username", value: "user" }, - { text: "user ID", value: "user_id" }, - { text: "score date", value: "stored" }, - { text: "score timestamp", value: "stored_timestamp" }, - ], - }, - trophyDataTypes: { - items: [ - { text: "ID", value: "id" }, - { text: "title", value: "title" }, - { text: "description", value: "description" }, - { text: "difficulty", value: "difficulty" }, - { text: "image URL", value: "image_url" }, - { text: "achievement date", value: "achieved" }, - ], - }, - timeTypes: { - items: [ - "timestamp", - "timezone", - "year", - "month", - "day", - "hour", - "minute", - "second", - ], - }, - tableDataTypes: { - items: [ - { text: "ID", value: "id" }, - { text: "name", value: "name" }, - { text: "description", value: "description" }, - { text: "primary", value: "primary" }, - ], - }, - openOrClose: { - items: [ - { text: "Open", value: "true" }, - { text: "Close", value: "" }, - ], - }, - globalOrPerUser: { - items: [ - { text: "global", value: "false" }, - { text: "user", value: "true" }, - ], - }, - indexOrID: { - items: [ - { text: "index", value: "true" }, - { text: "ID", value: "" }, - ], - }, - betterOrWorse: { - items: [ - { text: "better", value: "true" }, - { text: "worse", value: "" }, - ], - }, - }, - }; - } - gamejoltBool() { - return GameJolt.bOnGJ; - } - setGame({ ID, key }) { - GameJolt.iGameID = ID; - GameJolt.sGameKey = key; - } - session({ openOrClose }) { - return new Promise((resolve) => - GameJolt.SessionSetStatus(openOrClose, resolve) - ); - } - - /** - * Not necessary since the library handles pinging for you - */ - sessionPing() { - return new Promise((resolve) => GameJolt.SessionPing(resolve)); - } - sessionBool() { - return new Promise((resolve) => - GameJolt.SessionCheck((pResponse) => - resolve(pResponse.success == bool.t) - ) - ); - } - - /** - * Not necessary since the library handles logging in for you - */ - loginManual({ username, token }) { - return new Promise((resolve) => - GameJolt.UserLoginManual(username, token, resolve) - ); - } - - /** - * Not necessary since the library handles logging in for you - */ - loginAuto() { - return new Promise((resolve) => GameJolt.UserLoginAuto(resolve)); - } - loginAutoBool() { - return Boolean(GameJolt.asQueryParam["gjapi_username"]); - } - logout() { - return new Promise((resolve) => GameJolt.UserLogout(resolve)); - } - loginBool() { - return GameJolt.bLoggedIn; - } - loginUser() { - return GameJolt.sUserName; - } - userFetch({ fetchType, usernameOrID }) { - return new Promise((resolve) => - GameJolt.UserFetchComb(fetchType, usernameOrID, (pResponse) => - resolve( - pResponse.success == bool.t - ? (data.user = pResponse.users[0]) - : (err.user = pResponse.message) - ) - ) - ); - } - userFetchCurrent() { - return new Promise((resolve) => - GameJolt.UserFetchCurrent((pResponse) => - resolve( - pResponse.success == bool.t - ? (data.user = pResponse.users[0]) - : (err.user = pResponse.message) - ) - ) - ); - } - returnUserData({ userDataType }) { - if (!data.user) return err.get("user"); - return data.user[userDataType] || err.get("noItem"); - } - friendsFetch({ index }) { - if (!GameJolt.bLoggedIn) return err.get("noLogin"); - GameJolt.FriendsFetch((pResponse) => { - if (pResponse.success == bool.f) { - err.friends = pResponse.message; - return; - } - data.friends = pResponse.friends; - }); - if (!data.friends) return err.get("friends"); - if (!data.friends[index]) return err.get("noIndex"); - return data.friends[index].friend_id || err.get("noItem"); - } - trophyAchieve({ ID }) { - return new Promise((resolve) => GameJolt.TrophyAchieve(ID, resolve)); - } - trophyRemove({ ID }) { - return new Promise((resolve) => GameJolt.TrophyRemove(ID, resolve)); - } - trophyFetch({ indexOrID, value, trophyDataType }) { - if (!GameJolt.bLoggedIn) return err.get("noLogin"); - GameJolt.TrophyFetchComb( - indexOrID, - indexOrID ? GameJolt.TROPHY_ALL : value, - (pResponse) => { - if (pResponse.success == bool.f) { - err.trophies = pResponse.message; - return; - } - data.trophies = indexOrID - ? pResponse.trophies - : pResponse.trophies[0]; - } - ); - if (!data.trophies) return err.get("trophies"); - if (indexOrID) { - if (!data.trophies[value]) return err.get("noIndex"); - return data.trophies[value][trophyDataType] || err.get("noItem"); - } - return data.trophies[trophyDataType] || err.get("noItem"); - } - scoreAdd({ ID, value, text, extraData }) { - return new Promise((resolve) => - GameJolt.ScoreAdd(ID, value, text, extraData, resolve) - ); - } - scoreAddGuest({ ID, value, text, username, extraData }) { - return new Promise((resolve) => - GameJolt.ScoreAddGuest(ID, value, text, username, extraData, resolve) - ); - } - scoreFetch({ globalOrPerUser, ID, amount, betterOrWorse, value }) { - if (globalOrPerUser == bool.t && !GameJolt.bLoggedIn) { - err.scores = err.noLogin; - return; - } - return new Promise((resolve) => - GameJolt.ScoreFetchEx( - ID, - globalOrPerUser == bool.t - ? GameJolt.SCORE_ONLY_USER - : GameJolt.SCORE_ALL, - amount, - betterOrWorse, - value, - (pResponse) => - resolve( - pResponse.success == bool.t - ? (data.scores = pResponse.scores) - : (err.scores = pResponse.message) - ) - ) - ); - } - scoreFetchGuest({ ID, username, amount, betterOrWorse, value }) { - return new Promise((resolve) => - GameJolt.ScoreFetchGuestEx( - ID, - username, - amount, - betterOrWorse, - value, - (pResponse) => - resolve( - pResponse.success == bool.t - ? (data.scores = pResponse.scores) - : (err.scores = pResponse.message) - ) - ) - ); - } - returnScoreData({ index, scoreDataType }) { - if (!data.scores) return err.get("scores"); - if (!data.scores[index]) return err.get("noIndex"); - if (scoreDataType == "user") - return ( - data.scores[index].user || - data.scores[index].guest || - err.get("noItem") - ); - return data.scores[index][scoreDataType] || err.get("noItem"); - } - scoreGetRank({ ID, value }) { - return new Promise((resolve) => - GameJolt.ScoreGetRank(ID, value, (pResponse) => - resolve( - pResponse.success == bool.t - ? pResponse.rank - : err.show(pResponse.message) - ) - ) - ); - } - scoreGetTables({ index, tableDataType }) { - GameJolt.ScoreGetTables((pResponse) => { - if (pResponse.success == bool.f) { - err.tables = pResponse.message; - return; - } - data.tables = pResponse.tables; - }); - if (!data.tables) return err.get("tables"); - if (!data.tables[index]) return err.get("noIndex"); - return data.tables[index][tableDataType] || err.get("noItem"); - } - dataStoreSet({ globalOrPerUser, key, data }) { - return new Promise((resolve) => - GameJolt.DataStoreSet(globalOrPerUser == bool.t, key, data, resolve) - ); - } - dataStoreFetch({ globalOrPerUser, key }) { - if (globalOrPerUser == bool.t && !GameJolt.bLoggedIn) - return err.get("noLogin"); - return new Promise((resolve) => - GameJolt.DataStoreFetch(globalOrPerUser == bool.t, key, (pResponse) => - resolve( - pResponse.success == bool.t - ? pResponse.data - : err.show(pResponse.message) - ) - ) - ); - } - dataStoreUpdate({ globalOrPerUser, key, operationType, value }) { - return new Promise((resolve) => - GameJolt.DataStoreUpdate( - globalOrPerUser == bool.t, - key, - operationType, - value, - resolve - ) - ); - } - dataStoreRemove({ globalOrPerUser, key }) { - return new Promise((resolve) => - GameJolt.DataStoreRemove(globalOrPerUser == bool.t, key, resolve) - ); - } - dataStoreGetKey({ globalOrPerUser, pattern, index }) { - if (globalOrPerUser == bool.t && !GameJolt.bLoggedIn) - return err.get("noLogin"); - GameJolt.DataStoreGetKeysEx( - globalOrPerUser == bool.t, - pattern, - (pResponse) => { - if (pResponse.success == bool.f) { - err.keys = pResponse.message; - return; - } - if (!pResponse.keys) { - data.keys = ""; - err.keys = err.noIndex; - return; - } - data.keys = pResponse.keys; - } - ); - if (!data.keys) return err.get("keys"); - if (!data.keys[index]) return err.get("noIndex"); - return data.keys[index].key || err.get("noItem"); - } - timeFetch({ timeType }) { - return new Promise((resolve) => - GameJolt.TimeFetch((pResponse) => - resolve( - pResponse.success == bool.t - ? pResponse[timeType] - : err.show(pResponse.message) - ) - ) - ); - } - } - Scratch.extensions.register(new GameJoltAPI()); -})(Scratch); +// Name: Game Jolt +// ID: GameJoltAPI +// Description: Blocks that allow games to interact with the GameJolt API. Unofficial. +// By: softed + +((Scratch) => { + "use strict"; + + const md5 = (() => { + /*! + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + var hexcase = 0; + function hex_md5(a) { + return rstr2hex(rstr_md5(str2rstr_utf8(a))); + } + function hex_hmac_md5(a, b) { + return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a), str2rstr_utf8(b))); + } + function md5_vm_test() { + return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; + } + function rstr_md5(a) { + return binl2rstr(binl_md5(rstr2binl(a), a.length * 8)); + } + function rstr_hmac_md5(c, f) { + var e = rstr2binl(c); + if (e.length > 16) { + e = binl_md5(e, c.length * 8); + } + var a = Array(16), + d = Array(16); + for (var b = 0; b < 16; b++) { + a[b] = e[b] ^ 909522486; + d[b] = e[b] ^ 1549556828; + } + var g = binl_md5(a.concat(rstr2binl(f)), 512 + f.length * 8); + return binl2rstr(binl_md5(d.concat(g), 512 + 128)); + } + function rstr2hex(c) { + try { + hexcase; + } catch (g) { + hexcase = 0; + } + var f = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var b = ""; + var a; + for (var d = 0; d < c.length; d++) { + a = c.charCodeAt(d); + b += f.charAt((a >>> 4) & 15) + f.charAt(a & 15); + } + return b; + } + function str2rstr_utf8(c) { + var b = ""; + var d = -1; + var a, e; + while (++d < c.length) { + a = c.charCodeAt(d); + e = d + 1 < c.length ? c.charCodeAt(d + 1) : 0; + if (55296 <= a && a <= 56319 && 56320 <= e && e <= 57343) { + a = 65536 + ((a & 1023) << 10) + (e & 1023); + d++; + } + if (a <= 127) { + b += String.fromCharCode(a); + } else { + if (a <= 2047) { + b += String.fromCharCode(192 | ((a >>> 6) & 31), 128 | (a & 63)); + } else { + if (a <= 65535) { + b += String.fromCharCode( + 224 | ((a >>> 12) & 15), + 128 | ((a >>> 6) & 63), + 128 | (a & 63) + ); + } else { + if (a <= 2097151) { + b += String.fromCharCode( + 240 | ((a >>> 18) & 7), + 128 | ((a >>> 12) & 63), + 128 | ((a >>> 6) & 63), + 128 | (a & 63) + ); + } + } + } + } + } + return b; + } + function rstr2binl(b) { + var a = Array(b.length >> 2); + for (var c = 0; c < a.length; c++) { + a[c] = 0; + } + // eslint-disable-next-line no-redeclare + for (var c = 0; c < b.length * 8; c += 8) { + a[c >> 5] |= (b.charCodeAt(c / 8) & 255) << c % 32; + } + return a; + } + function binl2rstr(b) { + var a = ""; + for (var c = 0; c < b.length * 32; c += 8) { + a += String.fromCharCode((b[c >> 5] >>> c % 32) & 255); + } + return a; + } + function binl_md5(p, k) { + p[k >> 5] |= 128 << k % 32; + p[(((k + 64) >>> 9) << 4) + 14] = k; + var o = 1732584193; + var n = -271733879; + var m = -1732584194; + var l = 271733878; + for (var g = 0; g < p.length; g += 16) { + var j = o; + var h = n; + var f = m; + var e = l; + o = md5_ff(o, n, m, l, p[g + 0], 7, -680876936); + l = md5_ff(l, o, n, m, p[g + 1], 12, -389564586); + m = md5_ff(m, l, o, n, p[g + 2], 17, 606105819); + n = md5_ff(n, m, l, o, p[g + 3], 22, -1044525330); + o = md5_ff(o, n, m, l, p[g + 4], 7, -176418897); + l = md5_ff(l, o, n, m, p[g + 5], 12, 1200080426); + m = md5_ff(m, l, o, n, p[g + 6], 17, -1473231341); + n = md5_ff(n, m, l, o, p[g + 7], 22, -45705983); + o = md5_ff(o, n, m, l, p[g + 8], 7, 1770035416); + l = md5_ff(l, o, n, m, p[g + 9], 12, -1958414417); + m = md5_ff(m, l, o, n, p[g + 10], 17, -42063); + n = md5_ff(n, m, l, o, p[g + 11], 22, -1990404162); + o = md5_ff(o, n, m, l, p[g + 12], 7, 1804603682); + l = md5_ff(l, o, n, m, p[g + 13], 12, -40341101); + m = md5_ff(m, l, o, n, p[g + 14], 17, -1502002290); + n = md5_ff(n, m, l, o, p[g + 15], 22, 1236535329); + o = md5_gg(o, n, m, l, p[g + 1], 5, -165796510); + l = md5_gg(l, o, n, m, p[g + 6], 9, -1069501632); + m = md5_gg(m, l, o, n, p[g + 11], 14, 643717713); + n = md5_gg(n, m, l, o, p[g + 0], 20, -373897302); + o = md5_gg(o, n, m, l, p[g + 5], 5, -701558691); + l = md5_gg(l, o, n, m, p[g + 10], 9, 38016083); + m = md5_gg(m, l, o, n, p[g + 15], 14, -660478335); + n = md5_gg(n, m, l, o, p[g + 4], 20, -405537848); + o = md5_gg(o, n, m, l, p[g + 9], 5, 568446438); + l = md5_gg(l, o, n, m, p[g + 14], 9, -1019803690); + m = md5_gg(m, l, o, n, p[g + 3], 14, -187363961); + n = md5_gg(n, m, l, o, p[g + 8], 20, 1163531501); + o = md5_gg(o, n, m, l, p[g + 13], 5, -1444681467); + l = md5_gg(l, o, n, m, p[g + 2], 9, -51403784); + m = md5_gg(m, l, o, n, p[g + 7], 14, 1735328473); + n = md5_gg(n, m, l, o, p[g + 12], 20, -1926607734); + o = md5_hh(o, n, m, l, p[g + 5], 4, -378558); + l = md5_hh(l, o, n, m, p[g + 8], 11, -2022574463); + m = md5_hh(m, l, o, n, p[g + 11], 16, 1839030562); + n = md5_hh(n, m, l, o, p[g + 14], 23, -35309556); + o = md5_hh(o, n, m, l, p[g + 1], 4, -1530992060); + l = md5_hh(l, o, n, m, p[g + 4], 11, 1272893353); + m = md5_hh(m, l, o, n, p[g + 7], 16, -155497632); + n = md5_hh(n, m, l, o, p[g + 10], 23, -1094730640); + o = md5_hh(o, n, m, l, p[g + 13], 4, 681279174); + l = md5_hh(l, o, n, m, p[g + 0], 11, -358537222); + m = md5_hh(m, l, o, n, p[g + 3], 16, -722521979); + n = md5_hh(n, m, l, o, p[g + 6], 23, 76029189); + o = md5_hh(o, n, m, l, p[g + 9], 4, -640364487); + l = md5_hh(l, o, n, m, p[g + 12], 11, -421815835); + m = md5_hh(m, l, o, n, p[g + 15], 16, 530742520); + n = md5_hh(n, m, l, o, p[g + 2], 23, -995338651); + o = md5_ii(o, n, m, l, p[g + 0], 6, -198630844); + l = md5_ii(l, o, n, m, p[g + 7], 10, 1126891415); + m = md5_ii(m, l, o, n, p[g + 14], 15, -1416354905); + n = md5_ii(n, m, l, o, p[g + 5], 21, -57434055); + o = md5_ii(o, n, m, l, p[g + 12], 6, 1700485571); + l = md5_ii(l, o, n, m, p[g + 3], 10, -1894986606); + m = md5_ii(m, l, o, n, p[g + 10], 15, -1051523); + n = md5_ii(n, m, l, o, p[g + 1], 21, -2054922799); + o = md5_ii(o, n, m, l, p[g + 8], 6, 1873313359); + l = md5_ii(l, o, n, m, p[g + 15], 10, -30611744); + m = md5_ii(m, l, o, n, p[g + 6], 15, -1560198380); + n = md5_ii(n, m, l, o, p[g + 13], 21, 1309151649); + o = md5_ii(o, n, m, l, p[g + 4], 6, -145523070); + l = md5_ii(l, o, n, m, p[g + 11], 10, -1120210379); + m = md5_ii(m, l, o, n, p[g + 2], 15, 718787259); + n = md5_ii(n, m, l, o, p[g + 9], 21, -343485551); + o = safe_add(o, j); + n = safe_add(n, h); + m = safe_add(m, f); + l = safe_add(l, e); + } + return Array(o, n, m, l); + } + function md5_cmn(h, e, d, c, g, f) { + return safe_add(bit_rol(safe_add(safe_add(e, h), safe_add(c, f)), g), d); + } + function md5_ff(g, f, k, j, e, i, h) { + return md5_cmn((f & k) | (~f & j), g, f, e, i, h); + } + function md5_gg(g, f, k, j, e, i, h) { + return md5_cmn((f & j) | (k & ~j), g, f, e, i, h); + } + function md5_hh(g, f, k, j, e, i, h) { + return md5_cmn(f ^ k ^ j, g, f, e, i, h); + } + function md5_ii(g, f, k, j, e, i, h) { + return md5_cmn(k ^ (f | ~j), g, f, e, i, h); + } + function safe_add(a, d) { + var c = (a & 65535) + (d & 65535); + var b = (a >> 16) + (d >> 16) + (c >> 16); + return (b << 16) | (c & 65535); + } + function bit_rol(a, b) { + return (a << b) | (a >>> (32 - b)); + } + + return hex_md5; + })(); + + const GameJolt = (() => { + /*! + * This is a modified version of https://github.com/MausGames/game-jolt-api-js-library (Public Domain) + */ + var GJAPI = {}; + + GJAPI.err = { + noLogin: "No user logged in.", + login: "User already logged in.", + noFetch: "Fetch request not supported.", + + /** + * @param {string} code + */ + get: (code) => { + return { + success: false, + message: GJAPI.err[code] || code, + }; + }, + }; + + GJAPI.sStatus = "active"; + + GJAPI.iGameID = 0; + GJAPI.sGameKey = ""; + GJAPI.bAutoLogin = true; + + GJAPI.sAPI = "https://api.gamejolt.com/api/game/v1_2"; + GJAPI.sLogName = "[Game Jolt API]"; + GJAPI.iLogStack = 20; + + GJAPI.asQueryParam = (() => { + var asOutput = {}; + var asList = window.location.search.substring(1).split("&"); + + // loop through all parameters + for (var i = 0; i < asList.length; ++i) { + // separate key from value + var asPair = asList[i].split("="); + + // insert value into map + if (typeof asOutput[asPair[0]] === "undefined") + asOutput[asPair[0]] = asPair[1]; // create new entry + else if (typeof asOutput[asPair[0]] === "string") + asOutput[asPair[0]] = [asOutput[asPair[0]], asPair[1]]; + // extend into array + else asOutput[asPair[0]].push(asPair[1]); // append to array + } + + return asOutput; + })(); + + GJAPI.bOnGJ = window.location.hostname.match(/gamejolt\.net/) + ? true + : false; + + /** + * Log message and stack trace + * @param {string} sMessage + */ + GJAPI.LogTrace = (sMessage) => { + // prevent flooding + if (!GJAPI.iLogStack) return; + if (!--GJAPI.iLogStack) sMessage = "(╯°□°)╯︵ ┻━┻"; + + console.warn(GJAPI.sLogName + " " + sMessage); + console.trace(); + }; + + // ************** + // Main functions + GJAPI.SEND_FOR_USER = true; + GJAPI.SEND_GENERAL = false; + + /** + * @param {string} sURL + * @param {boolean} bSendUser + * @param {function} pCallback + */ + GJAPI.SendRequest = (sURL, bSendUser, pCallback) => { + // forward call to extended function + GJAPI.SendRequestEx(sURL, bSendUser, "json", "", pCallback); + }; + + /** + * @param {string} sURL + * @param {boolean} bSendUser + * @param {string} sFormat + * @param {string} sBodyData + * @param {function} pCallback + */ + GJAPI.SendRequestEx = (sURL, bSendUser, sFormat, sBodyData, pCallback) => { + // add main URL, game ID and format type + sURL = + GJAPI.sAPI + + encodeURI(sURL) + + (sURL.indexOf("/?") === -1 ? "?" : "&") + + "game_id=" + + GJAPI.iGameID + + "&format=" + + sFormat; + + // add credentials of current user (for user-related operations) + if (GJAPI.bLoggedIn && bSendUser) + sURL += + "&username=" + GJAPI.sUserName + "&user_token=" + GJAPI.sUserToken; + + // generate MD5 signature + sURL += "&signature=" + md5(sURL + GJAPI.sGameKey); + + // send off the request + __CreateAjax(sURL, sBodyData, (sResponse) => { + console.info(GJAPI.sLogName + " <" + sURL + "> " + sResponse); + if (sResponse === "" || typeof pCallback !== "function") return; + + switch (sFormat) { + case "json": + pCallback(JSON.parse(sResponse).response); + break; + + case "dump": + var iLineBreakIndex = sResponse.indexOf("\n"); + var sResult = sResponse.substr(0, iLineBreakIndex - 1); + var sData = sResponse.substr(iLineBreakIndex + 1); + + pCallback({ + success: sResult === "SUCCESS", + data: sData, + }); + break; + + default: + if (typeof pCallback == "function") pCallback(sResponse); + } + }); + }; + + // automatically retrieve and log in current user on Game Jolt + GJAPI.bLoggedIn = + GJAPI.bAutoLogin && + GJAPI.asQueryParam["gjapi_username"] && + GJAPI.asQueryParam["gjapi_token"] + ? true + : false; + GJAPI.sUserName = GJAPI.bLoggedIn + ? GJAPI.asQueryParam["gjapi_username"] + : ""; + GJAPI.sUserToken = GJAPI.bLoggedIn ? GJAPI.asQueryParam["gjapi_token"] : ""; + + // send some information to the console + console.info(GJAPI.asQueryParam); + console.info( + GJAPI.sLogName + + (GJAPI.bOnGJ ? " E" : " Not e") + + "mbedded on Game Jolt <" + + window.location.origin + + window.location.pathname + + ">" + ); + console.info( + GJAPI.sLogName + + (GJAPI.bLoggedIn ? " U" : " No u") + + "ser recognized <" + + GJAPI.sUserName + + ">" + ); + if (!window.location.hostname) + console.warn( + GJAPI.sLogName + + " XMLHttpRequest may not work properly on a local environment" + ); + + // ***************** + // Session functions + GJAPI.bSessionActive = true; + + /** + * @param {function} pCallback + */ + GJAPI.SessionOpen = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("SessionOpen() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + // check for already open session + if (GJAPI.iSessionHandle) { + pCallback(); + return; + } + + // send open-session request + GJAPI.SendRequest("/sessions/open/", GJAPI.SEND_FOR_USER, (pResponse) => { + // check for success + if (pResponse.success == "true") { + // add automatic session ping and close + GJAPI.iSessionHandle = window.setInterval(GJAPI.SessionPing, 30000); + window.addEventListener("beforeunload", GJAPI.SessionClose, false); + } + + if (typeof pCallback == "function") pCallback(pResponse); + }); + }; + + /** + * Send ping-session request + * @param {function} pCallback + */ + GJAPI.SessionPing = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("SessionPing() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + GJAPI.SendRequest( + "/sessions/ping/?status=" + GJAPI.sStatus, + GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + /** + * Send close-session request + * @param {function} pCallback + */ + GJAPI.SessionClose = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("SessionClose() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + if (GJAPI.iSessionHandle) { + // remove automatic session ping and close + window.clearInterval(GJAPI.iSessionHandle); + window.removeEventListener("beforeunload", GJAPI.SessionClose); + + GJAPI.iSessionHandle = 0; + } + + GJAPI.SendRequest("/sessions/close/", GJAPI.SEND_FOR_USER, pCallback); + }; + + // automatically start player session + if (GJAPI.bLoggedIn) GJAPI.SessionOpen(); + + // ************** + // User functions + + /** + * Send authentification request + * @param {string} sUserName + * @param {string} sUserToken + * @param {function} pCallback + */ + GJAPI.UserLoginManual = (sUserName, sUserToken, pCallback) => { + if (GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "UserLoginManual(" + + sUserName + + ", " + + sUserToken + + ") failed: user " + + GJAPI.sUserName + + " already logged in" + ); + pCallback(GJAPI.err.get("login")); + return; + } + + GJAPI.SendRequest( + "/users/auth/" + "?username=" + sUserName + "&user_token=" + sUserToken, + GJAPI.SEND_GENERAL, + (pResponse) => { + // check for success + if (pResponse.success == "true") { + // save login properties + GJAPI.bLoggedIn = true; + GJAPI.sUserName = sUserName; + GJAPI.sUserToken = sUserToken; + + // open session + GJAPI.SessionOpen(); + } + + // execute nested callback + if (typeof pCallback === "function") pCallback(pResponse); + } + ); + }; + + /** + * @param {function} pCallback + */ + GJAPI.UserLogout = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("UserLogout() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + // close session + GJAPI.SessionClose(); + + // reset login properties + GJAPI.bLoggedIn = false; + GJAPI.sUserName = ""; + GJAPI.sUserToken = ""; + + // reset trophy cache + GJAPI.abTrophyCache = {}; + pCallback({ success: true }); + }; + + /** + * Send fetch-user request + * @param {number} iUserID + * @param {function} pCallback + */ + GJAPI.UserFetchID = (iUserID, pCallback) => { + GJAPI.SendRequest( + "/users/?user_id=" + iUserID, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * Send fetch-ser request + * @param {string} sUserName + * @param {function} pCallback + */ + GJAPI.UserFetchName = (sUserName, pCallback) => { + GJAPI.SendRequest( + "/users/?username=" + sUserName, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * Send fetch-user request + * @param {function} pCallback + */ + GJAPI.UserFetchCurrent = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("UserFetchCurrent() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + GJAPI.UserFetchName(GJAPI.sUserName, pCallback); + }; + + // **************** + // Trophy functions + GJAPI.abTrophyCache = {}; + + GJAPI.TROPHY_ONLY_ACHIEVED = 1; + GJAPI.TROPHY_ONLY_NOTACHIEVED = -1; + GJAPI.TROPHY_ALL = 0; + + /** + * Send achieve-trophy request + * @param {number} iTrophyID + * @param {function} pCallback + */ + GJAPI.TrophyAchieve = (iTrophyID, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "TrophyAchieve(" + iTrophyID + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + // check for already achieved trophy + if (GJAPI.abTrophyCache[iTrophyID]) { + pCallback(GJAPI.err.get("Trophy already achieved.")); + return; + } + + GJAPI.SendRequest( + "/trophies/add-achieved/?trophy_id=" + iTrophyID, + GJAPI.SEND_FOR_USER, + function (pResponse) { + // check for success + if (pResponse.success == "true") { + // save status + GJAPI.abTrophyCache[iTrophyID] = true; + } + + // execute nested callback + if (typeof pCallback === "function") pCallback(pResponse); + } + ); + }; + + /** + * Send fetch-trophy request + * @param {number} iAchieved + * @param {function} pCallback + */ + GJAPI.TrophyFetch = (iAchieved, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "TrophyFetch(" + iAchieved + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + // only trophies with the requested status + var sTrophyData = + iAchieved === GJAPI.TROPHY_ALL + ? "" + : "?achieved=" + + (iAchieved >= GJAPI.TROPHY_ONLY_ACHIEVED ? "true" : "false"); + + GJAPI.SendRequest( + "/trophies/" + sTrophyData, + GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + /** + * Send fetch-trophy request + * @param {number} iTrophyID + * @param {function} pCallback + */ + GJAPI.TrophyFetchSingle = (iTrophyID, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "TrophyFetchSingle(" + iTrophyID + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + GJAPI.SendRequest( + "/trophies/?trophy_id=" + iTrophyID, + GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + // *************** + // Score functions + GJAPI.SCORE_ONLY_USER = true; + GJAPI.SCORE_ALL = false; + + /** + * Send add-score request + * @param {number} iScoreTableID + * @param {number} iScoreValue + * @param {string} sScoreText + * @param {string} sExtraData + * @param {function} pCallback + */ + GJAPI.ScoreAdd = ( + iScoreTableID, + iScoreValue, + sScoreText, + sExtraData, + pCallback + ) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "ScoreAdd(" + + iScoreTableID + + ", " + + iScoreValue + + ", " + + sScoreText + + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + GJAPI.ScoreAddGuest( + iScoreTableID, + iScoreValue, + sScoreText, + "", + sExtraData, + pCallback + ); + }; + + /** + * Send add-score request + * @param {number} iScoreTableID + * @param {number} iScoreValue + * @param {string} sScoreText + * @param {string} sGuestName + * @param {string} sExtraData + * @param {function} pCallback + */ + GJAPI.ScoreAddGuest = ( + iScoreTableID, + iScoreValue, + sScoreText, + sGuestName, + sExtraData, + pCallback + ) => { + // use current user data or guest name + var bIsGuest = sGuestName && sGuestName.length ? true : false; + + GJAPI.SendRequest( + "/scores/add/?sort=" + + iScoreValue + + "&score=" + + sScoreText + + (bIsGuest ? "&guest=" + sGuestName : "") + + (iScoreTableID ? "&table_id=" + iScoreTableID : "") + + (sExtraData ? "&extra_data=" + sExtraData : ""), + bIsGuest ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + /** + * Send fetch-score request + * @param {number} iScoreTableID + * @param {boolean} bOnlyUser + * @param {number} iLimit + * @param {function} pCallback + */ + GJAPI.ScoreFetch = (iScoreTableID, bOnlyUser, iLimit, pCallback) => { + if (!GJAPI.bLoggedIn && bOnlyUser) { + GJAPI.LogTrace( + "ScoreFetch(" + + iScoreTableID + + ", " + + bOnlyUser + + ", " + + iLimit + + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + + // only scores from the current user or all scores + var bFetchAll = bOnlyUser === GJAPI.SCORE_ONLY_USER ? false : true; + + GJAPI.SendRequest( + "/scores/?limit=" + + iLimit + + (iScoreTableID ? "&table_id=" + iScoreTableID : ""), + bFetchAll ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + // ******************** + // Data store functions + GJAPI.DATA_STORE_USER = true; + GJAPI.DATA_STORE_GLOBAL = false; + + /** + * Send set-data request + * @param {number} iStore + * @param {string} sKey + * @param {string} sData + * @param {function} pCallback + */ + GJAPI.DataStoreSet = (iStore, sKey, sData, pCallback) => { + GJAPI.SendRequest( + "/data-store/set/?key=" + sKey + "&data=" + sData, + iStore, + pCallback + ); + }; + + /** + * Send fetch-data request + * @param {number} iStore + * @param {string} sKey + * @param {function} pCallback + */ + GJAPI.DataStoreFetch = (iStore, sKey, pCallback) => { + GJAPI.SendRequest("/data-store/?key=" + sKey, iStore, pCallback); + }; + + /** + * Send update-data request + * @param {number} iStore + * @param {string} sKey + * @param {string} sOperation + * @param {string} sValue + * @param {function} pCallback + */ + GJAPI.DataStoreUpdate = (iStore, sKey, sOperation, sValue, pCallback) => { + GJAPI.SendRequest( + "/data-store/update/?key=" + + sKey + + "&operation=" + + sOperation + + "&value=" + + sValue, + iStore, + pCallback + ); + }; + + /** + * Send remove-data request + * @param {number} iStore + * @param {string} sKey + * @param {function} pCallback + */ + GJAPI.DataStoreRemove = (iStore, sKey, pCallback) => { + // send remove-data request + GJAPI.SendRequest("/data-store/remove/?key=" + sKey, iStore, pCallback); + }; + + /** + * Send get-keys request + * @param {number} iStore + * @param {function} pCallback + */ + GJAPI.DataStoreGetKeys = (iStore, pCallback) => { + GJAPI.SendRequest("/data-store/get-keys/", iStore, pCallback); + }; + + /** + * Create asynchronous request + * @param {string} sUrl + * @param {string} sBodyData + * @param {function} pCallback + */ + function __CreateAjax(sUrl, sBodyData, pCallback) { + if (typeof sBodyData !== "string") sBodyData = ""; + + Scratch.canFetch(sUrl).then((allowed) => { + if (!allowed) { + pCallback(GJAPI.err.get("noFetch")); + return; + } + + // canFetch() checked above + // eslint-disable-next-line no-restricted-syntax + var pRequest = new XMLHttpRequest(); + + // bind callback function + pRequest.onreadystatechange = () => { + if (pRequest.readyState === 4) pCallback(pRequest.responseText); + }; + + // send off the request + if (sBodyData !== "") { + pRequest.open("POST", sUrl); + pRequest.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded" + ); + pRequest.send(sBodyData); + } else { + pRequest.open("GET", sUrl); + pRequest.send(); + } + }); + } + + GJAPI.BETTER_THAN = true; + GJAPI.WORSE_THAN = false; + GJAPI.FETCH_USERNAME = true; + GJAPI.FETCH_ID = false; + GJAPI.FETCH_ALL = true; + GJAPI.FETCH_SINGLE = false; + + /** + * @param {function} pCallback + */ + GJAPI.TimeFetch = (pCallback) => { + GJAPI.SendRequest( + "/time/?game_id=" + GJAPI.iGameID, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + /** + * @param {function} pCallback + */ + GJAPI.FriendsFetch = (pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace("FriendsFetch() failed: no user logged in"); + pCallback(GJAPI.err.get("noLogin")); + return; + } + GJAPI.SendRequest( + "/friends/?game_id=" + + GJAPI.iGameID + + "&username=" + + GJAPI.sUserName + + "&user_token=" + + GJAPI.sUserToken, + GJAPI.SEND_FOR_USER, + pCallback + ); + }; + /** + * @param {number} iTrophyID + * @param {function} pCallback + */ + GJAPI.TrophyRemove = (iTrophyID, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "TrophyRemove(" + iTrophyID + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + // Check if the trophy is not achieved + if (!GJAPI.abTrophyCache[iTrophyID]) { + pCallback(GJAPI.err.get("Trophy already achieved.")); + return; + } + GJAPI.SendRequest( + "/trophies/remove-achieved/?game_id=" + + GJAPI.iGameID + + "&username=" + + GJAPI.sUserName + + "&user_token=" + + GJAPI.sUserToken + + "&trophy_id=" + + iTrophyID, + GJAPI.SEND_FOR_USER, + (pResponse) => { + // Update trophy status if the response succeded + if (pResponse.success == "true") { + GJAPI.abTrophyCache[iTrophyID] = false; + } + if (typeof pCallback == "function") { + pCallback(pResponse); + } + } + ); + }; + + /** + * @param {number} iScoreTableID + * @param {number} iScoreValue + * @param {function} pCallback + */ + GJAPI.ScoreGetRank = (iScoreTableID, iScoreValue, pCallback) => { + GJAPI.SendRequest( + "/scores/get-rank/?game_id=" + + GJAPI.iGameID + + "&sort=" + + iScoreValue + + "&table_id=" + + iScoreTableID, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * @param {function} pCallback + */ + GJAPI.ScoreGetTables = (pCallback) => { + GJAPI.SendRequest( + "/scores/tables/?game_id=" + GJAPI.iGameID, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * @param {function} pCallback + */ + GJAPI.SessionCheck = (pCallback) => { + GJAPI.SendRequest( + "/sessions/check/?game_id=" + + GJAPI.iGameID + + "&username=" + + GJAPI.sUserName + + "&user_token=" + + GJAPI.sUserToken, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * SessionOpen and SessionClose combined + * @param {boolean} bIsOpen + * @param {function} pCallback + */ + GJAPI.SessionSetStatus = (bIsOpen, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "SessionSetStatus(" + bIsOpen + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + GJAPI.bSessionActive = bIsOpen; + if (bIsOpen) { + if (GJAPI.iSessionHandle) { + pCallback({ success: true }); + return; + } + GJAPI.SendRequest( + "/sessions/open/", + GJAPI.SEND_FOR_USER, + function (pResponse) { + if (pResponse.success == "true") { + GJAPI.iSessionHandle = window.setInterval( + GJAPI.SessionPing, + 30000 + ); + window.addEventListener( + "beforeunload", + GJAPI.SessionClose, + false + ); + } + } + ); + if (typeof pCallback == "function") pCallback({ success: true }); + return; + } + if (GJAPI.iSessionHandle) { + window.clearInterval(GJAPI.iSessionHandle); + window.removeEventListener("beforeunload", GJAPI.SessionClose); + GJAPI.iSessionHandle = 0; + } + GJAPI.SendRequest("/sessions/close/", GJAPI.SEND_FOR_USER, pCallback); + }; + + /** + * UserFetchName and UserFetchID combined + * Use GJAPI.FETCH_USERNAME and GJAPI.FETCH_ID for better code readability + * @param {boolean} bIsUsername + * @param {string} sValue + * @param {function} pCallback + */ + GJAPI.UserFetchComb = (bIsUsername, sValue, pCallback) => { + GJAPI.SendRequest( + "/users/" + (bIsUsername ? "?username=" : "?user_id=") + sValue, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * ScoreFetch but with better_than and worse_than parameters + * Use GJAPI.BETTER_THAN and GJAPI.WORSE_THAN for better code readability + * If value is set to 0 it will work like riginal ScoreFetch + * @param {number} iScoreTableID + * @param {boolean} bOnlyUser + * @param {number} iLimit + * @param {boolean} bBetterOrWorse + * @param {number} iValue + * @param {function} pCallback + */ + GJAPI.ScoreFetchEx = ( + iScoreTableID, + bOnlyUser, + iLimit, + bBetterOrWorse, + iValue, + pCallback + ) => { + if (!GJAPI.bLoggedIn && bOnlyUser) { + GJAPI.LogTrace( + "ScoreFetch(" + + iScoreTableID + + ", " + + bOnlyUser + + ", " + + iLimit + + ", " + + bBetterOrWorse + + ", " + + iValue + + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + var bFetchAll = bOnlyUser == GJAPI.SCORE_ONLY_USER ? false : true; + GJAPI.SendRequest( + "/scores/" + + "?limit=" + + iLimit + + (iScoreTableID ? "&table_id=" + iScoreTableID : "") + + (bBetterOrWorse ? "&better_than=" : "&worse_than=") + + iValue, + bFetchAll ? GJAPI.SEND_GENERAL : GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + /** + * Unused in the extension because of ScoreFetchGuestEx + * @param {number} iScoreTableID + * @param {string} sName + * @param {number} iLimit + * @param {function} pCallback + */ + GJAPI.ScoreFetchGuest = (iScoreTableID, sName, iLimit, pCallback) => { + GJAPI.SendRequest( + "/scores/?limit=" + + iLimit + + (iScoreTableID ? "&table_id=" + iScoreTableID : "") + + "&guest=" + + sName, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * ScoreFetchGuest but with better_than and worse_than parameters + * Use GJAPI.BETTER_THAN and GJAPI.WORSE_THAN for better code readability + * If value is set to 0 it will work like original ScoreFetchGuest + * @param {number} iScoreTableID + * @param {string} sName + * @param {number} iLimit + * @param {boolean} bBetterOrWorse + * @param {number} iValue + * @param {function} pCallback + */ + GJAPI.ScoreFetchGuestEx = ( + iScoreTableID, + sName, + iLimit, + bBetterOrWorse, + iValue, + pCallback + ) => { + GJAPI.SendRequest( + "/scores/?limit=" + + iLimit + + (iScoreTableID ? "&table_id=" + iScoreTableID : "") + + "&guest=" + + sName + + (bBetterOrWorse ? "&better_than=" : "&worse_than=") + + iValue, + GJAPI.SEND_GENERAL, + pCallback + ); + }; + + /** + * TrophyFetch and TrophyFetchSingle combined + * Use GJAPI.FETCH_ALL and GJAPI.FETCH_SINGLE for better code readability + * @param {boolean} bIsAll + * @param {number} iValue + * @param {function} pCallback + */ + GJAPI.TrophyFetchComb = (bIsAll, iValue, pCallback) => { + if (!GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "TrophyFetchComb(" + + bIsAll + + ", " + + iValue + + ") failed: no user logged in" + ); + pCallback(GJAPI.err.get("noLogin")); + return; + } + if (bIsAll) { + var sTrophyData = + iValue === GJAPI.TROPHY_ALL + ? "" + : "?achieved=" + + (iValue >= GJAPI.TROPHY_ONLY_ACHIEVED ? "true" : "false"); + GJAPI.SendRequest( + "/trophies/" + sTrophyData, + GJAPI.SEND_FOR_USER, + pCallback + ); + return; + } + GJAPI.SendRequest( + "/trophies/?trophy_id=" + iValue, + GJAPI.SEND_FOR_USER, + pCallback + ); + }; + + /** + * Modified UserLoginManual to login users automatically if their username and token are detected + * @param {function} pCallback + */ + GJAPI.UserLoginAuto = (pCallback) => { + if (!GJAPI.bOnGJ) { + GJAPI.LogTrace("UserLoginAuto() failed: No username or token detected"); + pCallback(GJAPI.err.get("No username or token detected.")); + return; + } + if (GJAPI.bLoggedIn) { + GJAPI.LogTrace( + "UserLoginAuto() failed: user " + + GJAPI.sUserName + + " already logged in" + ); + pCallback(GJAPI.err.get("login")); + return; + } + GJAPI.SendRequest( + "/users/auth/" + + "?username=" + + GJAPI.asQueryParam["gjapi_username"] + + "&user_token=" + + GJAPI.asQueryParam["gjapi_token"], + GJAPI.SEND_GENERAL, + (pResponse) => { + if (pResponse.success == "true") { + GJAPI.bLoggedIn = true; + GJAPI.sUserName = GJAPI.asQueryParam["gjapi_username"]; + GJAPI.sUserToken = GJAPI.asQueryParam["gjapi_token"]; + GJAPI.SessionOpen(); + } + if (typeof pCallback === "function") { + pCallback(pResponse); + } + } + ); + }; + + /** + * DataStoreGetKeys but with a pattern parameter + * The placeholder character for patterns is * + * @param {number} iStore + * @param {string} sPattern + * @param {function} pCallback + */ + GJAPI.DataStoreGetKeysEx = (iStore, sPattern, pCallback) => { + GJAPI.SendRequest( + "/data-store/get-keys/?pattern=" + sPattern, + iStore, + pCallback + ); + }; + + GJAPI.SEQUENTIALLY = "sequentially"; + GJAPI.BREAK_ON_ERROR = "break_on_error"; + GJAPI.PARALLEL = "parallel"; + + /** + * @param {string[]} sRequests + * @param {string} sParam + * @param {function} pCallback + */ + GJAPI.SendBatchRequest = (sRequests, sParam, pCallback) => { + if (!sRequests) { + pCallback(GJAPI.err.get("No requests found.")); + return; + } + let sFinalURL = GJAPI.sAPI + "/batch?game_id=" + GJAPI.iGameID; + for (let i = 0; i < sRequests.length; i++) { + sFinalURL += + "&requests[]=" + + encodeURIComponent( + encodeURI(sRequests[i]) + + "&signature=" + + md5(sRequests[i] + GJAPI.sGameKey) + ); + } + switch (sParam) { + case "break_on_error": + sFinalURL += "&break_on_error=true"; + break; + case "parallel": + sFinalURL += "¶llel=true"; + break; + case "sequentially": // request is processed sequentially by default + default: + break; + } + sFinalURL += "&format=json"; + sFinalURL += "&signature=" + md5(sFinalURL + GJAPI.sGameKey); + + __CreateAjax(sFinalURL, "", (sResponse) => { + console.info(GJAPI.sLogName + " <" + sFinalURL + "> " + sResponse); + pCallback(JSON.parse(sResponse).response); + }); + }; + + return GJAPI; + })(); + + /** + * Used for storing API error messages + */ + let err = { + debug: true, + + noLogin: "No user logged in.", + noData: "Data not found.", + noIndex: "Data at such index not found.", + + /** + * Used for returning a standartized error message + * @param {string} code + */ + get: (code) => + err.debug + ? err[code] + ? "Error: " + err[code] + : "Error: Data not found." + : "", + + /** + * Used for returning a standartized error message + * @param {string} text + */ + show: (text) => (err.debug ? "Error: " + text : ""), + }; + + /** + * Used for storing API response objects + */ + let data = {}; + + /** + * The API response object's success property is a string and not a boolean + * So there is stuff like "pResponse.success == trueStr" + */ + const trueStr = "true"; + + /*! + * GameJolt icon by GameJolt + * Other icons by softed + * Can be used outside of this extension + */ + const icons = { + debug: + "data:image/png;base64,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", + GameJolt: + "data:image/png;base64,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", + main: "data:image/png;base64,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", + user: "data:image/png;base64,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", + trophy: + "data:image/png;base64,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", + score: + "data:image/png;base64,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", + store: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAAAXNSR0IArs4c6QAABVJJREFUeF7t3d1t20AQRWGrIwMpKQXEJaiEpICUFCAdMZBebAT6WcJDz/Lyy6uJ2Z1zz44oKZJOL/4hEETgFNSLVhB4ITQJoggQOipOzRCaA1EECB0Vp2YIzYEoAoSOilMzhOZAFAFCR8WpmWGhl2VZ4EKgi8DpdBpydeiiSxOE7orSuhcChOZBFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIkDoqDg10yb06/kNfQSGCfw9/xy6ltBDmFzUTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k2A0N0JWL+UAKFLcSrWTYDQ3QlYv5QAoUtxKtZNgNDdCVi/lAChS3Eq1k1geqH/vIz9Utz3848hlr/Pv4auU+82ptn5EfqO3oQm9IXA2Dhd8cObJvRtsRy421xMaBP6SiDlgBCa0IR+8KzKLYcD0npATGgCtgpY/aoJoQlNaLcc7wSqJ4x6n3tVx4Q2oU3or5zQQ2/ruQiBlQTafqdw5T5djsAQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQAUIPYXLRXggQei9J2ecQgTahX89vQxt0EQIXAtP/Xw5CE3UNAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANAUKvoeXa6QkQevqIbHANgemF9t12t+NM+equ6k+5E/rO8SfMbTDVAlbXIzShrwRSDjChCU3oBzfpvqzRAWk9ICY0AVsFdA9NwCgBCU1oQj+453XL4YBEHRBCE5rQX/kqx+hHsKrvxdTb5zuU316WoXfKp/+QLAH3KWD1GzqEvnOeHZB9HhBCE/pKIOUAE5rQhPak8J1A9T2gerftGn0EMaFNaBPahDahn702NjpRqx+RTGgT2oQ2oU1oE/o/AsuyDL2lM/qZwmeA/f0YBKa/5SD0MUSs6pLQVSTVmYIAoaeIwSaqCBC6iqQ6UxAg9BQx2EQVAUJXkVRnCgKEniIGm6giQOgqkupMQYDQU8RgE1UEphe6qlF1EPhIoO0zhWJAYAsChN6CqpptBAjdht7CWxAg9BZU1WwjQOg29BbeggCht6CqZhsBQreht/AWBAi9BVU12wgQug29hbcg0Ca0bx+9HWfX1wRUf+1Adb3p3/omNKEvBEYPMKHvPJ6NAqyeMOp97gATmtBXAikHmNCEJvSDZ53lP7zpHvpzD8FHu4UxoU1oE9qEfidwtAk4e78mtAltQpvQJvSzd++6XjWZfkL7ssZn6vj7RwKE5kMUAUJHxakZQnMgigCho+LUDKE5EEWA0FFxaobQHIgiQOioODVDaA5EESB0VJyaITQHoggQOipOzRCaA1EECB0Vp2YIzYEoAoSOilMzhOZAFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIkDoqDg1Q2gORBEgdFScmiE0B6IIEDoqTs0QmgNRBKYXOoq2ZqYh0PbDm9MQsJEoAoSOilMzhOZAFAFCR8WpGUJzIIoAoaPi1AyhORBFgNBRcWqG0ByIIlAudBQdzcQSOMV2prFDEiD0IWPPbZrQudkesjNCHzL23KYJnZvtITsj9CFjz22a0LnZHrIzQh8y9tymCZ2b7SE7+wcob7kPY5BclgAAAABJRU5ErkJggg==", + time: "data:image/png;base64,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", + batch: + "data:image/png;base64,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", + }; + + const docs = "https://extensions.turbowarp.org/gamejolt"; + + /** + * Mostly visual stuff for Scratch GUI + * The loader only uses getInfo().id and other methods + */ + class GameJoltAPI { + getInfo() { + return { + id: "GameJoltAPI", + name: "Game Jolt API", + color1: "#2F7F6F", + color2: "#2A2731", + color3: "#CCFF00", + menuIconURI: icons.GameJolt, + docsURI: docs, + blocks: [ + { + opcode: "gamejoltBool", + blockIconURI: icons.GameJolt, + blockType: Scratch.BlockType.BOOLEAN, + text: "On Game Jolt?", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Session Blocks", + }, + { + opcode: "setGame", + blockIconURI: icons.main, + blockType: Scratch.BlockType.COMMAND, + text: "Set game ID to [ID] and private key to [key]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + key: { + type: Scratch.ArgumentType.STRING, + defaultValue: "private key", + }, + }, + }, + { + opcode: "session", + blockIconURI: icons.main, + blockType: Scratch.BlockType.COMMAND, + text: "[openOrClose] session", + arguments: { + openOrClose: { + type: Scratch.ArgumentType.STRING, + menu: "openOrClose", + + /** + * Default value also has to be a string + * Or else it wouldn't display the menu item correctly + * Even if values match + */ + defaultValue: "true", + }, + }, + }, + { + opcode: "sessionPing", + blockIconURI: icons.main, + blockType: Scratch.BlockType.COMMAND, + text: "Ping session", + }, + { + opcode: "sessionSetStatus", + blockIconURI: icons.main, + blockType: Scratch.BlockType.COMMAND, + text: "Set session status to [status]", + arguments: { + status: { + type: Scratch.ArgumentType.STRING, + menu: "status", + defaultValue: "active", + }, + }, + }, + { + opcode: "sessionBool", + blockIconURI: icons.main, + blockType: Scratch.BlockType.BOOLEAN, + text: "Session open?", + disableMonitor: true, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "User Blocks", + }, + { + opcode: "loginManual", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Login with [username] and [token]", + arguments: { + username: { + type: Scratch.ArgumentType.STRING, + defaultValue: "username", + }, + token: { + type: Scratch.ArgumentType.STRING, + defaultValue: "private token", + }, + }, + }, + { + opcode: "loginAuto", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Login automatically", + }, + { + opcode: "loginAutoBool", + blockIconURI: icons.user, + blockType: Scratch.BlockType.BOOLEAN, + text: "Autologin available?", + }, + { + opcode: "logout", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Logout", + }, + { + opcode: "loginBool", + blockIconURI: icons.user, + blockType: Scratch.BlockType.BOOLEAN, + text: "Logged in?", + }, + { + opcode: "loginUser", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Logged in user's username", + }, + { + opcode: "userFetch", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch user's [usernameOrID] by [fetchType]", + arguments: { + usernameOrID: { + type: Scratch.ArgumentType.STRING, + defaultValue: "username", + }, + fetchType: { + type: Scratch.ArgumentType.STRING, + menu: "fetchTypes", + defaultValue: String(GameJolt.FETCH_USERNAME), + }, + }, + }, + { + opcode: "userFetchCurrent", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch logged in user", + }, + { + opcode: "returnUserData", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched user's [userDataType]", + arguments: { + userDataType: { + type: Scratch.ArgumentType.STRING, + menu: "userDataTypes", + defaultValue: "id", + }, + }, + }, + { + opcode: "returnUserDataJson", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched user's data in JSON", + }, + { + hideFromPalette: true, + opcode: "friendsFetch", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched user's friend ID at index[index] (Deprecated)", + arguments: { + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "friendsFetchNew", + blockIconURI: icons.user, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch user's friend IDs", + }, + { + opcode: "friendsReturn", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched user's friend ID at index[index]", + arguments: { + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "friendsReturnJson", + blockIconURI: icons.user, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched user's friend IDs in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Trophy Blocks", + }, + { + opcode: "trophyAchieve", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.COMMAND, + text: "Achieve trophy of ID [ID]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "trophyRemove", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.COMMAND, + text: "Remove trophy of ID [ID]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + hideFromPalette: true, + opcode: "trophyFetch", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched trophy [trophyDataType] at [indexOrID][value] (Deprecated)", + arguments: { + trophyDataType: { + type: Scratch.ArgumentType.STRING, + menu: "trophyDataTypes", + defaultValue: "id", + }, + indexOrID: { + type: Scratch.ArgumentType.STRING, + menu: "indexOrID", + defaultValue: String(GameJolt.FETCH_ALL), + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "trophyFetchId", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch trophy of ID[ID]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "trophyFetchAll", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [trophyFetchGroup] trophies", + arguments: { + trophyFetchGroup: { + type: Scratch.ArgumentType.STRING, + menu: "trophyFetchGroup", + defaultValue: "0", + }, + }, + }, + { + opcode: "trophyReturn", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched trophy [trophyDataType] at index [index]", + arguments: { + trophyDataType: { + type: Scratch.ArgumentType.STRING, + menu: "trophyDataTypes", + defaultValue: "id", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "trophyReturnJson", + blockIconURI: icons.trophy, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched trophies in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Score Blocks", + }, + { + opcode: "scoreAdd", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Add score [value] in table of ID [ID] with text [text] and comment [extraData]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + text: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1 point", + }, + extraData: { + type: Scratch.ArgumentType.STRING, + defaultValue: "optional", + }, + }, + }, + { + opcode: "scoreAddGuest", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]", + arguments: { + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + text: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1 point", + }, + extraData: { + type: Scratch.ArgumentType.STRING, + defaultValue: "optional", + }, + username: { + type: Scratch.ArgumentType.STRING, + defaultValue: "guest", + }, + }, + }, + { + opcode: "scoreFetchSimple", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]", + arguments: { + amount: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "scoreFetch", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]", + arguments: { + amount: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + betterOrWorse: { + type: Scratch.ArgumentType.STRING, + menu: "betterOrWorse", + defaultValue: "true", + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "scoreFetchGuestSimple", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [amount] [username] score/s in table of ID [ID]", + arguments: { + amount: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + username: { + type: Scratch.ArgumentType.STRING, + defaultValue: "guest", + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "scoreFetchGuest", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]", + arguments: { + amount: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + username: { + type: Scratch.ArgumentType.STRING, + defaultValue: "guest", + }, + betterOrWorse: { + type: Scratch.ArgumentType.STRING, + menu: "betterOrWorse", + defaultValue: "true", + }, + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "returnScoreData", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched score [scoreDataType] at index [index]", + arguments: { + scoreDataType: { + type: Scratch.ArgumentType.STRING, + menu: "scoreDataTypes", + defaultValue: "sort", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "returnScoreDataJson", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched score data in JSON", + }, + { + opcode: "scoreGetRank", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched rank of [value] in table of ID [ID]", + arguments: { + value: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + ID: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + hideFromPalette: true, + opcode: "scoreGetTables", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched table [tableDataType] at index[index] (Deprecated)", + arguments: { + tableDataType: { + type: Scratch.ArgumentType.STRING, + menu: "tableDataTypes", + defaultValue: "id", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "scoreFetchTables", + blockIconURI: icons.score, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch score tables", + }, + { + opcode: "scoreReturnTables", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched table [tableDataType] at index [index]", + arguments: { + tableDataType: { + type: Scratch.ArgumentType.STRING, + menu: "tableDataTypes", + defaultValue: "id", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "scoreReturnTablesJson", + blockIconURI: icons.score, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched tables in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Data Storage Blocks", + }, + { + opcode: "dataStoreSet", + blockIconURI: icons.store, + blockType: Scratch.BlockType.COMMAND, + text: "Set [globalOrPerUser] data at [key] to [data]", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + key: { + type: Scratch.ArgumentType.STRING, + defaultValue: "key", + }, + data: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data", + }, + }, + }, + { + opcode: "dataStoreFetch", + blockIconURI: icons.store, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched [globalOrPerUser] data at [key]", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + key: { + type: Scratch.ArgumentType.STRING, + defaultValue: "key", + }, + }, + }, + { + opcode: "dataStoreUpdate", + blockIconURI: icons.store, + blockType: Scratch.BlockType.COMMAND, + text: "Update [globalOrPerUser] data at [key] by [operationType] [value]", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + key: { + type: Scratch.ArgumentType.STRING, + defaultValue: "key", + }, + operationType: { + type: Scratch.ArgumentType.STRING, + menu: "operationTypes", + defaultValue: "add", + }, + value: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "dataStoreRemove", + blockIconURI: icons.store, + blockType: Scratch.BlockType.COMMAND, + text: "Remove [globalOrPerUser] data at [key]", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + key: { + type: Scratch.ArgumentType.STRING, + defaultValue: "key", + }, + }, + }, + { + hideFromPalette: true, + opcode: "dataStoreGetKey", + blockIconURI: icons.store, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched [globalOrPerUser] keys with pattern [pattern] at index [index] (Deprecated)", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + pattern: { + type: Scratch.ArgumentType.STRING, + defaultValue: "*", + }, + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "dataStoreFetchKeys", + blockIconURI: icons.store, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch all [globalOrPerUser] keys", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + }, + }, + { + opcode: "dataStoreFetchPatternKeys", + blockIconURI: icons.store, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch [globalOrPerUser] keys matching with [pattern]", + arguments: { + globalOrPerUser: { + type: Scratch.ArgumentType.STRING, + menu: "globalOrPerUser", + defaultValue: "false", + }, + pattern: { + type: Scratch.ArgumentType.STRING, + defaultValue: "*", + }, + }, + }, + { + opcode: "dataStoreReturnKeys", + blockIconURI: icons.store, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched key at index [index]", + arguments: { + index: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: "dataStoreReturnKeysJson", + blockIconURI: icons.store, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched keys in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Time Blocks", + }, + { + hideFromPalette: true, + opcode: "timeFetch", + blockIconURI: icons.time, + blockType: Scratch.BlockType.REPORTER, + text: "Server's current [timeType] (Deprecated)", + arguments: { + timeType: { + type: Scratch.ArgumentType.STRING, + menu: "timeTypes", + defaultValue: "timestamp", + }, + }, + }, + { + opcode: "timeFetchNew", + blockIconURI: icons.time, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch server's time", + }, + { + opcode: "timeReturn", + blockIconURI: icons.time, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched server's [timeType]", + arguments: { + timeType: { + type: Scratch.ArgumentType.STRING, + menu: "timeTypes", + defaultValue: "timestamp", + }, + }, + }, + { + opcode: "timeReturnJson", + blockIconURI: icons.time, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched server's time in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Batch Blocks", + }, + { + opcode: "batchAdd", + blockIconURI: icons.batch, + blockType: Scratch.BlockType.COMMAND, + text: "Add [namespace] request with [parameters] to batch", + arguments: { + namespace: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data-store/set", + }, + parameters: { + type: Scratch.ArgumentType.STRING, + defaultValue: '{"key":"key","data":"data"}', + }, + }, + }, + { + opcode: "batchClear", + blockIconURI: icons.batch, + blockType: Scratch.BlockType.COMMAND, + text: "Clear batch", + }, + { + opcode: "batchJson", + blockIconURI: icons.batch, + blockType: Scratch.BlockType.REPORTER, + text: "Batch in JSON", + }, + { + opcode: "batchCall", + blockIconURI: icons.batch, + blockType: Scratch.BlockType.COMMAND, + text: "Fetch batch [parameter]", + arguments: { + parameter: { + type: Scratch.ArgumentType.STRING, + menu: "batchParameters", + defaultValue: "sequentially", + }, + }, + }, + { + opcode: "batchReturnJson", + blockIconURI: icons.batch, + blockType: Scratch.BlockType.REPORTER, + text: "Fetched batch data in JSON", + }, + { + blockType: Scratch.BlockType.LABEL, + text: "Debug Blocks", + }, + { + opcode: "debug", + blockIconURI: icons.debug, + blockType: Scratch.BlockType.COMMAND, + text: "Turn debug mode [toggle]", + arguments: { + toggle: { + type: Scratch.ArgumentType.STRING, + menu: "debug", + defaultValue: "", + }, + }, + }, + { + opcode: "debugBool", + blockIconURI: icons.debug, + blockType: Scratch.BlockType.BOOLEAN, + text: "In debug mode?", + }, + { + opcode: "debugLastErr", + blockIconURI: icons.debug, + blockType: Scratch.BlockType.REPORTER, + text: "Last API error", + }, + ], + menus: { + debug: { + items: [ + { text: "on", value: "true" }, + { text: "off", value: "" }, + ], + }, + status: { + items: ["active", "idle"], + }, + fetchTypes: { + items: [ + { text: "username", value: "true" }, + { text: "ID", value: "" }, + ], + }, + userDataTypes: { + items: [ + { text: "ID", value: "id" }, + { text: "username", value: "username" }, + { text: "developer username", value: "developer_name" }, + { text: "description", value: "developer_description" }, + { text: "status", value: "status" }, + { text: "type", value: "type" }, + { text: "avatar URL", value: "avatar_url" }, + { text: "website", value: "website" }, + { text: "sign up date", value: "signed_up" }, + { text: "sign up timestamp", value: "signed_up_timestamp" }, + { text: "last login", value: "last_logged_in" }, + { + text: "last login timestamp", + value: "last_logged_in_timestamp", + }, + ], + }, + operationTypes: { + items: [ + { text: "adding", value: "add" }, + { text: "subtracting", value: "subtract" }, + { text: "multiplying by", value: "multiply" }, + { text: "dividing by", value: "divide" }, + { text: "appending", value: "append" }, + { text: "prepending", value: "prepend" }, + ], + }, + scoreDataTypes: { + items: [ + { text: "value", value: "sort" }, + { text: "text", value: "score" }, + { text: "comment", value: "extra_data" }, + { text: "username", value: "user" }, + { text: "user ID", value: "user_id" }, + { text: "score date", value: "stored" }, + { text: "score timestamp", value: "stored_timestamp" }, + ], + }, + trophyDataTypes: { + items: [ + { text: "ID", value: "id" }, + { text: "title", value: "title" }, + { text: "description", value: "description" }, + { text: "difficulty", value: "difficulty" }, + { text: "image URL", value: "image_url" }, + { text: "achievement date", value: "achieved" }, + ], + }, + timeTypes: { + items: [ + "timestamp", + "timezone", + "year", + "month", + "day", + "hour", + "minute", + "second", + ], + }, + tableDataTypes: { + items: [ + { text: "ID", value: "id" }, + { text: "name", value: "name" }, + { text: "description", value: "description" }, + { text: "primary", value: "primary" }, + ], + }, + openOrClose: { + items: [ + { text: "Open", value: "true" }, + { text: "Close", value: "" }, + ], + }, + globalOrPerUser: { + items: [ + { text: "global", value: "false" }, + { text: "user", value: "true" }, + ], + }, + trophyFetchGroup: { + items: [ + { text: "all", value: "0" }, + { text: "all achieved", value: "1" }, + { text: "all unachieved", value: "-1" }, + ], + }, + indexOrID: { + items: [ + { text: "index", value: "true" }, + { text: "ID", value: "" }, + ], + }, + betterOrWorse: { + items: [ + { text: "better", value: "true" }, + { text: "worse", value: "" }, + ], + }, + batchParameters: { + items: [ + { text: "sequentially", value: "sequentially" }, + { text: "sequentially, break on error", value: "break_on_error" }, + { text: "in parallel", value: "parallel" }, + ], + }, + }, + }; + } + debug({ toggle }) { + err.debug = toggle == trueStr; + } + debugBool() { + return err.debug; + } + debugLastErr() { + return err.last ? "Error: " + err.last : ""; + } + gamejoltBool() { + return GameJolt.bOnGJ; + } + setGame({ ID, key }) { + GameJolt.iGameID = ID; + GameJolt.sGameKey = key; + } + session({ openOrClose }) { + return new Promise((resolve) => + GameJolt.SessionSetStatus(openOrClose, (pResponse) => { + if (pResponse.success != trueStr) err.last = pResponse.message; + resolve(); + }) + ); + } + + /** + * Not necessary since the library handles pinging for you + */ + sessionPing() { + return new Promise((resolve) => + GameJolt.SessionPing((pResponse) => { + if (pResponse.success != trueStr) err.last = pResponse.message; + resolve(); + }) + ); + } + sessionSetStatus({ status }) { + GameJolt.sStatus = status; + } + sessionBool() { + return new Promise((resolve) => + GameJolt.SessionCheck((pResponse) => + resolve(pResponse.success == trueStr) + ) + ); + } + + /** + * Not necessary since the library handles logging in for you + */ + loginManual({ username, token }) { + return new Promise((resolve) => + GameJolt.UserLoginManual(username, token, (pResponse) => { + if (pResponse.success != trueStr) + [err.user, err.last] = [pResponse.message, pResponse.message]; + resolve(); + }) + ); + } + + /** + * Not necessary since the library handles logging in for you + */ + loginAuto() { + return new Promise((resolve) => + GameJolt.UserLoginAuto((pResponse) => { + if (pResponse.success != trueStr) + [err.user, err.last] = [pResponse.message, pResponse.message]; + resolve(); + }) + ); + } + loginAutoBool() { + return Boolean(GameJolt.asQueryParam["gjapi_username"]); + } + logout() { + return new Promise((resolve) => + GameJolt.UserLogout((pResponse) => { + if (pResponse.success != trueStr) + [err.user, err.last] = [pResponse.message, pResponse.message]; + resolve(); + }) + ); + } + loginBool() { + return GameJolt.bLoggedIn; + } + loginUser() { + return GameJolt.sUserName || err.get("noLogin"); + } + userFetch({ fetchType, usernameOrID }) { + return new Promise((resolve) => + GameJolt.UserFetchComb(fetchType, usernameOrID, (pResponse) => { + if (pResponse.success != trueStr) { + [err.user, err.last] = [pResponse.message, pResponse.message]; + data.user = undefined; + resolve(); + return; + } + data.user = pResponse.users[0]; + err.user = undefined; + resolve(); + }) + ); + } + userFetchCurrent() { + return new Promise((resolve) => + GameJolt.UserFetchCurrent((pResponse) => { + if (pResponse.success != trueStr) { + [err.user, err.last] = [pResponse.message, pResponse.message]; + data.user = undefined; + resolve(); + return; + } + data.user = pResponse.users[0]; + err.user = undefined; + resolve(); + }) + ); + } + returnUserData({ userDataType }) { + if (!data.user) return err.get("user"); + return data.user[userDataType] || err.get("noData"); + } + returnUserDataJson() { + return JSON.stringify(data.user) || err.get("user") || "{}"; + } + friendsFetch({ index }) { + if (!GameJolt.bLoggedIn) return err.get("noLogin"); + GameJolt.FriendsFetch((pResponse) => { + if (pResponse.success != trueStr) { + err.friends = pResponse.message; + return; + } + data.friends = pResponse.friends; + }); + if (!data.friends) return err.get("friends"); + if (!data.friends[index]) return err.get("noIndex"); + return data.friends[index].friend_id || err.get("noData"); + } + friendsFetchNew() { + return new Promise((resolve) => + GameJolt.FriendsFetch((pResponse) => { + if (pResponse.success != trueStr) { + [err.friends, err.last] = [pResponse.message, pResponse.message]; + data.friends = undefined; + resolve(); + return; + } + data.friends = pResponse.friends; + err.friends = undefined; + resolve(); + }) + ); + } + friendsReturn({ index }) { + if (!data.friends) return err.get("friends"); + if (!data.friends[Math.floor(index)]) return err.get("noIndex"); + return data.friends[Math.floor(index)].friend_id || err.get("noData"); + } + friendsReturnJson() { + return JSON.stringify(data.friends) || err.get("friends") || "{}"; + } + trophyAchieve({ ID }) { + return new Promise((resolve) => + GameJolt.TrophyAchieve(ID, (pResponse) => { + if (pResponse.success != trueStr) + [err.trophies, err.last] = [pResponse.message, pResponse.message]; + resolve(); + }) + ); + } + trophyRemove({ ID }) { + return new Promise((resolve) => + GameJolt.TrophyRemove(ID, (pResponse) => { + if (pResponse.success != trueStr) + [err.trophies, err.last] = [pResponse.message, pResponse.message]; + resolve(); + }) + ); + } + trophyFetch({ indexOrID, value, trophyDataType }) { + if (!GameJolt.bLoggedIn) return err.get("noLogin"); + GameJolt.TrophyFetchComb( + indexOrID, + indexOrID ? GameJolt.TROPHY_ALL : value, + (pResponse) => { + if (pResponse.success != trueStr) { + err.trophies = pResponse.message; + return; + } + data.trophies = indexOrID + ? pResponse.trophies + : pResponse.trophies[0]; + } + ); + if (!data.trophies) return err.get("trophies"); + if (indexOrID) { + if (!data.trophies[value]) return err.get("noIndex"); + return data.trophies[value][trophyDataType] || err.get("noData"); + } + return data.trophies[trophyDataType] || err.get("noData"); + } + trophyFetchAll({ trophyFetchGroup }) { + return new Promise((resolve) => + GameJolt.TrophyFetch(Number(trophyFetchGroup), (pResponse) => { + if (pResponse.success != trueStr) { + [err.trophies, err.last] = [pResponse.message, pResponse.message]; + data.trophies = undefined; + resolve(); + return; + } + data.trophies = pResponse.trophies; + err.trophies = undefined; + resolve(); + }) + ); + } + trophyFetchId({ ID }) { + return new Promise((resolve) => + GameJolt.TrophyFetchSingle(ID, (pResponse) => { + if (pResponse.success != trueStr) { + [err.trophies, err.last] = [pResponse.message, pResponse.message]; + data.trophies = undefined; + resolve(); + return; + } + data.trophies = pResponse.trophies; + err.trophies = undefined; + resolve(); + }) + ); + } + trophyReturn({ trophyDataType, index }) { + if (!data.trophies) return err.get("trophies"); + if (!data.trophies[Math.floor(index)]) return err.get("noIndex"); + return ( + data.trophies[Math.floor(index)][trophyDataType] || err.get("noData") + ); + } + trophyReturnJson() { + return JSON.stringify(data.trophies) || err.get("trophies") || "{}"; + } + scoreAdd({ ID, value, text, extraData }) { + return new Promise((resolve) => + GameJolt.ScoreAdd(ID, value, text, extraData, (pResponse) => { + if (pResponse.success != trueStr) err.last = pResponse.message; + resolve(); + }) + ); + } + scoreAddGuest({ ID, value, text, username, extraData }) { + return new Promise((resolve) => + GameJolt.ScoreAddGuest( + ID, + value, + text, + username, + extraData, + (pResponse) => { + if (pResponse.success != trueStr) err.last = pResponse.message; + resolve(); + } + ) + ); + } + scoreFetchSimple({ amount, globalOrPerUser, ID }) { + if (globalOrPerUser == trueStr && !GameJolt.bLoggedIn) { + err.scores = err.noLogin; + data.scores = undefined; + return; + } + return new Promise((resolve) => + GameJolt.ScoreFetch( + ID, + globalOrPerUser == trueStr + ? GameJolt.SCORE_ONLY_USER + : GameJolt.SCORE_ALL, + amount, + (pResponse) => { + if (pResponse.success != trueStr) { + [err.scores, err.last] = [pResponse.message, pResponse.message]; + data.scores = undefined; + resolve(); + return; + } + data.scores = pResponse.scores; + err.scores = undefined; + resolve(); + } + ) + ); + } + scoreFetch({ globalOrPerUser, ID, amount, betterOrWorse, value }) { + if (globalOrPerUser == trueStr && !GameJolt.bLoggedIn) { + err.scores = err.noLogin; + data.scores = undefined; + return; + } + return new Promise((resolve) => + GameJolt.ScoreFetchEx( + ID, + globalOrPerUser == trueStr + ? GameJolt.SCORE_ONLY_USER + : GameJolt.SCORE_ALL, + amount, + betterOrWorse, + value, + (pResponse) => { + if (pResponse.success != trueStr) { + [err.scores, err.last] = [pResponse.message, pResponse.message]; + data.scores = undefined; + resolve(); + return; + } + data.scores = pResponse.scores; + err.scores = undefined; + resolve(); + } + ) + ); + } + scoreFetchGuestSimple({ amount, username, ID }) { + return new Promise((resolve) => + GameJolt.ScoreFetchGuestEx(ID, username, amount, (pResponse) => { + if (pResponse.success != trueStr) { + [err.scores, err.last] = [pResponse.message, pResponse.message]; + data.scores = undefined; + resolve(); + return; + } + data.scores = pResponse.scores; + err.scores = undefined; + resolve(); + }) + ); + } + scoreFetchGuest({ ID, username, amount, betterOrWorse, value }) { + return new Promise((resolve) => + GameJolt.ScoreFetchGuestEx( + ID, + username, + amount, + betterOrWorse, + value, + (pResponse) => { + if (pResponse.success != trueStr) { + [err.scores, err.last] = [pResponse.message, pResponse.message]; + data.scores = undefined; + resolve(); + return; + } + data.scores = pResponse.scores; + err.scores = undefined; + resolve(); + } + ) + ); + } + returnScoreData({ index, scoreDataType }) { + if (!data.scores) return err.get("scores"); + if (!data.scores[index]) return err.get("noIndex"); + if (scoreDataType == "user") + return ( + data.scores[index].user || + data.scores[index].guest || + err.get("noData") + ); + return data.scores[index][scoreDataType] || err.get("noData"); + } + returnScoreDataJson() { + return JSON.stringify(data.scores) || err.get("scores") || "{}"; + } + scoreGetRank({ ID, value }) { + return new Promise((resolve) => + GameJolt.ScoreGetRank(ID, value, (pResponse) => { + if (pResponse.success != trueStr) { + err.last = pResponse.message; + resolve(err.get("last")); + return; + } + resolve(pResponse.rank); + }) + ); + } + scoreGetTables({ index, tableDataType }) { + GameJolt.ScoreGetTables((pResponse) => { + if (pResponse.success != trueStr) { + err.tables = pResponse.message; + return; + } + data.tables = pResponse.tables; + }); + if (!data.tables) return err.get("tables"); + if (!data.tables[index]) return err.get("noIndex"); + return data.tables[index][tableDataType] || err.get("noData"); + } + scoreFetchTables() { + return new Promise((resolve) => + GameJolt.ScoreGetTables((pResponse) => { + if (pResponse.success != trueStr) { + [err.tables, err.last] = [pResponse.message, pResponse.message]; + data.tables = undefined; + resolve(); + return; + } + data.tables = pResponse.tables; + err.tables = undefined; + resolve(); + }) + ); + } + scoreReturnTables({ tableDataType, index }) { + if (!data.tables) return err.get("tables"); + if (!data.tables[Math.floor(index)]) return err.get("noIndex"); + return ( + !data.tables[Math.floor(index)][tableDataType] || err.get("noData") + ); + } + scoreReturnTablesJson() { + return JSON.stringify(data.tables) || err.get("tables") || "{}"; + } + dataStoreSet({ globalOrPerUser, key, data }) { + return new Promise((resolve) => + GameJolt.DataStoreSet( + globalOrPerUser == trueStr, + key, + data, + (pResponse) => { + if (pResponse.success != trueStr) err.last = pResponse.message; + resolve(); + } + ) + ); + } + dataStoreFetch({ globalOrPerUser, key }) { + if (globalOrPerUser == trueStr && !GameJolt.bLoggedIn) + return err.get("noLogin"); + return new Promise((resolve) => + GameJolt.DataStoreFetch( + globalOrPerUser == trueStr, + key, + (pResponse) => { + if (pResponse.success != trueStr) { + err.last = pResponse.message; + resolve(err.get("last")); + return; + } + resolve(pResponse.data); + } + ) + ); + } + dataStoreUpdate({ globalOrPerUser, key, operationType, value }) { + return new Promise((resolve) => + GameJolt.DataStoreUpdate( + globalOrPerUser == trueStr, + key, + operationType, + value, + (pResponse) => { + if (pResponse.success != trueStr) { + err.last = pResponse.message; + } + resolve(); + } + ) + ); + } + dataStoreRemove({ globalOrPerUser, key }) { + return new Promise((resolve) => + GameJolt.DataStoreRemove( + globalOrPerUser == trueStr, + key, + (pResponse) => { + if (pResponse.success != trueStr) { + err.last = pResponse.message; + } + resolve(); + } + ) + ); + } + dataStoreGetKey({ globalOrPerUser, pattern, index }) { + if (globalOrPerUser == trueStr && !GameJolt.bLoggedIn) + return err.get("noLogin"); + GameJolt.DataStoreGetKeysEx( + globalOrPerUser == trueStr, + pattern, + (pResponse) => { + if (pResponse.success != trueStr) { + err.keys = pResponse.message; + data.keys = undefined; + return; + } + if (!pResponse.keys) { + err.keys = err.noIndex; + data.keys = undefined; + return; + } + data.keys = pResponse.keys; + } + ); + if (!data.keys) return err.get("keys"); + if (!data.keys[index]) return err.get("noIndex"); + return data.keys[index].key || err.get("noData"); + } + dataStoreFetchKeys({ globalOrPerUser }) { + return new Promise((resolve) => + GameJolt.DataStoreGetKeys(globalOrPerUser == trueStr, (pResponse) => { + if (pResponse.success != trueStr) { + [err.keys, err.last] = [pResponse.message, pResponse.message]; + data.keys = undefined; + resolve(); + return; + } + if (!pResponse.keys) { + data.keys = undefined; + err.keys = err.noIndex; + resolve(); + return; + } + data.keys = pResponse.keys; + err.keys = undefined; + resolve(); + }) + ); + } + dataStoreFetchPatternKeys({ globalOrPerUser, pattern }) { + return new Promise((resolve) => + GameJolt.DataStoreGetKeysEx( + globalOrPerUser == trueStr, + pattern, + (pResponse) => { + if (pResponse.success != trueStr) { + [err.keys, err.last] = [pResponse.message, pResponse.message]; + data.keys = undefined; + resolve(); + return; + } + if (!pResponse.keys) { + data.keys = undefined; + err.keys = err.noIndex; + resolve(); + return; + } + data.keys = pResponse.keys; + err.keys = undefined; + resolve(); + } + ) + ); + } + dataStoreReturnKeys({ index }) { + if (!data.keys) return err.get("keys"); + if (!data.keys[Math.floor(index)]) return err.get("noIndex"); + return data.keys[Math.floor(index)].key || err.get("noData"); + } + dataStoreReturnKeysJson() { + return JSON.stringify(data.keys) || err.get("keys") || "{}"; + } + timeFetch({ timeType }) { + return new Promise((resolve) => + GameJolt.TimeFetch((pResponse) => { + if (pResponse.success != trueStr) { + err.last = pResponse.message; + resolve(err.get("last")); + } + resolve(pResponse[timeType]); + }) + ); + } + timeFetchNew() { + return new Promise((resolve) => + GameJolt.TimeFetch((pResponse) => { + if (pResponse.success != trueStr) { + [err.time, err.last] = [pResponse.message, pResponse.message]; + data.time = undefined; + resolve(); + return; + } + data.time = pResponse; + data.time.success = undefined; + data.time.message = undefined; + err.time = undefined; + resolve(); + }) + ); + } + timeReturn({ timeType }) { + if (!data.time) return err.get("time"); + return data.time[timeType] || err.get("noData"); + } + timeReturnJson() { + return JSON.stringify(data.time) || err.get("time") || "{}"; + } + batchAdd({ namespace, parameters }) { + if (!data.batchRequests) data.batchRequests = []; + try { + data.batchRequests.push({ + namespace: namespace, + parameters: JSON.parse(parameters), + }); + } catch (err) { + data.batchRequests.push({ + namespace: namespace, + parameters: {}, + }); + } + } + batchClear() { + data.batchRequests = undefined; + } + batchJson() { + return JSON.stringify(data.batchRequests) || err.get("noData") || "{}"; + } + batchCall({ parameter }) { + return new Promise((resolve) => + GameJolt.SendBatchRequest( + data.batchRequests.map( + (I) => + `/${I.namespace + .split("/") + .map((i) => encodeURIComponent(i)) + .join("/")}/` + + `?game_id=${GameJolt.iGameID}` + + `&${new URLSearchParams(I.parameters).toString()}` + ), + parameter, + (pResponse) => { + if (pResponse.success != trueStr) { + [err.batch, err.last] = [pResponse.message, pResponse.message]; + data.batch = undefined; + resolve(); + return; + } + data.batch = pResponse.responses; + err.batch = undefined; + resolve(); + } + ) + ); + } + batchReturnJson() { + return JSON.stringify(data.batch) || err.get("batch") || "{}"; + } + } + Scratch.extensions.register(new GameJoltAPI()); +})(Scratch); From 5d35e74861b007e8c9c04653ab612c992760ad49 Mon Sep 17 00:00:00 2001 From: "Mike J. Renaker / \"MikeDEV" Date: Sun, 5 Nov 2023 18:30:34 -0500 Subject: [PATCH 039/196] cloudlink: Update to v0.1.0 (#1098) --- extensions/cloudlink.js | 2907 +++++++++++++++++++++++---------------- 1 file changed, 1709 insertions(+), 1198 deletions(-) diff --git a/extensions/cloudlink.js b/extensions/cloudlink.js index 8496737d54..64f05bb998 100644 --- a/extensions/cloudlink.js +++ b/extensions/cloudlink.js @@ -1,1838 +1,2349 @@ // Name: Cloudlink // ID: cloudlink -// Description: Powerful WebSocket extension for Scratch 3. +// Description: A powerful WebSocket extension for Scratch. // By: MikeDEV -// Copy of S4-0_nosuite.js as of 10/31/2022 /* eslint-disable */ - +// prettier-ignore (function (Scratch) { - var servers = {}; // Server list - let mWS = null; - // Get the server URL list - try { - Scratch.fetch( - "https://raw.githubusercontent.com/MikeDev101/cloudlink/master/serverlist.json" - ) - .then((response) => { - return response.text(); - }) - .then((data) => { - servers = JSON.parse(data); - }) - .catch((err) => { - console.log(err); - servers = {}; - }); - } catch (err) { - console.log(err); - servers = {}; + /* + CloudLink Extension for TurboWarp v0.1.1. + + This extension should be fully compatible with projects developed using + extensions S4.1, S4.0, and B3.0. + + Server versions supported via backward compatibility: + - CL3 0.1.5 (was called S2.2) + - CL3 0.1.7 + - CL4 0.1.8.x + - CL4 0.1.9.x + - CL4 0.2.0 (latest) + + MIT License + Copyright 2023 Mike J. Renaker / "MikeDEV". + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE + FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ + + // Require extension to be unsandboxed. + 'use strict'; + if (!Scratch.extensions.unsandboxed) { + throw new Error('The CloudLink extension must run unsandboxed.'); + } + + // Declare icons as static SVG URIs + const cl_icon = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyMjUuMzU0OCIgaGVpZ2h0PSIyMjUuMzU0OCIgdmlld0JveD0iMCwwLDIyNS4zNTQ4LDIyNS4zNTQ4Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3LjMyMjYsLTY3LjMyMjYpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xMjcuMzIyNiwxODBjMCwtNjIuMjMwMDEgNTAuNDQ3MzksLTExMi42Nzc0IDExMi42Nzc0LC0xMTIuNjc3NGM2Mi4yMzAwMSwwIDExMi42Nzc0LDUwLjQ0NzM5IDExMi42Nzc0LDExMi42Nzc0YzAsNjIuMjMwMDEgLTUwLjQ0NzM5LDExMi42Nzc0IC0xMTIuNjc3NCwxMTIuNjc3NGMtNjIuMjMwMDEsMCAtMTEyLjY3NzQsLTUwLjQ0NzM5IC0xMTIuNjc3NCwtMTEyLjY3NzR6IiBmaWxsPSIjMDBjMjhjIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS13aWR0aD0iMCIvPjxnIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLXdpZHRoPSIxIj48cGF0aCBkPSJNMjg2LjEyMDM3LDE1MC41NTc5NWMyMy4yNDA4NiwwIDQyLjA3ODksMTguODM5NDYgNDIuMDc4OSw0Mi4wNzg5YzAsMjMuMjM5NDQgLTE4LjgzODAzLDQyLjA3ODkgLTQyLjA3ODksNDIuMDc4OWgtOTIuMjQwNzRjLTIzLjI0MDg2LDAgLTQyLjA3ODksLTE4LjgzOTQ2IC00Mi4wNzg5LC00Mi4wNzg5YzAsLTIzLjIzOTQ0IDE4LjgzODAzLC00Mi4wNzg5IDQyLjA3ODksLTQyLjA3ODloNC4xODg4N2MxLjgxMTUzLC0yMS41NzA1NSAxOS44OTM1NywtMzguNTEyODkgNDEuOTMxNSwtMzguNTEyODljMjIuMDM3OTMsMCA0MC4xMTk5NywxNi45NDIzNCA0MS45MzE1LDM4LjUxMjg5eiIgZmlsbD0iI2ZmZmZmZiIvPjxwYXRoIGQ9Ik0yODkuMDg2NTUsMjEwLjM0MTE0djkuMDQ2NjdoLTI2LjkxNjYzaC05LjA0NjY3di05LjA0NjY3di01NC41MDMzOWg5LjA0NjY3djU0LjUwMzM5eiIgZmlsbD0iIzAwYzI4YyIvPjxwYXRoIGQ9Ik0yMjIuNDA5MjUsMjE5LjM4NzgxYy04LjM1MzIsMCAtMTYuMzY0MzEsLTMuMzE4MzQgLTIyLjI3MDksLTkuMjI0OTJjLTUuOTA2NjEsLTUuOTA2NTggLTkuMjI0OTEsLTEzLjkxNzY4IC05LjIyNDkxLC0yMi4yNzA4OWMwLC04LjM1MzIgMy4zMTgyOSwtMTYuMzY0MzEgOS4yMjQ5MSwtMjIuMjcwOWM1LjkwNjU5LC01LjkwNjYxIDEzLjkxNzcsLTkuMjI0OTEgMjIuMjcwOSwtOS4yMjQ5MWgyMS4xMDg5djguOTM0OThoLTIxLjEwODl2MC4xMDI1N2MtNS45NTYyOCwwIC0xMS42Njg2NCwyLjM2NjE2IC0xNS44ODAzNyw2LjU3Nzg5Yy00LjIxMTczLDQuMjExNzMgLTYuNTc3ODksOS45MjQwOCAtNi41Nzc4OSwxNS44ODAzN2MwLDUuOTU2MjggMi4zNjYxNiwxMS42Njg2NCA2LjU3Nzg5LDE1Ljg4MDM3YzQuMjExNzMsNC4yMTE3MyA5LjkyNDA4LDYuNTc3OTMgMTUuODgwMzcsNi41Nzc5M3YwLjEwMjUzaDIxLjEwODl2OC45MzQ5OHoiIGZpbGw9IiMwMGMyOGMiLz48L2c+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTEyLjY3NzQwNDA4NDA4MzkyOjExMi42Nzc0MDQwODQwODQwMy0tPg=="; + const cl_block = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNzYuMzk4NTQiIGhlaWdodD0iMTIyLjY3MDY5IiB2aWV3Qm94PSIwLDAsMTc2LjM5ODU0LDEyMi42NzA2OSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE1MS44MDA3MywtMTE4LjY2NDY2KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PGc+PHBhdGggZD0iTTI4Ni4xMjAzNywxNTcuMTc3NTVjMjMuMjQwODYsMCA0Mi4wNzg5LDE4LjgzOTQ2IDQyLjA3ODksNDIuMDc4OWMwLDIzLjIzOTQ0IC0xOC44MzgwMyw0Mi4wNzg5IC00Mi4wNzg5LDQyLjA3ODloLTkyLjI0MDc0Yy0yMy4yNDA4NiwwIC00Mi4wNzg5LC0xOC44Mzk0NiAtNDIuMDc4OSwtNDIuMDc4OWMwLC0yMy4yMzk0NCAxOC44MzgwMywtNDIuMDc4OSA0Mi4wNzg5LC00Mi4wNzg5aDQuMTg4ODdjMS44MTE1MywtMjEuNTcwNTUgMTkuODkzNTcsLTM4LjUxMjg5IDQxLjkzMTUsLTM4LjUxMjg5YzIyLjAzNzkzLDAgNDAuMTE5OTcsMTYuOTQyMzQgNDEuOTMxNSwzOC41MTI4OXoiIGZpbGw9IiNmZmZmZmYiLz48cGF0aCBkPSJNMjg5LjA4NjU1LDIxNi45NjA3NHY5LjA0NjY3aC0yNi45MTY2M2gtOS4wNDY2N3YtOS4wNDY2N3YtNTQuNTAzMzloOS4wNDY2N3Y1NC41MDMzOXoiIGZpbGw9IiMwMGMyOGMiLz48cGF0aCBkPSJNMjIyLjQwOTI1LDIyNi4wMDc0MWMtOC4zNTMyLDAgLTE2LjM2NDMxLC0zLjMxODM0IC0yMi4yNzA5LC05LjIyNDkyYy01LjkwNjYxLC01LjkwNjU4IC05LjIyNDkxLC0xMy45MTc2OCAtOS4yMjQ5MSwtMjIuMjcwODljMCwtOC4zNTMyIDMuMzE4MjksLTE2LjM2NDMxIDkuMjI0OTEsLTIyLjI3MDljNS45MDY1OSwtNS45MDY2MSAxMy45MTc3LC05LjIyNDkxIDIyLjI3MDksLTkuMjI0OTFoMjEuMTA4OXY4LjkzNDk4aC0yMS4xMDg5djAuMTAyNTdjLTUuOTU2MjgsMCAtMTEuNjY4NjQsMi4zNjYxNiAtMTUuODgwMzcsNi41Nzc4OWMtNC4yMTE3Myw0LjIxMTczIC02LjU3Nzg5LDkuOTI0MDggLTYuNTc3ODksMTUuODgwMzdjMCw1Ljk1NjI4IDIuMzY2MTYsMTEuNjY4NjQgNi41Nzc4OSwxNS44ODAzN2M0LjIxMTczLDQuMjExNzMgOS45MjQwOCw2LjU3NzkzIDE1Ljg4MDM3LDYuNTc3OTN2MC4xMDI1M2gyMS4xMDg5djguOTM0OTh6IiBmaWxsPSIjMDBjMjhjIi8+PC9nPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjg4LjE5OTI2OTk5OTk5OTk4OjYxLjMzNTM0NDk5OTk5OTk5LS0+"; + + // Declare VM + const vm = Scratch.vm; + const runtime = vm.runtime; + + /* + This versioning system is intended for future use with CloudLink. + + When the client sends the handshake request, it will provide the server with the following details: + { + "cmd": "handshake", + "val": { + "language": "Scratch", + "version": { + "editorType": String, + "fullString": String + } + } + } + + version.editorType - Provides info regarding the Scratch IDE this Extension variant natively supports. Intended for server-side version identification. + version.versionNumber - Numerical version info. Increment by 1 every Semantic Versioning Patch. Intended for server-side version identification. + version.versionString - Semantic Versioning string. Intended for source-code versioning only. + + The extension will auto-generate a version string by using generateVersionString(). + */ + const version = { + editorType: "TurboWarp", + versionNumber: 1, + versionString: "0.1.1", + }; + + // Store extension state + var clVars = { + + // Editor-specific variable for hiding old, legacy-support blocks. + hideCLDeprecatedBlocks: true, + + // WebSocket object. + socket: null, + + // Disable nags about old servers. + currentServerUrl: "", + lastServerUrl: "", + + // gmsg.queue - An array of all currently queued gmsg values. + // gmsg.varState - The value of the most recently received gmsg message. + // gmsg.hasNew - Returns true if a new gmsg value has been received. + gmsg: { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }, + + // pmsg.queue - An array of all currently queued pmsg values. + // pmsg.varState - The value of the most recently received pmsg message. + // pmsg.hasNew - Returns true if a new pmsg value has been received. + pmsg: { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }, + + // gvar.queue - An array of all currently queued gvar values. + // gvar.varStates - A dictionary storing each gvar variable. + gvar: { + queue: [], + varStates: {}, + eventHatTick: false, + }, + + // pvar.queue - An array of all currently queued pvar values. + // pvar.varStates - A dictionary storing each pvar variable. + pvar: { + queue: [], + varStates: {}, + eventHatTick: false, + }, + + // direct.queue - An array of all currently queued direct values. + // direct.varState - The value of the most recently received direct message. + // direct.hasNew - Returns true if a new direct value has been received. + direct: { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }, + + // statuscode.queue - An array of all currently queued statuscode values. + // statuscode.varState - The value of the most recently received statuscode message. + // statuscode.hasNew - Returns true if a new statuscode value has been received. + statuscode: { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }, + + // ulist stores all currently connected client objects in the server/all subscribed room(s). + ulist: [], + + // Message-Of-The-Day + motd: "", + + // Client IP address + client_ip: "", + + // Server version string + server_version: "", + + // listeners.enablerState - Set to true when "createListener" is used. + // listeners.enablerValue - Set to a new listener ID when "createListener" is used. + // listeners.current - Keeps track of all current listener IDs being awaited. + // listeners.varStates - Storage for all successfully awaited messages from specific listener IDs. + listeners: { + enablerState: false, + enablerValue: "", + current: [], + varStates: {}, + }, + + // rooms.enablerState - Set to true when "selectRoomsInNextPacket" is used. + // rooms.enablerValue - Set to a new list of rooms when "selectRoomsInNextPacket" is used. + // rooms.current - Keeps track of all current rooms being used. + // rooms.varStates - Storage for all per-room messages. + // rooms.isLinked - Set to true when a room link request is successful. False when unlinked. + // rooms.isAttemptingLink - Set to true when running "linkToRooms()". + // rooms.isAttemptingUnlink - Set to true when running "unlinkFromRooms()". + rooms: { + enablerState: false, + enablerValue: "", + isLinked: false, + isAttemptingLink: false, + isAttemptingUnlink: false, + current: [], + varStates: {}, + }, + + // Username state + username: { + attempted: false, + accepted: false, + temp: "", + value: "", + }, + + // Store user_obj messages. + myUserObject: {}, + + /* + linkState.status - Current state of the connection. + 0 - Ready + 1 - Connecting + 2 - Connected + 3 - Disconnected, gracefully (OK) + 4 - Disconnected, abruptly (Connection failed / dropped) + + linkState.isAttemptingGracefulDisconnect - Boolean used to ignore websocket codes when disconnecting. + + linkstate.disconnectType - Type of disconnect that has occurred. + 0 - Safely disconnected (connected OK and gracefully disconnected) + 1 - Connection dropped (connected OK but lost connection afterwards) + 2 - Connection failed (attempted connection but did not succeed) + + linkstate.identifiedProtocol - Enables backwards compatibility for CL servers. + 0 - CL3 0.1.5 "S2.2" - Doesn't support listeners, MOTD, or statuscodes. + 1 - CL3 0.1.7 - Doesn't support listeners, has early MOTD support, and early statuscode support. + 2 - CL4 0.1.8.x - First version to support listeners, and modern server_version support. First version to implement rooms support. + 3 - CL4 0.1.9.x - First version to implement the handshake command and better ulist events. + 4 - CL4 0.2.0 - Latest version. First version to implement client_obj and enhanced ulists. + */ + linkState: { + status: 0, + isAttemptingGracefulDisconnect: false, + disconnectType: 0, + identifiedProtocol: 0, + }, + + // Timeout of 500ms upon connection to try and handshake. Automatically aborted if server_version is received within that timespan. + handshakeTimeout: null, + + // Prevent accidentally sending the handshake command more than once per connection. + handshakeAttempted: false, + + // Storage for the publically available CloudLink instances. + serverList: {}, + } + + function generateVersionString() { + return `${version.editorType} ${version.versionString}`; } - function find_id(ID, ulist) { - // Thanks StackOverflow! - if (jsonCheck(ID) && !intCheck(ID)) { - return ulist.some( - (o) => - o.username === JSON.parse(ID).username && o.id == JSON.parse(ID).id - ); + // Makes values safe for Scratch to represent. + async function makeValueScratchSafe(data) { + if (typeof data == "object") { + try { + return JSON.stringify(data); + } catch (SyntaxError) { + return String(data); + } } else { - return ulist.some((o) => o.username === String(ID) || o.id == ID); + return String(data); } } - function jsonCheck(JSON_STRING) { + // Clears out and resets the various values of clVars upon disconnect. + function resetOnClose() { + window.clearTimeout(clVars.handshakeTimeout); + clVars.handshakeAttempted = false; + clVars.socket = null; + clVars.motd = ""; + clVars.client_ip = ""; + clVars.server_version = ""; + clVars.linkState.identifiedProtocol = 0; + clVars.linkState.isAttemptingGracefulDisconnect = false; + clVars.myUserObject = {}; + clVars.gmsg = { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }; + clVars.pmsg = { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }; + clVars.gvar = { + queue: [], + varStates: {}, + eventHatTick: false, + }; + clVars.pvar = { + queue: [], + varStates: {}, + eventHatTick: false, + }; + clVars.direct = { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }; + clVars.statuscode = { + queue: [], + varState: "", + hasNew: false, + eventHatTick: false, + }; + clVars.ulist = []; + clVars.listeners = { + enablerState: false, + enablerValue: "", + current: [], + varStates: {}, + }; + clVars.rooms = { + enablerState: false, + enablerValue: "", + isLinked: false, + isAttemptingLink: false, + isAttemptingUnlink: false, + current: [], + varStates: {}, + }; + clVars.username = { + attempted: false, + accepted: false, + temp: "", + value: "", + }; + } + + // CL-specific netcode needed for sending messages + async function sendMessage(message) { + // Prevent running this while disconnected + if (clVars.socket == null) { + console.warn("[CloudLink] Ignoring attempt to send a packet while disconnected."); + return; + } + + // See if the outgoing val argument can be converted into JSON + if (message.hasOwnProperty("val")) { + try { + message.val = JSON.parse(message.val); + } catch {} + } + + // Attach listeners + if (clVars.listeners.enablerState) { + + // 0.1.8.x was the first server version to support listeners. + if (clVars.linkState.identifiedProtocol >= 2) { + message.listener = clVars.listeners.enablerValue; + + // Create listener + clVars.listeners.varStates[String(args.ID)] = { + hasNew: false, + varState: {}, + eventHatTick: false, + }; + + } else { + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support listeners."); + } + clVars.listeners.enablerState = false; + } + + // Check if server supports rooms + if (((message.cmd == "link") || (message.cmd == "unlink")) && (clVars.linkState.identifiedProtocol < 2)) { + // 0.1.8.x was the first server version to support rooms. + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support room linking/unlinking."); + return; + } + + // Convert the outgoing message to JSON + let outgoing = ""; try { - JSON.parse(JSON_STRING); - return true; - } catch (err) { - return false; + outgoing = JSON.stringify(message); + } catch (SyntaxError) { + console.warn("[CloudLink] Failed to send a packet, invalid syntax:", message); + return; } + + // Send the message + console.log("[CloudLink] TX:", message); + clVars.socket.send(outgoing); } - function intCheck(value) { - return !isNaN(value); + // Only sends the handshake command. + function sendHandshake() { + if (clVars.handshakeAttempted) return; + console.log("[CloudLink] Sending handshake..."); + sendMessage({ + cmd: "handshake", + val: { + language: "Scratch", + version: { + editorType: version.editorType, + versionNumber: version.versionNumber, + }, + }, + listener: "handshake_cfg" + }); + clVars.handshakeAttempted = true; } - function autoConvert(value) { - // Check if the value is JSON / Dict first - try { - JSON.parse(value); - return JSON.parse(value); - } catch (err) {} + // Compare the version string of the server to known compatible variants to configure clVars.linkState.identifiedProtocol. + async function setServerVersion(version) { + console.log(`[CloudLink] Server version: ${String(version)}`); + clVars.server_version = version; + + // Auto-detect versions + const versions = { + "0.2.": 4, + "0.1.9": 3, + "0.1.8": 2, + "0.1.7": 1, + "0.1.5": 0, + "S2.2": 0, // 0.1.5 + "0.1.": 0, // 0.1.5 or legacy + "S2.": 0, // Legacy + "S1.": -1 // Obsolete + }; + + for (const [key, value] of Object.entries(versions)) { + if (version.includes(key)) { + if (clVars.linkState.identifiedProtocol < value) { + + // Disconnect if protcol is too old + if (value == -1) { + console.warn(`[CloudLink] Server is too old to enable leagacy support. Disconnecting.`); + return clVars.socket.close(1000, ""); + } + + // Set the identified protocol variant + clVars.linkState.identifiedProtocol = value; + } + } + }; + + // Log configured spec version + console.log(`[CloudLink] Configured protocol spec to v${clVars.linkState.identifiedProtocol}.`); + + // Don't nag user if they already trusted this server + if (clVars.currentServerUrl === clVars.lastServerUrl) return; + + // Ask user if they wish to stay connected if the server is unsupported + if ((clVars.linkState.identifiedProtocol < 4) && (!confirm( + `You have connected to an old CloudLink server, running version ${clVars.server_version}.\n\nFor your security and privacy, we recommend you disconnect from this server and connect to an up-to-date server.\n\nClick/tap \"OK\" to stay connected.` + ))) { + // Close the connection if they choose "Cancel" + clVars.linkState.isAttemptingGracefulDisconnect = true; + clVars.socket.close(1000, "Client going away (legacy server rejected by end user)"); + return; + } + + // Don't nag user the next time they connect to this server + clVars.lastServerUrl = clVars.currentServerUrl; + } - // Check if the value is an array + // CL-specific netcode needed to make the extension work + async function handleMessage(data) { + // Parse the message JSON + let packet = {}; try { - tmp = value; - tmp = tmp.replace(/'/g, '"'); - JSON.parse(tmp); - return JSON.parse(tmp); - } catch (err) {} + packet = JSON.parse(data) + } catch (SyntaxError) { + console.error("[CloudLink] Incoming message parse failure! Is this really a CloudLink server?", data); + return; + }; + + // Handle packet commands + if (!packet.hasOwnProperty("cmd")) { + console.error("[CloudLink] Incoming message read failure! This message doesn't contain the required \"cmd\" key. Is this really a CloudLink server?", packet); + return; + } + console.log("[CloudLink] RX:", packet); + switch (packet.cmd) { + case "gmsg": + clVars.gmsg.varState = packet.val; + clVars.gmsg.hasNew = true; + clVars.gmsg.queue.push(packet); + clVars.gmsg.eventHatTick = true; + break; + + case "pmsg": + clVars.pmsg.varState = packet.val; + clVars.pmsg.hasNew = true; + clVars.pmsg.queue.push(packet); + clVars.pmsg.eventHatTick = true; + break; + + case "gvar": + clVars.gvar.varStates[String(packet.name)] = { + hasNew: true, + varState: packet.val, + eventHatTick: true, + }; + clVars.gvar.queue.push(packet); + clVars.gvar.eventHatTick = true; + break; + + case "pvar": + clVars.pvar.varStates[String(packet.name)] = { + hasNew: true, + varState: packet.val, + eventHatTick: true, + }; + clVars.pvar.queue.push(packet); + clVars.pvar.eventHatTick = true; + break; + + case "direct": + // Handle events from older server versions + if (packet.val.hasOwnProperty("cmd")) { + switch (packet.val.cmd) { + // Server 0.1.5 (at least) + case "vers": + window.clearTimeout(clVars.handshakeTimeout); + setServerVersion(packet.val.val); + return; + + // Server 0.1.7 (at least) + case "motd": + console.log(`[CloudLink] Message of the day: \"${packet.val.val}\"`); + clVars.motd = packet.val.val; + return; + } + } + + // Store direct value + clVars.direct.varState = packet.val; + clVars.direct.hasNew = true; + clVars.direct.queue.push(packet); + clVars.direct.eventHatTick = true; + break; + + case "client_obj": + console.log("[CloudLink] Client object for this session:", packet.val); + clVars.myUserObject = packet.val; + break; + + case "statuscode": + // Store direct value + // Protocol v0 (0.1.5 and legacy) don't implement status codes. + if (clVars.linkState.identifiedProtocol == 0) { + console.warn("[CloudLink] Received a statuscode message while using protocol v0. This event shouldn't happen. It's likely that this server is modified (did MikeDEV overlook some unexpected behavior?)."); + return; + } + + // Protocol v1 (0.1.7) uses "val" to represent the code. + else if (clVars.linkState.identifiedProtocol == 1) { + clVars.statuscode.varState = packet.val; + } + + // Protocol v2 (0.1.8.x) uses "code" instead. + // Protocol v3-v4 (0.1.9.x - latest, 0.2.0) adds "code_id" to the payload. Ignored by Scratch clients. + else { + + // Handle setup listeners + if (packet.hasOwnProperty("listener")) { + switch (packet.listener) { + case "username_cfg": + + // Username accepted + if (packet.code.includes("I:100")) { + clVars.myUserObject = packet.val; + clVars.username.value = packet.val.username; + clVars.username.accepted = true; + console.log(`[CloudLink] Username has been set to \"${clVars.username.value}\" successfully!`); + + // Username rejected / error + } else { + console.log(`[CloudLink] Username rejected by the server! Error code ${packet.code}.}`); + } + return; + + case "handshake_cfg": + // Prevent handshake responses being stored in the statuscode variables + console.log("[CloudLink] Server responded to our handshake!"); + return; + + case "link": + // Room link accepted + if (!clVars.rooms.isAttemptingLink) return; + if (packet.code.includes("I:100")) { + clVars.rooms.isAttemptingLink = false; + clVars.rooms.isLinked = true; + console.log("[CloudLink] Room linked successfully!"); + + // Room link rejected / error + } else { + console.log(`[CloudLink] Room link rejected! Error code ${packet.code}.}`); + } + return; + + case "unlink": + // Room unlink accepted + if (!clVars.rooms.isAttemptingUnlink) return; + if (packet.code.includes("I:100")) { + clVars.rooms.isAttemptingUnlink = false; + clVars.rooms.isLinked = false; + console.log("[CloudLink] Room unlinked successfully!"); + + // Room link rejected / error + } else { + console.log(`[CloudLink] Room unlink rejected! Error code ${packet.code}.}`); + } + return; + } + } + + // Update state + clVars.statuscode.varState = packet.code; + } + + // Update state + clVars.statuscode.hasNew = true; + clVars.statuscode.queue.push(packet); + clVars.statuscode.eventHatTick = true; + break; + + case "ulist": + // Protocol v0-v1 (0.1.5 and legacy - 0.1.7) use a semicolon (;) separated string for the userlist. + if ( + (clVars.linkState.identifiedProtocol == 0) + || + (clVars.linkState.identifiedProtocol == 1) + ) { + // Split the username list string + clVars.ulist = String(packet.val).split(';'); + + // Get rid of blank entry at the end of the list + clVars.ulist.pop(clVars.ulist.length); + + // Check if username has been set (since older servers don't implement statuscodes or listeners) + if ((clVars.username.attempted) && (clVars.ulist.includes(clVars.username.temp))) { + clVars.username.value = clVars.username.temp; + clVars.username.accepted = true; + console.log(`[CloudLink] Username has been set to \"${clVars.username.value}\" successfully!`); + } + } + + // Protocol v2 (0.1.8.x) uses a list of objects w/ "username" and "id" instead. + else if (clVars.linkState.identifiedProtocol == 2) { + clVars.ulist = packet.val; + } + + // Protocol v3-v4 (0.1.9.x - latest, 0.2.0) uses "mode" to add/set/remove entries to the userlist. + else { + // Check for "mode" key + if (!packet.hasOwnProperty("mode")) { + console.warn("[CloudLink] Userlist message did not specify \"mode\" while running in protocol mode 3 or 4."); + return; + }; + // Handle methods + switch (packet.mode) { + case 'set': + clVars.ulist = packet.val; + break; + case 'add': + clVars.ulist.push(packet.val); + break; + case 'remove': + clVars.ulist.slice(clVars.ulist.indexOf(packet.val), 1); + break; + default: + console.warn(`[CloudLink] Unrecognised userlist mode: \"${packet.mode}\".`); + break; + } + } + + console.log("[CloudLink] Updating userlist:", clVars.ulist); + break; + + case "server_version": + window.clearTimeout(clVars.handshakeTimeout); + setServerVersion(packet.val); + break; + + case "client_ip": + console.log(`[CloudLink] Client IP address: ${packet.val}`); + console.warn("[CloudLink] This server has relayed your identified IP address to you. Under normal circumstances, this will be erased server-side when you disconnect, but you should still be careful. Unless you trust this server, it is not recommended to send login credentials or personal info."); + clVars.client_ip = packet.val; + break; - // Check if an int/float - if (!isNaN(value)) { - return Number(value); + case "motd": + console.log(`[CloudLink] Message of the day: \"${packet.val}\"`); + clVars.motd = packet.val; + break; + + default: + console.warn(`[CloudLink] Unrecognised command: \"${packet.cmd}\".`); + return; } - // Leave as the original value if none of the above work - return value; + // Handle listeners + if (packet.hasOwnProperty("listener")) { + if (clVars.listeners.current.includes(String(packet.listener))) { + + // Remove the listener from the currently listening list + clVars.listeners.current.splice( + clVars.listeners.current.indexOf(String(packet.listener)), + 1 + ); + + // Update listener states + clVars.listeners.varStates[String(packet.listener)] = { + hasNew: true, + varState: packet, + eventHatTick: true, + }; + } + } } - class CloudLink { - constructor(runtime, extensionId) { - // Extension stuff - this.runtime = runtime; - this.cl_icon = - "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyMjUuMzU0OCIgaGVpZ2h0PSIyMjUuMzU0OCIgdmlld0JveD0iMCwwLDIyNS4zNTQ4LDIyNS4zNTQ4Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3LjMyMjYsLTY3LjMyMjYpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xMjcuMzIyNiwxODBjMCwtNjIuMjMwMDEgNTAuNDQ3MzksLTExMi42Nzc0IDExMi42Nzc0LC0xMTIuNjc3NGM2Mi4yMzAwMSwwIDExMi42Nzc0LDUwLjQ0NzM5IDExMi42Nzc0LDExMi42Nzc0YzAsNjIuMjMwMDEgLTUwLjQ0NzM5LDExMi42Nzc0IC0xMTIuNjc3NCwxMTIuNjc3NGMtNjIuMjMwMDEsMCAtMTEyLjY3NzQsLTUwLjQ0NzM5IC0xMTIuNjc3NCwtMTEyLjY3NzR6IiBmaWxsPSIjMDBjMjhjIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS13aWR0aD0iMCIvPjxnIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLXdpZHRoPSIxIj48cGF0aCBkPSJNMjg2LjEyMDM3LDE1MC41NTc5NWMyMy4yNDA4NiwwIDQyLjA3ODksMTguODM5NDYgNDIuMDc4OSw0Mi4wNzg5YzAsMjMuMjM5NDQgLTE4LjgzODAzLDQyLjA3ODkgLTQyLjA3ODksNDIuMDc4OWgtOTIuMjQwNzRjLTIzLjI0MDg2LDAgLTQyLjA3ODksLTE4LjgzOTQ2IC00Mi4wNzg5LC00Mi4wNzg5YzAsLTIzLjIzOTQ0IDE4LjgzODAzLC00Mi4wNzg5IDQyLjA3ODksLTQyLjA3ODloNC4xODg4N2MxLjgxMTUzLC0yMS41NzA1NSAxOS44OTM1NywtMzguNTEyODkgNDEuOTMxNSwtMzguNTEyODljMjIuMDM3OTMsMCA0MC4xMTk5NywxNi45NDIzNCA0MS45MzE1LDM4LjUxMjg5eiIgZmlsbD0iI2ZmZmZmZiIvPjxwYXRoIGQ9Ik0yODkuMDg2NTUsMjEwLjM0MTE0djkuMDQ2NjdoLTI2LjkxNjYzaC05LjA0NjY3di05LjA0NjY3di01NC41MDMzOWg5LjA0NjY3djU0LjUwMzM5eiIgZmlsbD0iIzAwYzI4YyIvPjxwYXRoIGQ9Ik0yMjIuNDA5MjUsMjE5LjM4NzgxYy04LjM1MzIsMCAtMTYuMzY0MzEsLTMuMzE4MzQgLTIyLjI3MDksLTkuMjI0OTJjLTUuOTA2NjEsLTUuOTA2NTggLTkuMjI0OTEsLTEzLjkxNzY4IC05LjIyNDkxLC0yMi4yNzA4OWMwLC04LjM1MzIgMy4zMTgyOSwtMTYuMzY0MzEgOS4yMjQ5MSwtMjIuMjcwOWM1LjkwNjU5LC01LjkwNjYxIDEzLjkxNzcsLTkuMjI0OTEgMjIuMjcwOSwtOS4yMjQ5MWgyMS4xMDg5djguOTM0OThoLTIxLjEwODl2MC4xMDI1N2MtNS45NTYyOCwwIC0xMS42Njg2NCwyLjM2NjE2IC0xNS44ODAzNyw2LjU3Nzg5Yy00LjIxMTczLDQuMjExNzMgLTYuNTc3ODksOS45MjQwOCAtNi41Nzc4OSwxNS44ODAzN2MwLDUuOTU2MjggMi4zNjYxNiwxMS42Njg2NCA2LjU3Nzg5LDE1Ljg4MDM3YzQuMjExNzMsNC4yMTE3MyA5LjkyNDA4LDYuNTc3OTMgMTUuODgwMzcsNi41Nzc5M3YwLjEwMjUzaDIxLjEwODl2OC45MzQ5OHoiIGZpbGw9IiMwMGMyOGMiLz48L2c+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTEyLjY3NzQwNDA4NDA4MzkyOjExMi42Nzc0MDQwODQwODQwMy0tPg=="; - this.cl_block = - "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNzYuMzk4NTQiIGhlaWdodD0iMTIyLjY3MDY5IiB2aWV3Qm94PSIwLDAsMTc2LjM5ODU0LDEyMi42NzA2OSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE1MS44MDA3MywtMTE4LjY2NDY2KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PGc+PHBhdGggZD0iTTI4Ni4xMjAzNywxNTcuMTc3NTVjMjMuMjQwODYsMCA0Mi4wNzg5LDE4LjgzOTQ2IDQyLjA3ODksNDIuMDc4OWMwLDIzLjIzOTQ0IC0xOC44MzgwMyw0Mi4wNzg5IC00Mi4wNzg5LDQyLjA3ODloLTkyLjI0MDc0Yy0yMy4yNDA4NiwwIC00Mi4wNzg5LC0xOC44Mzk0NiAtNDIuMDc4OSwtNDIuMDc4OWMwLC0yMy4yMzk0NCAxOC44MzgwMywtNDIuMDc4OSA0Mi4wNzg5LC00Mi4wNzg5aDQuMTg4ODdjMS44MTE1MywtMjEuNTcwNTUgMTkuODkzNTcsLTM4LjUxMjg5IDQxLjkzMTUsLTM4LjUxMjg5YzIyLjAzNzkzLDAgNDAuMTE5OTcsMTYuOTQyMzQgNDEuOTMxNSwzOC41MTI4OXoiIGZpbGw9IiNmZmZmZmYiLz48cGF0aCBkPSJNMjg5LjA4NjU1LDIxNi45NjA3NHY5LjA0NjY3aC0yNi45MTY2M2gtOS4wNDY2N3YtOS4wNDY2N3YtNTQuNTAzMzloOS4wNDY2N3Y1NC41MDMzOXoiIGZpbGw9IiMwMGMyOGMiLz48cGF0aCBkPSJNMjIyLjQwOTI1LDIyNi4wMDc0MWMtOC4zNTMyLDAgLTE2LjM2NDMxLC0zLjMxODM0IC0yMi4yNzA5LC05LjIyNDkyYy01LjkwNjYxLC01LjkwNjU4IC05LjIyNDkxLC0xMy45MTc2OCAtOS4yMjQ5MSwtMjIuMjcwODljMCwtOC4zNTMyIDMuMzE4MjksLTE2LjM2NDMxIDkuMjI0OTEsLTIyLjI3MDljNS45MDY1OSwtNS45MDY2MSAxMy45MTc3LC05LjIyNDkxIDIyLjI3MDksLTkuMjI0OTFoMjEuMTA4OXY4LjkzNDk4aC0yMS4xMDg5djAuMTAyNTdjLTUuOTU2MjgsMCAtMTEuNjY4NjQsMi4zNjYxNiAtMTUuODgwMzcsNi41Nzc4OWMtNC4yMTE3Myw0LjIxMTczIC02LjU3Nzg5LDkuOTI0MDggLTYuNTc3ODksMTUuODgwMzdjMCw1Ljk1NjI4IDIuMzY2MTYsMTEuNjY4NjQgNi41Nzc4OSwxNS44ODAzN2M0LjIxMTczLDQuMjExNzMgOS45MjQwOCw2LjU3NzkzIDE1Ljg4MDM3LDYuNTc3OTN2MC4xMDI1M2gyMS4xMDg5djguOTM0OTh6IiBmaWxsPSIjMDBjMjhjIi8+PC9nPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjg4LjE5OTI2OTk5OTk5OTk4OjYxLjMzNTM0NDk5OTk5OTk5LS0+"; - - // Socket data - this.socketData = { - gmsg: [], - pmsg: [], - direct: [], - statuscode: [], - gvar: [], - pvar: [], - motd: "", - client_ip: "", - ulist: [], - server_version: "", - }; - this.varData = { - gvar: {}, - pvar: {}, - }; + // Basic netcode needed to make the extension work + async function newClient(url) { + if (!(await Scratch.canFetch(url))) { + console.warn("[CloudLink] Did not get permission to connect, aborting..."); + return; + } - this.queueableCmds = [ - "gmsg", - "pmsg", - "gvar", - "pvar", - "direct", - "statuscode", - ]; - this.varCmds = ["gvar", "pvar"]; - - // Listeners - this.socketListeners = {}; - this.socketListenersData = {}; - this.newSocketData = { - gmsg: false, - pmsg: false, - direct: false, - statuscode: false, - gvar: false, - pvar: false, - }; + // Set the link state to connecting + clVars.linkState.status = 1; + clVars.linkState.disconnectType = 0; - // Edge-triggered hat blocks - this.connect_hat = 0; - this.packet_hat = 0; - this.close_hat = 0; - - // Status stuff - this.isRunning = false; - this.isLinked = false; - this.version = "S4.0"; - this.link_status = 0; - this.username = ""; - this.tmp_username = ""; - this.isUsernameSyncing = false; - this.isUsernameSet = false; - this.disconnectWasClean = false; - this.wasConnectionDropped = false; - this.didConnectionFail = false; - this.protocolOk = false; - - // Listeners stuff - this.enableListener = false; - this.setListener = ""; - - // Rooms stuff - this.enableRoom = false; - this.isRoomSetting = false; - this.selectRoom = ""; - - // Remapping stuff - this.menuRemap = { - "Global data": "gmsg", - "Private data": "pmsg", - "Global variables": "gvar", - "Private variables": "pvar", - "Direct data": "direct", - "Status code": "statuscode", - "All data": "all", - }; + // Establish a connection to the server + console.log("[CloudLink] Connecting to server:", url); + try { + clVars.socket = new WebSocket(url); + } catch (e) { + console.warn("[CloudLink] An exception has occurred:", e); + return; + } + + // Bind connection established event + clVars.socket.onopen = function (event) { + clVars.currentServerUrl = url; + + // Set the link state to connected. + console.log("[CloudLink] Connected."); + + clVars.linkState.status = 2; + + // If a server_version message hasn't been received in over half a second, try to broadcast a handshake + clVars.handshakeTimeout = window.setTimeout(function() { + console.log("[CloudLink] Hmm... This server hasn't sent us it's server info. Going to attempt a handshake."); + sendHandshake(); + }, 500); + + // Fire event hats (only one not broken) + runtime.startHats('cloudlink_onConnect'); + + // Return promise (during setup) + return; + }; + + // Bind message handler event + clVars.socket.onmessage = function (event) { + handleMessage(event.data); + }; + + // Bind connection closed event + clVars.socket.onclose = function (event) { + switch (clVars.linkState.status) { + case 1: // Was connecting + // Set the link state to ungraceful disconnect. + console.log(`[CloudLink] Connection failed (${event.code}).`); + clVars.linkState.status = 4; + clVars.linkState.disconnectType = 1; + break; + + case 2: // Was already connected + if (event.wasClean || clVars.linkState.isAttemptingGracefulDisconnect) { + // Set the link state to graceful disconnect. + console.log(`[CloudLink] Disconnected (${event.code} ${event.reason}).`); + clVars.linkState.status = 3; + clVars.linkState.disconnectType = 0; + } else { + // Set the link state to ungraceful disconnect. + console.log(`[CloudLink] Lost connection (${event.code} ${event.reason}).`); + clVars.linkState.status = 4; + clVars.linkState.disconnectType = 2; + } + break; + } + + // Reset clVars values + resetOnClose(); + + // Run all onClose event blocks + runtime.startHats('cloudlink_onClose'); + // Return promise (during setup) + return; } + } + + // GET the serverList + try { + Scratch.fetch( + "https://mikedev101.github.io/cloudlink/serverlist.json" + ) + .then((response) => { + return response.text(); + }) + .then((data) => { + clVars.serverList = JSON.parse(data); + }) + .catch((err) => { + console.log("[CloudLink] An error has occurred while parsing the public server list:", err); + clVars.serverList = {}; + }); + } catch (err) { + console.log("[CloudLink] An error has occurred while fetching the public server list:", err); + clVars.serverList = {}; + } + // Declare the CloudLink library. + class CloudLink { getInfo() { return { - id: "cloudlink", - name: "CloudLink", - blockIconURI: this.cl_block, - menuIconURI: this.cl_icon, - docsURI: "https://hackmd.io/@MikeDEV/HJiNYwOfo", + id: 'cloudlink', + name: 'CloudLink', + blockIconURI: cl_block, + menuIconURI: cl_icon, + docsURI: "https://github.com/MikeDev101/cloudlink/wiki/Scratch-Client", blocks: [ + { opcode: "returnGlobalData", - blockType: "reporter", - text: "Global data", + blockType: Scratch.BlockType.REPORTER, + text: "Global data" }, + { opcode: "returnPrivateData", - blockType: "reporter", - text: "Private data", + blockType: Scratch.BlockType.REPORTER, + text: "Private data" }, + { opcode: "returnDirectData", - blockType: "reporter", - text: "Direct Data", + blockType: Scratch.BlockType.REPORTER, + text: "Direct data" }, + + "---", + { opcode: "returnLinkData", - blockType: "reporter", - text: "Link status", + blockType: Scratch.BlockType.REPORTER, + text: "Link status" }, + { opcode: "returnStatusCode", - blockType: "reporter", - text: "Status code", + blockType: Scratch.BlockType.REPORTER, + text: "Status code" }, + + "---", + { opcode: "returnUserListData", - blockType: "reporter", - text: "Usernames", + blockType: Scratch.BlockType.REPORTER, + text: "Usernames" }, + { opcode: "returnUsernameData", - blockType: "reporter", - text: "My username", + blockType: Scratch.BlockType.REPORTER, + text: "My username" }, + + "---", + { opcode: "returnVersionData", - blockType: "reporter", - text: "Extension version", + blockType: Scratch.BlockType.REPORTER, + text: "Extension version" }, + { opcode: "returnServerVersion", - blockType: "reporter", - text: "Server version", + blockType: Scratch.BlockType.REPORTER, + text: "Server version" }, + { opcode: "returnServerList", - blockType: "reporter", - text: "Server list", + blockType: Scratch.BlockType.REPORTER, + text: "Server list" }, + { opcode: "returnMOTD", - blockType: "reporter", - text: "Server MOTD", + blockType: Scratch.BlockType.REPORTER, + text: "Server MOTD" }, + + "---", + { opcode: "returnClientIP", - blockType: "reporter", - text: "My IP address", + blockType: Scratch.BlockType.REPORTER, + text: "My IP address" + }, + + { + opcode: "returnUserObject", + blockType: Scratch.BlockType.REPORTER, + text: "My user object" }, + + "---", + { opcode: "returnListenerData", - blockType: "reporter", + blockType: Scratch.BlockType.REPORTER, text: "Response for listener [ID]", arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "example-listener", }, }, }, + { opcode: "readQueueSize", - blockType: "reporter", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Size of queue for [TYPE]", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "allmenu", defaultValue: "All data", }, }, }, + { opcode: "readQueueData", - blockType: "reporter", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Packet queue for [TYPE]", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "allmenu", defaultValue: "All data", }, }, }, + + "---", + { opcode: "returnVarData", - blockType: "reporter", + blockType: Scratch.BlockType.REPORTER, text: "[TYPE] [VAR] data", arguments: { VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "varmenu", defaultValue: "Global variables", }, }, }, + + "---", + { opcode: "parseJSON", - blockType: "reporter", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "[PATH] of [JSON_STRING]", arguments: { PATH: { - type: "string", - defaultValue: "fruit/apples", + type: Scratch.ArgumentType.STRING, + defaultValue: 'fruit/apples', }, JSON_STRING: { - type: "string", - defaultValue: - '{"fruit": {"apples": 2, "bananas": 3}, "total_fruit": 5}', + type: Scratch.ArgumentType.STRING, + defaultValue: '{"fruit": {"apples": 2, "bananas": 3}, "total_fruit": 5}', }, }, }, + { opcode: "getFromJSONArray", - blockType: "reporter", - text: "Get [NUM] from JSON array [ARRAY]", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, + text: 'Get [NUM] from JSON array [ARRAY]', arguments: { NUM: { - type: "number", + type: Scratch.ArgumentType.NUMBER, defaultValue: 0, }, ARRAY: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: '["foo","bar"]', + } + } + }, + + { + opcode: "makeJSON", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, + text: "Convert [toBeJSONified] to JSON", + arguments: { + toBeJSONified: { + type: Scratch.ArgumentType.STRING, + defaultValue: '{"test": true}', + }, + }, + }, + + { + opcode: "isValidJSON", + blockType: Scratch.BlockType.BOOLEAN, + hideFromPalette: clVars.hideCLDeprecatedBlocks, + text: "Is [JSON_STRING] valid JSON?", + arguments: { + JSON_STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: '{"fruit": {"apples": 2, "bananas": 3}, "total_fruit": 5}', }, }, }, + + + "---", + { opcode: "fetchURL", - blockType: "reporter", - blockAllThreads: "true", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Fetch data from URL [url]", arguments: { url: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "https://extensions.turbowarp.org/hello.txt", }, }, }, + { opcode: "requestURL", - blockType: "reporter", - blockAllThreads: "true", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Send request with method [method] for URL [url] with data [data] and headers [headers]", arguments: { method: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "GET", }, url: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "https://extensions.turbowarp.org/hello.txt", }, data: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "{}", }, headers: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "{}", }, }, }, - { - opcode: "makeJSON", - blockType: "reporter", - text: "Convert [toBeJSONified] to JSON", - arguments: { - toBeJSONified: { - type: "string", - defaultValue: '{"test": true}', - }, - }, - }, + + "---", + { opcode: "onConnect", - blockType: "hat", + blockType: Scratch.BlockType.EVENT, text: "When connected", - blockAllThreads: "true", + isEdgeActivated: false, // Gets called by runtime.startHats }, + { opcode: "onClose", - blockType: "hat", + blockType: Scratch.BlockType.EVENT, text: "When disconnected", - blockAllThreads: "true", + isEdgeActivated: false, // Gets called by runtime.startHats }, + + "---", + { opcode: "onListener", - blockType: "hat", - text: "When I receive new packet with listener [ID]", - blockAllThreads: "true", + blockType: Scratch.BlockType.HAT, + text: "When I receive new message with listener [ID]", + isEdgeActivated: true, arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "example-listener", }, }, }, + { opcode: "onNewPacket", - blockType: "hat", - text: "When I receive new [TYPE] packet", - blockAllThreads: "true", + blockType: Scratch.BlockType.HAT, + text: "When I receive new [TYPE] message", + isEdgeActivated: true, arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "almostallmenu", defaultValue: "Global data", }, }, }, + { opcode: "onNewVar", - blockType: "hat", + blockType: Scratch.BlockType.HAT, text: "When I receive new [TYPE] data for [VAR]", - blockAllThreads: "true", + isEdgeActivated: true, arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "varmenu", defaultValue: "Global variables", }, VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + + "---", + { opcode: "getComState", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Connected?", }, + { opcode: "getRoomState", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Linked to rooms?", }, + { opcode: "getComLostConnectionState", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Lost connection?", }, + { opcode: "getComFailedConnectionState", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Failed to connnect?", }, + { opcode: "getUsernameState", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Username synced?", }, + { opcode: "returnIsNewData", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Got New [TYPE]?", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "datamenu", defaultValue: "Global data", }, }, }, + { opcode: "returnIsNewVarData", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Got New [TYPE] data for variable [VAR]?", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "varmenu", - defaultValue: "Global variables", + defaultValue: 'Global variables', }, VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + { opcode: "returnIsNewListener", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "Got new packet with listener [ID]?", - blockAllThreads: "true", arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "example-listener", }, }, }, + { opcode: "checkForID", - blockType: "Boolean", + blockType: Scratch.BlockType.BOOLEAN, text: "ID [ID] connected?", arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Another name", }, }, }, - { - opcode: "isValidJSON", - blockType: "Boolean", - text: "Is [JSON_STRING] valid JSON?", - arguments: { - JSON_STRING: { - type: "string", - defaultValue: - '{"fruit": {"apples": 2, "bananas": 3}, "total_fruit": 5}', - }, - }, - }, + + "---", + { opcode: "openSocket", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Connect to [IP]", - blockAllThreads: "true", arguments: { IP: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "ws://127.0.0.1:3000/", - }, - }, + } + } }, + { opcode: "openSocketPublicServers", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Connect to server [ID]", - blockAllThreads: "true", arguments: { ID: { - type: "number", - defaultValue: "", - }, - }, + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + } + } }, + { opcode: "closeSocket", - blockType: "command", - blockAllThreads: "true", - text: "Disconnect", + blockType: Scratch.BlockType.COMMAND, + text: "Disconnect" }, + + "---", + { opcode: "setMyName", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Set [NAME] as username", - blockAllThreads: "true", arguments: { NAME: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "A name", }, }, }, + + "---", + { opcode: "createListener", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Attach listener [ID] to next packet", - blockAllThreads: "true", arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "example-listener", }, }, + }, + + "---", + { - opcode: "linkToRooms", - blockType: "command", + opcode: 'linkToRooms', + blockType: Scratch.BlockType.COMMAND, text: "Link to room(s) [ROOMS]", - blockAllThreads: "true", arguments: { ROOMS: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: '["test"]', }, - }, + } }, + { opcode: "selectRoomsInNextPacket", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Select room(s) [ROOMS] for next packet", - blockAllThreads: "true", arguments: { ROOMS: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: '["test"]', }, }, }, + { opcode: "unlinkFromRooms", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Unlink from all rooms", - blockAllThreads: "true", }, + + "---", + { opcode: "sendGData", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Send [DATA]", - blockAllThreads: "true", arguments: { DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + { opcode: "sendPData", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Send [DATA] to [ID]", - blockAllThreads: "true", arguments: { DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Another name", }, }, }, + { opcode: "sendGDataAsVar", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Send variable [VAR] with data [DATA]", - blockAllThreads: "true", arguments: { DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Banana", }, VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + { opcode: "sendPDataAsVar", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Send variable [VAR] to [ID] with data [DATA]", - blockAllThreads: "true", arguments: { DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Banana", }, ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Another name", }, VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + + "---", + { opcode: "runCMDnoID", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Send command without ID [CMD] [DATA]", - blockAllThreads: "true", arguments: { CMD: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "direct", }, DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "val", }, }, }, + { opcode: "runCMD", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, + hideFromPalette: clVars.hideCLDeprecatedBlocks, text: "Send command [CMD] [ID] [DATA]", - blockAllThreads: "true", arguments: { CMD: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "direct", }, ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "id", }, DATA: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "val", }, }, }, + + "---", + { opcode: "resetNewData", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Reset got new [TYPE] status", - blockAllThreads: "true", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "datamenu", defaultValue: "Global data", }, }, }, + { opcode: "resetNewVarData", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Reset got new [TYPE] [VAR] status", - blockAllThreads: "true", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "varmenu", defaultValue: "Global variables", }, VAR: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "Apple", }, }, }, + + "---", + { opcode: "resetNewListener", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Reset got new [ID] listener status", - blockAllThreads: "true", arguments: { ID: { - type: "string", + type: Scratch.ArgumentType.STRING, defaultValue: "example-listener", }, }, }, + + "---", + { opcode: "clearAllPackets", - blockType: "command", + blockType: Scratch.BlockType.COMMAND, text: "Clear all packets for [TYPE]", arguments: { TYPE: { - type: "string", + type: Scratch.ArgumentType.STRING, menu: "allmenu", defaultValue: "All data", }, - }, + } }, - ], - menus: { - coms: { - items: ["Connected", "Username synced"], + + "---", + + { + func: "showOldBlocks", + blockType: Scratch.BlockType.BUTTON, + text: "Show old blocks", + hideFromPalette: !clVars.hideCLDeprecatedBlocks, }, + + { + func: "hideOldBlocks", + blockType: Scratch.BlockType.BUTTON, + text: "Hide old blocks", + hideFromPalette: clVars.hideCLDeprecatedBlocks, + }, + + "---", + + ], + menus: { datamenu: { - items: [ - "Global data", - "Private data", - "Direct data", - "Status code", - ], + items: ['Global data', 'Private data', 'Direct data', 'Status code'] }, varmenu: { - items: ["Global variables", "Private variables"], + items: ['Global variables', 'Private variables'] }, allmenu: { - items: [ - "Global data", - "Private data", - "Direct data", - "Status code", - "Global variables", - "Private variables", - "All data", - ], + items: ['Global data', 'Private data', 'Direct data', 'Status code', "Global variables", "Private variables", "All data"] }, almostallmenu: { - items: [ - "Global data", - "Private data", - "Direct data", - "Status code", - "Global variables", - "Private variables", - ], + items: ['Global data', 'Private data', 'Direct data', 'Status code', "Global variables", "Private variables"] }, - }, + } }; } - // Code for blocks go here - - returnGlobalData() { - if (this.socketData.gmsg.length != 0) { - let data = this.socketData.gmsg[this.socketData.gmsg.length - 1].val; - - if (typeof data == "object") { - data = JSON.stringify(data); // Make the JSON safe for Scratch - } - - return data; - } else { - return ""; + // Credit to LilyMakesThings' "Lily's toolbox" for this feature. + showOldBlocks() { + if ( + confirm( + "Do you want to display old blocks?\n\nThese blocks are not recommended for use in newer CloudLink projects as they are deprecated or have better implementation in other extensions." + ) + ) { + clVars.hideCLDeprecatedBlocks = false; + vm.extensionManager.refreshBlocks(); } } - returnPrivateData() { - if (this.socketData.pmsg.length != 0) { - let data = this.socketData.pmsg[this.socketData.pmsg.length - 1].val; + // Credit to LilyMakesThings' "Lily's toolbox" for this feature. + hideOldBlocks() { + clVars.hideCLDeprecatedBlocks = true; + vm.extensionManager.refreshBlocks(); + } - if (typeof data == "object") { - data = JSON.stringify(data); // Make the JSON safe for Scratch - } + // Reporter - Returns gmsg values. + returnGlobalData() { + return makeValueScratchSafe(clVars.gmsg.varState); + } - return data; - } else { - return ""; - } + // Reporter - Returns pmsg values. + returnPrivateData() { + return makeValueScratchSafe(clVars.pmsg.varState); } + // Reporter - Returns direct values. returnDirectData() { - if (this.socketData.direct.length != 0) { - let data = - this.socketData.direct[this.socketData.direct.length - 1].val; - - if (typeof data == "object") { - data = JSON.stringify(data); // Make the JSON safe for Scratch - } - - return data; - } else { - return ""; - } + return makeValueScratchSafe(clVars.direct.varState); } + // Reporter - Returns current link state. returnLinkData() { - return String(this.link_status); + return makeValueScratchSafe(clVars.linkState.status); } + // Reporer - Returns status code values. returnStatusCode() { - if (this.socketData.statuscode.length != 0) { - let data = - this.socketData.statuscode[this.socketData.statuscode.length - 1] - .code; - - if (typeof data == "object") { - data = JSON.stringify(data); // Make the JSON safe for Scratch - } - - return data; - } else { - return ""; - } + return makeValueScratchSafe(clVars.statuscode.varState); } + // Reporter - Returns ulist value. returnUserListData() { - return JSON.stringify(this.socketData.ulist); + return makeValueScratchSafe(clVars.ulist); } + // Reporter - Returns currently set username. returnUsernameData() { - let data = this.username; - - if (typeof data == "object") { - data = JSON.stringify(data); // Make the JSON safe for Scratch - } - - return data; + return makeValueScratchSafe(clVars.username.value); } + // Reporter - Returns current client version. returnVersionData() { - return String(this.version); + return generateVersionString(); } + // Reporter - Returns reported server version. returnServerVersion() { - return String(this.socketData.server_version); + return makeValueScratchSafe(clVars.server_version); } + // Reporter - Returns the serverlist value. returnServerList() { - return JSON.stringify(servers); + return makeValueScratchSafe(clVars.serverList); } + // Reporter - Returns the reported Message-Of-The-Day. returnMOTD() { - return String(this.socketData.motd); + return makeValueScratchSafe(clVars.motd); } + // Reporter - Returns the reported IP address of the client. returnClientIP() { - return String(this.socketData.client_ip); + return makeValueScratchSafe(clVars.client_ip); } - returnListenerData({ ID }) { - const self = this; - if (this.isRunning && this.socketListeners.hasOwnProperty(String(ID))) { - return JSON.stringify(this.socketListenersData[ID]); - } else { - return "{}"; - } + // Reporter - Returns the reported user object of the client (Snowflake ID, UUID, Username) + returnUserObject() { + return makeValueScratchSafe(clVars.myUserObject); } - readQueueSize({ TYPE }) { - if (this.menuRemap[String(TYPE)] == "all") { - let tmp_size = 0; - tmp_size = tmp_size + this.socketData.gmsg.length; - tmp_size = tmp_size + this.socketData.pmsg.length; - tmp_size = tmp_size + this.socketData.direct.length; - tmp_size = tmp_size + this.socketData.statuscode.length; - tmp_size = tmp_size + this.socketData.gvar.length; - tmp_size = tmp_size + this.socketData.pvar.length; - return tmp_size; - } else { - return this.socketData[this.menuRemap[String(TYPE)]].length; + // Reporter - Returns data for a specific listener ID. + // ID - String (listener ID) + returnListenerData(args) { + if (!clVars.listeners.varStates.hasOwnProperty(String(args.ID))) { + console.warn(`[CloudLink] Listener ID ${args.ID} does not exist!`); + return ""; } + return clVars.listeners.varStates[String(args.ID)].varState; } - readQueueData({ TYPE }) { - if (this.menuRemap[String(TYPE)] == "all") { - let tmp_socketData = JSON.parse(JSON.stringify(this.socketData)); // Deep copy - - delete tmp_socketData.motd; - delete tmp_socketData.client_ip; - delete tmp_socketData.ulist; - delete tmp_socketData.server_version; + // Reporter - Returns the size of the message queue. + // TYPE - String (menu allmenu) + readQueueSize(args) { + switch (args.TYPE) { + case 'Global data': + return clVars.gmsg.queue.length; + case 'Private data': + return clVars.pmsg.queue.length; + case 'Direct data': + return clVars.direct.queue.length; + case 'Status code': + return clVars.statuscode.queue.length; + case 'Global variables': + return clVars.gvar.queue.length; + case 'Private variables': + return clVars.pvar.queue.length; + case 'All data': + return ( + clVars.gmsg.queue.length + + clVars.pmsg.queue.length + + clVars.direct.queue.length + + clVars.statuscode.queue.length + + clVars.gvar.queue.length + + clVars.pvar.queue.length + ); + } + } - return JSON.stringify(tmp_socketData); - } else { - return JSON.stringify(this.socketData[this.menuRemap[String(TYPE)]]); + // Reporter - Returns all values of the message queue. + // TYPE - String (menu allmenu) + readQueueData(args) { + switch (args.TYPE) { + case 'Global data': + return makeValueScratchSafe(clVars.gmsg.queue); + case 'Private data': + return makeValueScratchSafe(clVars.pmsg.queue); + case 'Direct data': + return makeValueScratchSafe(clVars.direct.queue); + case 'Status code': + return makeValueScratchSafe(clVars.statuscode.queue); + case 'Global variables': + return makeValueScratchSafe(clVars.gvar.queue); + case 'Private variables': + return makeValueScratchSafe(clVars.pvar.queue); + case 'All data': + return makeValueScratchSafe({ + gmsg: clVars.gmsg.queue, + pmsg: clVars.pmsg.queue, + direct: clVars.direct.queue, + statuscode: clVars.statuscode.queue, + gvar: clVars.gvar.queue, + pvar: clVars.pvar.queue + }); } } - returnVarData({ TYPE, VAR }) { - if (this.isRunning) { - if (this.varData.hasOwnProperty(this.menuRemap[TYPE])) { - if (this.varData[this.menuRemap[TYPE]].hasOwnProperty(VAR)) { - return this.varData[this.menuRemap[TYPE]][VAR].value; - } else { - return ""; - } - } else { + // Reporter - Returns a gvar/pvar value. + // TYPE - String (menu varmenu), VAR - String (variable name) + returnVarData(args) { + switch (args.TYPE) { + case 'Global variables': + if (!clVars.gvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Global variable ${args.VAR} does not exist!`); return ""; } - } else { - return ""; + return clVars.gvar.varStates[String(args.VAR)].varState; + case 'Private variables': + if (!clVars.pvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Private variable ${args.VAR} does not exist!`); + return ""; + } + return clVars.pvar.varStates[String(args.VAR)].varState; } } - parseJSON({ PATH, JSON_STRING }) { + // Reporter - Gets a JSON key value from a JSON string. + // PATH - String, JSON_STRING - String + parseJSON(args) { try { - const path = PATH.toString() - .split("/") - .map((prop) => decodeURIComponent(prop)); - if (path[0] === "") path.splice(0, 1); - if (path[path.length - 1] === "") path.splice(-1, 1); + const path = args.PATH.toString().split('/').map(prop => decodeURIComponent(prop)); + if (path[0] === '') path.splice(0, 1); + if (path[path.length - 1] === '') path.splice(-1, 1); let json; try { - json = JSON.parse(" " + JSON_STRING); + json = JSON.parse(' ' + args.JSON_STRING); } catch (e) { return e.message; - } - path.forEach((prop) => (json = json[prop])); - if (json === null) return "null"; - else if (json === undefined) return ""; - else if (typeof json === "object") return JSON.stringify(json); + }; + path.forEach(prop => json = json[prop]); + if (json === null) return 'null'; + else if (json === undefined) return ''; + else if (typeof json === 'object') return JSON.stringify(json); else return json.toString(); } catch (err) { - return ""; - } + return ''; + }; } - getFromJSONArray({ NUM, ARRAY }) { - var json_array = JSON.parse(ARRAY); - if (json_array[NUM] == "undefined") { + // Reporter - Returns an entry from a JSON array (0-based). + // NUM - Number, ARRAY - String (JSON Array) + getFromJSONArray(args) { + var json_array = JSON.parse(args.ARRAY); + if (json_array[args.NUM] == "undefined") { return ""; } else { - let data = json_array[NUM]; - - if (typeof data == "object") { + let data = json_array[args.NUM]; + + if (typeof (data) == "object") { data = JSON.stringify(data); // Make the JSON safe for Scratch } - + return data; } } + // Reporter - Returns a RESTful GET promise. + // url - String fetchURL(args) { - return Scratch.fetch(args.url, { - method: "GET", - }).then((response) => response.text()); + return Scratch.fetch(args.url, {method: "GET"}) + .then(response => response.text()) + .catch(error => { + console.warn(`[CloudLink] Fetch error: ${error}`); + }); } + // Reporter - Returns a RESTful request promise. + // url - String, method - String, data - String, headers - String requestURL(args) { if (args.method == "GET" || args.method == "HEAD") { return Scratch.fetch(args.url, { method: args.method, - headers: JSON.parse(args.headers), - }).then((response) => response.text()); + headers: JSON.parse(args.headers) + }) + .then(response => response.text()) + .catch(error => { + console.warn(`[CloudLink] Request error: ${error}`); + }); } else { return Scratch.fetch(args.url, { method: args.method, headers: JSON.parse(args.headers), - body: JSON.parse(args.data), - }).then((response) => response.text()); - } - } - - isValidJSON({ JSON_STRING }) { - return jsonCheck(JSON_STRING); - } - - makeJSON({ toBeJSONified }) { - if (typeof toBeJSONified == "string") { - try { - JSON.parse(toBeJSONified); - return String(toBeJSONified); - } catch (err) { - return "Not JSON!"; - } - } else if (typeof toBeJSONified == "object") { - return JSON.stringify(toBeJSONified); - } else { - return "Not JSON!"; + body: JSON.parse(args.data) + }) + .then(response => response.text()) + .catch(error => { + console.warn(`[CloudLink] Request error: ${error}`); + }); } } - - onConnect() { - const self = this; - if (self.connect_hat == 0 && self.isRunning && self.protocolOk) { - self.connect_hat = 1; + + // Event + // ID - String (listener) + onListener(args) { + // Must be connected + if (clVars.socket == null) return false; + if (clVars.linkState.status != 2) return false; + + // Listener must exist + if (!clVars.listeners.varStates.hasOwnProperty(args.ID)) return false; + + // Run event + if (clVars.listeners.varStates[args.ID].eventHatTick) { + clVars.listeners.varStates[args.ID].eventHatTick = false; return true; - } else { - return false; } + return false; } - onClose() { - const self = this; - if (self.close_hat == 0 && !self.isRunning) { - self.close_hat = 1; - return true; - } else { - return false; - } - } + // Event + // TYPE - String (menu almostallmenu) + onNewPacket(args) { + // Must be connected + if (clVars.socket == null) return false; + if (clVars.linkState.status != 2) return false; + + // Run event + switch (args.TYPE) { + case 'Global data': + if (clVars.gmsg.eventHatTick) { + clVars.gmsg.eventHatTick = false; + return true; + } + break; - onListener({ ID }) { - const self = this; - if (this.isRunning && this.socketListeners.hasOwnProperty(String(ID))) { - if (self.socketListeners[String(ID)]) { - self.socketListeners[String(ID)] = false; - return true; - } else { - return false; - } - } else { - return false; + case 'Private data': + if (clVars.pmsg.eventHatTick) { + clVars.pmsg.eventHatTick = false; + return true; + } + break; + + case 'Direct data': + if (clVars.direct.eventHatTick) { + clVars.direct.eventHatTick = false; + return true; + } + break; + + case 'Status code': + if (clVars.statuscode.eventHatTick) { + clVars.statuscode.eventHatTick = false; + return true; + } + break; + + case 'Global variables': + if (clVars.gvar.eventHatTick) { + clVars.gvar.eventHatTick = false; + return true; + } + break; + + case 'Private variables': + if (clVars.pvar.eventHatTick) { + clVars.pvar.eventHatTick = false; + return true; + } + break; } + return false; } - onNewPacket({ TYPE }) { - const self = this; - if (this.isRunning && this.newSocketData[this.menuRemap[String(TYPE)]]) { - self.newSocketData[this.menuRemap[String(TYPE)]] = false; - return true; - } else { - return false; + // Event + // TYPE - String (varmenu), VAR - String (variable name) + onNewVar(args) { + // Must be connected + if (clVars.socket == null) return false; + if (clVars.linkState.status != 2) return false; + + // Run event + switch (args.TYPE) { + case 'Global variables': + + // Variable must exist + if (!clVars.gvar.varStates.hasOwnProperty(String(args.VAR))) break; + if (clVars.gvar.varStates[String(args.VAR)].eventHatTick) { + clVars.gvar.varStates[String(args.VAR)].eventHatTick = false; + return true; + } + + break; + + case 'Private variables': + + // Variable must exist + if (!clVars.pvar.varStates.hasOwnProperty(String(args.VAR))) break; + if (clVars.pvar.varStates[String(args.VAR)].eventHatTick) { + clVars.pvar.varStates[String(args.VAR)].eventHatTick = false; + return true; + } + + break; } + return false; } - onNewVar({ TYPE, VAR }) { - const self = this; - if (this.isRunning) { - if (this.varData.hasOwnProperty(this.menuRemap[TYPE])) { - if (this.varData[this.menuRemap[TYPE]].hasOwnProperty(VAR)) { - if (this.varData[this.menuRemap[TYPE]][VAR].isNew) { - self.varData[this.menuRemap[TYPE]][VAR].isNew = false; - return true; - } else { - return false; - } - } else { - return false; - } - } else { - return false; + // Reporter - Returns a JSON-ified value. + // toBeJSONified - String + makeJSON(args) { + if (typeof(args.toBeJSONified) == "string") { + try { + JSON.parse(args.toBeJSONified); + return String(args.toBeJSONified); + } catch(err) { + return "Not JSON!"; } + } else if (typeof(args.toBeJSONified) == "object") { + return JSON.stringify(args.toBeJSONified); } else { - return false; - } + return "Not JSON!"; + }; } + // Boolean - Returns true if connected. getComState() { - return String(this.link_status == 2 || this.protocolOk); + return ((clVars.linkState.status == 2) && (clVars.socket != null)); } + // Boolean - Returns true if linked to rooms (other than "default") getRoomState() { - return this.isLinked; + return ((clVars.socket != null) && (clVars.rooms.isLinked)); } + // Boolean - Returns true if the connection was dropped. getComLostConnectionState() { - return this.wasConnectionDropped; + return ((clVars.linkState.status == 4) && (clVars.linkState.disconnectType == 2)); } + // Boolean - Returns true if the client failed to establish a connection. getComFailedConnectionState() { - return this.didConnectionFail; + return ((clVars.linkState.status == 4) && (clVars.linkState.disconnectType == 1)); } + // Boolean - Returns true if the username was set successfully. getUsernameState() { - return this.isUsernameSet; + return ((clVars.socket != null) && (clVars.username.accepted)); } - returnIsNewData({ TYPE }) { - if (this.isRunning) { - return this.newSocketData[this.menuRemap[String(TYPE)]]; - } else { - return false; + // Boolean - Returns true if there is new gmsg/pmsg/direct/statuscode data. + // TYPE - String (menu datamenu) + returnIsNewData(args) { + + // Must be connected + if (clVars.socket == null) return false; + + // Run event + switch (args.TYPE) { + case 'Global data': + return clVars.gmsg.hasNew; + case 'Private data': + return clVars.pmsg.hasNew; + case 'Direct data': + return clVars.direct.hasNew; + case 'Status code': + return clVars.statuscode.hasNew; } } - returnIsNewVarData({ TYPE, VAR }) { - if (this.isRunning) { - if (this.varData.hasOwnProperty(this.menuRemap[TYPE])) { - if (this.varData[this.menuRemap[TYPE]].hasOwnProperty(VAR)) { - return this.varData[this.menuRemap[TYPE]][VAR].isNew; - } else { + // Boolean - Returns true if there is new gvar/pvar data. + // TYPE - String (menu varmenu), VAR - String (variable name) + returnIsNewVarData(args) { + switch (args.TYPE) { + case 'Global variables': + if (!clVars.gvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Global variable ${args.VAR} does not exist!`); return false; } - } else { - return false; - } - } else { + return clVars.gvar.varStates[String(args.ID)].hasNew; + case 'Private variables': + if (!clVars.pvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Private variable ${args.VAR} does not exist!`); + return false; + } + return clVars.pvar.varStates[String(args.ID)].hasNew; + } + } + + // Boolean - Returns true if a listener has a new value. + // ID - String (listener ID) + returnIsNewListener(args) { + if (!clVars.listeners.varStates.hasOwnProperty(String(args.ID))) { + console.warn(`[CloudLink] Listener ID ${args.ID} does not exist!`); return false; } + return clVars.listeners.varStates[String(args.ID)].hasNew; } - returnIsNewListener({ ID }) { - if (this.isRunning) { - if (this.socketListeners.hasOwnProperty(String(ID))) { - return this.socketListeners[ID]; + // Boolean - Returns true if a username/ID/UUID/object exists in the userlist. + // ID - String (username or user object) + checkForID(args) { + + // Legacy ulist handling + if (clVars.ulist.includes(args.ID)) return true; + + // New ulist handling + if (clVars.linkState.identifiedProtocol > 2) { + if (this.isValidJSON(args.ID)) { + return clVars.ulist.some(o => ( + (o.username === JSON.parse(args.ID).username) + && + (o.id == JSON.parse(args.ID).id) + )); } else { - return false; + return clVars.ulist.some(o => ( + (o.username === String(args.ID)) + || + (o.id == args.ID) + )); } - } else { + } else return false; + } + + // Boolean - Returns true if the input JSON is valid. + // JSON_STRING - String + isValidJSON(args) { + try { + JSON.parse(args.JSON_STRING); + return true; + } catch { return false; - } + }; } - checkForID({ ID }) { - return find_id(ID, this.socketData.ulist); + // Command - Establishes a connection to a server. + // IP - String (websocket URL) + openSocket(args) { + if (clVars.socket != null) { + console.warn("[CloudLink] Already connected to a server."); + return; + }; + return newClient(args.IP); } - async openSocket({ IP }) { - const self = this; - if (!self.isRunning) { - if (!(await Scratch.canFetch(IP))) { - return; - } + // Command - Establishes a connection to a selected server. + // ID - Number (server entry #) + openSocketPublicServers(args) { + if (clVars.socket != null) { + console.warn("[CloudLink] Already connected to a server."); + return; + }; + if (!clVars.serverList.hasOwnProperty(String(args.ID))) { + console.warn("[CloudLink] Not a valid server ID!"); + return; + }; + return newClient(clVars.serverList[String(args.ID)]["url"]); + } - console.log("Starting socket."); - self.link_status = 1; + // Command - Closes the connection. + closeSocket() { + if (clVars.socket == null) { + console.warn("[CloudLink] Already disconnected."); + return; + }; + console.log("[CloudLink] Disconnecting..."); + clVars.linkState.isAttemptingGracefulDisconnect = true; + clVars.socket.close(1000, "Client going away"); + } - self.disconnectWasClean = false; - self.wasConnectionDropped = false; - self.didConnectionFail = false; + // Command - Sets the username of the client on the server. + // NAME - String + setMyName(args) { + // Must be connected to set a username. + if (clVars.socket == null) return; - mWS = new WebSocket(String(IP)); + // Prevent running if an attempt is currently processing. + if (clVars.username.attempted) { + console.warn("[CloudLink] Already attempting to set username!"); + return; + }; - mWS.onerror = function () { - self.isRunning = false; - }; + // Prevent running if the username is already set. + if (clVars.username.accepted) { + console.warn("[CloudLink] Already set username!"); + return; + }; - mWS.onopen = function () { - self.isRunning = true; - self.packet_queue = {}; - self.link_status = 2; + // Update state + clVars.username.attempted = true; + clVars.username.temp = args.NAME; - // Send the handshake request to get server to detect client protocol - mWS.send( - JSON.stringify({ cmd: "handshake", listener: "setprotocol" }) - ); + // Send the command + return sendMessage({ cmd: "setid", val: args.NAME, listener: "username_cfg" }); + } - console.log("Successfully opened socket."); - }; + // Command - Prepares the next transmitted message to have a listener ID attached to it. + // ID - String (listener ID) + createListener(args) { + + // Must be connected to set a username. + if (clVars.socket == null) return; + + // Require server support + if (clVars.linkState.identifiedProtocol < 2) { + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support listeners."); + return; + } - mWS.onmessage = function (event) { - let tmp_socketData = JSON.parse(event.data); - console.log("RX:", tmp_socketData); + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before creating a listener!"); + return; + }; - if (self.queueableCmds.includes(tmp_socketData.cmd)) { - self.socketData[tmp_socketData.cmd].push(tmp_socketData); - } else { - if (tmp_socketData.cmd == "ulist") { - // ulist functionality has been changed in server 0.1.9 - if (tmp_socketData.hasOwnProperty("mode")) { - if (tmp_socketData.mode == "set") { - self.socketData["ulist"] = tmp_socketData.val; - } else if (tmp_socketData.mode == "add") { - if ( - !self.socketData.ulist.some( - (o) => - o.username === tmp_socketData.val.username && - o.id == tmp_socketData.val.id - ) - ) { - self.socketData["ulist"].push(tmp_socketData.val); - } else { - console.log( - "Could not perform ulist method add, client", - tmp_socketData.val, - "already exists" - ); - } - } else if (tmp_socketData.mode == "remove") { - if ( - self.socketData.ulist.some( - (o) => - o.username === tmp_socketData.val.username && - o.id == tmp_socketData.val.id - ) - ) { - // This is by far the fugliest thing I have ever written in JS, or in any programming language... thanks I hate it - self.socketData["ulist"] = self.socketData["ulist"].filter( - (user) => - !(user.username === tmp_socketData.val.username) && - !(user.id == tmp_socketData.val.id) - ); - } else { - console.log( - "Could not perform ulist method remove, client", - tmp_socketData.val, - "was not found" - ); - } - } else { - console.log( - "Could not understand ulist method:", - tmp_socketData.mode - ); - } - } else { - // Retain compatibility wtih existing servers - self.socketData["ulist"] = tmp_socketData.val; - } - } else { - self.socketData[tmp_socketData.cmd] = tmp_socketData.val; - } - } + // Must be used once per packet + if (clVars.listeners.enablerState) { + console.warn("[CloudLink] Cannot create multiple listeners at a time!"); + return; + } + + // Update state + clVars.listeners.enablerState = true; + clVars.listeners.enablerValue = args.ID; + } - if (self.newSocketData.hasOwnProperty(tmp_socketData.cmd)) { - self.newSocketData[tmp_socketData.cmd] = true; - } + // Command - Subscribes to various rooms on a server. + // ROOMS - String (JSON Array or single string) + linkToRooms(args) { - if (self.varCmds.includes(tmp_socketData.cmd)) { - self.varData[tmp_socketData.cmd][tmp_socketData.name] = { - value: tmp_socketData.val, - isNew: true, - }; - } - if (tmp_socketData.hasOwnProperty("listener")) { - if (tmp_socketData.listener == "setusername") { - self.socketListeners["setusername"] = true; - if (tmp_socketData.code == "I:100 | OK") { - self.username = tmp_socketData.val; - self.isUsernameSyncing = false; - self.isUsernameSet = true; - console.log( - "Username was accepted by the server, and has been set to:", - self.username - ); - } else { - console.warn( - "Username was rejected by the server. Error code:", - String(tmp_socketData.code) - ); - self.isUsernameSyncing = false; - } - } else if (tmp_socketData.listener == "roomLink") { - self.isRoomSetting = false; - self.socketListeners["roomLink"] = true; - if (tmp_socketData.code == "I:100 | OK") { - console.log("Linking to room(s) was accepted by the server!"); - self.isLinked = true; - } else { - console.warn( - "Linking to room(s) was rejected by the server. Error code:", - String(tmp_socketData.code) - ); - self.enableRoom = false; - self.isLinked = false; - self.selectRoom = ""; - } - } else if ( - tmp_socketData.listener == "setprotocol" && - !this.protocolOk - ) { - console.log( - "Server successfully set client protocol to cloudlink!" - ); - self.socketData.statuscode = []; - self.protocolOk = true; - self.socketListeners["setprotocol"] = true; - } else { - if ( - self.socketListeners.hasOwnProperty(tmp_socketData.listener) - ) { - self.socketListeners[tmp_socketData.listener] = true; - } - } - self.socketListenersData[tmp_socketData.listener] = tmp_socketData; - } - self.packet_hat = 0; - }; + // Must be connected to set a username. + if (clVars.socket == null) return; - mWS.onclose = function () { - self.isRunning = false; - self.connect_hat = 0; - self.packet_hat = 0; - self.protocolOk = false; - if (self.close_hat == 1) { - self.close_hat = 0; - } - self.socketData = { - gmsg: [], - pmsg: [], - direct: [], - statuscode: [], - gvar: [], - pvar: [], - motd: "", - client_ip: "", - ulist: [], - server_version: "", - }; - self.newSocketData = { - gmsg: false, - pmsg: false, - direct: false, - statuscode: false, - gvar: false, - pvar: false, - }; - self.socketListeners = {}; - self.username = ""; - self.tmp_username = ""; - self.isUsernameSyncing = false; - self.isUsernameSet = false; - self.enableListener = false; - self.setListener = ""; - self.enableRoom = false; - self.selectRoom = ""; - self.isLinked = false; - self.isRoomSetting = false; - - if (self.link_status != 1) { - if (self.disconnectWasClean) { - self.link_status = 3; - console.log("Socket closed."); - self.wasConnectionDropped = false; - self.didConnectionFail = false; - } else { - self.link_status = 4; - console.error("Lost connection to the server."); - self.wasConnectionDropped = true; - self.didConnectionFail = false; - } - } else { - self.link_status = 4; - console.error("Failed to connect to server."); - self.wasConnectionDropped = false; - self.didConnectionFail = true; - } - }; - } else { - console.warn("Socket is already open."); + // Require server support + if (clVars.linkState.identifiedProtocol < 2) { + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support rooms."); + return; } - } - openSocketPublicServers({ ID }) { - if (servers.hasOwnProperty(ID)) { - console.log("Connecting to:", servers[ID].url); - this.openSocket({ IP: servers[ID].url }); - } - } + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before linking to rooms!"); + return; + }; - closeSocket() { - const self = this; - if (this.isRunning) { - console.log("Closing socket..."); - mWS.close(1000, "script closure"); - self.disconnectWasClean = true; - } else { - console.warn("Socket is not open."); - } - } + // Prevent running if already linked. + if (clVars.rooms.isLinked) { + console.warn("[CloudLink] Already linked to rooms!"); + return; + }; - setMyName({ NAME }) { - const self = this; - if (this.isRunning) { - if (!this.isUsernameSyncing) { - if (!this.isUsernameSet) { - if (String(NAME) != "") { - if (!(String(NAME).length > 20)) { - if ( - !( - String(NAME) == "%CA%" || - String(NAME) == "%CC%" || - String(NAME) == "%CD%" || - String(NAME) == "%MS%" - ) - ) { - let tmp_msg = { - cmd: "setid", - val: String(NAME), - listener: "setusername", - }; - - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); - - self.tmp_username = String(NAME); - self.isUsernameSyncing = true; - } else { - console.log("Blocking attempt to use reserved usernames"); - } - } else { - console.log( - "Blocking attempt to use username larger than 20 characters, username is " + - String(NAME).length + - " characters long" - ); - } - } else { - console.log("Blocking attempt to use blank username"); - } - } else { - console.warn("Username already has been set!"); - } - } else { - console.warn("Username is still syncing!"); - } - } - } + // Prevent running if a room link is in progress. + if (clVars.rooms.isAttemptingLink) { + console.warn("[CloudLink] Currently linking to rooms! Please wait!"); + return; + }; - createListener({ ID }) { - self = this; - if (this.isRunning) { - if (!this.enableListener) { - self.enableListener = true; - self.setListener = String(ID); - } else { - console.warn("Listeners were already created!"); - } - } else { - console.log("Cannot assign a listener to a packet while disconnected"); - } + clVars.rooms.isAttemptingLink = true; + return sendMessage({ cmd: "link", val: args.ROOMS, listener: "link" }); } - linkToRooms({ ROOMS }) { - const self = this; - - if (this.isRunning) { - if (!this.isRoomSetting) { - if (!(String(ROOMS).length > 1000)) { - let tmp_msg = { - cmd: "link", - val: autoConvert(ROOMS), - listener: "roomLink", - }; + // Command - Specifies specific subscribed rooms to transmit messages to. + // ROOMS - String (JSON Array or single string) + selectRoomsInNextPacket(args) { - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Must be connected to user rooms. + if (clVars.socket == null) return; - self.isRoomSetting = true; - } else { - console.warn( - "Blocking attempt to send a room ID / room list larger than 1000 bytes (1 KB), room ID / room list is " + - String(ROOMS).length + - " bytes" - ); - } - } else { - console.warn("Still linking to rooms!"); - } - } else { - console.warn("Socket is not open."); + // Require server support + if (clVars.linkState.identifiedProtocol < 2) { + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support rooms."); + return; } - } - selectRoomsInNextPacket({ ROOMS }) { - const self = this; - if (this.isRunning) { - if (this.isLinked) { - if (!this.enableRoom) { - if (!(String(ROOMS).length > 1000)) { - self.enableRoom = true; - self.selectRoom = ROOMS; - } else { - console.warn( - "Blocking attempt to select a room ID / room list larger than 1000 bytes (1 KB), room ID / room list is " + - String(ROOMS).length + - " bytes" - ); - } - } else { - console.warn("Rooms were already selected!"); - } - } else { - console.warn("Not linked to any room(s)!"); - } - } else { - console.warn("Socket is not open."); + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before selecting rooms!"); + return; + }; + + // Require once per packet + if (clVars.rooms.enablerState) { + console.warn("[CloudLink] Cannot use the room selector more than once at a time!"); + return; } - } - unlinkFromRooms() { - const self = this; - if (this.isRunning) { - if (this.isLinked) { - let tmp_msg = { - cmd: "unlink", - val: "", - }; + // Prevent running if not linked. + if (!clVars.rooms.isLinked) { + console.warn("[CloudLink] Cannot use room selector while not linked to rooms!"); + return; + }; - if (this.enableListener) { - tmp_msg["listener"] = autoConvert(this.setListener); - } + clVars.rooms.enablerState = true; + clVars.rooms.enablerValue = args.ROOMS; + } - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Command - Unsubscribes from all rooms and re-subscribes to the the "default" room on the server. + unlinkFromRooms() { - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } + // Must be connected to user rooms. + if (clVars.socket == null) return; - self.isLinked = false; - } else { - console.warn("Not linked to any rooms!"); - } - } else { - console.warn("Socket is not open."); + // Require server support + if (clVars.linkState.identifiedProtocol < 2) { + console.warn("[CloudLink] Server is too old! Must be at least 0.1.8.x to support rooms."); + return; } - } - sendGData({ DATA }) { - const self = this; - if (this.isRunning) { - if (!(String(DATA).length > 1000)) { - let tmp_msg = { - cmd: "gmsg", - val: autoConvert(DATA), - }; + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before unjoining rooms!"); + return; + }; - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } + // Prevent running if already unlinked. + if (!clVars.rooms.isLinked) { + console.warn("[CloudLink] Already unlinked from rooms!"); + return; + }; - if (this.enableRoom) { - tmp_msg["rooms"] = autoConvert(this.selectRoom); - } + // Prevent running if a room unlink is in progress. + if (clVars.rooms.isAttemptingUnlink) { + console.warn("[CloudLink] Currently unlinking from rooms! Please wait!"); + return; + }; - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + clVars.rooms.isAttemptingUnlink = true; + return sendMessage({ cmd: "unlink", val: "", listener: "unlink" }); + } - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet larger than 1000 bytes (1 KB), packet is " + - String(DATA).length + - " bytes" - ); - } - } else { - console.warn("Socket is not open."); - } + // Command - Sends a gmsg value. + // DATA - String + sendGData(args) { + + // Must be connected. + if (clVars.socket == null) return; + + return sendMessage({ cmd: "gmsg", val: args.DATA }); } - sendPData({ DATA, ID }) { - const self = this; - if (this.isRunning) { - if (!(String(DATA).length > 1000)) { - let tmp_msg = { - cmd: "pmsg", - val: autoConvert(DATA), - id: autoConvert(ID), - }; + // Command - Sends a pmsg value. + // DATA - String, ID - String (recipient ID) + sendPData(args) { - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } - if (this.enableRoom) { - tmp_msg["rooms"] = autoConvert(this.selectRoom); - } + // Must be connected. + if (clVars.socket == null) return; - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before sending private messages!"); + return; + }; - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet larger than 1000 bytes (1 KB), packet is " + - String(DATA).length + - " bytes" - ); - } - } else { - console.warn("Socket is not open."); - } + return sendMessage({ cmd: "pmsg", val: args.DATA, id: args.ID }); } - sendGDataAsVar({ VAR, DATA }) { - const self = this; - if (this.isRunning) { - if (!(String(DATA).length > 1000)) { - let tmp_msg = { - cmd: "gvar", - name: VAR, - val: autoConvert(DATA), - }; - - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } - if (this.enableRoom) { - tmp_msg["rooms"] = autoConvert(this.selectRoom); - } + // Command - Sends a gvar value. + // DATA - String, VAR - String (variable name) + sendGDataAsVar(args) { - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Must be connected. + if (clVars.socket == null) return; - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet larger than 1000 bytes (1 KB), packet is " + - String(DATA).length + - " bytes" - ); - } - } else { - console.warn("Socket is not open."); - } + return sendMessage({ cmd: "gvar", val: args.DATA, name: args.VAR }); } - sendPDataAsVar({ VAR, ID, DATA }) { - const self = this; - if (this.isRunning) { - if (!(String(DATA).length > 1000)) { - let tmp_msg = { - cmd: "pvar", - name: VAR, - val: autoConvert(DATA), - id: autoConvert(ID), - }; + // Command - Sends a pvar value. + // DATA - String, VAR - String (variable name), ID - String (recipient ID) + sendPDataAsVar(args) { - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } - if (this.enableRoom) { - tmp_msg["rooms"] = autoConvert(this.selectRoom); - } + // Must be connected. + if (clVars.socket == null) return; - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before sending private variables!"); + return; + }; - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet larger than 1000 bytes (1 KB), packet is " + - String(DATA).length + - " bytes" - ); - } - } else { - console.warn("Socket is not open."); - } + return sendMessage({ cmd: "pvar", val: args.DATA, name: args.VAR, id: args.ID }); } - runCMDnoID({ CMD, DATA }) { - const self = this; - if (this.isRunning) { - if (!(String(CMD).length > 100) || !(String(DATA).length > 1000)) { - let tmp_msg = { - cmd: String(CMD), - val: autoConvert(DATA), - }; - - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } - if (this.enableRoom) { - tmp_msg["rooms"] = String(this.selectRoom); - } + // Command - Sends a raw-format command without specifying an ID. + // CMD - String (command), DATA - String + runCMDnoID(args) { - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Must be connected. + if (clVars.socket == null) return; - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet with questionably long arguments" - ); - } - } else { - console.warn("Socket is not open."); - } + return sendMessage({ cmd: args.CMD, val: args.DATA }); } - runCMD({ CMD, ID, DATA }) { - const self = this; - if (this.isRunning) { - if ( - !(String(CMD).length > 100) || - !(String(ID).length > 20) || - !(String(DATA).length > 1000) - ) { - let tmp_msg = { - cmd: String(CMD), - id: autoConvert(ID), - val: autoConvert(DATA), - }; + // Command - Sends a raw-format command with an ID. + // CMD - String (command), DATA - String, ID - String (recipient ID) + runCMD(args) { - if (this.enableListener) { - tmp_msg["listener"] = String(this.setListener); - } - if (this.enableRoom) { - tmp_msg["rooms"] = String(this.selectRoom); - } + // Must be connected. + if (clVars.socket == null) return; - console.log("TX:", tmp_msg); - mWS.send(JSON.stringify(tmp_msg)); + // Prevent running if the username hasn't been set. + if (!clVars.username.accepted) { + console.warn("[CloudLink] Username must be set before using this command!"); + return; + }; - if (this.enableListener) { - if (!self.socketListeners.hasOwnProperty(this.setListener)) { - self.socketListeners[this.setListener] = false; - } - self.enableListener = false; - } - if (this.enableRoom) { - self.enableRoom = false; - self.selectRoom = ""; - } - } else { - console.warn( - "Blocking attempt to send packet with questionably long arguments" - ); - } - } else { - console.warn("Socket is not open."); - } + return sendMessage({ cmd: args.CMD, val: args.DATA, id: args.ID }); } - resetNewData({ TYPE }) { - const self = this; - if (this.isRunning) { - self.newSocketData[this.menuRemap[String(TYPE)]] = false; + // Command - Resets the "returnIsNewData" boolean state. + // TYPE - String (menu datamenu) + resetNewData(args) { + switch (args.TYPE) { + case 'Global data': + clVars.gmsg.hasNew = false; + break; + case 'Private data': + clVars.pmsg.hasNew = false; + break; + case 'Direct data': + clVars.direct.hasNew = false; + break; + case 'Status code': + clVars.statuscode.hasNew = false; + break; } } - resetNewVarData({ TYPE, VAR }) { - const self = this; - if (this.isRunning) { - if (this.varData.hasOwnProperty(this.menuRemap[TYPE])) { - if (this.varData[this.menuRemap[TYPE]].hasOwnProperty(VAR)) { - self.varData[this.menuRemap[TYPE]][VAR].isNew = false; + // Command - Resets the "returnIsNewVarData" boolean state. + // TYPE - String (menu varmenu), VAR - String (variable name) + resetNewVarData(args) { + switch (args.TYPE) { + case 'Global variables': + if (!clVars.gvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Global variable ${args.VAR} does not exist!`); + return; } - } + clVars.gvar.varStates[String(args.ID)].hasNew = false; + case 'Private variables': + if (!clVars.pvar.varStates.hasOwnProperty(String(args.VAR))) { + console.warn(`[CloudLink] Private variable ${args.VAR} does not exist!`); + return false; + } + clVars.pvar.varStates[String(args.ID)].hasNew = false; } } - resetNewListener({ ID }) { - const self = this; - if (this.isRunning) { - if (this.socketListeners.hasOwnProperty(String(ID))) { - self.socketListeners[String(ID)] = false; - } + // Command - Resets the "returnIsNewListener" boolean state. + // ID - Listener ID + resetNewListener(args) { + if (!clVars.listeners.varStates.hasOwnProperty(String(args.ID))) { + console.warn(`[CloudLink] Listener ID ${args.ID} does not exist!`); + return; } + clVars.listeners.varStates[String(args.ID)].hasNew = false; } - clearAllPackets({ TYPE }) { - const self = this; - if (this.menuRemap[String(TYPE)] == "all") { - self.socketData.gmsg = []; - self.socketData.pmsg = []; - self.socketData.direct = []; - self.socketData.statuscode = []; - self.socketData.gvar = []; - self.socketData.pvar = []; - } else { - self.socketData[this.menuRemap[String(TYPE)]] = []; + // Command - Clears all packet queues. + // TYPE - String (menu allmenu) + clearAllPackets(args) { + switch (args.TYPE) { + case 'Global data': + clVars.gmsg.queue = []; + break; + case 'Private data': + clVars.pmsg.queue = []; + break; + case 'Direct data': + clVars.direct.queue = []; + break; + case 'Status code': + clVars.statuscode.queue = []; + break; + case 'Global variables': + clVars.gvar.queue = []; + break; + case 'Private variables': + clVars.pvar.queue = []; + break; + case 'All data': + clVars.gmsg.queue = []; + clVars.pmsg.queue = []; + clVars.direct.queue = []; + clVars.statuscode.queue = []; + clVars.gvar.queue = []; + clVars.pvar.queue = []; + break; } } } - - console.log("CloudLink 4.0 loaded. Detecting unsandboxed mode."); - Scratch.extensions.register(new CloudLink(Scratch.vm.runtime)); + Scratch.extensions.register(new CloudLink()); })(Scratch); From ef527758ec41f21ccbe3564aeae00df4ed302a8c Mon Sep 17 00:00:00 2001 From: godslayerakp <74981904+RedMan13@users.noreply.github.com> Date: Sun, 5 Nov 2023 15:31:32 -0800 Subject: [PATCH 040/196] Add godslayerakp/ws extension (#826) --- docs/godslayerakp/ws.md | 127 +++++++++ extensions/extensions.json | 1 + extensions/godslayerakp/ws.js | 499 ++++++++++++++++++++++++++++++++++ images/godslayerakp/ws.png | Bin 0 -> 27890 bytes 4 files changed, 627 insertions(+) create mode 100644 docs/godslayerakp/ws.md create mode 100644 extensions/godslayerakp/ws.js create mode 100644 images/godslayerakp/ws.png diff --git a/docs/godslayerakp/ws.md b/docs/godslayerakp/ws.md new file mode 100644 index 0000000000..e9b09a01da --- /dev/null +++ b/docs/godslayerakp/ws.md @@ -0,0 +1,127 @@ +# WebSocket + +This extension lets you communicate directly with most [WebSocket](https://en.wikipedia.org/wiki/WebSocket) servers. This is the protocol that things like cloud variables and Cloudlink use. + +These are rather low level blocks. They let you establish the connection, but your project still needs to know what kinds of messages to send and how to read messages from the server. + +## Blocks + +```scratch +connect to [wss://...] :: #307eff +``` +You have to run this block before any of the other blocks can do anything. You need to provide a valid WebSocket URL. + +The URL should start with `ws://` or `wss://`. For security reasons, `ws://` URLs will usually only work if the WebSocket is running on your computer (for example, `ws://localhost:8000`). + +Something simple to play with is the echo server: `wss://echoserver.redman13.repl.co`. Any message you send to it, it'll send right back to you. + +Note that connections are **per sprite**. Each sprite (or clone) can connect to one server at a time. Multiple sprites can connect to the same or different servers as much as your computer allows, but note those will all be separate connections. + +--- + +```scratch +when connected :: hat #307eff +``` +
    + +```scratch + +``` +Connecting to the server can take some time. Use these blocks to know when the connection was successful. After this, you can start sending and receiving messages. + +When the connection is lost, any blocks under the hat will also be stopped. + +--- + +```scratch +when message received :: hat #307eff +``` +
    + +```scratch +(received message data :: #307eff) +``` + +These blocks let you receive messages from the server. The hat block block will run once for each message the server sends with the data stored in the round reporter block. + +Note that WebSocket supports two types of messages: + + - **Text messages**: The data in the block will just be the raw text from the server. + - **Binary messages**: The data in the block will be a base64-encoded data: URL of the data, as it may not be safe to store directly in a string. You can use other extensions to convert this to something useful, such as fetch, depending on what data it contains. + +If multiple messages are received in a single frame or if your message processing logic causes delays (for example, using wait blocks), messages after the first one will be placed in a **queue**. Once your script finishes, if there's anything in the queue, the "when message received" block will run again the next frame. + +--- + +```scratch +send message (...) :: #307eff +``` + +This is the other side: it lets you send messages to the server. Only text messages are supported; binary messages are not yet supported. + +There's no queue this time. The messages are sent over as fast as your internet connection and the server will allow. + +--- + +```scratch +when connection closes :: hat #307eff +``` +
    + +```scratch + +``` +
    + +These let you detect when either the server closes the connection or your project closes the connection. They don't distinguish. Note that connections have separate blocks. + +Servers can close connections for a lot of reasons: perhaps it's restarting, or perhaps your project tried to do something the server didn't like. + +```scratch +(closing code :: #307eff) +``` +
    + +```scratch +(closing message :: #307eff) +``` + +These blocks can help you gain some insight. Closing code is a number from the WebSocket protocol. There is a [big table](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code#value) of possible values, but generally there is very little to gain from looking at these. + +Servers can also send a text "reason" when they close the connection, although almost no servers actually do this. + +```scratch +close connection :: #307eff +``` +
    + +```scratch +close connection with code (1000) :: #307eff +``` +
    + +```scratch +close connection with reason (...) and code (1000) :: #307eff +``` + +Your project can also close the connection whenever it wants. All of these blocks do basically the same thing. + +Just like how the server can send a code and a reason when it closes the connection, you can send those to the server. Note some limitations: + + - **Code** can be either the number 1000 ("Normal Closure") or an integer in the range 3000-4999 (meaning depends on what server you're talking to). Anything not in this range will be converted to 1000. Few servers will look at this. + - **Reason** can be any text up to 123-bytes long when encoded as UTF-8. Usually that just means up to 123 characters, but things like Emoji are technically multiple characters. Regardless very few servers will even bother to look at this. + +--- + +```scratch +when connection errors :: hat #307eff +``` +
    + +```scratch + +``` + +Sometimes things don't go so well. Maybe your internet connection died, the server is down, or you typed in the wrong URL. There's a lot of things that can go wrong. These let you try to handle that. + +Unfortunately we can't give much insight as to what caused the errors. Your browser tells us very little, but even if it did give us more information, it probably wouldn't be very helpful. diff --git a/extensions/extensions.json b/extensions/extensions.json index 2781ff7955..c6892eb6d7 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -73,6 +73,7 @@ "qxsck/var-and-list", "vercte/dictionaries", "godslayerakp/http", + "godslayerakp/ws", "Lily/CommentBlocks", "veggiecan/LongmanDictionary", "CubesterYT/TurboHook", diff --git a/extensions/godslayerakp/ws.js b/extensions/godslayerakp/ws.js new file mode 100644 index 0000000000..bb49c3a876 --- /dev/null +++ b/extensions/godslayerakp/ws.js @@ -0,0 +1,499 @@ +// Name: WebSocket +// ID: gsaWebsocket +// Description: Manually connect to WebSocket servers. +// By: RedMan13 + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("can not load outside unsandboxed mode"); + } + + const blobToDataURL = (blob) => + new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result); + fr.onerror = () => + reject(new Error(`Failed to read as data URL: ${fr.error}`)); + fr.readAsDataURL(blob); + }); + + /* ------- BLOCKS -------- */ + const { BlockType, Cast, ArgumentType } = Scratch; + const vm = Scratch.vm; + const runtime = vm.runtime; + + /** + * @typedef WebSocketInfo + * @property {boolean} instanceReplaced + * @property {boolean} manuallyClosed + * @property {boolean} errored + * @property {string} closeMessage + * @property {number} closeCode + * @property {string} data + * @property {string[]} messageQueue + * @property {WebSocket|null} websocket + * @property {VM.Thread[]} connectThreads + * @property {boolean} messageThreadsRunning + * @property {VM.Thread[]} messageThreads + * @property {object[]} sendOnceConnected + */ + + /** + * @param {unknown} exitCode + * @return {number} a valid code that won't throw an error in WebSocket#close() + */ + const toCloseCode = (exitCode) => { + const casted = Cast.toNumber(exitCode); + // Only valid values are 1000 or the range 3000-4999 + if (casted === 1000 || (casted >= 3000 && casted <= 4999)) { + return casted; + } + return 1000; + }; + + /** + * @param {unknown} reason + * @returns {string} a valid reason that won't throw an error in WebSocket#close() + */ + const toCloseReason = (reason) => { + const casted = Cast.toString(reason); + + // Reason can't be longer than 123 UTF-8 bytes + // We can't just truncate by reason.length as that would not work for eg. emoji + const encoder = new TextEncoder(); + let encoded = encoder.encode(casted); + encoded = encoded.slice(0, 123); + + // Now we have another problem: If the 123 byte cut-off produced invalid UTF-8, we + // need to keep cutting off bytes until it's valid. + const decoder = new TextDecoder(); + while (encoded.byteLength > 0) { + try { + const decoded = decoder.decode(encoded); + return decoded; + } catch (e) { + encoded = encoded.slice(0, encoded.byteLength - 1); + } + } + + return ""; + }; + + class WebSocketExtension { + /** + * no need to install runtime as it comes with Scratch var + */ + constructor() { + /** @type {Record} */ + this.instances = {}; + + runtime.on("targetWasRemoved", (target) => { + const instance = this.instances[target.id]; + if (instance) { + instance.manuallyClosed = true; + if (instance.websocket) { + instance.websocket.close(); + } + delete this.instances[target.id]; + } + }); + } + getInfo() { + return { + id: "gsaWebsocket", + name: "WebSocket", + docsURI: "https://extensions.turbowarp.org/godslayerakp/ws", + color1: "#307eff", + color2: "#2c5eb0", + blocks: [ + { + opcode: "newInstance", + blockType: BlockType.COMMAND, + arguments: { + URL: { + type: ArgumentType.STRING, + defaultValue: "wss://echoserver.redman13.repl.co", + }, + }, + text: "connect to [URL]", + }, + "---", + { + opcode: "onOpen", + blockType: BlockType.EVENT, + isEdgeActivated: false, + shouldRestartExistingThreads: true, + text: "when connected", + }, + { + opcode: "isConnected", + blockType: BlockType.BOOLEAN, + text: "is connected?", + disableMonitor: true, + }, + "---", + { + opcode: "onMessage", + blockType: BlockType.EVENT, + isEdgeActivated: false, + shouldRestartExistingThreads: true, + text: "when message received", + }, + { + opcode: "messageData", + blockType: BlockType.REPORTER, + text: "received message data", + disableMonitor: true, + }, + "---", + { + opcode: "sendMessage", + blockType: BlockType.COMMAND, + arguments: { + PAYLOAD: { + type: ArgumentType.STRING, + defaultValue: "hello!", + }, + }, + text: "send message [PAYLOAD]", + }, + "---", + { + opcode: "onError", + blockType: BlockType.EVENT, + isEdgeActivated: false, + shouldRestartExistingThreads: true, + text: "when connection errors", + }, + { + opcode: "hasErrored", + blockType: BlockType.BOOLEAN, + text: "has connection errored?", + disableMonitor: true, + }, + "---", + { + opcode: "onClose", + blockType: BlockType.EVENT, + isEdgeActivated: false, + shouldRestartExistingThreads: true, + text: "when connection closes", + }, + { + opcode: "isClosed", + blockType: BlockType.BOOLEAN, + text: "is connection closed?", + disableMonitor: true, + }, + { + opcode: "closeCode", + blockType: BlockType.REPORTER, + text: "closing code", + disableMonitor: true, + }, + { + opcode: "closeMessage", + blockType: BlockType.REPORTER, + text: "closing message", + disableMonitor: true, + }, + { + opcode: "closeWithoutReason", + blockType: BlockType.COMMAND, + text: "close connection", + }, + { + opcode: "closeWithCode", + blockType: BlockType.COMMAND, + arguments: { + CODE: { + type: ArgumentType.NUMBER, + defaultValue: "1000", + }, + }, + text: "close connection with code [CODE]", + }, + { + opcode: "closeWithReason", + blockType: BlockType.COMMAND, + arguments: { + CODE: { + type: ArgumentType.NUMBER, + defaultValue: "1000", + }, + REASON: { + type: ArgumentType.STRING, + defaultValue: "fulfilled", + }, + }, + text: "close connection with reason [REASON] and code [CODE]", + }, + ], + }; + } + + newInstance(args, util) { + const target = util.target; + + let url = Cast.toString(args.URL); + if (!/^(ws|wss):/is.test(url)) { + // url doesnt start with a valid connection type + // so we just assume its formated without it + if (/^(?!(ws|http)s?:\/\/).*$/is.test(url)) { + url = `wss://${url}`; + } else if (/^(http|https):/is.test(url)) { + const urlParts = url.split(":"); + urlParts[0] = url.toLowerCase().startsWith("https") ? "wss" : "ws"; + url = urlParts.join(":"); + } else { + // we couldnt fix the url... + return; + } + } + + const oldInstance = this.instances[util.target.id]; + if (oldInstance) { + oldInstance.instanceReplaced = true; + if (oldInstance.websocket) { + oldInstance.websocket.close(); + } + } + + /** @type {WebSocketInfo} */ + const instance = { + instanceReplaced: false, + manuallyClosed: false, + errored: false, + closeMessage: "", + closeCode: 0, + data: "", + websocket: null, + messageThreadsRunning: false, + connectThreads: [], + messageThreads: [], + messageQueue: [], + sendOnceConnected: [], + }; + this.instances[util.target.id] = instance; + + return Scratch.canFetch(url) + .then( + (allowed) => + new Promise((resolve) => { + if ( + !allowed || + instance.instanceReplaced || + instance.manuallyClosed + ) { + resolve(); + return; + } + + // canFetch() checked above + // eslint-disable-next-line no-restricted-syntax + const websocket = new WebSocket(url); + instance.websocket = websocket; + + const beforeExecute = () => { + if (instance.messageThreadsRunning) { + const stillRunning = instance.messageThreads.some((i) => + runtime.isActiveThread(i) + ); + if (!stillRunning) { + const isQueueEmpty = instance.messageQueue.length === 0; + if (isQueueEmpty) { + instance.messageThreadsRunning = false; + instance.messageThreads = []; + } else { + instance.data = instance.messageQueue.shift(); + instance.messageThreads = runtime.startHats( + "gsaWebsocket_onMessage", + null, + target + ); + } + } + } + }; + + const onStopAll = () => { + instance.instanceReplaced = true; + instance.manuallyClosed = true; + instance.websocket.close(); + }; + + vm.runtime.on("BEFORE_EXECUTE", beforeExecute); + vm.runtime.on("PROJECT_STOP_ALL", onStopAll); + + const cleanup = () => { + vm.runtime.off("BEFORE_EXECUTE", beforeExecute); + vm.runtime.off("PROJECT_STOP_ALL", onStopAll); + + for (const thread of instance.connectThreads) { + thread.status = 4; // STATUS_DONE + } + + resolve(); + }; + + websocket.onopen = (e) => { + if (instance.instanceReplaced || instance.manuallyClosed) { + cleanup(); + websocket.close(); + return; + } + + for (const item of instance.sendOnceConnected) { + websocket.send(item); + } + instance.sendOnceConnected.length = 0; + + instance.connectThreads = runtime.startHats( + "gsaWebsocket_onOpen", + null, + target + ); + resolve(); + }; + + websocket.onclose = (e) => { + if (instance.instanceReplaced) return; + instance.closeMessage = e.reason || ""; + instance.closeCode = e.code; + runtime.startHats("gsaWebsocket_onClose", null, target); + cleanup(); + }; + + websocket.onerror = (e) => { + if (instance.instanceReplaced) return; + console.error("websocket error", e); + instance.errored = true; + runtime.startHats("gsaWebsocket_onError", null, target); + cleanup(); + }; + + websocket.onmessage = async (e) => { + if (instance.instanceReplaced || instance.manuallyClosed) + return; + + let data = e.data; + + // Convert binary messages to a data: uri + // TODO: doing this right now might break order? + if (data instanceof Blob) { + data = await blobToDataURL(data); + } + + if (instance.messageThreadsRunning) { + instance.messageQueue.push(data); + } else { + instance.data = data; + instance.messageThreads = runtime.startHats( + "gsaWebsocket_onMessage", + null, + target + ); + instance.messageThreadsRunning = true; + } + }; + }) + ) + .catch((error) => { + console.error("could not open websocket connection", error); + }); + } + + isConnected(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return false; + return ( + !!instance.websocket && instance.websocket.readyState === WebSocket.OPEN + ); + } + + messageData(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return ""; + return instance.data; + } + + isClosed(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return false; + return ( + !!instance.websocket && + instance.websocket.readyState === WebSocket.CLOSED + ); + } + + closeCode(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return 0; + return instance.closeCode; + } + + closeMessage(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return ""; + return instance.closeMessage; + } + + hasErrored(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return false; + return instance.errored; + } + + sendMessage(args, utils) { + const PAYLOAD = Cast.toString(args.PAYLOAD); + const instance = this.instances[utils.target.id]; + if (!instance) return; + + if ( + !instance.websocket || + instance.websocket.readyState === WebSocket.CONNECTING + ) { + // Trying to send now will throw an error. Send it once we get connected. + instance.sendOnceConnected.push(PAYLOAD); + } else { + // CLOSING and CLOSED states won't throw an error, just silently ignore + instance.websocket.send(PAYLOAD); + } + } + + closeWithoutReason(_, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return; + instance.manuallyClosed = true; + if (instance.websocket) { + instance.websocket.close(); + } + } + + closeWithCode(args, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return; + instance.manuallyClosed = true; + if (instance.websocket) { + instance.websocket.close(toCloseCode(args.CODE)); + } + } + + closeWithReason(args, utils) { + const instance = this.instances[utils.target.id]; + if (!instance) return; + instance.manuallyClosed = true; + if (instance.websocket) { + instance.websocket.close( + toCloseCode(args.CODE), + toCloseReason(args.REASON) + ); + } + } + } + + // @ts-ignore + Scratch.extensions.register(new WebSocketExtension()); +})(Scratch); diff --git a/images/godslayerakp/ws.png b/images/godslayerakp/ws.png new file mode 100644 index 0000000000000000000000000000000000000000..e8fb334daabfa40847f0d5a078883bcb6f61400f GIT binary patch literal 27890 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 Date: Sun, 5 Nov 2023 19:00:21 -0500 Subject: [PATCH 041/196] Add Pen+ V6 (#535) A massive rewrite of the extension no longer compatible so it is a different file to allow use of the old one. Maybe closes https://github.com/TurboWarp/extensions/issues/170 Closes https://github.com/TurboWarp/extensions/issues/226 --- extensions/extensions.json | 1 + extensions/obviousAlexC/penPlus.js | 2704 ++++++++++++++++++++++++++++ extensions/penplus.js | 6 +- 3 files changed, 2708 insertions(+), 3 deletions(-) create mode 100644 extensions/obviousAlexC/penPlus.js diff --git a/extensions/extensions.json b/extensions/extensions.json index c6892eb6d7..d49a2c7c67 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -19,6 +19,7 @@ "iframe", "Xeltalliv/clippingblending", "clipboard", + "obviousAlexC/penPlus", "penplus", "Lily/Skins", "obviousAlexC/SensingPlus", diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js new file mode 100644 index 0000000000..3d6c9fd469 --- /dev/null +++ b/extensions/obviousAlexC/penPlus.js @@ -0,0 +1,2704 @@ +// Name: Pen Plus V6 +// ID: penP +// Description: Advanced rendering capabilities. +// By: ObviousAlexC + +(function (Scratch) { + "use strict"; + + //?some smaller optimizations just store the multiplacation for later + const f32_4 = 4 * Float32Array.BYTES_PER_ELEMENT; + const f32_8 = 8 * Float32Array.BYTES_PER_ELEMENT; + const f32_10 = 10 * Float32Array.BYTES_PER_ELEMENT; + const d2r = 0.0174533; + + //?Declare most of the main repo's we are going to use around the scratch vm + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = runtime.renderer; + const shaderManager = renderer._shaderManager; + + const canvas = renderer.canvas; + const gl = renderer._gl; + let currentFilter = gl.NEAREST; + + let nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + //?create the depth buffer's texture + //*Create it in scratch's gl so that we have it stored in there! + let depthBufferTexture = gl.createTexture(); + + //?Make a function for updating the depth canvas to fit the scratch stage + const depthFrameBuffer = gl.createFramebuffer(); + const depthColorBuffer = gl.createRenderbuffer(); + const depthDepthBuffer = gl.createRenderbuffer(); + + let lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + + //?Buffer handling and pen loading + { + gl.bindTexture(gl.TEXTURE_2D, depthBufferTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + nativeSize[0], + nativeSize[1], + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null + ); + + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, depthBufferTexture); + gl.activeTexture(gl.TEXTURE0); + + gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthColorBuffer); + gl.renderbufferStorage( + gl.RENDERBUFFER, + gl.RGBA8 || gl.RGBA4, + nativeSize[0], + nativeSize[1] + ); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.RENDERBUFFER, + depthColorBuffer + ); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthDepthBuffer); + gl.renderbufferStorage( + gl.RENDERBUFFER, + gl.DEPTH_COMPONENT16, + nativeSize[0], + nativeSize[1] + ); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, + depthDepthBuffer + ); + + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + depthBufferTexture, + 0 + ); + + gl.enable(gl.DEPTH_TEST); + + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + + const updateCanvasSize = () => { + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + + gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthColorBuffer); + gl.renderbufferStorage( + gl.RENDERBUFFER, + gl.RGBA8 || gl.RGBA4, + nativeSize[0], + nativeSize[1] + ); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthDepthBuffer); + gl.renderbufferStorage( + gl.RENDERBUFFER, + gl.DEPTH_COMPONENT16, + nativeSize[0], + nativeSize[1] + ); + + gl.activeTexture(gl.TEXTURE1); + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + nativeSize[0], + nativeSize[1], + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null + ); + + gl.activeTexture(gl.TEXTURE0); + + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + }; + + //?Call it to have it consistant + updateCanvasSize(); + + //?Call every frame because I don't know of a way to detect when the stage is resized + + window.addEventListener("resize", updateCanvasSize); + vm.runtime.on("STAGE_SIZE_CHANGED", () => { + updateCanvasSize(); + }); + + vm.runtime.on("BEFORE_EXECUTE", () => { + if ( + (renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize) != nativeSize + ) { + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + updateCanvasSize(); + } + }); + + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + + //?Make sure pen is loaded! + if (!Scratch.vm.extensionManager.isExtensionLoaded("pen")) { + runtime.extensionManager.loadExtensionIdSync("pen"); + } + } + + //?Ported from Pen+ version 5 + //?Just a costume library for data uris + const penPlusCostumeLibrary = {}; + let penPlusImportWrapMode = gl.CLAMP_TO_EDGE; + + //?Debug for depth + penPlusCostumeLibrary["!Debug_Depth"] = depthBufferTexture; + + const checkForPen = (util) => { + const curTarget = util.target; + const customState = curTarget["_customState"]; + if (!customState["Scratch.pen"]) { + customState["Scratch.pen"] = { + penDown: false, + color: 66.66, + saturation: 100, + brightness: 100, + transparency: 0, + _shade: 50, + penAttributes: { + color4f: [0, 0, 1, 1], + diameter: 1, + }, + }; + } + }; + + //*Define PEN+ variables >:) + const triangleDefaultAttributes = [ + // U V TINT R G B Z W transparency U V TINT R G B Z W transparency U V TINT R G B Z W transparency + 0, + 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, + 1, + ]; + const squareDefaultAttributes = [ + // width* height* rotation u-mul u v-mul v r g b transparency + 1, 1, 90, 1, 0, 1, 0, 1, 1, 1, 1, 1, + ]; + + const triangleAttributesOfAllSprites = {}; //!it dawned on me I have to do this + + const squareAttributesOfAllSprites = {}; //?Doing this for part 2 + + //?Get Shaders + const penPlusShaders = { + untextured: { + Shaders: { + vert: ` + attribute highp vec4 a_position; + attribute highp vec4 a_color; + varying highp vec4 v_color; + + varying highp float v_depth; + + void main() + { + v_color = a_color; + v_depth = a_position.z; + gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + } + `, + frag: ` + varying highp vec4 v_color; + + uniform mediump vec2 u_res; + uniform sampler2D u_depthTexture; + + varying highp float v_depth; + + void main() + { + gl_FragColor = v_color; + highp vec4 v_depthPart = texture2D(u_depthTexture,gl_FragCoord.xy/u_res); + highp float v_depthcalc = v_depthPart.r + floor((v_depthPart.g + floor(v_depthPart.b * 100.0 )) * 100.0); + + highp float v_inDepth = v_depth; + + if (v_depth < 0.0 ) { + v_inDepth = 0.0; + } + if (v_depth > 10000.0 ) { + v_inDepth = 10000.0; + } + + if (v_depthcalc < v_inDepth){ + gl_FragColor.a = 0.0; + } + + gl_FragColor.rgb *= gl_FragColor.a; + } + `, + }, + ProgramInf: null, + }, + textured: { + Shaders: { + vert: ` + attribute highp vec4 a_position; + attribute highp vec4 a_color; + attribute highp vec2 a_texCoord; + + varying highp vec4 v_color; + varying highp vec2 v_texCoord; + + varying highp float v_depth; + + void main() + { + v_color = a_color; + v_texCoord = a_texCoord; + v_depth = a_position.z; + gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + } + `, + frag: ` + uniform sampler2D u_texture; + + varying highp vec2 v_texCoord; + varying highp vec4 v_color; + + uniform mediump vec2 u_res; + uniform sampler2D u_depthTexture; + + varying highp float v_depth; + + void main() + { + gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; + highp vec4 v_depthPart = texture2D(u_depthTexture,gl_FragCoord.xy/u_res); + highp float v_depthcalc = v_depthPart.r + floor((v_depthPart.g + floor(v_depthPart.b * 100.0 )) * 100.0); + + highp float v_inDepth = v_depth; + + if (v_depth < 0.0 ) { + v_inDepth = 0.0; + } + if (v_depth > 10000.0 ) { + v_inDepth = 10000.0; + } + + if (v_depthcalc < v_inDepth){ + gl_FragColor.a = 0.0; + } + + gl_FragColor.rgb *= gl_FragColor.a; + + } + `, + }, + ProgramInf: null, + }, + depth: { + Shaders: { + vert: ` + attribute highp vec4 a_position; + + varying highp float v_depth; + + void main() + { + v_depth = a_position.z; + gl_Position = a_position * vec4(a_position.w,a_position.w,a_position.w * 0.0001,1); + } + `, + frag: ` + varying highp float v_depth; + + void main() + { + if (v_depth >= 10000.0) { + gl_FragColor = vec4(1,1,1,1); + } + else { + highp float d_100 = floor(v_depth / 100.0); + gl_FragColor = vec4( + mod(v_depth,1.0), + mod( floor( v_depth - mod(v_depth,1.0) )/100.0,1.0), + mod( floor( d_100 - mod(d_100,1.0) )/100.0,1.0), + 1); + } + } + `, + }, + ProgramInf: null, + }, + pen: { + program: null, + }, + createAndCompileShaders: (vert, frag) => { + //? compile vertex Shader + const vertShader = gl.createShader(gl.VERTEX_SHADER); + try { + gl.shaderSource(vertShader, vert.trim()); + gl.compileShader(vertShader); + if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { + throw gl.getShaderInfoLog(vertShader); + } + } catch (error) { + console.error(error); + } + + //? compile fragment Shader + const fragShader = gl.createShader(gl.FRAGMENT_SHADER); + try { + gl.shaderSource(fragShader, frag.trim()); + gl.compileShader(fragShader); + if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { + throw gl.getShaderInfoLog(fragShader); + } + } catch (error) { + console.error(error); + } + + //? compile program + const program = gl.createProgram(); + try { + gl.attachShader(program, vertShader); + gl.attachShader(program, fragShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw gl.getProgramInfoLog(program); + } + + gl.validateProgram(program); + if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS)) { + throw gl.getProgramInfoLog(program); + } + } catch (error) { + console.error(error); + } + + return { + program: program, + vert: vertShader, + frag: fragShader, + }; + }, + }; + + //? Create program info + { + penPlusShaders.untextured.ProgramInf = + penPlusShaders.createAndCompileShaders( + penPlusShaders.untextured.Shaders.vert, + penPlusShaders.untextured.Shaders.frag + ); + penPlusShaders.textured.ProgramInf = penPlusShaders.createAndCompileShaders( + penPlusShaders.textured.Shaders.vert, + penPlusShaders.textured.Shaders.frag + ); + + penPlusShaders.depth.ProgramInf = penPlusShaders.createAndCompileShaders( + penPlusShaders.depth.Shaders.vert, + penPlusShaders.depth.Shaders.frag + ); + } + + //?Untextured + const a_position_Location_untext = gl.getAttribLocation( + penPlusShaders.untextured.ProgramInf.program, + "a_position" + ); + const a_color_Location_untext = gl.getAttribLocation( + penPlusShaders.untextured.ProgramInf.program, + "a_color" + ); + + //?Textured + const a_position_Location_text = gl.getAttribLocation( + penPlusShaders.textured.ProgramInf.program, + "a_position" + ); + const a_color_Location_text = gl.getAttribLocation( + penPlusShaders.textured.ProgramInf.program, + "a_color" + ); + const a_textCoord_Location_text = gl.getAttribLocation( + penPlusShaders.textured.ProgramInf.program, + "a_texCoord" + ); + + //?Uniforms + const u_texture_Location_text = gl.getUniformLocation( + penPlusShaders.textured.ProgramInf.program, + "u_texture" + ); + + const u_depthTexture_Location_untext = gl.getUniformLocation( + penPlusShaders.untextured.ProgramInf.program, + "u_depthTexture" + ); + + const u_depthTexture_Location_text = gl.getUniformLocation( + penPlusShaders.textured.ProgramInf.program, + "u_depthTexture" + ); + + const u_res_Location_untext = gl.getUniformLocation( + penPlusShaders.untextured.ProgramInf.program, + "u_res" + ); + + const u_res_Location_text = gl.getUniformLocation( + penPlusShaders.textured.ProgramInf.program, + "u_res" + ); + + //?Depth + const a_position_Location_depth = gl.getAttribLocation( + penPlusShaders.depth.ProgramInf.program, + "a_position" + ); + + //?Enables Attributes + const vertexBuffer = gl.createBuffer(); + const depthVertexBuffer = gl.createBuffer(); + let vertexBufferData = null; + + { + gl.enableVertexAttribArray(a_position_Location_untext); + gl.enableVertexAttribArray(a_color_Location_untext); + gl.enableVertexAttribArray(a_position_Location_text); + gl.enableVertexAttribArray(a_color_Location_text); + gl.enableVertexAttribArray(a_textCoord_Location_text); + gl.enableVertexAttribArray(a_position_Location_depth); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ARRAY_BUFFER, depthVertexBuffer); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + } + + //?Override pen Clear with pen+ + renderer.penClear = (penSkinID) => { + const lastCC = gl.getParameter(gl.COLOR_CLEAR_VALUE); + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + //Pen+ Overrides default pen Clearing + gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + gl.clearColor(1, 1, 1, 1); + gl.clear(gl.DEPTH_BUFFER_BIT); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + gl.clearColor(lastCC[0], lastCC[1], lastCC[2], lastCC[3]); + + //? ^ just clear the depth buffer for when its added. + + //Old clearing + renderer.dirty = true; + const skin = /** @type {PenSkin} */ renderer._allSkins[penSkinID]; + skin.clear(); + }; + + //Pen+ advanced options update + //I plan to add more later + const penPlusAdvancedSettings = { + wValueUnderFlow: false, + useDepthBuffer: true, + _ClampZ: false, + _maxDepth: 1000, + }; + + //?Have this here for ez pz tri drawing on the canvas + const triFunctions = { + drawTri: (curProgram, x1, y1, x2, y2, x3, y3, penColor, targetID) => { + //? get triangle attributes for current sprite. + const triAttribs = triangleAttributesOfAllSprites[targetID]; + + if (triAttribs) { + vertexBufferData = new Float32Array([ + x1, + -y1, + triAttribs[5], + triAttribs[6], + penColor[0] * triAttribs[2], + penColor[1] * triAttribs[3], + penColor[2] * triAttribs[4], + penColor[3] * triAttribs[7], + + x2, + -y2, + triAttribs[13], + triAttribs[14], + penColor[0] * triAttribs[10], + penColor[1] * triAttribs[11], + penColor[2] * triAttribs[12], + penColor[3] * triAttribs[15], + + x3, + -y3, + triAttribs[21], + triAttribs[22], + penColor[0] * triAttribs[18], + penColor[1] * triAttribs[19], + penColor[2] * triAttribs[20], + penColor[3] * triAttribs[23], + ]); + } else { + vertexBufferData = new Float32Array([ + x1, + -y1, + 1, + 1, + penColor[0], + penColor[1], + penColor[2], + penColor[3], + + x2, + -y2, + 1, + 1, + penColor[0], + penColor[1], + penColor[2], + penColor[3], + + x3, + -y3, + 1, + 1, + penColor[0], + penColor[1], + penColor[2], + penColor[3], + ]); + } + + //? Bind Positional Data + + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.STATIC_DRAW); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + + gl.vertexAttribPointer( + a_position_Location_untext, + 4, + gl.FLOAT, + false, + f32_8, + 0 + ); + gl.vertexAttribPointer( + a_color_Location_untext, + 4, + gl.FLOAT, + false, + f32_8, + f32_4 + ); + + gl.useProgram(penPlusShaders.untextured.ProgramInf.program); + + gl.uniform1i(u_depthTexture_Location_untext, 1); + + gl.uniform2fv(u_res_Location_untext, nativeSize); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + //? Hacky fix but it works. + + if (penPlusAdvancedSettings.useDepthBuffer) { + triFunctions.drawDepthTri(targetID, x1, y1, x2, y2, x3, y3); + } + gl.useProgram(penPlusShaders.pen.program); + }, + + drawTextTri: (curProgram, x1, y1, x2, y2, x3, y3, targetID, texture) => { + //? get triangle attributes for current sprite. + const triAttribs = triangleAttributesOfAllSprites[targetID]; + + if (triAttribs) { + vertexBufferData = new Float32Array([ + x1, + -y1, + penPlusAdvancedSettings.useDepthBuffer ? triAttribs[5] : 0, + triAttribs[6], + triAttribs[2], + triAttribs[3], + triAttribs[4], + triAttribs[7], + triAttribs[0], + triAttribs[1], + + x2, + -y2, + penPlusAdvancedSettings.useDepthBuffer ? triAttribs[13] : 0, + triAttribs[14], + triAttribs[10], + triAttribs[11], + triAttribs[12], + triAttribs[15], + triAttribs[8], + triAttribs[9], + + x3, + -y3, + penPlusAdvancedSettings.useDepthBuffer ? triAttribs[21] : 0, + triAttribs[22], + triAttribs[18], + triAttribs[19], + triAttribs[20], + triAttribs[23], + triAttribs[16], + triAttribs[17], + ]); + } else { + vertexBufferData = new Float32Array([ + x1, + -y1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + + x2, + -y2, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + + x3, + -y3, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + ]); + } + //? Bind Positional Data + gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.DYNAMIC_DRAW); + + gl.vertexAttribPointer( + a_position_Location_text, + 4, + gl.FLOAT, + false, + f32_10, + 0 + ); + gl.vertexAttribPointer( + a_color_Location_text, + 4, + gl.FLOAT, + false, + f32_10, + f32_4 + ); + gl.vertexAttribPointer( + a_textCoord_Location_text, + 2, + gl.FLOAT, + false, + f32_10, + f32_8 + ); + + gl.useProgram(penPlusShaders.textured.ProgramInf.program); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, currentFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, currentFilter); + gl.uniform1i(u_texture_Location_text, 0); + + gl.uniform1i(u_depthTexture_Location_text, 1); + + gl.uniform2fv(u_res_Location_text, nativeSize); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + if (penPlusAdvancedSettings.useDepthBuffer) { + triFunctions.drawDepthTri(targetID, x1, y1, x2, y2, x3, y3); + } + gl.useProgram(penPlusShaders.pen.program); + }, + + //? this is so I don't have to go through the hassle of replacing default scratch shaders + //? many of curse words where exchanged between me and a pillow while writing this extension + //? but I have previaled! + drawDepthTri: (targetID, x1, y1, x2, y2, x3, y3) => { + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + const triAttribs = triangleAttributesOfAllSprites[targetID]; + gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + + if (triAttribs) { + vertexBufferData = new Float32Array([ + x1, + -y1, + triAttribs[5], + triAttribs[6], + + x2, + -y2, + triAttribs[13], + triAttribs[14], + + x3, + -y3, + triAttribs[21], + triAttribs[22], + ]); + } else { + vertexBufferData = new Float32Array([ + x1, + -y1, + 0, + 1, + + x2, + -y2, + 0, + 1, + + x3, + -y3, + 0, + 1, + ]); + } + + //? Bind Positional Data + gl.bindBuffer(gl.ARRAY_BUFFER, depthVertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.DYNAMIC_DRAW); + + gl.vertexAttribPointer( + a_position_Location_depth, + 4, + gl.FLOAT, + false, + f32_4, + 0 + ); + + gl.useProgram(penPlusShaders.depth.ProgramInf.program); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + }, + + setValueAccordingToCaseTriangle: ( + targetId, + attribute, + value, + wholeTri, + offset + ) => { + offset = offset + attribute || attribute; + let valuetoSet = 0; + switch (attribute) { + //U + case 0: + valuetoSet = value; + break; + //V + case 1: + valuetoSet = value; + break; + + //100 since that is what scratch users are accustomed to. + //R + case 2: + valuetoSet = Math.min(Math.max(value, 0), 100) * 0.01; + break; + //G + case 3: + valuetoSet = Math.min(Math.max(value, 0), 100) * 0.01; + break; + //B + case 4: + valuetoSet = Math.min(Math.max(value, 0), 100) * 0.01; + break; + + //Clamp to 0 so we can't go behind the stage. + //Z + case 5: + if (penPlusAdvancedSettings._ClampZ) { + if (value < 0) { + valuetoSet = 0; + break; + } + //convert to depth space for best accuracy + valuetoSet = Math.min( + (value * 10000) / penPlusAdvancedSettings._maxDepth, + 10000 + ); + break; + } + //convert to depth space for best accuracy + valuetoSet = (value * 10000) / penPlusAdvancedSettings._maxDepth; + break; + + //Clamp to 1 so we don't accidentally clip. + //W + case 6: + if (penPlusAdvancedSettings.wValueUnderFlow == true) { + valuetoSet = value; + } else { + valuetoSet = Math.max(value, 1); + } + break; + //Transparency + //Same story as color + case 7: + valuetoSet = Math.min(Math.max(value, 0), 1000) * 0.01; + break; + + //Just break if value isn't valid + default: + break; + } + //Check if the index even exists. + if (attribute >= 0 && attribute <= 7) { + if (wholeTri) { + triangleAttributesOfAllSprites[targetId][attribute] = valuetoSet; + triangleAttributesOfAllSprites[targetId][attribute + 8] = valuetoSet; + triangleAttributesOfAllSprites[targetId][attribute + 16] = valuetoSet; + } else { + triangleAttributesOfAllSprites[targetId][offset] = valuetoSet; + } + } + }, + }; + + const lilPenDabble = (InativeSize, curTarget, util) => { + checkForPen(util); + + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + + Scratch.vm.renderer.penLine( + Scratch.vm.renderer._penSkinId, + { + color4f: [1, 1, 1, 0.011], + diameter: 1, + }, + InativeSize[0] / 2, + InativeSize[1] / 2, + InativeSize[0] / 2, + InativeSize[1] / 2 + ); + }; + + //?Color Library + const colors = { + hexToRgb: (hex) => { + if (typeof hex == "string") { + const splitHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return { + r: parseInt(splitHex[1], 16), + g: parseInt(splitHex[2], 16), + b: parseInt(splitHex[3], 16), + }; + } + hex = Scratch.Cast.toNumber(hex); + return { + r: Math.floor(hex / 65536), + g: Math.floor(hex / 256) % 256, + b: hex % 256, + }; + }, + + rgbtoSColor: ({ R, G, B }) => { + R = Math.min(Math.max(R, 0), 100) * 2.55; + G = Math.min(Math.max(G, 0), 100) * 2.55; + B = Math.min(Math.max(B, 0), 100) * 2.55; + return (Math.ceil(R) * 256 + Math.ceil(G)) * 256 + Math.ceil(B); + }, + }; + + const textureFunctions = { + createBlankPenPlusTextureInfo: function ( + width, + height, + color, + name, + clamp + ) { + const texture = penPlusCostumeLibrary[name] + ? penPlusCostumeLibrary[name].texture + : gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + // Fill the texture with a 1x1 blue pixel. + + const pixelData = new Uint8Array(width * height * 4); + + const decodedColor = colors.hexToRgb(color); + + for (let pixelID = 0; pixelID < pixelData.length / 4; pixelID++) { + pixelData[pixelID * 4] = decodedColor.r; + pixelData[pixelID * 4 + 1] = decodedColor.g; + pixelData[pixelID * 4 + 2] = decodedColor.b; + pixelData[pixelID * 4 + 3] = 255; + } + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, clamp); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, clamp); + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + width, + height, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixelData + ); + + penPlusCostumeLibrary[name] = { + texture: texture, + width: width, + height: height, + }; + }, + createPenPlusTextureInfo: function (url, name, clamp) { + const texture = penPlusCostumeLibrary[name] + ? penPlusCostumeLibrary[name].texture + : gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + // Fill the texture with a 1x1 blue pixel. + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255]) + ); + + // Let's assume all images are not a power of 2 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, clamp); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, clamp); + return new Promise((resolve, reject) => { + Scratch.canFetch(url).then((allowed) => { + if (!allowed) { + reject(false); + } + // Permission is checked earlier. + // eslint-disable-next-line no-restricted-syntax + const image = new Image(); + image.onload = function () { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + image + ); + penPlusCostumeLibrary[name] = { + texture: texture, + width: image.width, + height: image.height, + }; + resolve(texture); + }; + image.crossOrigin = "anonymous"; + image.src = url; + }); + }); + }, + + getTextureData: (texture, width, height) => { + //?Initilize the temp framebuffer and assign it + const readBuffer = gl.createFramebuffer(); + + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + + gl.bindFramebuffer(gl.FRAMEBUFFER, readBuffer); + + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + texture, + 0 + ); + + //?make sure to unbind the framebuffer and delete it! + const removeBuffer = () => { + gl.deleteFramebuffer(readBuffer); + }; + + //?if sucessful read + if ( + gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE + ) { + //?Make an array to write the pixels onto + let dataArray = new Uint8Array(width * height * 4); + gl.readPixels( + 0, + 0, + width, + height, + gl.RGBA, + gl.UNSIGNED_BYTE, + dataArray + ); + + //?Remove Buffer data and return data + removeBuffer(); + return dataArray; + } + + //?If not return undefined + removeBuffer(); + return undefined; + }, + + getTextureAsURI: (texture, width, height) => { + //?Initilize the temp framebuffer and assign it + const readBuffer = gl.createFramebuffer(); + + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + + gl.bindFramebuffer(gl.FRAMEBUFFER, readBuffer); + + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + texture, + 0 + ); + + //?make sure to unbind the framebuffer and delete it! + const removeBuffer = () => { + gl.deleteFramebuffer(readBuffer); + }; + + //?if sucessful read + if ( + gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE + ) { + //?Make an array to write the pixels onto + let dataArray = new Uint8Array(width * height * 4); + gl.readPixels( + 0, + 0, + width, + height, + gl.RGBA, + gl.UNSIGNED_BYTE, + dataArray + ); + + //Make an invisible canvas + const dataURICanvas = document.createElement("canvas"); + dataURICanvas.width = width; + dataURICanvas.height = height; + const dataURIContext = dataURICanvas.getContext("2d"); + + // Copy the pixels to a 2D canvas + const imageData = dataURIContext.createImageData(width, height); + imageData.data.set(dataArray); + dataURIContext.putImageData(imageData, 0, 0); + + //?Remove Buffer data and return data + removeBuffer(); + return dataURICanvas.toDataURL(); + } + + //?If not return undefined + removeBuffer(); + return undefined; + }, + }; + + class extension { + getInfo() { + return { + blocks: [ + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Pen Properties", + }, + { + disableMonitor: true, + opcode: "isPenDown", + blockType: Scratch.BlockType.BOOLEAN, + text: "pen is down?", + arguments: {}, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "getPenHSV", + blockType: Scratch.BlockType.REPORTER, + text: "pen [HSV]", + arguments: { + HSV: { + type: Scratch.ArgumentType.STRING, + defaultValue: "color", + menu: "hsvMenu", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "drawDot", + blockType: Scratch.BlockType.COMMAND, + text: "draw dot at [x] [y]", + arguments: { + x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "drawLine", + blockType: Scratch.BlockType.COMMAND, + text: "draw line from [x1] [y1] to [x2] [y2]", + arguments: { + x1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + y1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + x2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + y2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + }, + filter: "sprite", + }, + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Square Pen Blocks", + }, + { + disableMonitor: true, + opcode: "squareDown", + blockType: Scratch.BlockType.COMMAND, + text: "stamp pen square", + arguments: {}, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "squareTexDown", + blockType: Scratch.BlockType.COMMAND, + text: "stamp pen square with the texture of [tex]", + arguments: { + tex: { type: Scratch.ArgumentType.STRING, menu: "costumeMenu" }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "setStampAttribute", + blockType: Scratch.BlockType.COMMAND, + text: "set pen square's [target] to [number]", + arguments: { + target: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + menu: "stampSquare", + }, + number: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "getStampAttribute", + blockType: Scratch.BlockType.REPORTER, + text: "get pen square's [target]", + arguments: { + target: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + menu: "stampSquare", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "tintSquare", + blockType: Scratch.BlockType.COMMAND, + text: "tint pen square to [color]", + arguments: { + color: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "resetSquareAttributes", + blockType: Scratch.BlockType.COMMAND, + text: "reset square Attributes", + arguments: {}, + filter: "sprite", + }, + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Triangle Blocks", + }, + { + disableMonitor: true, + opcode: "setTriangleFilterMode", + blockType: Scratch.BlockType.COMMAND, + text: "set triangle filter mode to [filter]", + arguments: { + filter: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 9728, + menu: "filterType", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "setTrianglePointAttribute", + blockType: Scratch.BlockType.COMMAND, + text: "set triangle point [point]'s [attribute] to [value]", + arguments: { + point: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + menu: "pointMenu", + }, + attribute: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + menu: "triAttribute", + }, + value: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "setWholeTrianglePointAttribute", + blockType: Scratch.BlockType.COMMAND, + text: "set triangle's [wholeAttribute] to [value]", + arguments: { + wholeAttribute: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + menu: "wholeTriAttribute", + }, + value: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "tintTriPoint", + blockType: Scratch.BlockType.COMMAND, + text: "tint triangle point [point] to [color]", + arguments: { + point: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + menu: "pointMenu", + }, + color: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "tintTri", + blockType: Scratch.BlockType.COMMAND, + text: "tint triangle to [color]", + arguments: { + point: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + menu: "pointMenu", + }, + color: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "getTrianglePointAttribute", + blockType: Scratch.BlockType.REPORTER, + text: "get triangle point [point]'s [attribute]", + arguments: { + point: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + menu: "pointMenu", + }, + attribute: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 2, + menu: "triAttribute", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "resetWholeTriangleAttributes", + blockType: Scratch.BlockType.COMMAND, + text: "reset triangle attributes", + arguments: {}, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "drawSolidTri", + blockType: Scratch.BlockType.COMMAND, + text: "draw triangle between [x1] [y1], [x2] [y2] and [x3] [y3]", + arguments: { + x1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + y1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + x2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + y2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + x3: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + y3: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "drawTexTri", + blockType: Scratch.BlockType.COMMAND, + text: "draw textured triangle between [x1] [y1], [x2] [y2] and [x3] [y3] with the texture [tex]", + arguments: { + x1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + y1: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + x2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + y2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + x3: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + y3: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + tex: { type: Scratch.ArgumentType.STRING, menu: "costumeMenu" }, + }, + filter: "sprite", + }, + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Color", + }, + { + disableMonitor: true, + opcode: "RGB2HEX", + blockType: Scratch.BlockType.REPORTER, + text: "red [R] green [G] blue [B]", + arguments: { + R: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + G: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + B: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + }, + }, + { + disableMonitor: true, + opcode: "HSV2RGB", + blockType: Scratch.BlockType.REPORTER, + text: "hue [H] saturation [S] value [V]", + arguments: { + H: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + S: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + V: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + }, + }, + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Images", + }, + { + disableMonitor: true, + opcode: "setDURIclampmode", + blockType: Scratch.BlockType.COMMAND, + text: "set imported image wrap mode to [clampMode]", + arguments: { + clampMode: { + type: Scratch.ArgumentType.STRING, + defaultValue: "33071", + menu: "wrapType", + }, + }, + }, + { + disableMonitor: true, + opcode: "addBlankIMG", + blockType: Scratch.BlockType.COMMAND, + text: "add blank image that is [color] and the size of [width], [height] named [name] to Pen+ Library", + arguments: { + color: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#ffffff", + }, + width: { type: Scratch.ArgumentType.NUMBER, defaultValue: 128 }, + height: { type: Scratch.ArgumentType.NUMBER, defaultValue: 128 }, + name: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Image", + }, + }, + }, + { + disableMonitor: true, + opcode: "addIMGfromDURI", + blockType: Scratch.BlockType.COMMAND, + text: "add image named [name] from [dataURI] to Pen+ Library", + arguments: { + dataURI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.png", + }, + name: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Image", + }, + }, + }, + { + disableMonitor: true, + opcode: "removeIMGfromDURI", + blockType: Scratch.BlockType.COMMAND, + text: "remove image named [name] from Pen+ Library", + arguments: { + name: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Image", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "doesIMGexist", + blockType: Scratch.BlockType.BOOLEAN, + text: "does [name] exist in Pen+ Library", + arguments: { + name: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Image", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "getCostumeDataURI", + blockType: Scratch.BlockType.REPORTER, + text: "get data uri for costume [costume]", + arguments: { + costume: { + type: Scratch.ArgumentType.STRING, + menu: "getCostumeDataURI_costume_Menu", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "getDimensionOf", + blockType: Scratch.BlockType.REPORTER, + text: "get the [dimension] of [costume] in pen+ costume library", + arguments: { + dimension: { + type: Scratch.ArgumentType.STRING, + menu: "getDimensionOf_dimension_Menu", + }, + costume: { + type: Scratch.ArgumentType.STRING, + menu: "penPlusCostumes", + }, + }, + filter: "sprite", + }, + { + disableMonitor: true, + opcode: "setpixelcolor", + blockType: Scratch.BlockType.COMMAND, + text: "set pixel [x] [y]'s color to [color] in [costume]", + arguments: { + x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + color: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + costume: { + type: Scratch.ArgumentType.STRING, + menu: "penPlusCostumes", + }, + }, + }, + { + disableMonitor: true, + opcode: "getpixelcolor", + blockType: Scratch.BlockType.REPORTER, + text: "get pixel [x] [y]'s color in [costume]", + arguments: { + x: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + costume: { + type: Scratch.ArgumentType.STRING, + menu: "penPlusCostumes", + }, + }, + }, + { + disableMonitor: true, + opcode: "getPenPlusCostumeURI", + blockType: Scratch.BlockType.REPORTER, + text: "get data uri of [costume] in the pen+ costume library", + arguments: { + costume: { + type: Scratch.ArgumentType.STRING, + menu: "penPlusCostumes", + }, + }, + }, + { + opcode: "__NOUSEOPCODE", + blockType: Scratch.BlockType.LABEL, + text: "Advanced options", + }, + { + disableMonitor: true, + opcode: "turnAdvancedSettingOff", + blockType: Scratch.BlockType.COMMAND, + text: "turn advanced setting [Setting] [onOrOff]", + arguments: { + Setting: { + type: Scratch.ArgumentType.STRING, + menu: "advancedSettingsMenu", + }, + onOrOff: { type: Scratch.ArgumentType.STRING, menu: "onOffMenu" }, + }, + }, + { + disableMonitor: true, + opcode: "setAdvancedOptionValueTo", + blockType: Scratch.BlockType.COMMAND, + text: "set [setting] to [value]", + arguments: { + setting: { + type: Scratch.ArgumentType.STRING, + menu: "advancedSettingValuesMenu", + }, + value: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1000", + }, + }, + }, + ], + menus: { + hsvMenu: { + items: [ + "color", + "saturation", + "brightness", + "transparency", + "size", + ], + acceptReporters: true, + }, + stampSquare: { + items: [ + { text: "Width", value: "0" }, + { text: "Height", value: "1" }, + { text: "Rotation", value: "2" }, + { text: "U-Multiplier", value: "3" }, + { text: "U-Offset", value: "4" }, + { text: "V-Multiplier", value: "5" }, + { text: "V-Offset", value: "6" }, + { text: "Red Tint", value: "7" }, + { text: "Green Tint", value: "8" }, + { text: "Blue Tint", value: "9" }, + { text: "Transparency", value: "10" }, + { text: "depth value", value: "11" }, + ], + acceptReporters: true, + }, + triAttribute: { + items: [ + { text: "U value", value: "0" }, + { text: "V value", value: "1" }, + { text: "red tint", value: "2" }, + { text: "green tint", value: "3" }, + { text: "blue tint", value: "4" }, + { text: "transparency", value: "7" }, + { text: "corner pinch", value: "6" }, + { text: "depth value", value: "5" }, + ], + acceptReporters: true, + }, + wholeTriAttribute: { + items: [ + { text: "red tint", value: "2" }, + { text: "green tint", value: "3" }, + { text: "blue tint", value: "4" }, + { text: "transparency", value: "7" }, + { text: "depth value", value: "5" }, + ], + acceptReporters: true, + }, + filterType: { + items: [ + { text: "Closest", value: "9728" }, + { text: "Linear", value: "9729" }, + ], + acceptReporters: true, + }, + wrapType: { + items: [ + { text: "Clamp", value: "33071" }, + { text: "Repeat", value: "10497" }, + { text: "Mirrored", value: "33648" }, + ], + acceptReporters: true, + }, + pointMenu: { items: ["1", "2", "3"], acceptReporters: true }, + onOffMenu: { items: ["on", "off"], acceptReporters: true }, + costumeMenu: { items: "costumeMenuFunction", acceptReporters: true }, + penPlusCostumes: { + items: "penPlusCostumesFunction", + acceptReporters: true, + }, + advancedSettingsMenu: { + items: [ + { text: "allow 'Corner Pinch < 1'", value: "wValueUnderFlow" }, + { text: "toggle depth buffer", value: "useDepthBuffer" }, + { text: "clamp depth value", value: "_ClampZ" }, + ], + acceptReporters: true, + }, + advancedSettingValuesMenu: { + items: [{ text: "maximum depth value", value: "depthMax" }], + acceptReporters: false, + }, + getCostumeDataURI_costume_Menu: { + items: "getCostumeDataURI_costume_MenuFunction", + acceptReporters: true, + }, + getDimensionOf_dimension_Menu: { + items: ["width", "height"], + acceptReporters: true, + }, + }, + name: "Pen+ V6", + id: "penP", + menuIconURI: + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzMi45OTk3MiIgaGVpZ2h0PSIzMi44ODIwNyIgdmlld0JveD0iMCwwLDMyLjk5OTcyLDMyLjg4MjA3Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjI0LC0xNjMuOTk5OTMpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTIyOC43NTMsMTk0LjYwMmwtNC4yNSwxLjc4bDEuNzgzLC00LjIzN2MxLjIxOCwtMi44OTIgMi45MDcsLTUuNDIzIDUuMDMsLTcuNTM4bDE5Ljc1LC0xOS42NzdjMC44NDYsLTAuODQyIDIuNjUsLTAuNDEgNC4wMzIsMC45NjdjMS4zOCwxLjM3NSAxLjgxNiwzLjE3MyAwLjk3LDQuMDE1bC0xOS43NSwxOS42NzhjLTIuMTIzLDIuMTE2IC00LjY2NCwzLjggLTcuNTY1LDUuMDEyIiBmaWxsPSIjZmZmZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzU3NWU3NSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTIzNi44NTgsMTczLjQyOGMwLDAgMi42MTYsMi4yMiA0LjM1LC0xLjU0NmMzLjc1MiwtOC4xNSA4LjIwMiwtNS43NzIgOC4yMDIsLTUuNzcyIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzU3NWU3NSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI1Ni40MiwxNjguODI1YzAsMC40NjMgLTAuMTQsMC44NzMgLTAuNDMyLDEuMTY0bC05LjMzNSw5LjNjMC4yODIsLTAuMjkgMC40MSwtMC42NjggMC40MSwtMS4xMmMwLC0wLjg3NCAtMC41MDcsLTEuOTYzIC0xLjQwNiwtMi44NjhjLTEuMzYyLC0xLjM1OCAtMy4xNDcsLTEuOCAtNC4wMDIsLTAuOTlsOS4zMzUsLTkuMzAxYzAuODQ0LC0wLjg0IDIuNjUsLTAuNDEgNC4wMzUsMC45NmMwLjg5OCwwLjkwNCAxLjM5NiwxLjk4MiAxLjM5NiwyLjg1NU0yMzAuNTE1LDE5My43NzRjLTAuNTczLDAuMzAyIC0xLjE1NywwLjU3IC0xLjc2NCwwLjgzbC00LjI1MSwxLjc3OGwxLjc4NiwtNC4yMzVjMC4yNTgsLTAuNjA0IDAuNTMsLTEuMTg2IDAuODMzLC0xLjc1N2MwLjY5LDAuMTgzIDEuNDQ4LDAuNjI1IDIuMTA4LDEuMjgyYzAuNjYsMC42NTggMS4xMDIsMS40MTIgMS4yODcsMi4xMDIiIGZpbGw9IiM0Yzk3ZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjNTc1ZTc1IiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMjU2LjQ5OCwxNjguNzQ4YzAsMC40NjQgLTAuMTQsMC44NzQgLTAuNDMzLDEuMTY1bC0xOS43NDIsMTkuNjhjLTIuMTMsMi4xMSAtNC42NzMsMy43OTMgLTcuNTcyLDUuMDFsLTQuMjUxLDEuNzc3bDAuOTc0LC0yLjMxNmwxLjkyNSwtMC44MDhjMi44OTgsLTEuMjE4IDUuNDQsLTIuOSA3LjU3LC01LjAxbDE5Ljc0MywtMTkuNjhjMC4yOTIsLTAuMjkyIDAuNDMyLC0wLjcwMiAwLjQzMiwtMS4xNjVjMCwtMC42NDYgLTAuMjcsLTEuNCAtMC43OCwtMi4xMjJjMC4yNSwwLjE3MiAwLjUsMC4zNzcgMC43MzcsMC42MTRjMC44OTgsMC45MDUgMS4zOTYsMS45ODMgMS4zOTYsMi44NTYiIGZpbGw9IiM1NzVlNzUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjNTc1ZTc1IiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIG9wYWNpdHk9IjAuMTUiLz48cGF0aCBkPSJNMjM4LjQ1LDE3Mi44M2MwLDAuNSAtMC40MDQsMC45MDUgLTAuOTA0LDAuOTA1Yy0wLjUsMCAtMC45MDUsLTAuNDA1IC0wLjkwNSwtMC45MDRjMCwtMC41IDAuNDA3LC0wLjkwMyAwLjkwNiwtMC45MDNjMC41LDAgMC45MDQsMC40MDQgMC45MDQsMC45MDR6IiBmaWxsPSIjNTc1ZTc1IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzU3NWU3NSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTI0NC45OTgwNywxODcuMDUyOThoOS41MTc2NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PHBhdGggZD0iTTI0OS43NTY4OSwxOTEuODExOHYtOS41MTc2NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTY6MTYuMDAwMDY5MjMwODQyMTQzLS0+", + blockIconURI: + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIHN0cm9rZT0iIzU3NUU3NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04Ljc1MyAzNC42MDJsLTQuMjUgMS43OCAxLjc4My00LjIzN2MxLjIxOC0yLjg5MiAyLjkwNy01LjQyMyA1LjAzLTcuNTM4TDMxLjA2NiA0LjkzYy44NDYtLjg0MiAyLjY1LS40MSA0LjAzMi45NjcgMS4zOCAxLjM3NSAxLjgxNiAzLjE3My45NyA0LjAxNUwxNi4zMTggMjkuNTljLTIuMTIzIDIuMTE2LTQuNjY0IDMuOC03LjU2NSA1LjAxMiIgZmlsbD0iI0ZGRiIvPjxwYXRoIGQ9Ik0yOS40MSA2LjExcy00LjQ1LTIuMzc4LTguMjAyIDUuNzcyYy0xLjczNCAzLjc2Ni00LjM1IDEuNTQ2LTQuMzUgMS41NDYiLz48cGF0aCBkPSJNMzYuNDIgOC44MjVjMCAuNDYzLS4xNC44NzMtLjQzMiAxLjE2NGwtOS4zMzUgOS4zYy4yODItLjI5LjQxLS42NjguNDEtMS4xMiAwLS44NzQtLjUwNy0xLjk2My0xLjQwNi0yLjg2OC0xLjM2Mi0xLjM1OC0zLjE0Ny0xLjgtNC4wMDItLjk5TDMwLjk5IDUuMDFjLjg0NC0uODQgMi42NS0uNDEgNC4wMzUuOTYuODk4LjkwNCAxLjM5NiAxLjk4MiAxLjM5NiAyLjg1NU0xMC41MTUgMzMuNzc0Yy0uNTczLjMwMi0xLjE1Ny41Ny0xLjc2NC44M0w0LjUgMzYuMzgybDEuNzg2LTQuMjM1Yy4yNTgtLjYwNC41My0xLjE4Ni44MzMtMS43NTcuNjkuMTgzIDEuNDQ4LjYyNSAyLjEwOCAxLjI4Mi42Ni42NTggMS4xMDIgMS40MTIgMS4yODcgMi4xMDIiIGZpbGw9IiM0Qzk3RkYiLz48cGF0aCBkPSJNMzYuNDk4IDguNzQ4YzAgLjQ2NC0uMTQuODc0LS40MzMgMS4xNjVsLTE5Ljc0MiAxOS42OGMtMi4xMyAyLjExLTQuNjczIDMuNzkzLTcuNTcyIDUuMDFMNC41IDM2LjM4bC45NzQtMi4zMTYgMS45MjUtLjgwOGMyLjg5OC0xLjIxOCA1LjQ0LTIuOSA3LjU3LTUuMDFsMTkuNzQzLTE5LjY4Yy4yOTItLjI5Mi40MzItLjcwMi40MzItMS4xNjUgMC0uNjQ2LS4yNy0xLjQtLjc4LTIuMTIyLjI1LjE3Mi41LjM3Ny43MzcuNjE0Ljg5OC45MDUgMS4zOTYgMS45ODMgMS4zOTYgMi44NTYiIGZpbGw9IiM1NzVFNzUiIG9wYWNpdHk9Ii4xNSIvPjxwYXRoIGQ9Ik0xOC40NSAxMi44M2MwIC41LS40MDQuOTA1LS45MDQuOTA1cy0uOTA1LS40MDUtLjkwNS0uOTA0YzAtLjUuNDA3LS45MDMuOTA2LS45MDMuNSAwIC45MDQuNDA0LjkwNC45MDR6IiBmaWxsPSIjNTc1RTc1Ii8+PC9nPjwvc3ZnPg==", + }; + } + costumeMenuFunction() { + const myCostumes = runtime._editingTarget.sprite.costumes; + + let readCostumes = []; + for ( + let curCostumeID = 0; + curCostumeID < myCostumes.length; + curCostumeID++ + ) { + const currentCostume = myCostumes[curCostumeID].name; + readCostumes.push(currentCostume); + } + + const keys = Object.keys(penPlusCostumeLibrary); + if (keys.length > 0) { + for (let curCostumeID = 0; curCostumeID < keys.length; curCostumeID++) { + const currentCostume = keys[curCostumeID]; + readCostumes.push(currentCostume); + } + } + + return readCostumes; + } + penPlusCostumesFunction() { + const readCostumes = []; + const keys = Object.keys(penPlusCostumeLibrary); + if (keys.length > 0) { + for (let curCostumeID = 0; curCostumeID < keys.length; curCostumeID++) { + const currentCostume = keys[curCostumeID]; + readCostumes.push(currentCostume); + } + return readCostumes; + } + + return ["no pen+ costumes!"]; + } + isPenDown(args, util) { + checkForPen(util); + const curTarget = util.target; + return curTarget["_customState"]["Scratch.pen"].penDown; + } + getPenHSV({ HSV }, util) { + checkForPen(util); + const curTarget = util.target; + if (HSV == "size") { + return curTarget["_customState"]["Scratch.pen"].penAttributes.diameter; + } + return curTarget["_customState"]["Scratch.pen"][HSV]; + } + drawDot({ x, y }, util) { + checkForPen(util); + const curTarget = util.target; + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + + curTarget.runtime.ext_pen.penDown(null, util); + + Scratch.vm.renderer.penPoint( + Scratch.vm.renderer._penSkinId, + attrib, + x, + y + ); + + curTarget.runtime.ext_pen.penUp(null, util); + } + drawLine({ x1, y1, x2, y2 }, util) { + checkForPen(util); + const curTarget = util.target; + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + + curTarget.runtime.ext_pen.penDown(null, util); + + Scratch.vm.renderer.penLine( + Scratch.vm.renderer._penSkinId, + attrib, + x1, + y1, + x2, + y2 + ); + + curTarget.runtime.ext_pen.penUp(null, util); + } + squareDown(arg, util) { + //Just a simple thing to allow for pen drawing + const curTarget = util.target; + + checkForPen(util); + + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + const diam = attrib.diameter; + + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + lilPenDabble(nativeSize, curTarget, util); // Do this so the renderer doesn't scream at us + + if ( + typeof triangleAttributesOfAllSprites["squareStamp_" + curTarget.id] == + "undefined" + ) { + triangleAttributesOfAllSprites["squareStamp_" + curTarget.id] = + triangleDefaultAttributes; + } + + if (typeof squareAttributesOfAllSprites[curTarget.id] == "undefined") { + squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; + } + + const myAttributes = squareAttributesOfAllSprites[curTarget.id]; + + //trying my best to reduce memory usage + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + const dWidth = 1 / nativeSize[0]; + const dHeight = 1 / nativeSize[1]; + + const spritex = curTarget.x; + const spritey = curTarget.y; + + //correction for HQ pen + const typSize = renderer._nativeSize; + const mul = renderer.useHighQualityRender + ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) + : 2; + + //Predifine stuff so there aren't as many calculations + const wMulX = mul * myAttributes[0]; + const wMulY = mul * myAttributes[1]; + + const offDiam = 0.5 * diam; + + const sprXoff = spritex * mul; + const sprYoff = spritey * mul; + //Paratheses because I know some obscure browser will screw this up. + let x1 = Scratch.Cast.toNumber(-offDiam) * wMulX; + let x2 = Scratch.Cast.toNumber(offDiam) * wMulX; + let x3 = Scratch.Cast.toNumber(offDiam) * wMulX; + let x4 = Scratch.Cast.toNumber(-offDiam) * wMulX; + + let y1 = Scratch.Cast.toNumber(offDiam) * wMulY; + let y2 = Scratch.Cast.toNumber(offDiam) * wMulY; + let y3 = Scratch.Cast.toNumber(-offDiam) * wMulY; + let y4 = Scratch.Cast.toNumber(-offDiam) * wMulY; + + function rotateTheThings(ox1, oy1, ox2, oy2, ox3, oy3, ox4, oy4) { + let sin = Math.sin(myAttributes[2] * d2r); + let cos = Math.cos(myAttributes[2] * d2r); + + x1 = ox1 * sin + oy1 * cos; + y1 = ox1 * cos - oy1 * sin; + + x2 = ox2 * sin + oy2 * cos; + y2 = ox2 * cos - oy2 * sin; + + x3 = ox3 * sin + oy3 * cos; + y3 = ox3 * cos - oy3 * sin; + + x4 = ox4 * sin + oy4 * cos; + y4 = ox4 * cos - oy4 * sin; + } + + rotateTheThings(x1, y1, x2, y2, x3, y3, x4, y4); + + x1 += sprXoff; + y1 += sprYoff; + + x2 += sprXoff; + y2 += sprYoff; + + x3 += sprXoff; + y3 += sprYoff; + + x4 += sprXoff; + y4 += sprYoff; + + x1 *= dWidth; + y1 *= dHeight; + + x2 *= dWidth; + y2 *= dHeight; + + x3 *= dWidth; + y3 *= dHeight; + + x4 *= dWidth; + y4 *= dHeight; + + const Attribute_ID = "squareStamp_" + curTarget.id; + + triangleAttributesOfAllSprites[Attribute_ID][2] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][3] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][4] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][5] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][7] = myAttributes[10]; + triangleAttributesOfAllSprites[Attribute_ID][10] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][11] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][12] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][13] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][15] = myAttributes[10]; + triangleAttributesOfAllSprites[Attribute_ID][18] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][19] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][20] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][21] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][23] = myAttributes[10]; + + triFunctions.drawTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x2, + y2, + x3, + y3, + attrib.color4f, + Attribute_ID + ); + + triFunctions.drawTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x3, + y3, + x4, + y4, + attrib.color4f, + Attribute_ID + ); + } + squareTexDown({ tex }, util) { + //Just a simple thing to allow for pen drawing + const curTarget = util.target; + + let currentTexture = null; + if (penPlusCostumeLibrary[tex]) { + currentTexture = penPlusCostumeLibrary[tex].texture; + } else { + const costIndex = curTarget.getCostumeIndexByName( + Scratch.Cast.toString(tex) + ); + if (costIndex >= 0) { + const curCostume = curTarget.sprite.costumes_[costIndex]; + if (costIndex != curTarget.currentCostume) { + curTarget.setCostume(costIndex); + } + + currentTexture = renderer._allSkins[curCostume.skinId].getTexture(); + } + } + + checkForPen(util); + + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + const diam = attrib.diameter; + + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + lilPenDabble(nativeSize, curTarget, util); // Do this so the renderer doesn't scream at us + + if (!triangleAttributesOfAllSprites["squareStamp_" + curTarget.id]) { + triangleAttributesOfAllSprites["squareStamp_" + curTarget.id] = + triangleDefaultAttributes; + } + + if (!squareAttributesOfAllSprites[curTarget.id]) { + squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; + } + + const myAttributes = squareAttributesOfAllSprites[curTarget.id]; + + //trying my best to reduce memory usage + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + const dWidth = 1 / nativeSize[0]; + const dHeight = 1 / nativeSize[1]; + + const spritex = curTarget.x; + const spritey = curTarget.y; + + //correction for HQ pen + const typSize = renderer._nativeSize; + const mul = renderer.useHighQualityRender + ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) + : 2; + + //Predifine stuff so there aren't as many calculations + const wMulX = mul * myAttributes[0]; + const wMulY = mul * myAttributes[1]; + + const offDiam = 0.5 * diam; + + const sprXoff = spritex * mul; + const sprYoff = spritey * mul; + //Paratheses because I know some obscure browser will screw this up. + let x1 = Scratch.Cast.toNumber(-offDiam) * wMulX; + let x2 = Scratch.Cast.toNumber(offDiam) * wMulX; + let x3 = Scratch.Cast.toNumber(offDiam) * wMulX; + let x4 = Scratch.Cast.toNumber(-offDiam) * wMulX; + + let y1 = Scratch.Cast.toNumber(offDiam) * wMulY; + let y2 = Scratch.Cast.toNumber(offDiam) * wMulY; + let y3 = Scratch.Cast.toNumber(-offDiam) * wMulY; + let y4 = Scratch.Cast.toNumber(-offDiam) * wMulY; + + function rotateTheThings(ox1, oy1, ox2, oy2, ox3, oy3, ox4, oy4) { + let sin = Math.sin(myAttributes[2] * d2r); + let cos = Math.cos(myAttributes[2] * d2r); + + x1 = ox1 * sin + oy1 * cos; + y1 = ox1 * cos - oy1 * sin; + + x2 = ox2 * sin + oy2 * cos; + y2 = ox2 * cos - oy2 * sin; + + x3 = ox3 * sin + oy3 * cos; + y3 = ox3 * cos - oy3 * sin; + + x4 = ox4 * sin + oy4 * cos; + y4 = ox4 * cos - oy4 * sin; + } + + rotateTheThings(x1, y1, x2, y2, x3, y3, x4, y4); + + x1 += sprXoff; + y1 += sprYoff; + + x2 += sprXoff; + y2 += sprYoff; + + x3 += sprXoff; + y3 += sprYoff; + + x4 += sprXoff; + y4 += sprYoff; + + x1 *= dWidth; + y1 *= dHeight; + + x2 *= dWidth; + y2 *= dHeight; + + x3 *= dWidth; + y3 *= dHeight; + + x4 *= dWidth; + y4 *= dHeight; + + if (currentTexture != null && typeof currentTexture != "undefined") { + const Attribute_ID = "squareStamp_" + curTarget.id; + triangleAttributesOfAllSprites[Attribute_ID][0] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][1] = + (1 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][2] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][3] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][4] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][5] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][7] = myAttributes[10]; + + triangleAttributesOfAllSprites[Attribute_ID][8] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][9] = + (1 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][10] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][11] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][12] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][13] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][15] = myAttributes[10]; + + triangleAttributesOfAllSprites[Attribute_ID][16] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][17] = + (0 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][18] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][19] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][20] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][21] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][23] = myAttributes[10]; + + triFunctions.drawTextTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x2, + y2, + x3, + y3, + Attribute_ID, + currentTexture + ); + + triangleAttributesOfAllSprites[Attribute_ID][0] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][1] = + (1 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][8] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][9] = + (0 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][16] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][17] = + (0 + myAttributes[6]) * myAttributes[5]; + + triFunctions.drawTextTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x3, + y3, + x4, + y4, + Attribute_ID, + currentTexture + ); + } + } + setStampAttribute({ target, number }, util) { + const curTarget = util.target; + if (!squareAttributesOfAllSprites[curTarget.id]) { + squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; + } + + let valuetoSet = 0; + + const attributeNum = Scratch.Cast.toNumber(target); + if (attributeNum >= 7) { + if (attributeNum == 11) { + if (penPlusAdvancedSettings._ClampZ) { + Math.min( + Math.max(number / penPlusAdvancedSettings._maxDepth, 0), + 1 + ); + return; + } + valuetoSet = number / penPlusAdvancedSettings._maxDepth; + squareAttributesOfAllSprites[curTarget.id][attributeNum] = + number / penPlusAdvancedSettings._maxDepth; + return; + } + squareAttributesOfAllSprites[curTarget.id][attributeNum] = + Math.min(Math.max(number, 0), 100) * 0.01; + return; + } + squareAttributesOfAllSprites[curTarget.id][attributeNum] = number; + } + getStampAttribute({ target }, util) { + const curTarget = util.target; + if (!squareAttributesOfAllSprites[curTarget.id]) { + squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; + } + + return squareAttributesOfAllSprites[curTarget.id][ + Scratch.Cast.toNumber(target) + ]; + } + tintSquare({ color }, util) { + const curTarget = util.target; + + if (!squareAttributesOfAllSprites[curTarget.id]) { + squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; + } + + const calcColor = colors.hexToRgb(color); + + squareAttributesOfAllSprites[curTarget.id][7] = calcColor.r / 255; + squareAttributesOfAllSprites[curTarget.id][8] = calcColor.g / 255; + squareAttributesOfAllSprites[curTarget.id][9] = calcColor.b / 255; + } + resetSquareAttributes(args, util) { + const curTarget = util.target; + squareAttributesOfAllSprites[curTarget.id] = [ + 1, 1, 90, 1, 0, 1, 0, 1, 1, 1, 1, 0, + ]; + } + setTriangleFilterMode({ filter }) { + currentFilter = filter; + } + setTrianglePointAttribute({ point, attribute, value }, util) { + const trianglePointStart = (point - 1) * 8; + + const targetId = util.target.id; + + if (!triangleAttributesOfAllSprites[targetId]) { + triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; + } + triFunctions.setValueAccordingToCaseTriangle( + targetId, + Scratch.Cast.toNumber(attribute), + value, + false, + trianglePointStart + ); + } + setWholeTrianglePointAttribute({ wholeAttribute, value }, util) { + const targetId = util.target.id; + + if (!triangleAttributesOfAllSprites[targetId]) { + triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; + } + triFunctions.setValueAccordingToCaseTriangle( + targetId, + Scratch.Cast.toNumber(wholeAttribute), + value, + true, + 0 + ); + } + tintTriPoint({ point, color }, util) { + const curTarget = util.target; + + const trianglePointStart = (point - 1) * 8; + + const targetId = util.target.id; + + if (!triangleAttributesOfAllSprites[targetId]) { + triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; + } + + const calcColor = colors.hexToRgb(color); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 2, + calcColor.r / 2.55, + false, + trianglePointStart + ); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 3, + calcColor.g / 2.55, + false, + trianglePointStart + ); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 4, + calcColor.b / 2.55, + false, + trianglePointStart + ); + } + tintTri({ point, color }, util) { + const curTarget = util.target; + + const trianglePointStart = (point - 1) * 8; + + const targetId = util.target.id; + + if (!triangleAttributesOfAllSprites[targetId]) { + triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; + } + + const calcColor = colors.hexToRgb(color); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 2, + calcColor.r / 2.55, + true, + trianglePointStart + ); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 3, + calcColor.g / 2.55, + true, + trianglePointStart + ); + + triFunctions.setValueAccordingToCaseTriangle( + targetId, + 4, + calcColor.b / 2.55, + true, + trianglePointStart + ); + } + getTrianglePointAttribute({ point, attribute }, util) { + const trianglePointStart = (point - 1) * 8; + + const targetId = util.target.id; + + if (!triangleAttributesOfAllSprites[targetId]) { + triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; + } + let value = + triangleAttributesOfAllSprites[targetId][ + trianglePointStart + attribute + ]; + + if ((attribute >= 2 && attribute <= 4) || attribute == 7) { + value *= 100; + } + return value; + } + resetWholeTriangleAttributes(args, util) { + const targetId = util.target.id; + triangleAttributesOfAllSprites[targetId] = [ + 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, + 1, 1, 1, + ]; + } + drawSolidTri({ x1, y1, x2, y2, x3, y3 }, util) { + const curTarget = util.target; + checkForPen(util); + const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; + + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + //if (triangleAttributesOfAllSprites[curTarget.id]) { + // triangleAttributesOfAllSprites[curTarget.id][5] = 1; + // triangleAttributesOfAllSprites[curTarget.id][13] = 1; + // triangleAttributesOfAllSprites[curTarget.id][21] = 1; + //} + + //?Renderer Freaks out if we don't do this so do it. + lilPenDabble(nativeSize, curTarget, util); // Do this so the renderer doesn't scream at us + + //trying my best to reduce memory usage + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + const dWidth = 1 / nativeSize[0]; + const dHeight = 1 / nativeSize[1]; + + //correction for HQ pen + const typSize = renderer._nativeSize; + const mul = renderer.useHighQualityRender + ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) + : 2; + //Paratheses because I know some obscure browser will screw this up. + x1 = Scratch.Cast.toNumber(x1) * dWidth * mul; + x2 = Scratch.Cast.toNumber(x2) * dWidth * mul; + x3 = Scratch.Cast.toNumber(x3) * dWidth * mul; + + y1 = Scratch.Cast.toNumber(y1) * dHeight * mul; + y2 = Scratch.Cast.toNumber(y2) * dHeight * mul; + y3 = Scratch.Cast.toNumber(y3) * dHeight * mul; + + triFunctions.drawTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x2, + y2, + x3, + y3, + attrib.color4f, + curTarget.id + ); + } + drawTexTri({ x1, y1, x2, y2, x3, y3, tex }, util) { + const curTarget = util.target; + let currentTexture = null; + if (penPlusCostumeLibrary[tex]) { + currentTexture = penPlusCostumeLibrary[tex].texture; + } else { + const costIndex = curTarget.getCostumeIndexByName( + Scratch.Cast.toString(tex) + ); + if (costIndex >= 0) { + const curCostume = curTarget.sprite.costumes_[costIndex]; + if (costIndex != curTarget.currentCostume) { + curTarget.setCostume(costIndex); + } + + currentTexture = renderer._allSkins[curCostume.skinId].getTexture(); + } + } + + nativeSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + + //?Renderer Freaks out if we don't do this so do it. + lilPenDabble(nativeSize, curTarget, util); // Do this so the renderer doesn't scream at us + + //trying my best to reduce memory usage + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + const dWidth = 1 / nativeSize[0]; + const dHeight = 1 / nativeSize[1]; + + //correction for HQ pen + const typSize = renderer._nativeSize; + const mul = renderer.useHighQualityRender + ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) + : 2; + //Paratheses because I know some obscure browser will screw this up. + x1 = Scratch.Cast.toNumber(x1) * dWidth * mul; + x2 = Scratch.Cast.toNumber(x2) * dWidth * mul; + x3 = Scratch.Cast.toNumber(x3) * dWidth * mul; + + y1 = Scratch.Cast.toNumber(y1) * dHeight * mul; + y2 = Scratch.Cast.toNumber(y2) * dHeight * mul; + y3 = Scratch.Cast.toNumber(y3) * dHeight * mul; + + if (currentTexture != null && typeof currentTexture != "undefined") { + triFunctions.drawTextTri( + gl.getParameter(gl.CURRENT_PROGRAM), + x1, + y1, + x2, + y2, + x3, + y3, + curTarget.id, + currentTexture + ); + } + } + RGB2HEX({ R, G, B }) { + return colors.rgbtoSColor({ R: R, G: G, B: B }); + } + HSV2RGB({ H, S, V }) { + S = S / 100; + V = V / 100; + S = Math.min(Math.max(S, 0), 1); + V = Math.min(Math.max(V, 0), 1); + H = H % 360; + const C = V * S; + const X = C * (1 - Math.abs(((H / 60) % 2) - 1)); + const M = V - C; + let Primes = [0, 0, 0]; + if (H >= 0 && H < 60) { + Primes[0] = C; + Primes[1] = X; + } else if (H >= 60 && H < 120) { + Primes[0] = X; + Primes[1] = C; + } else if (H >= 120 && H < 180) { + Primes[1] = C; + Primes[2] = X; + } else if (H >= 180 && H < 240) { + Primes[1] = X; + Primes[2] = C; + } else if (H >= 240 && H < 300) { + Primes[0] = X; + Primes[2] = C; + } + if (H >= 300 && H < 360) { + Primes[0] = C; + Primes[2] = X; + } + Primes[0] = (Primes[0] + M) * 255; + Primes[1] = (Primes[1] + M) * 255; + Primes[2] = (Primes[2] + M) * 255; + return colors.rgbtoSColor({ + R: Primes[0] / 2.55, + G: Primes[1] / 2.55, + B: Primes[2] / 2.55, + }); + } + setDURIclampmode({ clampMode }) { + penPlusImportWrapMode = clampMode; + } + addBlankIMG({ color, width, height, name }) { + //Just a simple thing to allow for pen drawing + textureFunctions.createBlankPenPlusTextureInfo( + width, + height, + color, + "!" + name, + penPlusImportWrapMode + ); + } + addIMGfromDURI({ dataURI, name }) { + //Just a simple thing to allow for pen drawing + textureFunctions.createPenPlusTextureInfo( + dataURI, + "!" + name, + penPlusImportWrapMode + ); + } + removeIMGfromDURI({ name }, util) { + //Just a simple thing to allow for pen drawing + if (penPlusCostumeLibrary["!" + name]) { + delete penPlusCostumeLibrary["!" + name]; + } + } + doesIMGexist({ name }, util) { + //Just a simple thing to allow for pen drawing + return typeof penPlusCostumeLibrary["!" + name] != "undefined"; + } + getCostumeDataURI({ costume }, util) { + //Just a simple thing to allow for pen drawing + const curTarget = util.target; + const costIndex = curTarget.getCostumeIndexByName( + Scratch.Cast.toString(costume) + ); + if (costIndex >= 0) { + const curCostume = + curTarget.sprite.costumes_[costIndex].asset.encodeDataURI(); + return curCostume; + } + } + getCostumeDataURI_costume_MenuFunction() { + const myCostumes = runtime._editingTarget.sprite.costumes; + + let readCostumes = []; + for ( + let curCostumeID = 0; + curCostumeID < myCostumes.length; + curCostumeID++ + ) { + const currentCostume = myCostumes[curCostumeID].name; + readCostumes.push(currentCostume); + } + + return readCostumes; + } + getDimensionOf({ dimension, costume }, util) { + //Just a simple thing to allow for pen drawing + const costIndex = penPlusCostumeLibrary[costume]; + if (costIndex) { + return costIndex[dimension]; + } + } + setpixelcolor({ x, y, color, costume }) { + const curCostume = penPlusCostumeLibrary[costume]; + if (curCostume) { + const textureData = textureFunctions.getTextureData( + curCostume.texture, + curCostume.width, + curCostume.height + ); + if (textureData) { + x = Math.floor(x - 1); + y = Math.floor(y - 1); + const colorIndex = (y * curCostume.width + x) * 4; + if ( + textureData[colorIndex] != undefined && + x < curCostume.width && + x >= 0 + ) { + const retColor = colors.hexToRgb(color); + textureData[colorIndex] = retColor.r; + textureData[colorIndex + 1] = retColor.g; + textureData[colorIndex + 2] = retColor.b; + textureData[colorIndex + 3] = 255; + + gl.bindTexture(gl.TEXTURE_2D, curCostume.texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + curCostume.width, + curCostume.height, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + textureData + ); + } + } + } + } + getpixelcolor({ x, y, costume }) { + const curCostume = penPlusCostumeLibrary[costume]; + if (curCostume) { + const textureData = textureFunctions.getTextureData( + curCostume.texture, + curCostume.width, + curCostume.height + ); + if (textureData) { + x = Math.floor(x - 1); + y = Math.floor(y - 1); + const colorIndex = (y * curCostume.width + x) * 4; + if (textureData[colorIndex] && x < curCostume.width && x >= 0) { + return colors.rgbtoSColor({ + R: textureData[colorIndex] / 2.55, + G: textureData[colorIndex + 1] / 2.55, + B: textureData[colorIndex + 2] / 2.55, + }); + } + return colors.rgbtoSColor({ R: 100, G: 100, B: 100 }); + } + } + } + getPenPlusCostumeURI({ costume }) { + const curCostume = penPlusCostumeLibrary[costume]; + if (curCostume) { + const textureData = textureFunctions.getTextureAsURI( + curCostume.texture, + curCostume.width, + curCostume.height + ); + if (textureData) { + return textureData; + } + return ""; + } + } + turnAdvancedSettingOff({ Setting, onOrOff }) { + if (onOrOff == "on") { + penPlusAdvancedSettings[Setting] = true; + return; + } + penPlusAdvancedSettings[Setting] = false; + } + setAdvancedOptionValueTo({ setting, value }) { + switch (setting) { + case "depthMax": + penPlusAdvancedSettings._maxDepth = Math.max(value, 100); + break; + + default: + break; + } + } + runtime = Scratch.vm.runtime; + } + + //? A small hack to stop the renderer from immediatly dying. And to allow for immediate use + { + if (!Scratch.vm.renderer._penSkinId) { + window.vm.renderer.createPenSkin(); + } + renderer.penClear(Scratch.vm.renderer._penSkinId); + Scratch.vm.renderer.penLine( + Scratch.vm.renderer._penSkinId, + { + color4f: [0, 0, 1, 1], + diameter: 1, + }, + 0, + 0, + 0, + 0 + ); + + penPlusShaders.pen.program = shaderManager._shaderCache.line[0].program; + } + + Scratch.extensions.register(new extension()); +})(Scratch); diff --git a/extensions/penplus.js b/extensions/penplus.js index fb11d2de3c..79a5b47c3d 100644 --- a/extensions/penplus.js +++ b/extensions/penplus.js @@ -1,6 +1,6 @@ -// Name: Pen Plus +// Name: Pen Plus V5 (Old) // ID: betterpen -// Description: Advanced rendering capabilities. +// Description: Replaced by Pen Plus V6. // By: ObviousAlexC /* eslint-disable no-empty-pattern */ @@ -2422,7 +2422,7 @@ Other various small fixes getInfo() { return { id: "betterpen", - name: "Pen+", + name: "Pen+ V5", color1: "#0e9a6b", color2: "#0b7f58", color3: "#096647", From 057d5aa5efdb6e8ffec59bb28cb89bc7e0347ab0 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 5 Nov 2023 18:15:49 -0600 Subject: [PATCH 042/196] Add Clay/htmlEncode extension (#1126) Co-authored-by: ClaytonTDM --- extensions/Clay/htmlEncode.js | 54 +++++++++++++++++++++++++++++++++++ extensions/extensions.json | 1 + 2 files changed, 55 insertions(+) create mode 100644 extensions/Clay/htmlEncode.js diff --git a/extensions/Clay/htmlEncode.js b/extensions/Clay/htmlEncode.js new file mode 100644 index 0000000000..37ee534e50 --- /dev/null +++ b/extensions/Clay/htmlEncode.js @@ -0,0 +1,54 @@ +// Name: HTML Encode +// ID: clayhtmlencode +// Description: Escape untrusted text to safely include in HTML. +// By: ClaytonTDM + +(function (Scratch) { + "use strict"; + + class HtmlEncode { + getInfo() { + return { + id: "claytonhtmlencode", + name: "HTML Encode", + blocks: [ + { + opcode: "encode", + blockType: Scratch.BlockType.REPORTER, + text: "encode [text] as HTML-safe", + arguments: { + text: { + type: Scratch.ArgumentType.STRING, + // don't use a script tag as the example here as the closing script + // tag might break things when this extension gets inlined in packed + // projects + defaultValue: "

    Hello!

    ", + }, + }, + }, + ], + }; + } + + encode({ text }) { + return Scratch.Cast.toString(text).replace(/["'&<>]/g, (a) => { + switch (a) { + case "&": + return "&"; + case '"': + return "'"; + case "'": + return """; + case ">": + return ">"; + case "<": + return "<"; + } + // this should never happen... + return ""; + }); + } + } + + Scratch.extensions.register(new HtmlEncode()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index d49a2c7c67..06b95e22b9 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -17,6 +17,7 @@ "sound", "Lily/Video", "iframe", + "Clay/htmlEncode", "Xeltalliv/clippingblending", "clipboard", "obviousAlexC/penPlus", From 9adb4ac5ab5909daffe75f59de21ad87f0a76de2 Mon Sep 17 00:00:00 2001 From: DNin01 <106490990+DNin01@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:21:18 -0800 Subject: [PATCH 043/196] true-fantom/math: Add clamp and scale blocks (#981) Resolves #888 --- extensions/true-fantom/math.js | 106 ++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/extensions/true-fantom/math.js b/extensions/true-fantom/math.js index 5b98469b3c..7d5f2753e6 100644 --- a/extensions/true-fantom/math.js +++ b/extensions/true-fantom/math.js @@ -115,11 +115,11 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -130,11 +130,11 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -145,7 +145,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -157,7 +157,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -172,7 +172,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -187,7 +187,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -202,7 +202,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -217,7 +217,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -232,7 +232,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -247,7 +247,7 @@ arguments: { A: { type: Scratch.ArgumentType.STRING, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.STRING, @@ -325,6 +325,53 @@ }, }, "---", + { + opcode: "clamp_block", + blockType: Scratch.BlockType.REPORTER, + text: "clamp [A] between [B] and [C]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "", + }, + B: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + C: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "100", + }, + }, + }, + { + opcode: "scale_block", + blockType: Scratch.BlockType.REPORTER, + text: "map [A] from range [m1] - [M1] to range [m2] - [M2]", + arguments: { + A: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "", + }, + m1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + M1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "100", + }, + m2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + M2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + "---", { opcode: "trunc2_block", blockType: Scratch.BlockType.REPORTER, @@ -332,7 +379,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.NUMBER, @@ -347,7 +394,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -359,11 +406,11 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -375,7 +422,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, B: { type: Scratch.ArgumentType.NUMBER, @@ -407,7 +454,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -419,7 +466,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -430,7 +477,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -441,7 +488,7 @@ arguments: { A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "\n", + defaultValue: "", }, }, }, @@ -496,6 +543,23 @@ exactly_cont_block({ A, B }) { return cast.toString(A).includes(cast.toString(B)); } + clamp_block({ A, B, C }) { + if (cast.compare(A, B) < 0) { + return B; + } else if (cast.compare(A, C) > 0) { + return C; + } else { + return A; + } + } + scale_block({ A, m1, M1, m2, M2 }) { + return ( + ((cast.toNumber(A) - cast.toNumber(m1)) * + (cast.toNumber(M2) - cast.toNumber(m2))) / + (cast.toNumber(M1) - cast.toNumber(m1)) + + cast.toNumber(m2) + ); + } trunc2_block({ A, B }) { let n = Math.floor(cast.toNumber(B)); if (n >= 1) { From e6447d50827b3ba0ac84a7d1269cfdadb7ba5c7b Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 5 Nov 2023 21:37:50 -0600 Subject: [PATCH 044/196] Add .DS_Store and thumbs.db to .gitignore (#1129) --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 03f6901597..960930abc4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules # Final built website build + +# Various operating system caches +thumbs.db +.DS_Store From 8f8f43354867e909f638a2e71cf4a3bd3a9828f0 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 5 Nov 2023 21:38:04 -0600 Subject: [PATCH 045/196] obviousAlexC/penPlus: add image (#1130) --- images/obviousAlexC/penPlus.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 images/obviousAlexC/penPlus.svg diff --git a/images/obviousAlexC/penPlus.svg b/images/obviousAlexC/penPlus.svg new file mode 100644 index 0000000000..4d772ba7e6 --- /dev/null +++ b/images/obviousAlexC/penPlus.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4335062f2e25c81e3dce873fbe9662e0cc3bb56b Mon Sep 17 00:00:00 2001 From: "Mike J. Renaker / \"MikeDEV" Date: Wed, 8 Nov 2023 18:44:22 -0500 Subject: [PATCH 046/196] cloudlink: Update to v0.1.2 (#1134) This is a critical bugfix that restores functionality for projects that rely upon the username block for obtaining user objects. This also fixes a connection timing bug. --- extensions/cloudlink.js | 69 ++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/extensions/cloudlink.js b/extensions/cloudlink.js index 64f05bb998..9efc975739 100644 --- a/extensions/cloudlink.js +++ b/extensions/cloudlink.js @@ -8,7 +8,7 @@ (function (Scratch) { /* - CloudLink Extension for TurboWarp v0.1.1. + CloudLink Extension for TurboWarp v0.1.2. This extension should be fully compatible with projects developed using extensions S4.1, S4.0, and B3.0. @@ -74,8 +74,8 @@ */ const version = { editorType: "TurboWarp", - versionNumber: 1, - versionString: "0.1.1", + versionNumber: 2, + versionString: "0.1.2", }; // Store extension state @@ -324,7 +324,7 @@ } // CL-specific netcode needed for sending messages - async function sendMessage(message) { + function sendMessage(message) { // Prevent running this while disconnected if (clVars.socket == null) { console.warn("[CloudLink] Ignoring attempt to send a packet while disconnected."); @@ -434,6 +434,12 @@ // Log configured spec version console.log(`[CloudLink] Configured protocol spec to v${clVars.linkState.identifiedProtocol}.`); + // Fix timing bug + clVars.linkState.status = 2; + + // Fire event hats (only one not broken) + runtime.startHats('cloudlink_onConnect'); + // Don't nag user if they already trusted this server if (clVars.currentServerUrl === clVars.lastServerUrl) return; @@ -510,7 +516,7 @@ // Server 0.1.5 (at least) case "vers": window.clearTimeout(clVars.handshakeTimeout); - setServerVersion(packet.val.val); + await setServerVersion(packet.val.val); return; // Server 0.1.7 (at least) @@ -668,7 +674,7 @@ case "server_version": window.clearTimeout(clVars.handshakeTimeout); - setServerVersion(packet.val); + await setServerVersion(packet.val); break; case "client_ip": @@ -734,17 +740,12 @@ // Set the link state to connected. console.log("[CloudLink] Connected."); - clVars.linkState.status = 2; - // If a server_version message hasn't been received in over half a second, try to broadcast a handshake clVars.handshakeTimeout = window.setTimeout(function() { console.log("[CloudLink] Hmm... This server hasn't sent us it's server info. Going to attempt a handshake."); sendHandshake(); }, 500); - // Fire event hats (only one not broken) - runtime.startHats('cloudlink_onConnect'); - // Return promise (during setup) return; }; @@ -792,7 +793,7 @@ // GET the serverList try { Scratch.fetch( - "https://mikedev101.github.io/cloudlink/serverlist.json" + "https://raw.githubusercontent.com/MikeDev101/cloudlink/master/serverlist.json" ) .then((response) => { return response.text(); @@ -861,11 +862,18 @@ }, { - opcode: "returnUsernameData", + opcode: "returnUsernameDataNew", blockType: Scratch.BlockType.REPORTER, text: "My username" }, + { + opcode: "returnUsernameData", + blockType: Scratch.BlockType.REPORTER, + hideFromPalette: clVars.hideCLDeprecatedBlocks, + text: "(OLD - DO NOT USE IN NEW PROJECTS) My username" + }, + "---", { @@ -1564,10 +1572,20 @@ } // Reporter - Returns currently set username. - returnUsernameData() { + returnUsernameDataNew() { return makeValueScratchSafe(clVars.username.value); } + // Reporter - (OLD) Returns currently set username (returns user object to retain compatibility with old projects). + returnUsernameData() { + return makeValueScratchSafe(clVars.myUserObject); + } + + // Reporter - Returns the reported user object of the client (Snowflake ID, UUID, Username) - Intended replacement for the old username reporter block. + returnUserObject() { + return makeValueScratchSafe(clVars.myUserObject); + } + // Reporter - Returns current client version. returnVersionData() { return generateVersionString(); @@ -1593,11 +1611,6 @@ return makeValueScratchSafe(clVars.client_ip); } - // Reporter - Returns the reported user object of the client (Snowflake ID, UUID, Username) - returnUserObject() { - return makeValueScratchSafe(clVars.myUserObject); - } - // Reporter - Returns data for a specific listener ID. // ID - String (listener ID) returnListenerData(args) { @@ -2049,7 +2062,7 @@ clVars.username.temp = args.NAME; // Send the command - return sendMessage({ cmd: "setid", val: args.NAME, listener: "username_cfg" }); + sendMessage({ cmd: "setid", val: args.NAME, listener: "username_cfg" }); } // Command - Prepares the next transmitted message to have a listener ID attached to it. @@ -2114,7 +2127,7 @@ }; clVars.rooms.isAttemptingLink = true; - return sendMessage({ cmd: "link", val: args.ROOMS, listener: "link" }); + sendMessage({ cmd: "link", val: args.ROOMS, listener: "link" }); } // Command - Specifies specific subscribed rooms to transmit messages to. @@ -2183,7 +2196,7 @@ }; clVars.rooms.isAttemptingUnlink = true; - return sendMessage({ cmd: "unlink", val: "", listener: "unlink" }); + sendMessage({ cmd: "unlink", val: "", listener: "unlink" }); } // Command - Sends a gmsg value. @@ -2193,7 +2206,7 @@ // Must be connected. if (clVars.socket == null) return; - return sendMessage({ cmd: "gmsg", val: args.DATA }); + sendMessage({ cmd: "gmsg", val: args.DATA }); } // Command - Sends a pmsg value. @@ -2209,7 +2222,7 @@ return; }; - return sendMessage({ cmd: "pmsg", val: args.DATA, id: args.ID }); + sendMessage({ cmd: "pmsg", val: args.DATA, id: args.ID }); } // Command - Sends a gvar value. @@ -2219,7 +2232,7 @@ // Must be connected. if (clVars.socket == null) return; - return sendMessage({ cmd: "gvar", val: args.DATA, name: args.VAR }); + sendMessage({ cmd: "gvar", val: args.DATA, name: args.VAR }); } // Command - Sends a pvar value. @@ -2235,7 +2248,7 @@ return; }; - return sendMessage({ cmd: "pvar", val: args.DATA, name: args.VAR, id: args.ID }); + sendMessage({ cmd: "pvar", val: args.DATA, name: args.VAR, id: args.ID }); } // Command - Sends a raw-format command without specifying an ID. @@ -2245,7 +2258,7 @@ // Must be connected. if (clVars.socket == null) return; - return sendMessage({ cmd: args.CMD, val: args.DATA }); + sendMessage({ cmd: args.CMD, val: args.DATA }); } // Command - Sends a raw-format command with an ID. @@ -2261,7 +2274,7 @@ return; }; - return sendMessage({ cmd: args.CMD, val: args.DATA, id: args.ID }); + sendMessage({ cmd: args.CMD, val: args.DATA, id: args.ID }); } // Command - Resets the "returnIsNewData" boolean state. From 1cbc1cf8ce76e48246b73b6bb6fa3f73c62e8d89 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Tue, 14 Nov 2023 23:30:24 -0600 Subject: [PATCH 047/196] qxsck/var-and-list: support cloud variables (#1144) closes #1142 --- extensions/qxsck/var-and-list.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/qxsck/var-and-list.js b/extensions/qxsck/var-and-list.js index 09f10dea61..f2fe5663a7 100644 --- a/extensions/qxsck/var-and-list.js +++ b/extensions/qxsck/var-and-list.js @@ -316,6 +316,12 @@ ); if (variable) { variable.value = args.VALUE; + if (variable.isCloud) { + util.runtime.ioDevices.cloud.requestUpdateVariable( + variable.name, + variable.value + ); + } } } getList(args, util) { From 14bad83018aa08efbc0ab9d5dc12d3e2f400e11c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:31:54 -0600 Subject: [PATCH 048/196] build(deps-dev): bump eslint from 8.52.0 to 8.53.0 (#1128) --- package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index e33ce1f6ac..031e704254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,15 +20,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -60,9 +60,9 @@ } }, "@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "dev": true }, "@humanwhocodes/config-array": { @@ -156,9 +156,9 @@ } }, "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true }, "acorn-jsx": { @@ -432,15 +432,15 @@ "dev": true }, "eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -1191,9 +1191,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { diff --git a/package.json b/package.json index 36b36b41d4..f2d888700e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "markdown-it": "^13.0.2" }, "devDependencies": { - "eslint": "^8.52.0", + "eslint": "^8.53.0", "prettier": "^3.0.3" }, "private": true From fd669f0d462c61bb1d11e76f07802e8ad9b2360a Mon Sep 17 00:00:00 2001 From: Obvious Alex C <76855369+David-Orangemoon@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:36:22 -0500 Subject: [PATCH 049/196] obviousAlexC/penPlus: add sample project (#1133) --- samples/Pen Plus.sb3 | Bin 0 -> 31212 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 samples/Pen Plus.sb3 diff --git a/samples/Pen Plus.sb3 b/samples/Pen Plus.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..0f5a4ef16b9f5ed7eb5dad3145e919cff1c19807 GIT binary patch literal 31212 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 literal 0 HcmV?d00001 From 6f1ef9838c8ca6d24acbf02e2c250f5591fa3a95 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 15 Nov 2023 00:03:24 -0600 Subject: [PATCH 050/196] godslayerakp/ws: A couple fixes and add sample project (#1127) --- docs/godslayerakp/ws.md | 2 ++ extensions/godslayerakp/ws.js | 66 ++++++++++++++++++---------------- samples/Cloud Variables.sb3 | Bin 0 -> 3878 bytes 3 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 samples/Cloud Variables.sb3 diff --git a/docs/godslayerakp/ws.md b/docs/godslayerakp/ws.md index e9b09a01da..eb9600b70b 100644 --- a/docs/godslayerakp/ws.md +++ b/docs/godslayerakp/ws.md @@ -125,3 +125,5 @@ when connection errors :: hat #307eff Sometimes things don't go so well. Maybe your internet connection died, the server is down, or you typed in the wrong URL. There's a lot of things that can go wrong. These let you try to handle that. Unfortunately we can't give much insight as to what caused the errors. Your browser tells us very little, but even if it did give us more information, it probably wouldn't be very helpful. + +A connection can either close or error; it won't do both. diff --git a/extensions/godslayerakp/ws.js b/extensions/godslayerakp/ws.js index bb49c3a876..cb257109cb 100644 --- a/extensions/godslayerakp/ws.js +++ b/extensions/godslayerakp/ws.js @@ -26,8 +26,7 @@ /** * @typedef WebSocketInfo - * @property {boolean} instanceReplaced - * @property {boolean} manuallyClosed + * @property {boolean} destroyed * @property {boolean} errored * @property {string} closeMessage * @property {number} closeCode @@ -92,7 +91,7 @@ runtime.on("targetWasRemoved", (target) => { const instance = this.instances[target.id]; if (instance) { - instance.manuallyClosed = true; + instance.destroyed = true; if (instance.websocket) { instance.websocket.close(); } @@ -255,7 +254,7 @@ const oldInstance = this.instances[util.target.id]; if (oldInstance) { - oldInstance.instanceReplaced = true; + oldInstance.destroyed = true; if (oldInstance.websocket) { oldInstance.websocket.close(); } @@ -263,8 +262,7 @@ /** @type {WebSocketInfo} */ const instance = { - instanceReplaced: false, - manuallyClosed: false, + destroyed: false, errored: false, closeMessage: "", closeCode: 0, @@ -282,11 +280,11 @@ .then( (allowed) => new Promise((resolve) => { - if ( - !allowed || - instance.instanceReplaced || - instance.manuallyClosed - ) { + if (!allowed) { + throw new Error("Not allowed"); + } + + if (instance.destroyed) { resolve(); return; } @@ -319,9 +317,8 @@ }; const onStopAll = () => { - instance.instanceReplaced = true; - instance.manuallyClosed = true; - instance.websocket.close(); + instance.destroyed = true; + websocket.close(); }; vm.runtime.on("BEFORE_EXECUTE", beforeExecute); @@ -339,7 +336,7 @@ }; websocket.onopen = (e) => { - if (instance.instanceReplaced || instance.manuallyClosed) { + if (instance.destroyed) { cleanup(); websocket.close(); return; @@ -359,24 +356,31 @@ }; websocket.onclose = (e) => { - if (instance.instanceReplaced) return; - instance.closeMessage = e.reason || ""; - instance.closeCode = e.code; - runtime.startHats("gsaWebsocket_onClose", null, target); - cleanup(); + if (!instance.errored) { + instance.closeMessage = e.reason || ""; + instance.closeCode = e.code; + cleanup(); + + if (!instance.destroyed) { + runtime.startHats("gsaWebsocket_onClose", null, target); + } + } }; websocket.onerror = (e) => { - if (instance.instanceReplaced) return; console.error("websocket error", e); instance.errored = true; - runtime.startHats("gsaWebsocket_onError", null, target); cleanup(); + + if (!instance.destroyed) { + runtime.startHats("gsaWebsocket_onError", null, target); + } }; websocket.onmessage = async (e) => { - if (instance.instanceReplaced || instance.manuallyClosed) + if (instance.destroyed) { return; + } let data = e.data; @@ -402,6 +406,11 @@ ) .catch((error) => { console.error("could not open websocket connection", error); + + instance.errored = true; + if (!instance.destroyed) { + runtime.startHats("gsaWebsocket_onError", null, target); + } }); } @@ -422,10 +431,7 @@ isClosed(_, utils) { const instance = this.instances[utils.target.id]; if (!instance) return false; - return ( - !!instance.websocket && - instance.websocket.readyState === WebSocket.CLOSED - ); + return instance.closeCode !== 0; } closeCode(_, utils) { @@ -466,7 +472,7 @@ closeWithoutReason(_, utils) { const instance = this.instances[utils.target.id]; if (!instance) return; - instance.manuallyClosed = true; + instance.destroyed = true; if (instance.websocket) { instance.websocket.close(); } @@ -475,7 +481,7 @@ closeWithCode(args, utils) { const instance = this.instances[utils.target.id]; if (!instance) return; - instance.manuallyClosed = true; + instance.destroyed = true; if (instance.websocket) { instance.websocket.close(toCloseCode(args.CODE)); } @@ -484,7 +490,7 @@ closeWithReason(args, utils) { const instance = this.instances[utils.target.id]; if (!instance) return; - instance.manuallyClosed = true; + instance.destroyed = true; if (instance.websocket) { instance.websocket.close( toCloseCode(args.CODE), diff --git a/samples/Cloud Variables.sb3 b/samples/Cloud Variables.sb3 new file mode 100644 index 0000000000000000000000000000000000000000..e80bf67c6064fd37ad67e5268942c4ff972bf480 GIT binary patch literal 3878 zcma)9cQ736_Fh)+k;R4t(Ysw%4WhGpBqCa{*eKCUg0O-`w5Ut;CDw`%QA6}4(b;T} zs1eb=L@!ICT;Kfuxc7cDznS}O}nm7rib=INeHVj~Msx}Zu?v%DAO^^ z$V0V<{q8;T_Knw?{CZGp=hsVW-*2mASD|Z}pvhdwZMXJmX#@zLR2Gu(Wbi>UD`qLb4lT)eL% zeuAqvLbu&Ha)YQWGVm)Ur(AqCSZwJ9z$bxX*Y<5DiAb0qH(&3k@<%5dz*3;`To0L& z1ct$j4t$&H3vxWT4W5+ND@R)6^-@E%&$QZTb{u#J-@f>!-RS7M=U`f7nHp-C$2aqJ8WP1*tQ&jzsve9@lT(|CL7N}D)hJi8 zwAr^j>p3;Ee;}f@3;9BK$=(Oo8XTEwi_FFP7d&>ADJpf7Stt?X`yCL;BrxH% zU|QAWH`aI-&K%RFpBB_yom)t+R>PSq@<1>{3eQx0tJrjAl;f|SqZ!ZZVED=9p4>!K zix8pIlEChPS~y=idUy^__c(WxupFSOdLGBoNRPYMrQ1N1>dkl`9=be?engV)h4_VTZdQ+-c!?48KLo+X*jdxQdoZimnST)DagJaWK87m8<#&I={!;0tNj85)cL-+_ zab{8FBD~=Q8p(g~cp~UU;Ssi`K0UpH$@a~|M93oXY%DS=%cNUwIFlv_MdR9#P1mpV z1WQTQr#=OGZPWHA>|C{M<{m-v-OkwD^%4n&@$Q_x)1l3;m5B@~+^@E{*;g8sha;k# z9hZjYj+YMCjUx^s?HmVO{qK#vn@{}tPzlERr6l`b4(H#QLE$=9s-T9g-QuO~W-La5!`- zI_pE;;NDNBt+mQHE}wi8pQHiT6Tf+(GugZ^MiV6L<|G|6q&cTe-#S*@UfymVCcA^~^(S{Z}W1h_J7 zZ^+o~J;(~znfqiXO?sP*bGE5*5qGJ&ft!b1YbN4(LWZE#D?P2sTfp<@+^Y)SHt+;_e6KW~|WpM#r7n_=HBxtxlwZ^?o zLs%-buTku3#<5cGA3e`({aIFZ2lsk;{#sq)T_*I2VPHw4bXE~O{Y2^_$N5%0&jCvN4Wr&fGij8A=f8;D2M~YC<|(hxnyuC*=RwHeX%}iLNMu)SZadU0rd^92Hl}yfZx6BaNQEuTEnM;J@H#Ne!j);4AKXcEy0ph6 zkX(O93HyCUcJ(8>j)x}C$nGoxQ(paE`cbA>3#Ly@jXBtlM3 z4gzz7xVb2}xhlh)WdcJUc$*rt#PEY=!xN{PX?ts6J*9B-@yiWHv3L5qs>WxgY;#PJ z&=ny|q;q0b}R7N0FT$Ejvm0=JlLILLN0=poogi!pO zRJE1q^BG09cCvM?ghc}YzGKYZ;;@`%f4d~!J}gUk8Yy60)9bK>TSIMGaIQa|TE8y- zWPELBr0vDx2H^<8gkxDGVAH>JTNm#xW)tPWq+t40qUDrG#l_bNJ{PyaR1_MXTgV^N{+ipfu=)+QFMMW)kjNmhmrxzvvlFw^fly!3>D2hEmoiG*9#vmMhR4sh z3;LexCf5C!Hf!41Bt{Q8UtKB3oS78wjRdmHf;{6-J~Qow6tDz0w2ao&`oAxfw%}R`?BD!W)Q@Y{F<^0_mF6wEVcL0@^?;A3)4-wdR0dXN zHDHWJTVNeIeCf+<-Q6HMv7|aN=KZu=149{t0(gkYb`;o(e*Q8Y)(Xp^VYltoZJs5k zS@l#Vfhj$a0crx0hTU$9=)14N<>ZE#6m4`D+Uif);=uORLji-l@#m1=_wa zSqHMIbp*0RhcTfFM<=er+(a8YZ1&G~NcCG!e&o2}_cFbC z(!kOfvCyQU>vsPatJq2y3b5UH>R-Htd2ao9Hs1U=A4OqdOhE|({QsWuV*38${HKHb wC-R>X^l#YD3lI9g2+}`0{&W8S-LdQ9!~csKSQt~&{EY%$#QeoA$MSdeFNX;j0{{R3 literal 0 HcmV?d00001 From e8627aebb7a8dbf793391b5420aae44ab64e01f5 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 15 Nov 2023 11:14:07 -0600 Subject: [PATCH 051/196] Make extensions translatable - part 1 (#1146) --- .gitignore | 3 +- development/build-production.js | 8 +- development/builder.js | 337 +++++++++++++++++++- development/parse-extension-metadata.js | 4 + development/parse-extension-translations.js | 123 +++++++ extensions/.eslintrc.js | 4 + extensions/0832/rxFS2.js | 45 --- extensions/Lily/McUtils.js | 1 + extensions/NOname-awa/graphics2d.js | 18 -- extensions/qxsck/data-analysis.js | 12 - extensions/qxsck/var-and-list.js | 20 -- package-lock.json | 2 +- package.json | 2 + translations/extension-metadata.json | 289 +++++++++++++++++ translations/extension-runtime.json | 90 ++++++ 15 files changed, 842 insertions(+), 116 deletions(-) create mode 100644 development/parse-extension-translations.js create mode 100644 translations/extension-metadata.json create mode 100644 translations/extension-runtime.json diff --git a/.gitignore b/.gitignore index 960930abc4..4a32dff77b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # npm node_modules -# Final built website +# Final built website and localizations build +build-l10n # Various operating system caches thumbs.db diff --git a/development/build-production.js b/development/build-production.js index 39f50abffe..acfe878b40 100644 --- a/development/build-production.js +++ b/development/build-production.js @@ -1,10 +1,14 @@ const pathUtil = require("path"); const Builder = require("./builder"); -const outputDirectory = pathUtil.join(__dirname, "..", "build"); +const outputDirectory = pathUtil.join(__dirname, "../build"); +const l10nOutput = pathUtil.join(__dirname, "../build-l10n"); const builder = new Builder("production"); const build = builder.build(); + build.export(outputDirectory); +console.log(`Built to ${outputDirectory}`); -console.log(`Saved to ${outputDirectory}`); +build.exportL10N(l10nOutput); +console.log(`Exported L10N to ${l10nOutput}`); diff --git a/development/builder.js b/development/builder.js index 9f8a0c40c5..73dbca1bd0 100644 --- a/development/builder.js +++ b/development/builder.js @@ -3,12 +3,17 @@ const AdmZip = require("adm-zip"); const pathUtil = require("path"); const compatibilityAliases = require("./compatibility-aliases"); const parseMetadata = require("./parse-extension-metadata"); -const featuredExtensionSlugs = require("../extensions/extensions.json"); /** * @typedef {'development'|'production'|'desktop'} Mode */ +/** + * @typedef TranslatableString + * @property {string} string The English version of the string + * @property {string} developer_comment Helper text to help translators + */ + /** * Recursively read a directory. * @param {string} directory @@ -39,6 +44,107 @@ const recursiveReadDirectory = (directory) => { return result; }; +/** + * Synchronous create a directory and any parents. Does nothing if the folder already exists. + * @param {string} directory + */ +const mkdirp = (directory) => { + try { + fs.mkdirSync(directory, { + recursive: true, + }); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } +}; + +/** + * @param {Record>} allTranslations + * @param {string} idPrefix + * @returns {Record>|null} + */ +const filterTranslationsByPrefix = (allTranslations, idPrefix) => { + let translationsEmpty = true; + const filteredTranslations = {}; + + for (const [locale, strings] of Object.entries(allTranslations)) { + let localeEmpty = true; + const filteredStrings = {}; + + for (const [id, string] of Object.entries(strings)) { + if (id.startsWith(idPrefix)) { + filteredStrings[id.substring(idPrefix.length)] = string; + localeEmpty = false; + } + } + + if (!localeEmpty) { + filteredTranslations[locale] = filteredStrings; + translationsEmpty = false; + } + } + + return translationsEmpty ? null : filteredTranslations; +}; + +/** + * @param {Record>} allTranslations + * @param {string} idFilter + * @returns {Record} + */ +const filterTranslationsByID = (allTranslations, idFilter) => { + let stringsEmpty = true; + const result = {}; + + for (const [locale, strings] of Object.entries(allTranslations)) { + const translated = strings[idFilter]; + if (translated) { + result[locale] = translated; + stringsEmpty = false; + } + } + + return stringsEmpty ? null : result; +}; + +/** + * @param {string} oldCode + * @param {string} insertCode + */ +const insertAfterCommentsBeforeCode = (oldCode, insertCode) => { + let index = 0; + while (true) { + if (oldCode.substring(index, index + 2) === "//") { + // Line comment + const end = oldCode.indexOf("\n", index); + if (end === -1) { + // This file is only line comments + index = oldCode.length; + break; + } + index = end; + } else if (oldCode.substring(index, index + 2) === "/*") { + // Block comment + const end = oldCode.indexOf("*/", index); + if (end === -1) { + throw new Error("Block comment never ends"); + } + index = end + 2; + } else if (/\s/.test(oldCode.charAt(index))) { + // Whitespace + index++; + } else { + break; + } + } + + const before = oldCode.substring(0, index); + const after = oldCode.substring(index); + return before + insertCode + after; +}; + class BuildFile { constructor(source) { this.sourcePath = source; @@ -59,12 +165,53 @@ class BuildFile { validate() { // no-op by default } + + /** + * @returns {Record>|null} + */ + getStrings() { + // no-op by default, to be overridden + return null; + } } class ExtensionFile extends BuildFile { - constructor(absolutePath, featured) { + /** + * @param {string} absolutePath Full path to the .js file, eg. /home/.../extensions/fetch.js + * @param {string} slug Just the extension ID from the path, eg. fetch + * @param {boolean} featured true if the extension is the homepage + * @param {Record>} allTranslations All extension runtime translations + * @param {Mode} mode + */ + constructor(absolutePath, slug, featured, allTranslations, mode) { super(absolutePath); + /** @type {string} */ + this.slug = slug; + /** @type {boolean} */ this.featured = featured; + /** @type {Record>} */ + this.allTranslations = allTranslations; + /** @type {Mode} */ + this.mode = mode; + } + + read() { + const data = fs.readFileSync(this.sourcePath, "utf-8"); + + if (this.mode !== "development") { + const translations = filterTranslationsByPrefix( + this.allTranslations, + `${this.slug}@` + ); + if (translations !== null) { + return insertAfterCommentsBeforeCode( + data, + `Scratch.translate.setup(${JSON.stringify(translations)});` + ); + } + } + + return data; } getMetadata() { @@ -116,10 +263,59 @@ class ExtensionFile extends BuildFile { } } } + + getStrings() { + if (!this.featured) { + return null; + } + + const metadata = this.getMetadata(); + const slug = this.slug; + + const getMetadataDescription = (part) => { + let result = `${part} of the '${metadata.name}' extension in the extension gallery.`; + if (metadata.context) { + result += ` ${metadata.context}`; + } + return result; + }; + const metadataStrings = { + [`${slug}@name`]: { + string: metadata.name, + developer_comment: getMetadataDescription("Name"), + }, + [`${slug}@description`]: { + string: metadata.description, + developer_comment: getMetadataDescription("Description"), + }, + }; + + const parseTranslations = require("./parse-extension-translations"); + const jsCode = fs.readFileSync(this.sourcePath, "utf-8"); + const unprefixedRuntimeStrings = parseTranslations(jsCode); + const runtimeStrings = Object.fromEntries( + Object.entries(unprefixedRuntimeStrings).map(([key, value]) => [ + `${slug}@${key}`, + value, + ]) + ); + + return { + "extension-metadata": metadataStrings, + "extension-runtime": runtimeStrings, + }; + } } class HomepageFile extends BuildFile { - constructor(extensionFiles, extensionImages, withDocs, samples, mode) { + constructor( + extensionFiles, + extensionImages, + featuredSlugs, + withDocs, + samples, + mode + ) { super(pathUtil.join(__dirname, "homepage-template.ejs")); /** @type {Record} */ @@ -128,6 +324,9 @@ class HomepageFile extends BuildFile { /** @type {Record} */ this.extensionImages = extensionImages; + /** @type {string[]} */ + this.featuredSlugs = featuredSlugs; + /** @type {Map} */ this.withDocs = withDocs; @@ -179,7 +378,7 @@ class HomepageFile extends BuildFile { .map((i) => i[0]); const extensionMetadata = Object.fromEntries( - featuredExtensionSlugs.map((slug) => [ + this.featuredSlugs.map((slug) => [ slug, { ...this.extensionFiles[slug].getMetadata(), @@ -203,7 +402,14 @@ class HomepageFile extends BuildFile { } class JSONMetadataFile extends BuildFile { - constructor(extensionFiles, extensionImages, withDocs, samples) { + constructor( + extensionFiles, + extensionImages, + featuredSlugs, + withDocs, + samples, + allTranslations + ) { super(null); /** @type {Record} */ @@ -212,11 +418,17 @@ class JSONMetadataFile extends BuildFile { /** @type {Record} */ this.extensionImages = extensionImages; + /** @type {string[]} */ + this.featuredSlugs = featuredSlugs; + /** @type {Set} */ this.withDocs = withDocs; /** @type {Map} */ this.samples = samples; + + /** @type {Record>} */ + this.allTranslations = allTranslations; } getType() { @@ -225,7 +437,7 @@ class JSONMetadataFile extends BuildFile { read() { const extensions = []; - for (const extensionSlug of featuredExtensionSlugs) { + for (const extensionSlug of this.featuredSlugs) { const extension = {}; const file = this.extensionFiles[extensionSlug]; const metadata = file.getMetadata(); @@ -233,8 +445,28 @@ class JSONMetadataFile extends BuildFile { extension.slug = extensionSlug; extension.id = metadata.id; + + // English fields extension.name = metadata.name; extension.description = metadata.description; + + // For other languages, translations go here. + // This system is a bit silly to avoid backwards-incompatible JSON changes. + const nameTranslations = filterTranslationsByID( + this.allTranslations, + `${extensionSlug}@name` + ); + if (nameTranslations) { + extension.nameTranslations = nameTranslations; + } + const descriptionTranslations = filterTranslationsByID( + this.allTranslations, + `${extensionSlug}@description` + ); + if (descriptionTranslations) { + extension.descriptionTranslations = descriptionTranslations; + } + if (image) { extension.image = image; } @@ -385,6 +617,7 @@ class SampleFile extends BuildFile { class Build { constructor() { + /** @type {Record} */ this.files = {}; } @@ -398,15 +631,7 @@ class Build { } export(root) { - try { - fs.rmSync(root, { - recursive: true, - }); - } catch (e) { - if (e.code !== "ENOENT") { - throw e; - } - } + mkdirp(root); for (const [relativePath, file] of Object.entries(this.files)) { const directoryName = pathUtil.dirname(relativePath); @@ -416,6 +641,50 @@ class Build { fs.writeFileSync(pathUtil.join(root, relativePath), file.read()); } } + + /** + * @returns {Record>} + */ + generateL10N() { + const allStrings = {}; + + for (const file of Object.values(this.files)) { + const fileStrings = file.getStrings(); + if (!fileStrings) { + continue; + } + + for (const [group, strings] of Object.entries(fileStrings)) { + if (!allStrings[group]) { + allStrings[group] = {}; + } + + for (const [key, value] of Object.entries(strings)) { + if (allStrings[key]) { + throw new Error( + `L10N collision: multiple instances of ${key} in group ${group}` + ); + } + allStrings[group][key] = value; + } + } + } + + return allStrings; + } + + /** + * @param {string} root + */ + exportL10N(root) { + mkdirp(root); + + const groups = this.generateL10N(); + for (const [name, strings] of Object.entries(groups)) { + const filename = pathUtil.join(root, `exported-${name}.json`); + fs.writeFileSync(filename, JSON.stringify(strings, null, 2)); + } + } } class Builder { @@ -439,11 +708,35 @@ class Builder { this.imagesRoot = pathUtil.join(__dirname, "../images"); this.docsRoot = pathUtil.join(__dirname, "../docs"); this.samplesRoot = pathUtil.join(__dirname, "../samples"); + this.translationsRoot = pathUtil.join(__dirname, "../translations"); } build() { const build = new Build(this.mode); + const featuredExtensionSlugs = JSON.parse( + fs.readFileSync( + pathUtil.join(this.extensionsRoot, "extensions.json"), + "utf-8" + ) + ); + + /** + * Look up by [group][locale][id] + * @type {Record>>} + */ + const translations = {}; + for (const [filename, absolutePath] of recursiveReadDirectory( + this.translationsRoot + )) { + if (!filename.endsWith(".json")) { + continue; + } + const group = filename.split(".")[0]; + const data = JSON.parse(fs.readFileSync(absolutePath, "utf-8")); + translations[group] = data; + } + /** @type {Record} */ const extensionFiles = {}; for (const [filename, absolutePath] of recursiveReadDirectory( @@ -454,7 +747,13 @@ class Builder { } const extensionSlug = filename.split(".")[0]; const featured = featuredExtensionSlugs.includes(extensionSlug); - const file = new ExtensionFile(absolutePath, featured); + const file = new ExtensionFile( + absolutePath, + extensionSlug, + featured, + translations["extension-runtime"], + this.mode + ); extensionFiles[extensionSlug] = file; build.files[`/${filename}`] = file; } @@ -530,6 +829,7 @@ class Builder { build.files["/index.html"] = new HomepageFile( extensionFiles, extensionImages, + featuredExtensionSlugs, extensionsWithDocs, samples, this.mode @@ -541,8 +841,10 @@ class Builder { new JSONMetadataFile( extensionFiles, extensionImages, + featuredExtensionSlugs, extensionsWithDocs, - samples + samples, + translations["extension-metadata"] ); for (const [oldPath, newPath] of Object.entries(compatibilityAliases)) { @@ -581,6 +883,7 @@ class Builder { `${this.websiteRoot}/**/*`, `${this.docsRoot}/**/*`, `${this.samplesRoot}/**/*`, + `${this.translationsRoot}/**/*`, ], { ignoreInitial: true, diff --git a/development/parse-extension-metadata.js b/development/parse-extension-metadata.js index 632f01216c..935a322bcc 100644 --- a/development/parse-extension-metadata.js +++ b/development/parse-extension-metadata.js @@ -24,6 +24,7 @@ class Extension { this.by = []; /** @type {Person[]} */ this.original = []; + this.context = ""; } } @@ -94,6 +95,9 @@ const parseMetadata = (extensionCode) => { case "original": metadata.original.push(parsePerson(value)); break; + case "context": + metadata.context = value; + break; default: // TODO break; diff --git a/development/parse-extension-translations.js b/development/parse-extension-translations.js new file mode 100644 index 0000000000..624a7ca7c4 --- /dev/null +++ b/development/parse-extension-translations.js @@ -0,0 +1,123 @@ +const espree = require("espree"); +const esquery = require("esquery"); +const parseMetadata = require("./parse-extension-metadata"); + +/** + * @fileoverview Parses extension code to find calls to Scratch.translate() and statically + * evaluate its arguments. + */ + +const evaluateAST = (node) => { + if (node.type == "Literal") { + return node.value; + } + + if (node.type === "ObjectExpression") { + const object = {}; + for (const { key, value } of node.properties) { + // Normally Identifier refers to a variable, but inside of key we treat it as a string. + let evaluatedKey; + if (key.type === "Identifier") { + evaluatedKey = key.name; + } else { + evaluatedKey = evaluateAST(key); + } + + object[evaluatedKey] = evaluateAST(value); + } + return object; + } + + console.error(`Can't evaluate node:`, node); + throw new Error(`Can't evaluate ${node.type} node at build-time`); +}; + +/** + * Generate default ID for a translation that has no explicit ID. + * @param {string} string + * @returns {string} + */ +const defaultIdForString = (string) => { + // hardcoded in VM + return `_${string}`; +}; + +/** + * @param {string} js + * @returns {Record} + */ +const parseTranslations = (js) => { + const metadata = parseMetadata(js); + if (!metadata.name) { + throw new Error(`Extension needs a // Name: to generate translations`); + } + + let defaultDescription = `Part of the '${metadata.name}' extension.`; + if (metadata.context) { + defaultDescription += ` ${metadata.context}`; + } + + const ast = espree.parse(js, { + ecmaVersion: 2022, + }); + const selector = esquery.parse( + 'CallExpression[callee.object.name="Scratch"][callee.property.name="translate"]' + ); + const matches = esquery.match(ast, selector); + + const result = {}; + for (const match of matches) { + const args = match.arguments; + if (args.length !== 1) { + throw new Error(`Scratch.translate() must have exactly 1 argument`); + } + + const evaluated = evaluateAST(args[0]); + + let id; + let english; + let description; + + if (typeof evaluated === "string") { + id = defaultIdForString(evaluated); + english = evaluated; + description = defaultDescription; + } else if (typeof evaluated === "object" && evaluated !== null) { + english = evaluated.default; + id = evaluated.id || defaultIdForString(english); + + description = [defaultDescription, evaluated.description] + .filter((i) => i) + .join(" "); + } else { + throw new Error( + `Not a valid argument for Scratch.translate(): ${evaluated}` + ); + } + + if (typeof id !== "string") { + throw new Error( + `Scratch.translate() passed a value for id that is not a string: ${id}` + ); + } + if (typeof english !== "string") { + throw new Error( + `Scratch.translate() passed a value for default that is not a string: ${english}` + ); + } + if (typeof description !== "string") { + throw new Error( + `Scratch.translate() passed a value for description that is not a string: ${description}` + ); + } + + result[id] = { + string: english, + developer_comment: description, + }; + } + + return result; +}; + +module.exports = parseTranslations; diff --git a/extensions/.eslintrc.js b/extensions/.eslintrc.js index 3fc982110c..969f9a4ef4 100644 --- a/extensions/.eslintrc.js +++ b/extensions/.eslintrc.js @@ -84,6 +84,10 @@ module.exports = { { selector: 'Program > :not(ExpressionStatement[expression.type=CallExpression][expression.callee.type=/FunctionExpression/])', message: 'All extension code must be within (function (Scratch) { ... })(Scratch);' + }, + { + selector: 'CallExpression[callee.object.object.name=Scratch][callee.object.property.name=translate][callee.property.name=setup]', + message: 'Do not call Scratch.translate.setup() yourself. Just use Scratch.translate() and let the build script handle it.' } ] } diff --git a/extensions/0832/rxFS2.js b/extensions/0832/rxFS2.js index 871ef0b1ca..0ba12451ef 100644 --- a/extensions/0832/rxFS2.js +++ b/extensions/0832/rxFS2.js @@ -15,51 +15,6 @@ (function (Scratch) { "use strict"; - Scratch.translate.setup({ - zh: { - start: "新建 [STR] ", - folder: "设置 [STR] 为 [STR2] ", - folder_default: "大主教大祭司主宰世界!", - sync: "将 [STR] 的位置更改为 [STR2] ", - del: "删除 [STR] ", - webin: "从网络加载 [STR]", - open: "打开 [STR]", - clean: "清空文件系统", - in: "从 [STR] 导入文件系统", - out: "导出文件系统", - list: "列出 [STR] 下的所有文件", - search: "搜索 [STR]", - }, - ru: { - start: "Создать [STR]", - folder: "Установить [STR] в [STR2]", - folder_default: "Архиепископ Верховный жрец Правитель мира!", - sync: "Изменить расположение [STR] на [STR2]", - del: "Удалить [STR]", - webin: "Загрузить [STR] из Интернета", - open: "Открыть [STR]", - clean: "Очистить файловую систему", - in: "Импортировать файловую систему из [STR]", - out: "Экспортировать файловую систему", - list: "Список всех файлов в [STR]", - search: "Поиск [STR]", - }, - jp: { - start: "新規作成 [STR]", - folder: "[STR] を [STR2] に設定する", - folder_default: "大主教大祭司世界の支配者!", - sync: "[STR] の位置を [STR2] に変更する", - del: "[STR] を削除する", - webin: "[STR] をウェブから読み込む", - open: "[STR] を開く", - clean: "ファイルシステムをクリアする", - in: "[STR] からファイルシステムをインポートする", - out: "ファイルシステムをエクスポートする", - list: "[STR] にあるすべてのファイルをリストする", - search: "[STR] を検索する", - }, - }); - var rxFSfi = new Array(); var rxFSsy = new Array(); var Search, i, str, str2; diff --git a/extensions/Lily/McUtils.js b/extensions/Lily/McUtils.js index 889c32e80b..8cf8177d8d 100644 --- a/extensions/Lily/McUtils.js +++ b/extensions/Lily/McUtils.js @@ -2,6 +2,7 @@ // ID: lmsmcutils // Description: Helpful utilities for any fast food employee. // By: LilyMakesThings +// Context: Joke extension based on McDonalds, a fast food chain. /*! * Credit to NexusKitten (NamelessCat) for the idea diff --git a/extensions/NOname-awa/graphics2d.js b/extensions/NOname-awa/graphics2d.js index 44bf8d5f3a..6c74ebedfe 100644 --- a/extensions/NOname-awa/graphics2d.js +++ b/extensions/NOname-awa/graphics2d.js @@ -5,24 +5,6 @@ (function (Scratch) { "use strict"; - Scratch.translate.setup({ - zh: { - name: "图形 2D", - line_section: "([x1],[y1])到([x2],[y2])的距离", - ray_direction: "([x1],[y1])到([x2],[y2])的方向", - triangle: "三角形([x1],[y1])([x2],[y2])([x3],[y3])的 [CS]", - triangle_s: "三角形 [s1] [s2] [s3] 的面积", - quadrilateral: - "四边形([x1],[y1])([x2],[y2])([x3],[y3])([x4],[y4])的 [CS]", - graph: "图形 [graph] 的 [CS]", - round: "[rd] 为 [a] 的圆的 [CS]", - pi: "派", - radius: "半径", - diameter: "直径", - area: "面积", - circumference: "周长", - }, - }); class graph { getInfo() { return { diff --git a/extensions/qxsck/data-analysis.js b/extensions/qxsck/data-analysis.js index b8732c5180..195d95f71d 100644 --- a/extensions/qxsck/data-analysis.js +++ b/extensions/qxsck/data-analysis.js @@ -5,18 +5,6 @@ (function (Scratch) { "use strict"; - Scratch.translate.setup({ - zh: { - name: "数据分析", - average: "[NUMBERS] 的平均数", - maximum: "[NUMBERS] 的最大数", - minimum: "[NUMBERS] 的最小数", - median: "[NUMBERS] 的中位数", - mode: "[NUMBERS] 的众数", - variance: "[NUMBERS] 的方差", - }, - }); - class dataAnalysis { getInfo() { return { diff --git a/extensions/qxsck/var-and-list.js b/extensions/qxsck/var-and-list.js index f2fe5663a7..68779b7866 100644 --- a/extensions/qxsck/var-and-list.js +++ b/extensions/qxsck/var-and-list.js @@ -5,26 +5,6 @@ (function (Scratch) { "use strict"; - Scratch.translate.setup({ - zh: { - name: "变量与列表", - getVar: "[VAR] 的值", - seriVarsToJson: "将以 [START] 为开头的所有变量转换为json", - setVar: "将变量 [VAR] 的值设置为 [VALUE]", - getList: "列表 [LIST] 的值", - getValueOfList: "列表 [LIST] 的第 [INDEX] 项", - seriListsToJson: "将以 [START] 为开头的所有列表转换为json", - clearList: "清空列表 [LIST]", - deleteOfList: "删除列表 [LIST] 的第 [INDEX] 项", - addValueInList: "在列表 [LIST] 末尾添加 [VALUE]", - replaceOfList: "替换列表 [LIST] 的第 [INDEX] 项为 [VALUE]", - getIndexOfList: "列表 [LIST] 中第一个 [VALUE] 的位置", - getIndexesOfList: "列表 [LIST] 中 [VALUE] 的位置", - length: "列表 [LIST] 的长度", - listContains: "列表 [LIST] 包括 [VALUE] 吗?", - copyList: "将列表 [LIST1] 复制到列表 [LIST2]", - }, - }); class VarAndList { getInfo() { return { diff --git a/package-lock.json b/package-lock.json index 031e704254..fb28fd4b84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,7 +138,7 @@ }, "@turbowarp/types": { "version": "git+https://github.com/TurboWarp/types-tw.git#cfaa5d3ce5dc96f16f7da9054325a4f1ed457662", - "from": "git+https://github.com/TurboWarp/types-tw.git#cfaa5d3ce5dc96f16f7da9054325a4f1ed457662" + "from": "git+https://github.com/TurboWarp/types-tw.git#tw" }, "@ungap/structured-clone": { "version": "1.2.0", diff --git a/package.json b/package.json index f2d888700e..365e4787ab 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ }, "devDependencies": { "eslint": "^8.53.0", + "espree": "^9.6.1", + "esquery": "^1.5.0", "prettier": "^3.0.3" }, "private": true diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json new file mode 100644 index 0000000000..10f48de680 --- /dev/null +++ b/translations/extension-metadata.json @@ -0,0 +1,289 @@ +{ + "ca": { + "runtime-options@name": "Opcions d'execució" + }, + "cs": { + "runtime-options@name": "Nastavení běhu" + }, + "de": { + "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools Ihres Browsers integrierten JavaScript-Konsole interagieren", + "-SIPC-/time@name": "Zeit", + "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem", + "CubesterYT/TurboHook@description": "Ermöglicht die Benutzung von Webhooks", + "Lily/SoundExpanded@description": "Fügt mehr Blöcke hinzu, die mit Klängen zu tun haben", + "Lily/TempVariables2@name": "Temporäre Variablen", + "NOname-awa/more-comparisons@name": "Mehr Vergleiche", + "ar@name": "Erweiterte Realität", + "battery@name": "Batterie", + "clipboard@name": "Zwischenablage", + "files@name": "Dateien", + "lab/text@name": "Animierter Text", + "mdwalters/notifications@name": "Benachrichtigungen", + "runtime-options@name": "Laufzeit-Optionen", + "shreder95ua/resolution@name": "Bildschirmauflösung", + "sound@name": "Klänge", + "true-fantom/math@name": "Mathe", + "veggiecan/LongmanDictionary@name": "Longman Wörterbuch" + }, + "es": { + "CST1229/zip@description": "Crea y edita archivos de formato .zip, incluyendo archivos .sb3.", + "Lily/MoreTimers@description": "Controlar varios contadores a la vez.", + "NOname-awa/graphics2d@name": "Gráficos 2D", + "runtime-options@name": "Opciones de Runtime", + "text@name": "Texto" + }, + "fr": { + "runtime-options@name": "Options d'exécution" + }, + "hu": { + "runtime-options@name": "Lefutási Opciók" + }, + "it": { + "-SIPC-/consoles@description": "Blocchi che interagiscono con la console Javascript degli strumenti per sviluppatori del browser. ", + "-SIPC-/consoles@name": "Console Javascript", + "-SIPC-/time@description": "Blocchi per interagire con i timestamp Unix e altre stringhe rappresentanti ora e data.", + "-SIPC-/time@name": "Unix Time", + "0832/rxFS2@description": "Blocchi per gestire un filesystem vituale in memoria.", + "Alestore/nfcwarp@description": "Permette di leggere i dati da dispositivi NFC (NDEF). Funziona solo in Chrome e Android.", + "Alestore/nfcwarp@name": "NFC Warp", + "CST1229/zip@description": "Crea e modifica i file in formato zip, inclusi i file sb3.", + "Clay/htmlEncode@description": "Inserisce caratteri escape nelle stringhe per poterle includere in tutta sicurezza nell'HTML. ", + "Clay/htmlEncode@name": "HTML Encoding", + "CubesterYT/TurboHook@description": "Permette di usare i webhook.", + "CubesterYT/WindowControls@description": "Sposta, ridimensiona e rinomina le finestre, entra in modalità schermo intero, restituisce le dimensioni dello schermo, e molto altro.", + "CubesterYT/WindowControls@name": "Controllo Finestre", + "DNin/wake-lock@description": "Impedisce al tuo computer di andare in standby.", + "DNin/wake-lock@name": "Blocco Standby", + "DT/cameracontrols@description": "Sposta la parte visibile dello Stage.", + "DT/cameracontrols@name": "Controllo Webcam (Ancora Presenti Diversi Bug)", + "JeremyGamer13/tween@description": "Metodi di interpolazione per rendere più fluide le animazioni.", + "JeremyGamer13/tween@name": "Animazioni Interpolate", + "Lily/AllMenus@description": "Categoria speciale che contiene tutti i menu di tutte le categorie e tutte le estensioni di Scratch.", + "Lily/AllMenus@name": "Tutti i Menu", + "Lily/Cast@description": "Converte i valori da un tipo all'altro.", + "Lily/Cast@name": "Conversione", + "Lily/ClonesPlus@description": "Estensione delle possibilità dei cloni di Scratch.", + "Lily/ClonesPlus@name": "Cloni Plus", + "Lily/CommentBlocks@description": "Aggiunge commenti ai tuoi script.", + "Lily/CommentBlocks@name": "Blocchi per Commenti", + "Lily/HackedBlocks@description": "Vari \"blocchi hackerati\" che funzionano in Scratch ma non sono visibili nell'elenco dei blocchi.", + "Lily/HackedBlocks@name": "Collezione di Blocchi Nascosti", + "Lily/LooksPlus@description": "Espande la categoria Aspetto, permette di mostrare/nascondere, di ottenere i dati SVG dei costumi e di modificare i costumi SVG degli sprite.", + "Lily/LooksPlus@name": "Aspetto Plus", + "Lily/McUtils@description": "Blocchi utili per qualunque impiegato di un fast food.", + "Lily/McUtils@name": "Utilità FastFood", + "Lily/MoreEvents@description": "Avvia i tuoi script in nuovi modi.", + "Lily/MoreEvents@name": "Altri Eventi", + "Lily/MoreTimers@description": "Permette di gestire più cronometri.", + "Lily/MoreTimers@name": "Altri Cronometri", + "Lily/Skins@description": "Cambia il costumi dei tuoi sprite con altre immagini o altri costumi.", + "Lily/Skins@name": "Altro Costumi Plus ", + "Lily/SoundExpanded@description": "Aggiunge altri blocchi per gestire i suoni.", + "Lily/SoundExpanded@name": "Suoni Plus", + "Lily/TempVariables2@description": "Crea variabili usa e getta o variabili limitate ai singoli thread.", + "Lily/TempVariables2@name": "Variabili Temporanee", + "Lily/Video@description": "Riproduce un video dal suo URL .", + "Lily/lmsutils@description": "Conosciuta in precedenza come \"Utilità per LMS\".", + "Lily/lmsutils@name": "Strumenti di Lily", + "Longboost/color_channels@description": "Mostra o timbra solo i canali RGB selezionati.", + "Longboost/color_channels@name": "Canali RGB", + "NOname-awa/graphics2d@description": "Blocchi per calcolare distanza, angoli e aree in due dimensioni.", + "NOname-awa/graphics2d@name": "Grafica 2D", + "NOname-awa/more-comparisons@description": "Ulteriori blocchi per fare confronti.", + "NOname-awa/more-comparisons@name": "Altri Confronti", + "NexusKitten/controlcontrols@description": "Mostra e nasconde i pulsanti di controllo dei progetti.", + "NexusKitten/controlcontrols@name": "Gestione Pulsanti di Controllo", + "NexusKitten/moremotion@description": "Altri blocchi relativi al movimento.", + "NexusKitten/moremotion@name": "Movimento Plus", + "NexusKitten/sgrab@description": "Ottieni informazioni sui progetti e sugli utenti Scratch. ", + "Skyhigh173/bigint@description": "Blocchi matematici che funzionano su numeri interi (ossia senza decimali) infinitamente grandi.", + "Skyhigh173/bigint@name": "Numeri Illimitati", + "Skyhigh173/json@description": "Gestisce stringhe e array JSON.", + "TheShovel/CanvasEffects@description": "Applica effetti visivi a tutto lo Stage.", + "TheShovel/CanvasEffects@name": "Effetti Stage", + "TheShovel/ColorPicker@description": "Accede al contagocce di sistema.", + "TheShovel/ColorPicker@name": "Contagocce", + "TheShovel/CustomStyles@description": "Personalizza l'aspetto dei monitor delle variabili e della casella CHIEDI del tuo progetto.", + "TheShovel/CustomStyles@name": "Stili Personalizzati", + "TheShovel/LZ-String@description": "Comprime e decomprime testi usando l'algoritmo lz-string.", + "TheShovel/LZ-String@name": "Compressione LZ", + "TheShovel/ShovelUtils@description": "Blocchi vari.", + "TheShovel/ShovelUtils@name": "Utilità Varie", + "Xeltalliv/clippingblending@description": "Ritaglio di immagini al di fuori di una zona rettangolare predefinita e modalità aggiuntive di mescolamento dei colori.", + "Xeltalliv/clippingblending@name": "Ritaglio e Fusione", + "XeroName/Deltatime@description": "Blocchi deltatime di precisione.", + "ZXMushroom63/searchApi@description": "Interagisce con i parametri di ricerca dell'URL, la parte dell'URL dopo il punto interrogativo.", + "ZXMushroom63/searchApi@name": "Parametri di Ricerca URL", + "ar@description": "Mostra immagini della webcam e ne traccia il movimento, permettendo ai progetti 3D di sovrapporsi correttamente agli oggetti del mondo reale.", + "ar@name": "Realtà Aumentata", + "battery@description": "Accede alle informazioni sulla batteria di telefoni e portatili. Può non funzionare su tutti i dispositivi o tutti i browser.", + "battery@name": "Batteria", + "bitwise@description": "Blocchi che operano su numeri binari.", + "bitwise@name": "Operazioni su Bit", + "box2d@description": "Fisica bidimensionale.", + "box2d@name": "Fisica Box2D", + "clipboard@description": "Legge e scrive gli appunti di sistema.", + "clipboard@name": "Appunti", + "clouddata-ping@description": "Determina se un server di variabili cloud è probabilmente attivo.", + "clouddata-ping@name": "Ping Dati Cloud", + "cloudlink@description": "Una potente estensione WebSocket per Scratch.", + "cs2627883/numericalencoding@description": "Codifica stringhe come numeri per memorizzarle nelle variabili cloud.", + "cs2627883/numericalencoding@name": "Codifica Numerica", + "cursor@description": "Usa puntatori del mouse personalizzati o nasconde il puntatore. Permette anche di rimpiazzare il puntatore con le immagini di un qualunque costume.", + "cursor@name": "Puntatore Mouse", + "encoding@description": "Codifica e decodifica stringhe nei corrispondenti numeri unicode, base 64 o URL.", + "encoding@name": "Codifica", + "fetch@description": "Invia richieste al web.", + "files@description": "Legge e scarica file.", + "files@name": "File", + "gamejolt@description": "Blocchi che permettono ai giochi di interagire con l'API GemeJolt. Non ufficiale.", + "gamepad@description": "Accede direttamente ai gamepad invece di mappare soltanto i pulsanti in tasti.", + "godslayerakp/http@description": "Estensione completa per interagire con siti web esterni.", + "godslayerakp/ws@description": "Connessione manuale a server WebSocket.", + "iframe@description": "Mostra pagine web o HTML nello Stage.", + "itchio@description": "Blocchi che interagiscono con il sito itch.io. Non ufficiale.", + "lab/text@description": "Un modo semplice per mostrare e animare il testo. Compatibie con i blocchi sperimentali \"Testo Animato\" di Scratch Lab.", + "lab/text@name": "Testo Animato", + "local-storage@description": "Memorizza dati persistenti. Come i cookie, ma in modo migliore.", + "local-storage@name": "Memoria Locale", + "mdwalters/notifications@description": "Mostra le notifiche.", + "mdwalters/notifications@name": "Notifiche", + "navigator@description": "Dettagli relativi al browser utente e al sistema operativo.", + "navigator@name": "Browser e SO", + "obviousAlexC/SensingPlus@description": "Un'estensione della categoria Sensori.", + "obviousAlexC/SensingPlus@name": "Sensori Plus", + "obviousAlexC/newgroundsIO@description": "Blocchi che permettono ai giochi di interagire con l'API Newgrounds API. Non ufficiale.", + "obviousAlexC/penPlus@description": "Capacità di rendering avanzate.", + "obviousAlexC/penPlus@name": "Penna Plus V6", + "penplus@description": "Rimpiazzata da Penna Plus V6.", + "penplus@name": "Penna Plus V5 (Vecchio)", + "pointerlock@description": "Aggiunge blocchi per bloccare il mouse. I blocchi\" x/y del mouse\" restituiscono di quanto è cambiata la posizione rispetto al frame precedente mentre il puntatore è bloccato. Rimpiazza il \"blocco puntatore\" sperimentale.", + "pointerlock@name": "Blocco Puntatore", + "qxsck/data-analysis@description": "Blocchi che calcolano medie, mediane, massimi, minimi, varianze e mode.", + "qxsck/data-analysis@name": "Analisi dei Dati", + "qxsck/var-and-list@description": "Ulteriori blocchi per gestione delle variabili e delle liste.", + "qxsck/var-and-list@name": "Variabili e liste", + "rixxyx@description": "Blocchi vari.", + "runtime-options@description": "Restituisce e modifica le impostazioni per la modalità turbo, il framerate, l'interpolazione, i limiti dei cloni, le dimensioni dello Stage e altro ancora.", + "runtime-options@name": "Opzioni Esecuzione", + "shreder95ua/resolution@description": "Restituisce la risoluzione dello schermo principale.", + "shreder95ua/resolution@name": "Risoluzione Schermo", + "sound@description": "Riproduce suoni dai loro URL.", + "sound@name": "Suoni", + "stretch@description": "Stira gli sprite in orizzontale e in verticale.", + "stretch@name": "Stira", + "text@description": "Manipola caratteri e testi.", + "text@name": "Testo", + "true-fantom/base@description": "Converte i numeri tra basi diverse.", + "true-fantom/base@name": "Basi", + "true-fantom/couplers@description": "Alcuni blocchi adattatori.", + "true-fantom/couplers@name": "Adattatori", + "true-fantom/math@description": "Diversi blocchi di tipo operatore, dall'esponente alle funzioni trigonometriche.", + "true-fantom/math@name": "Matematica", + "true-fantom/network@description": "Vari blocchi per interagire con la rete", + "true-fantom/network@name": "Rete", + "true-fantom/regexp@description": "Interfaccia complet per lavorare con le Espressioni Regolari.", + "utilities@description": "Diversi blocchi interessanti.", + "utilities@name": "Utilità", + "veggiecan/LongmanDictionary@description": "Permette al tuo progetto di recuperare le definizioni delle parole inglesi del Dizionario Longman.", + "veggiecan/LongmanDictionary@name": "Dizionario Longman", + "veggiecan/browserfullscreen@description": "Entra e esce nella modalità schermo intero.", + "veggiecan/browserfullscreen@name": "Modalità Schermo Intero", + "vercte/dictionaries@description": "Usa la struttura dizionario (coppie attributo/valore) nei tuoi progetti e converti da e in JSON.", + "vercte/dictionaries@name": "Dizionari" + }, + "ja": { + "-SIPC-/consoles@name": "コンソール", + "-SIPC-/time@name": "時間", + "Clay/htmlEncode@name": "HTMLエンコード", + "runtime-options@name": "ランタイムのオプション", + "text@name": "テキスト" + }, + "ja-hira": { + "runtime-options@name": "ランタイムのオプション" + }, + "ko": { + "runtime-options@name": "실행 설정", + "text@name": "텍스트" + }, + "lt": { + "runtime-options@name": "Paleidimo laiko parinktys" + }, + "nl": { + "runtime-options@name": "Looptijdopties", + "text@name": "Tekst" + }, + "pl": { + "runtime-options@name": "Opcje Uruchamiania" + }, + "pt": { + "runtime-options@name": "Opções de Execução" + }, + "pt-br": { + "runtime-options@name": "Opções de Execução" + }, + "ru": { + "runtime-options@name": "Опции Выполнения" + }, + "sl": { + "runtime-options@name": "Možnosti izvajanja" + }, + "sv": { + "runtime-options@name": "Körtidsalternativ" + }, + "tr": { + "runtime-options@name": "Çalışma Zamanı Seçenekleri", + "text@name": "Metin" + }, + "uk": { + "runtime-options@name": "Параметри виконання" + }, + "zh-cn": { + "-SIPC-/consoles@description": "存取JS开发者控制台", + "-SIPC-/consoles@name": "控制台", + "-SIPC-/time@description": "处理UNIX时间戳和日期字符串", + "-SIPC-/time@name": "时间", + "0832/rxFS2@description": "创建并使用虚拟档案系统", + "Alestore/nfcwarp@name": "NFC", + "JeremyGamer13/tween@name": "缓动", + "Lily/AllMenus@name": "全部菜单", + "Lily/Cast@description": "转换Scratch的资料类型", + "Lily/Cast@name": "类型转换", + "Lily/ClonesPlus@name": "克隆+", + "Lily/CommentBlocks@description": "给代码添加注释
    ", + "Lily/CommentBlocks@name": "注释
    ", + "Lily/LooksPlus@name": "外观+", + "Lily/MoreEvents@name": "更多事件", + "Lily/MoreTimers@name": "更多计时器", + "Lily/Video@name": "视频", + "Lily/lmsutils@name": "Lily 的工具箱", + "NOname-awa/graphics2d@name": "图形 2D", + "NexusKitten/controlcontrols@name": "控件控制", + "NexusKitten/moremotion@name": "更多运动", + "Skyhigh173/json@description": "处理JSON字符串和数组", + "box2d@name": "Box2D 物理引擎", + "clipboard@name": "剪切板", + "encoding@name": "编码", + "files@name": "文件", + "lab/text@name": "动画文字", + "obviousAlexC/SensingPlus@name": "侦测+", + "obviousAlexC/penPlus@name": "画笔+ V6", + "penplus@name": "画笔+ V5(旧)", + "qxsck/data-analysis@name": "数据分析", + "qxsck/var-and-list@name": "变量与列表", + "runtime-options@name": "运行选项", + "sound@name": "声音
    ", + "stretch@name": "伸缩
    ", + "text@name": "文本", + "true-fantom/base@name": "进制转换", + "true-fantom/math@name": "数学", + "true-fantom/network@name": "网络
    ", + "true-fantom/regexp@name": "正则表达式", + "utilities@name": "工具", + "veggiecan/LongmanDictionary@name": "朗文辞典", + "veggiecan/browserfullscreen@name": "全荧幕" + }, + "zh-tw": { + "runtime-options@name": "運行選項" + } +} \ No newline at end of file diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json new file mode 100644 index 0000000000..fae2c56f04 --- /dev/null +++ b/translations/extension-runtime.json @@ -0,0 +1,90 @@ +{ + "es": { + "0832/rxFS2@del": "Eliminar [STR]", + "0832/rxFS2@folder": "Fijar [STR] a [STR2]", + "0832/rxFS2@folder_default": "¡rxFS es bueno!", + "0832/rxFS2@open": "Abrir [STR]", + "0832/rxFS2@start": "Crear [STR]", + "0832/rxFS2@sync": "Cambiar la ubicación de [STR] a [STR2]", + "0832/rxFS2@webin": "Cargar [STR] de la web", + "NOname-awa/graphics2d@area": "área", + "NOname-awa/graphics2d@diameter": "diámetro", + "NOname-awa/graphics2d@name": "Gráficos 2D", + "NOname-awa/graphics2d@radius": "radio" + }, + "it": { + "0832/rxFS2@clean": "Svuota il file system", + "0832/rxFS2@del": "Rimuovi [STR]", + "0832/rxFS2@folder": "Imposta [STR] a [STR2]", + "0832/rxFS2@folder_default": "rxFS funziona!", + "0832/rxFS2@in": "Importa il file system da [STR]", + "0832/rxFS2@list": "Elenco dei file in [STR]", + "0832/rxFS2@open": "Apri [STR]", + "0832/rxFS2@out": "Esporta il file system", + "0832/rxFS2@search": "Cerca [STR]", + "0832/rxFS2@start": "Crea [STR]", + "0832/rxFS2@sync": "Cambia la posizione di [STR] in [STR2]", + "0832/rxFS2@webin": "Leggi [STR] dal web", + "NOname-awa/graphics2d@circumference": "circonferenza", + "NOname-awa/graphics2d@diameter": "diametro", + "NOname-awa/graphics2d@graph": "[CS] del grafo [graph]", + "NOname-awa/graphics2d@line_section": "distanza tra ([x1],[y1]) e ([x2],[y2])", + "NOname-awa/graphics2d@name": "Grafica 2D", + "NOname-awa/graphics2d@pi": "pi greco", + "NOname-awa/graphics2d@quadrilateral": "[CS] del quadrangolo ([x1],[y1]) ([x2],[y2]) ([x3],[y3]) ([x4],[y4])", + "NOname-awa/graphics2d@radius": "raggio", + "NOname-awa/graphics2d@ray_direction": "direzione da ([x1],[y1]) a ([x2],[y2])", + "NOname-awa/graphics2d@round": "[CS] del cerchio [rd][a]", + "NOname-awa/graphics2d@triangle": "[CS] del triangolo ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", + "NOname-awa/graphics2d@triangle_s": "area del triangolo [s1] [s2] [s3]", + "qxsck/data-analysis@average": "media di [NUMBERS]", + "qxsck/data-analysis@maximum": "massimo di [NUMBERS]", + "qxsck/data-analysis@median": "mediana di [NUMBERS]", + "qxsck/data-analysis@minimum": "minimo di [NUMBERS]", + "qxsck/data-analysis@mode": "moda di [NUMBERS]", + "qxsck/data-analysis@name": "Analisi dei Dati", + "qxsck/data-analysis@variance": "varianza di [NUMBERS]", + "qxsck/var-and-list@addValueInList": "aggiungi [VALUE] a [LIST]", + "qxsck/var-and-list@clearList": "cancella tutto da lista [LIST]", + "qxsck/var-and-list@copyList": "copia [LIST1] in [LIST2]", + "qxsck/var-and-list@deleteOfList": "cancella [INDEX] da [LIST]", + "qxsck/var-and-list@getIndexOfList": "prima occorrenza di [VALUE] in [LIST]", + "qxsck/var-and-list@getIndexesOfList": "occorrenze di [VALUE] in [LIST]", + "qxsck/var-and-list@getList": "valore di [LIST]", + "qxsck/var-and-list@getValueOfList": "elemento [INDEX] di [LIST]", + "qxsck/var-and-list@getVar": "valore di [VAR]", + "qxsck/var-and-list@length": "lunghezza di [LIST]", + "qxsck/var-and-list@listContains": "[LIST] contiene [VALUE]", + "qxsck/var-and-list@name": "Variabili e liste", + "qxsck/var-and-list@replaceOfList": "sostituisci elemento [INDEX] di [LIST] con [VALUE]", + "qxsck/var-and-list@seriListsToJson": "converti in json tutte le liste che iniziano con [START] ", + "qxsck/var-and-list@seriVarsToJson": "converti in json tutte le variabili che iniziano con [START]", + "qxsck/var-and-list@setVar": "porta il valore di [VAR] a [VALUE]" + }, + "zh-cn": { + "0832/rxFS2@clean": "清空文件系统", + "0832/rxFS2@del": "删除 [STR]", + "0832/rxFS2@folder": "设置 [STR] 为 [STR2]", + "0832/rxFS2@folder_default": "rxFS 好用!", + "0832/rxFS2@in": "从 [STR] 导入文件系统", + "0832/rxFS2@list": "列出 [STR] 下的所有文件", + "0832/rxFS2@open": "打开 [STR]", + "0832/rxFS2@out": "导出文件系统", + "0832/rxFS2@search": "搜索 [STR]", + "0832/rxFS2@start": "新建 [STR]", + "0832/rxFS2@sync": "将 [STR] 的位置改为 [STR2]", + "0832/rxFS2@webin": "从网络加载 [STR]", + "NOname-awa/graphics2d@graph": "图形 [graph] 的 [CS]", + "NOname-awa/graphics2d@line_section": "([x1],[y1])到([x2],[y2])的长度", + "NOname-awa/graphics2d@name": "图形 2D", + "NOname-awa/graphics2d@pi": "派", + "NOname-awa/graphics2d@quadrilateral": "矩形([x1],[y1])([x2],[y2])([x3],[y3])([x4],[y4])的 [CS]", + "NOname-awa/graphics2d@ray_direction": "([x1],[y1])的([x2],[y2])的距离", + "NOname-awa/graphics2d@round": "[rd] 为 [a] 的圆的 [CS]", + "NOname-awa/graphics2d@triangle": "三角形([x1],[y1])([x2],[y2])([x3],[y3])的 [CS]", + "NOname-awa/graphics2d@triangle_s": "三角形 [s1] [s2] [s3] 的面积", + "qxsck/data-analysis@name": "数据分析", + "qxsck/var-and-list@copyList": "复制 [LIST1] 到 [LIST2]", + "qxsck/var-and-list@name": "变量与列表" + } +} \ No newline at end of file From 47783cab023f4d5bc94179aa55c4426a278419b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:15:02 -0600 Subject: [PATCH 052/196] build(deps): bump @turbowarp/types from `cfaa5d3` to `39457f3` (#1148) --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb28fd4b84..d4c779ba4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -137,8 +137,8 @@ "integrity": "sha512-DUay/UeKoYht03tfcBfp8+m8RSrOtr7eMxFTXujdUXfoiRM7xnQNy6SutufeFmIOdVZU65w3vstLcV3K+6Mhyg==" }, "@turbowarp/types": { - "version": "git+https://github.com/TurboWarp/types-tw.git#cfaa5d3ce5dc96f16f7da9054325a4f1ed457662", - "from": "git+https://github.com/TurboWarp/types-tw.git#tw" + "version": "git+https://github.com/TurboWarp/types-tw.git#39457f33b1f09f6a256ea312d3727fc9cbbfa13b", + "from": "git+https://github.com/TurboWarp/types-tw.git#39457f33b1f09f6a256ea312d3727fc9cbbfa13b" }, "@ungap/structured-clone": { "version": "1.2.0", From 363da74987b996a35a4c04e7f3184d6c3448451f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:18:35 -0600 Subject: [PATCH 053/196] build(deps-dev): bump prettier from 3.0.3 to 3.1.0 (#1147) --- extensions/JeremyGamer13/tween.js | 28 ++++++++------- extensions/NOname-awa/cn-number.js | 22 ++++++------ extensions/TheShovel/LZ-String.js | 46 ++++++++++++------------- extensions/box2d.js | 16 ++++----- extensions/obviousAlexC/SensingPlus.js | 4 +-- extensions/obviousAlexC/newgroundsIO.js | 4 +-- extensions/true-fantom/network.js | 16 ++++----- extensions/true-fantom/regexp.js | 20 +++++------ extensions/turboloader/audiostream.js | 4 +-- package-lock.json | 6 ++-- package.json | 2 +- 11 files changed, 85 insertions(+), 83 deletions(-) diff --git a/extensions/JeremyGamer13/tween.js b/extensions/JeremyGamer13/tween.js index 41df879589..850dc6bb12 100644 --- a/extensions/JeremyGamer13/tween.js +++ b/extensions/JeremyGamer13/tween.js @@ -119,10 +119,10 @@ return x === 0 ? 0 : x === 1 - ? 1 - : x < 0.5 - ? Math.pow(2, 20 * x - 10) / 2 - : (2 - Math.pow(2, -20 * x + 10)) / 2; + ? 1 + : x < 0.5 + ? Math.pow(2, 20 * x - 10) / 2 + : (2 - Math.pow(2, -20 * x + 10)) / 2; } default: return 0; @@ -178,27 +178,29 @@ return x === 0 ? 0 : x === 1 - ? 1 - : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); + ? 1 + : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4); } case "out": { const c4 = (2 * Math.PI) / 3; return x === 0 ? 0 : x === 1 - ? 1 - : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; + ? 1 + : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1; } case "in out": { const c5 = (2 * Math.PI) / 4.5; return x === 0 ? 0 : x === 1 - ? 1 - : x < 0.5 - ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 - : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + - 1; + ? 1 + : x < 0.5 + ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / + 2 + : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / + 2 + + 1; } default: return 0; diff --git a/extensions/NOname-awa/cn-number.js b/extensions/NOname-awa/cn-number.js index a3db7e1208..10e72cbb02 100644 --- a/extensions/NOname-awa/cn-number.js +++ b/extensions/NOname-awa/cn-number.js @@ -90,15 +90,15 @@ String(Number2).slice(-j).charAt(0) == "2" && j >= 3 ? "两" : k == "0" - ? j == 1 - ? "" - : i.slice(-1) == "零" - ? "" - : String(Number2).slice(-1) == "0" && - (j != 3 || String(Number2).charAt(2) == "0") - ? " " - : "零" - : C_Number[k - 1] + ? j == 1 + ? "" + : i.slice(-1) == "零" + ? "" + : String(Number2).slice(-1) == "0" && + (j != 3 || String(Number2).charAt(2) == "0") + ? " " + : "零" + : C_Number[k - 1] ); i = String(i) + @@ -106,8 +106,8 @@ i.slice(-1) == "零" || i.slice(-1) == " " ? "" : unit[j - 1] == "个" - ? "" - : unit[j - 1] + ? "" + : unit[j - 1] ); } else { if (j != 1) { diff --git a/extensions/TheShovel/LZ-String.js b/extensions/TheShovel/LZ-String.js index 635ed972ef..26c6612b3e 100644 --- a/extensions/TheShovel/LZ-String.js +++ b/extensions/TheShovel/LZ-String.js @@ -63,10 +63,10 @@ return null == r ? "" : "" == r - ? null - : i._decompress(r.length, 32, function (n) { - return t(o, r.charAt(n)); - }); + ? null + : i._decompress(r.length, 32, function (n) { + return t(o, r.charAt(n)); + }); }, compressToUTF16: function (o) { return null == o @@ -79,10 +79,10 @@ return null == r ? "" : "" == r - ? null - : i._decompress(r.length, 16384, function (o) { - return r.charCodeAt(o) - 32; - }); + ? null + : i._decompress(r.length, 16384, function (o) { + return r.charCodeAt(o) - 32; + }); }, compressToUint8Array: function (r) { for ( @@ -121,11 +121,11 @@ return null == r ? "" : "" == r - ? null - : ((r = r.replace(/ /g, "+")), - i._decompress(r.length, 32, function (o) { - return t(n, r.charAt(o)); - })); + ? null + : ((r = r.replace(/ /g, "+")), + i._decompress(r.length, 32, function (o) { + return t(n, r.charAt(o)); + })); }, compress: function (o) { return i._compress(o, 16, function (o) { @@ -231,10 +231,10 @@ return null == r ? "" : "" == r - ? null - : i._decompress(r.length, 32768, function (o) { - return r.charCodeAt(o); - }); + ? null + : i._decompress(r.length, 32768, function (o) { + return r.charCodeAt(o); + }); }, _decompress: function (o, n, e) { var t, @@ -336,12 +336,12 @@ // @ts-ignore }) : "undefined" != typeof module && null != module - ? (module.exports = LZString) - : "undefined" != typeof angular && - null != angular && - angular.module("LZString", []).factory("LZString", function () { - return LZString; - }); + ? (module.exports = LZString) + : "undefined" != typeof angular && + null != angular && + angular.module("LZString", []).factory("LZString", function () { + return LZString; + }); /* eslint-enable */ class lzcompress { diff --git a/extensions/box2d.js b/extensions/box2d.js index 6f0e498058..b6dbe689fd 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -2124,13 +2124,13 @@ dX > 0 ? (aabb.upperBound.x - this.p1.x) / dX : dX < 0 - ? (aabb.lowerBound.x - this.p1.x) / dX - : Number.POSITIVE_INFINITY, + ? (aabb.lowerBound.x - this.p1.x) / dX + : Number.POSITIVE_INFINITY, dY > 0 ? (aabb.upperBound.y - this.p1.y) / dY : dY < 0 - ? (aabb.lowerBound.y - this.p1.y) / dY - : Number.POSITIVE_INFINITY + ? (aabb.lowerBound.y - this.p1.y) / dY + : Number.POSITIVE_INFINITY ); this.p2.x = this.p1.x + dX * lambda; this.p2.y = this.p1.y + dY * lambda; @@ -2142,13 +2142,13 @@ dX > 0 ? (aabb.upperBound.x - this.p2.x) / dX : dX < 0 - ? (aabb.lowerBound.x - this.p2.x) / dX - : Number.POSITIVE_INFINITY, + ? (aabb.lowerBound.x - this.p2.x) / dX + : Number.POSITIVE_INFINITY, dY > 0 ? (aabb.upperBound.y - this.p2.y) / dY : dY < 0 - ? (aabb.lowerBound.y - this.p2.y) / dY - : Number.POSITIVE_INFINITY + ? (aabb.lowerBound.y - this.p2.y) / dY + : Number.POSITIVE_INFINITY ); this.p1.x = this.p2.x + dX * lambda; this.p1.y = this.p2.y + dY * lambda; diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index 0cf92b63eb..d84eea22c5 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -15,8 +15,8 @@ typeof webkitSpeechRecognition !== "undefined" ? window.webkitSpeechRecognition : typeof window.SpeechRecognition !== "undefined" - ? window.SpeechRecognition - : null; + ? window.SpeechRecognition + : null; let recognizedSpeech = ""; let recording = false; diff --git a/extensions/obviousAlexC/newgroundsIO.js b/extensions/obviousAlexC/newgroundsIO.js index 8696825fb0..f06999da35 100644 --- a/extensions/obviousAlexC/newgroundsIO.js +++ b/extensions/obviousAlexC/newgroundsIO.js @@ -843,8 +843,8 @@ typeof options["social"] === "undefined" ? false : options["social"] - ? true - : false, + ? true + : false, skip: typeof options["skip"] !== "number" ? 0 : options["skip"], limit: typeof options["limit"] !== "number" ? 10 : options["limit"], }; diff --git a/extensions/true-fantom/network.js b/extensions/true-fantom/network.js index a5303fa7d5..4c80e0a0a2 100644 --- a/extensions/true-fantom/network.js +++ b/extensions/true-fantom/network.js @@ -552,8 +552,8 @@ Number(WIDTH) < 100 ? 100 : Number(WIDTH) > window.screen.width - ? window.screen.width - : Number(WIDTH) + ? window.screen.width + : Number(WIDTH) }`; params += isNaN(HEIGHT) ? "" @@ -561,8 +561,8 @@ Number(HEIGHT) < 100 ? 100 : Number(HEIGHT) > window.screen.height - ? window.screen.height - : Number(HEIGHT) + ? window.screen.height + : Number(HEIGHT) }`; params += isNaN(LEFT) ? "" @@ -570,8 +570,8 @@ Number(LEFT) < 0 ? 0 : Number(LEFT) > window.screen.width - ? window.screen.width - : Number(LEFT) + ? window.screen.width + : Number(LEFT) }`; params += isNaN(TOP) ? "" @@ -579,8 +579,8 @@ Number(TOP) < 0 ? 0 : Number(TOP) > window.screen.height - ? window.screen.height - : Number(TOP) + ? window.screen.height + : Number(TOP) }`; Scratch.openWindow(String(USER_URL), params); } diff --git a/extensions/true-fantom/regexp.js b/extensions/true-fantom/regexp.js index 45e5c4ff87..6d7df47cb2 100644 --- a/extensions/true-fantom/regexp.js +++ b/extensions/true-fantom/regexp.js @@ -49,14 +49,14 @@ return isObject(val) ? val : isArray(val) - ? val.reduce( - (array, currentValue, currentIndex) => ({ - ...array, - [currentIndex + 1]: currentValue, - }), - {} - ) - : { 1: val }; + ? val.reduce( + (array, currentValue, currentIndex) => ({ + ...array, + [currentIndex + 1]: currentValue, + }), + {} + ) + : { 1: val }; }; const dataValues = (val) => { @@ -551,8 +551,8 @@ redat.global ? data : Object.keys(data)[0] - ? { [Object.keys(data)[0]]: Object.values(data)[0] } - : {} + ? { [Object.keys(data)[0]]: Object.values(data)[0] } + : {} ); case "map": data = Array.from(str.matchAll(gredat)).map((val) => [ diff --git a/extensions/turboloader/audiostream.js b/extensions/turboloader/audiostream.js index 5fe176e307..c75f051eea 100644 --- a/extensions/turboloader/audiostream.js +++ b/extensions/turboloader/audiostream.js @@ -540,8 +540,8 @@ ? 0.1 : Math.abs(VAL) / 700 : VAL > 700 - ? 15 - : VAL / 50 + 1; + ? 15 + : VAL / 50 + 1; } am_setvolume({ VAL }, util) { diff --git a/package-lock.json b/package-lock.json index d4c779ba4d..a48398b8bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1176,9 +1176,9 @@ "dev": true }, "prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true }, "proxy-addr": { diff --git a/package.json b/package.json index 365e4787ab..d62d006187 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint": "^8.53.0", "espree": "^9.6.1", "esquery": "^1.5.0", - "prettier": "^3.0.3" + "prettier": "^3.1.0" }, "private": true } From 4fe747509bf24ce278dbe9be8918e94fd4d08959 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 15 Nov 2023 21:01:23 -0600 Subject: [PATCH 054/196] L10N - Part 2 of many (#1149) --- development/builder.js | 12 +- development/parse-extension-translations.js | 4 +- extensions/-SIPC-/consoles.js | 2 +- extensions/.eslintrc.js | 12 ++ extensions/battery.js | 18 +-- extensions/box2d.js | 74 +++++----- extensions/clipboard.js | 14 +- extensions/clouddata-ping.js | 4 +- extensions/cursor.js | 41 ++++-- extensions/encoding.js | 28 ++-- extensions/fetch.js | 2 +- extensions/files.js | 45 ++++--- extensions/gamepad.js | 66 ++++----- extensions/iframe.js | 58 +++++--- extensions/lab/text.js | 142 ++++++++++++++------ extensions/local-storage.js | 26 ++-- extensions/navigator.js | 26 ++-- extensions/pointerlock.js | 10 +- extensions/runtime-options.js | 51 ++++--- extensions/sound.js | 6 +- extensions/stretch.js | 18 +-- 21 files changed, 403 insertions(+), 256 deletions(-) diff --git a/development/builder.js b/development/builder.js index 73dbca1bd0..d96572e154 100644 --- a/development/builder.js +++ b/development/builder.js @@ -648,8 +648,16 @@ class Build { generateL10N() { const allStrings = {}; - for (const file of Object.values(this.files)) { - const fileStrings = file.getStrings(); + for (const [filePath, file] of Object.entries(this.files)) { + let fileStrings; + try { + fileStrings = file.getStrings(); + } catch (error) { + console.error(error); + throw new Error( + `Error getting translations from ${filePath}: ${error}, see above` + ); + } if (!fileStrings) { continue; } diff --git a/development/parse-extension-translations.js b/development/parse-extension-translations.js index 624a7ca7c4..55e2570ff7 100644 --- a/development/parse-extension-translations.js +++ b/development/parse-extension-translations.js @@ -68,8 +68,8 @@ const parseTranslations = (js) => { const result = {}; for (const match of matches) { const args = match.arguments; - if (args.length !== 1) { - throw new Error(`Scratch.translate() must have exactly 1 argument`); + if (args.length === 0) { + throw new Error(`Scratch.translate() needs at least 1 argument`); } const evaluated = evaluateAST(args[0]); diff --git a/extensions/-SIPC-/consoles.js b/extensions/-SIPC-/consoles.js index 3454a518e1..9fca8f1d85 100644 --- a/extensions/-SIPC-/consoles.js +++ b/extensions/-SIPC-/consoles.js @@ -1,6 +1,6 @@ // Name: Consoles // ID: sipcconsole -// Description: Blocks that interact the JavaScript console built in to your browser's developer tools. +// Description: Blocks that interact with the JavaScript console built in to your browser's developer tools. // By: -SIPC- (function (Scratch) { diff --git a/extensions/.eslintrc.js b/extensions/.eslintrc.js index 969f9a4ef4..115a0a81cd 100644 --- a/extensions/.eslintrc.js +++ b/extensions/.eslintrc.js @@ -88,6 +88,18 @@ module.exports = { { selector: 'CallExpression[callee.object.object.name=Scratch][callee.object.property.name=translate][callee.property.name=setup]', message: 'Do not call Scratch.translate.setup() yourself. Just use Scratch.translate() and let the build script handle it.' + }, + { + selector: 'MethodDefinition[key.name=getInfo] Property[key.name=id][value.callee.property.name=translate]', + message: 'Do not translate extension ID' + }, + { + selector: 'MethodDefinition[key.name=docsURI] Property[key.name=id][value.callee.property.name=translate]', + message: 'Do not translate docsURI' + }, + { + selector: 'MethodDefinition[key.name=getInfo] Property[key.name=opcode][value.callee.property.name=translate]', + message: 'Do not translate block opcode' } ] } diff --git a/extensions/battery.js b/extensions/battery.js index fcb432b803..bda7f9ede6 100644 --- a/extensions/battery.js +++ b/extensions/battery.js @@ -61,51 +61,51 @@ class BatteryExtension { getInfo() { return { - name: "Battery", + name: Scratch.translate("Battery"), id: "battery", blocks: [ { opcode: "charging", blockType: Scratch.BlockType.BOOLEAN, - text: "charging?", + text: Scratch.translate("charging?"), }, { opcode: "level", blockType: Scratch.BlockType.REPORTER, - text: "battery level", + text: Scratch.translate("battery level"), }, { opcode: "chargeTime", blockType: Scratch.BlockType.REPORTER, - text: "seconds until charged", + text: Scratch.translate("seconds until charged"), }, { opcode: "dischargeTime", blockType: Scratch.BlockType.REPORTER, - text: "seconds until empty", + text: Scratch.translate("seconds until empty"), }, { opcode: "chargingChanged", blockType: Scratch.BlockType.EVENT, - text: "when charging changed", + text: Scratch.translate("when charging changed"), isEdgeActivated: false, }, { opcode: "levelChanged", blockType: Scratch.BlockType.EVENT, - text: "when battery level changed", + text: Scratch.translate("when battery level changed"), isEdgeActivated: false, }, { opcode: "chargeTimeChanged", blockType: Scratch.BlockType.EVENT, - text: "when time until charged changed", + text: Scratch.translate("when time until charged changed"), isEdgeActivated: false, }, { opcode: "dischargeTimeChanged", blockType: Scratch.BlockType.EVENT, - text: "when time until empty changed", + text: Scratch.translate("when time until empty changed"), isEdgeActivated: false, }, ], diff --git a/extensions/box2d.js b/extensions/box2d.js index b6dbe689fd..ad14eadc58 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12119,8 +12119,6 @@ // const Cast = require('../../util/cast'); // const Runtime = require('../../engine/runtime'); // const RenderedTarget = require('../../sprites/rendered-target'); - // const formatMessage = require('format-message'); - const formatMessage = (obj) => obj.default; // const MathUtil = require('../../util/math-util'); // const Timer = require('../../util/timer'); // const Matter = require('matterJs/matter'); @@ -12506,7 +12504,7 @@ getInfo() { return { id: "griffpatch", - name: formatMessage({ + name: Scratch.translate({ id: "griffpatch.categoryName", default: "Physics", description: "Label for the Griffpatch extension category", @@ -12520,7 +12518,7 @@ { opcode: "setStage", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setStage", default: "set stage boundaries to [stageType]", description: "Set the stage type", @@ -12536,7 +12534,7 @@ { opcode: "setGravity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setGravity", default: "set gravity to x: [gx] y: [gy]", description: "Set the gravity", @@ -12555,7 +12553,7 @@ { opcode: "getGravityX", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getGravityX", default: "gravity x", description: "Get the gravity's x value", @@ -12564,7 +12562,7 @@ { opcode: "getGravityY", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getGravityY", default: "gravity y", description: "Get the gravity's y value", @@ -12576,7 +12574,7 @@ { opcode: "setPhysics", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setPhysics", default: "enable for [shape] mode [mode]", description: "Enable Physics for this Sprite", @@ -12598,7 +12596,7 @@ // { // opcode: 'setPhysics', // blockType: BlockType.COMMAND, - // text: formatMessage({ + // text: Scratch.translate({ // id: 'griffpatch.setPhysics', // default: 'enable physics for sprite [shape]', // description: 'Enable Physics for this Sprite' @@ -12614,7 +12612,7 @@ // { // opcode: 'setPhysicsAll', // blockType: BlockType.COMMAND, - // text: formatMessage({ + // text: Scratch.translate({ // id: 'griffpatch.setPhysicsAll', // default: 'enable physics for all sprites', // description: 'Enable Physics For All Sprites' @@ -12626,7 +12624,7 @@ { opcode: "doTick", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.doTick", default: "step simulation", description: "Run a single tick of the physics simulation", @@ -12638,7 +12636,7 @@ { opcode: "setPosition", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setPosition", default: "go to x: [x] y: [y] [space]", description: "Position Sprite", @@ -12669,7 +12667,7 @@ { opcode: "setVelocity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setVelocity", default: "set velocity to sx: [sx] sy: [sy]", description: "Set Velocity", @@ -12689,7 +12687,7 @@ { opcode: "changeVelocity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.changeVelocity", default: "change velocity by sx: [sx] sy: [sy]", description: "Change Velocity", @@ -12708,7 +12706,7 @@ }, { opcode: "getVelocityX", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getVelocityX", default: "x velocity", description: "get the x velocity", @@ -12718,7 +12716,7 @@ }, { opcode: "getVelocityY", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getVelocityY", default: "y velocity", description: "get the y velocity", @@ -12732,7 +12730,7 @@ { opcode: "applyForce", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.applyForce", default: "push with force [force] in direction [dir]", description: "Push this object in a given direction", @@ -12752,7 +12750,7 @@ { opcode: "applyAngForce", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.applyAngForce", default: "spin with force [force]", description: "Push this object in a given direction", @@ -12771,7 +12769,7 @@ { opcode: "setAngVelocity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setAngVelocity", default: "set angular velocity to [force]", description: "Set the angular velocity of the sprite", @@ -12787,7 +12785,7 @@ { opcode: "getAngVelocity", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getAngVelocity", default: "angular velocity", description: "Get the angular velocity of the sprite", @@ -12800,7 +12798,7 @@ { opcode: "setStatic", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setStatic", default: "set fixed to [static]", description: "Sets whether this block is static or dynamic", @@ -12816,7 +12814,7 @@ }, { opcode: "getStatic", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getStatic", default: "fixed?", description: "get whether this sprite is fixed", @@ -12831,7 +12829,7 @@ { opcode: "setDensity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setDensity", default: "set density to [density]", description: "Set the density of the object", @@ -12849,7 +12847,7 @@ opcode: "setDensityValue", func: "setDensity", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setDensityValue", default: "set density to [density]", description: "Set the density of the object", @@ -12865,7 +12863,7 @@ { opcode: "getDensity", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getDensity", default: "density", description: "Get the density of the object", @@ -12878,7 +12876,7 @@ { opcode: "setFriction", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setFriction", default: "set friction to [friction]", description: "Set the friction of the object", @@ -12896,7 +12894,7 @@ opcode: "setFrictionValue", func: "setFriction", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setFrictionValue", default: "set friction to [friction]", description: "Set the friction value of the object", @@ -12912,7 +12910,7 @@ { opcode: "getFriction", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getFriction", default: "friction", description: "Get the friction of the object", @@ -12925,7 +12923,7 @@ { opcode: "setRestitution", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setRestitution", default: "set bounce to [restitution]", description: "Set the bounce of the object", @@ -12943,7 +12941,7 @@ opcode: "setRestitutionValue", func: "setRestitution", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setRestitutionValue", default: "set bounce to [restitution]", description: "Set the bounce value of the object", @@ -12959,7 +12957,7 @@ { opcode: "getRestitution", blockType: BlockType.REPORTER, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getRestitution", default: "bounce", description: "Get the bounce value of the object", @@ -12972,7 +12970,7 @@ { opcode: "setProperties", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setProperties", default: "set density [density] roughness [friction] bounce [restitution]", @@ -13001,7 +12999,7 @@ // { // opcode: 'pinSprite', // blockType: BlockType.COMMAND, - // text: formatMessage({ + // text: Scratch.translate({ // id: 'griffpatch.pinSprite', // default: 'pin to world at sprite\'s x: [x] y: [y]', // description: 'Pin the sprite' @@ -13022,7 +13020,7 @@ { opcode: "getTouching", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getTouching", default: "list sprites touching [where]", description: "get the name of any sprites we are touching", @@ -13045,7 +13043,7 @@ { opcode: "setScroll", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.setScroll", default: "set scroll to x: [ox] y: [oy]", description: "Sets whether this block is static or dynamic", @@ -13064,7 +13062,7 @@ { opcode: "changeScroll", blockType: BlockType.COMMAND, - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.changeScroll", default: "change scroll by x: [ox] y: [oy]", description: "Sets whether this block is static or dynamic", @@ -13082,7 +13080,7 @@ }, { opcode: "getScrollX", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getScrollX", default: "x scroll", description: "get the x scroll", @@ -13091,7 +13089,7 @@ }, { opcode: "getScrollY", - text: formatMessage({ + text: Scratch.translate({ id: "griffpatch.getScrollY", default: "y scroll", description: "get the y scroll", diff --git a/extensions/clipboard.js b/extensions/clipboard.js index 07f1752857..ddde09bcf2 100644 --- a/extensions/clipboard.js +++ b/extensions/clipboard.js @@ -44,7 +44,7 @@ getInfo() { return { id: "clipboard", - name: "Clipboard", + name: Scratch.translate("Clipboard"), blockIconURI: extensionicon, color1: "#008080", color2: "#006666", @@ -52,20 +52,20 @@ { opcode: "whenCopied", blockType: Scratch.BlockType.EVENT, - text: "when something is copied", + text: Scratch.translate("when something is copied"), isEdgeActivated: false, }, { opcode: "whenPasted", blockType: Scratch.BlockType.EVENT, - text: "when something is pasted", + text: Scratch.translate("when something is pasted"), isEdgeActivated: false, }, "---", { opcode: "setClipboard", blockType: Scratch.BlockType.COMMAND, - text: "copy to clipboard: [TEXT]", + text: Scratch.translate("copy to clipboard: [TEXT]"), arguments: { TEXT: { type: Scratch.ArgumentType.STRING, @@ -75,19 +75,19 @@ { opcode: "resetClipboard", blockType: Scratch.BlockType.COMMAND, - text: "reset clipboard", + text: Scratch.translate("reset clipboard"), }, "---", { opcode: "clipboard", blockType: Scratch.BlockType.REPORTER, - text: "clipboard", + text: Scratch.translate("clipboard"), disableMonitor: true, }, { opcode: "getLastPastedText", blockType: Scratch.BlockType.REPORTER, - text: "last pasted text", + text: Scratch.translate("last pasted text"), disableMonitor: true, }, ], diff --git a/extensions/clouddata-ping.js b/extensions/clouddata-ping.js index 93a130de1e..f7d60db590 100644 --- a/extensions/clouddata-ping.js +++ b/extensions/clouddata-ping.js @@ -97,12 +97,12 @@ getInfo() { return { id: "clouddataping", - name: "Ping Cloud Data", + name: Scratch.translate("Ping Cloud Data"), blocks: [ { opcode: "ping", blockType: Scratch.BlockType.BOOLEAN, - text: "is cloud data server [SERVER] up?", + text: Scratch.translate("is cloud data server [SERVER] up?"), arguments: { SERVER: { type: Scratch.ArgumentType.STRING, diff --git a/extensions/cursor.js b/extensions/cursor.js index ae478f76aa..a8a25f6fb6 100644 --- a/extensions/cursor.js +++ b/extensions/cursor.js @@ -140,6 +140,19 @@ return [+a || 0, +b || 0]; }; + /** + * @param {string} size eg. "48x84" + * @returns {string} + */ + const formatUnreliableSize = (size) => + Scratch.translate( + { + default: "{size} (unreliable)", + description: "[size] is replaced with a size in pixels such as '48x48'", + }, + { size } + ); + const cursors = [ "default", "pointer", @@ -188,12 +201,12 @@ getInfo() { return { id: "MouseCursor", - name: "Mouse Cursor", + name: Scratch.translate("Mouse Cursor"), blocks: [ { opcode: "setCur", blockType: Scratch.BlockType.COMMAND, - text: "set cursor to [cur]", + text: Scratch.translate("set cursor to [cur]"), arguments: { cur: { type: Scratch.ArgumentType.STRING, @@ -205,7 +218,9 @@ { opcode: "setCursorImage", blockType: Scratch.BlockType.COMMAND, - text: "set cursor to current costume center: [position] max size: [size]", + text: Scratch.translate( + "set cursor to current costume center: [position] max size: [size]" + ), arguments: { position: { type: Scratch.ArgumentType.STRING, @@ -222,12 +237,12 @@ { opcode: "hideCur", blockType: Scratch.BlockType.COMMAND, - text: "hide cursor", + text: Scratch.translate("hide cursor"), }, { opcode: "getCur", blockType: Scratch.BlockType.REPORTER, - text: "cursor", + text: Scratch.translate("cursor"), }, ], menus: { @@ -239,11 +254,11 @@ acceptReporters: true, items: [ // [x, y] where x is [0=left, 100=right] and y is [0=top, 100=bottom] - { text: "top left", value: "0,0" }, - { text: "top right", value: "100,0" }, - { text: "bottom left", value: "0,100" }, - { text: "bottom right", value: "100,100" }, - { text: "center", value: "50,50" }, + { text: Scratch.translate("top left"), value: "0,0" }, + { text: Scratch.translate("top right"), value: "100,0" }, + { text: Scratch.translate("bottom left"), value: "0,100" }, + { text: Scratch.translate("bottom right"), value: "100,100" }, + { text: Scratch.translate("center"), value: "50,50" }, ], }, imageSizes: { @@ -257,9 +272,9 @@ { text: "12x12", value: "12x12" }, { text: "16x16", value: "16x16" }, { text: "32x32", value: "32x32" }, - { text: "48x48 (unreliable)", value: "48x48" }, - { text: "64x64 (unreliable)", value: "64x64" }, - { text: "128x128 (unreliable)", value: "128x128" }, + { text: formatUnreliableSize("48x48"), value: "48x48" }, + { text: formatUnreliableSize("64x64"), value: "64x64" }, + { text: formatUnreliableSize("128x128"), value: "128x128" }, ], }, }, diff --git a/extensions/encoding.js b/extensions/encoding.js index f506508052..d561267088 100644 --- a/extensions/encoding.js +++ b/extensions/encoding.js @@ -414,7 +414,7 @@ getInfo() { return { id: "Encoding", - name: "Encoding", + name: Scratch.translate("Encoding"), color1: "#6495ed", color2: "#739fee", color3: "#83aaf0", @@ -424,7 +424,7 @@ { opcode: "encode", blockType: Scratch.BlockType.REPORTER, - text: "Encode [string] in [code]", + text: Scratch.translate("Encode [string] in [code]"), arguments: { string: { type: Scratch.ArgumentType.STRING, @@ -440,11 +440,11 @@ { opcode: "decode", blockType: Scratch.BlockType.REPORTER, - text: "Decode [string] with [code]", + text: Scratch.translate("Decode [string] with [code]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "VHVyYm9XYXJw", + defaultValue: btoa(Scratch.translate("apple")), }, code: { type: Scratch.ArgumentType.STRING, @@ -456,11 +456,11 @@ { opcode: "hash", blockType: Scratch.BlockType.REPORTER, - text: "Hash [string] with [hash]", + text: Scratch.translate("Hash [string] with [hash]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "apple", + defaultValue: Scratch.translate("apple"), }, hash: { type: Scratch.ArgumentType.STRING, @@ -475,7 +475,9 @@ { opcode: "Conversioncodes", blockType: Scratch.BlockType.REPORTER, - text: "Convert the character [string] to [CodeList]", + text: Scratch.translate( + "Convert the character [string] to [CodeList]" + ), arguments: { string: { type: Scratch.ArgumentType.STRING, @@ -491,7 +493,9 @@ { opcode: "Restorecode", blockType: Scratch.BlockType.REPORTER, - text: "[string] corresponding to the [CodeList] character", + text: Scratch.translate( + "[string] corresponding to the [CodeList] character" + ), arguments: { string: { type: Scratch.ArgumentType.STRING, @@ -510,7 +514,9 @@ { opcode: "Randomstrings", blockType: Scratch.BlockType.REPORTER, - text: "Randomly generated [position] character string", + text: Scratch.translate( + "Randomly generated [position] character string" + ), arguments: { position: { type: Scratch.ArgumentType.NUMBER, @@ -521,7 +527,9 @@ { opcode: "Fontgenerationstring", blockType: Scratch.BlockType.REPORTER, - text: "Use [wordbank] to generate a random [position] character string", + text: Scratch.translate( + "Use [wordbank] to generate a random [position] character string" + ), arguments: { wordbank: { type: Scratch.ArgumentType.STRING, diff --git a/extensions/fetch.js b/extensions/fetch.js index 10ff8e13e4..92e76abc7b 100644 --- a/extensions/fetch.js +++ b/extensions/fetch.js @@ -9,7 +9,7 @@ getInfo() { return { id: "fetch", - name: "Fetch", + name: Scratch.translate("Fetch"), blocks: [ { opcode: "get", diff --git a/extensions/files.js b/extensions/files.js index 686906f150..be3b6fd0b5 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -164,14 +164,23 @@ }); const title = document.createElement("div"); - title.textContent = "Select or drop file"; + title.textContent = Scratch.translate("Select or drop file"); title.style.fontSize = "1.5em"; title.style.marginBottom = "8px"; modal.appendChild(title); const subtitle = document.createElement("div"); - const formattedAccept = accept || "any"; - subtitle.textContent = `Accepted formats: ${formattedAccept}`; + const formattedAccept = accept || Scratch.translate("any"); + subtitle.textContent = Scratch.translate( + { + default: "Accepted formats: {formats}", + description: + "[formats] is replaced with a comma-separated list of file types eg: .txt, .mp3, .png or the word any", + }, + { + formats: formattedAccept, + } + ); modal.appendChild(subtitle); // To avoid the script getting stalled forever, if cancel isn't supported, we'll just forcibly @@ -260,7 +269,7 @@ getInfo() { return { id: "files", - name: "Files", + name: Scratch.translate("Files"), color1: "#fcb103", color2: "#db9a37", color3: "#db8937", @@ -268,14 +277,14 @@ { opcode: "showPicker", blockType: Scratch.BlockType.REPORTER, - text: "open a file", + text: Scratch.translate("open a file"), disableMonitor: true, hideFromPalette: true, }, { opcode: "showPickerExtensions", blockType: Scratch.BlockType.REPORTER, - text: "open a [extension] file", + text: Scratch.translate("open a [extension] file"), arguments: { extension: { type: Scratch.ArgumentType.STRING, @@ -288,7 +297,7 @@ { opcode: "showPickerAs", blockType: Scratch.BlockType.REPORTER, - text: "open a file as [as]", + text: Scratch.translate("open a file as [as]"), arguments: { as: { type: Scratch.ArgumentType.STRING, @@ -299,7 +308,7 @@ { opcode: "showPickerExtensionsAs", blockType: Scratch.BlockType.REPORTER, - text: "open a [extension] file as [as]", + text: Scratch.translate("open a [extension] file as [as]"), arguments: { extension: { type: Scratch.ArgumentType.STRING, @@ -317,22 +326,22 @@ { opcode: "download", blockType: Scratch.BlockType.COMMAND, - text: "download [text] as [file]", + text: Scratch.translate("download [text] as [file]"), arguments: { text: { type: Scratch.ArgumentType.STRING, - defaultValue: "Hello, world!", + defaultValue: Scratch.translate("Hello, world!"), }, file: { type: Scratch.ArgumentType.STRING, - defaultValue: "save.txt", + defaultValue: Scratch.translate("save.txt"), }, }, }, { opcode: "downloadURL", blockType: Scratch.BlockType.COMMAND, - text: "download URL [url] as [file]", + text: Scratch.translate("download URL [url] as [file]"), arguments: { url: { type: Scratch.ArgumentType.STRING, @@ -340,7 +349,7 @@ }, file: { type: Scratch.ArgumentType.STRING, - defaultValue: "save.txt", + defaultValue: Scratch.translate("save.txt"), }, }, }, @@ -350,7 +359,7 @@ { opcode: "setOpenMode", blockType: Scratch.BlockType.COMMAND, - text: "set open file selector mode to [mode]", + text: Scratch.translate("set open file selector mode to [mode]"), arguments: { mode: { type: Scratch.ArgumentType.STRING, @@ -365,7 +374,7 @@ acceptReporters: true, items: [ { - text: "text", + text: Scratch.translate("text"), value: AS_TEXT, }, { @@ -378,16 +387,16 @@ acceptReporters: true, items: [ { - text: "show modal", + text: Scratch.translate("show modal"), value: MODE_MODAL, }, { - text: "open selector immediately", + text: Scratch.translate("open selector immediately"), value: MODE_IMMEDIATELY_SHOW_SELECTOR, }, { // Will not work if the browser doesn't think we are responding to a click event. - text: "only show selector (unreliable)", + text: Scratch.translate("only show selector (unreliable)"), value: MODE_ONLY_SELECTOR, }, ], diff --git a/extensions/gamepad.js b/extensions/gamepad.js index ada40f8604..3b2e7a8ba0 100644 --- a/extensions/gamepad.js +++ b/extensions/gamepad.js @@ -79,12 +79,12 @@ getInfo() { return { id: "Gamepad", - name: "Gamepad", + name: Scratch.translate("Gamepad"), blocks: [ { opcode: "gamepadConnected", blockType: Scratch.BlockType.BOOLEAN, - text: "gamepad [pad] connected?", + text: Scratch.translate("gamepad [pad] connected?"), arguments: { pad: { type: Scratch.ArgumentType.NUMBER, @@ -96,7 +96,7 @@ { opcode: "buttonDown", blockType: Scratch.BlockType.BOOLEAN, - text: "button [b] on pad [i] pressed?", + text: Scratch.translate("button [b] on pad [i] pressed?"), arguments: { b: { type: Scratch.ArgumentType.NUMBER, @@ -113,7 +113,7 @@ { opcode: "buttonValue", blockType: Scratch.BlockType.REPORTER, - text: "value of button [b] on pad [i]", + text: Scratch.translate("value of button [b] on pad [i]"), arguments: { b: { type: Scratch.ArgumentType.NUMBER, @@ -130,7 +130,7 @@ { opcode: "axisValue", blockType: Scratch.BlockType.REPORTER, - text: "value of axis [b] on pad [i]", + text: Scratch.translate("value of axis [b] on pad [i]"), arguments: { b: { type: Scratch.ArgumentType.NUMBER, @@ -150,7 +150,7 @@ { opcode: "axisDirection", blockType: Scratch.BlockType.REPORTER, - text: "direction of axes [axis] on pad [pad]", + text: Scratch.translate("direction of axes [axis] on pad [pad]"), arguments: { axis: { type: Scratch.ArgumentType.NUMBER, @@ -167,7 +167,7 @@ { opcode: "axisMagnitude", blockType: Scratch.BlockType.REPORTER, - text: "magnitude of axes [axis] on pad [pad]", + text: Scratch.translate("magnitude of axes [axis] on pad [pad]"), arguments: { axis: { type: Scratch.ArgumentType.NUMBER, @@ -186,7 +186,7 @@ { opcode: 'buttonPressedReleased', blockType: Scratch.BlockType.EVENT, - text: 'button [b] [pr] of pad [i]', + text: Scratch.translate('button [b] [pr] of pad [i]'), arguments: { b: { type: Scratch.ArgumentType.NUMBER, @@ -208,7 +208,7 @@ { opcode: 'axisMoved', blockType: Scratch.BlockType.EVENT, - text: 'axis [b] of pad [i] moved', + text: Scratch.translate('axis [b] of pad [i] moved'), arguments: { b: { type: Scratch.ArgumentType.NUMBER, @@ -228,7 +228,9 @@ { opcode: "rumble", blockType: Scratch.BlockType.COMMAND, - text: "rumble strong [s] and weak [w] for [t] sec. on pad [i]", + text: Scratch.translate( + "rumble strong [s] and weak [w] for [t] sec. on pad [i]" + ), arguments: { s: { type: Scratch.ArgumentType.NUMBER, @@ -255,7 +257,7 @@ acceptReporters: true, items: [ { - text: "any", + text: Scratch.translate("any"), value: "any", }, { @@ -281,7 +283,7 @@ items: [ // Based on an Xbox controller { - text: "any", + text: Scratch.translate("any"), value: "any", }, { @@ -301,51 +303,51 @@ value: "4", }, { - text: "Left bumper (5)", + text: Scratch.translate("Left bumper (5)"), value: "5", }, { - text: "Right bumper (6)", + text: Scratch.translate("Right bumper (6)"), value: "6", }, { - text: "Left trigger (7)", + text: Scratch.translate("Left trigger (7)"), value: "7", }, { - text: "Right trigger (8)", + text: Scratch.translate("Right trigger (8)"), value: "8", }, { - text: "Select/View (9)", + text: Scratch.translate("Select/View (9)"), value: "9", }, { - text: "Start/Menu (10)", + text: Scratch.translate("Start/Menu (10)"), value: "10", }, { - text: "Left stick (11)", + text: Scratch.translate("Left stick (11)"), value: "11", }, { - text: "Right stick (12)", + text: Scratch.translate("Right stick (12)"), value: "12", }, { - text: "D-pad up (13)", + text: Scratch.translate("D-pad up (13)"), value: "13", }, { - text: "D-pad down (14)", + text: Scratch.translate("D-pad down (14)"), value: "14", }, { - text: "D-pad left (15)", + text: Scratch.translate("D-pad left (15)"), value: "15", }, { - text: "D-pad right (16)", + text: Scratch.translate("D-pad right (16)"), value: "16", }, ], @@ -355,19 +357,19 @@ items: [ // Based on an Xbox controller { - text: "Left stick horizontal (1)", + text: Scratch.translate("Left stick horizontal (1)"), value: "1", }, { - text: "Left stick vertical (2)", + text: Scratch.translate("Left stick vertical (2)"), value: "2", }, { - text: "Right stick horizontal (3)", + text: Scratch.translate("Right stick horizontal (3)"), value: "3", }, { - text: "Right stick vertical (4)", + text: Scratch.translate("Right stick vertical (4)"), value: "4", }, ], @@ -377,11 +379,11 @@ items: [ // Based on an Xbox controller { - text: "Left stick (1 & 2)", + text: Scratch.translate("Left stick (1 & 2)"), value: "1", }, { - text: "Right stick (3 & 4)", + text: Scratch.translate("Right stick (3 & 4)"), value: "3", }, ], @@ -389,11 +391,11 @@ /* pressReleaseMenu: [ { - text: 'press', + text: Scratch.translate('press'), value: 1 }, { - text: 'release', + text: Scratch.translate('release'), value: 0 } ], diff --git a/extensions/iframe.js b/extensions/iframe.js index 29224ef9d6..ce87728a2c 100644 --- a/extensions/iframe.js +++ b/extensions/iframe.js @@ -1,6 +1,7 @@ // Name: Iframe // ID: iframe // Description: Display webpages or HTML over the stage. +// Context: "iframe" is an HTML element that lets websites embed other websites. (function (Scratch) { "use strict"; @@ -122,13 +123,13 @@ class IframeExtension { getInfo() { return { - name: "Iframe", + name: Scratch.translate("Iframe"), id: "iframe", blocks: [ { opcode: "display", blockType: Scratch.BlockType.COMMAND, - text: "show website [URL]", + text: Scratch.translate("show website [URL]"), arguments: { URL: { type: Scratch.ArgumentType.STRING, @@ -139,11 +140,11 @@ { opcode: "displayHTML", blockType: Scratch.BlockType.COMMAND, - text: "show HTML [HTML]", + text: Scratch.translate("show HTML [HTML]"), arguments: { HTML: { type: Scratch.ArgumentType.STRING, - defaultValue: "

    It works!

    ", + defaultValue: `

    ${Scratch.translate("It works!")}

    `, }, }, }, @@ -151,23 +152,23 @@ { opcode: "show", blockType: Scratch.BlockType.COMMAND, - text: "show iframe", + text: Scratch.translate("show iframe"), }, { opcode: "hide", blockType: Scratch.BlockType.COMMAND, - text: "hide iframe", + text: Scratch.translate("hide iframe"), }, { opcode: "close", blockType: Scratch.BlockType.COMMAND, - text: "close iframe", + text: Scratch.translate("close iframe"), }, "---", { opcode: "get", blockType: Scratch.BlockType.REPORTER, - text: "iframe [MENU]", + text: Scratch.translate("iframe [MENU]"), arguments: { MENU: { type: Scratch.ArgumentType.STRING, @@ -178,7 +179,7 @@ { opcode: "setX", blockType: Scratch.BlockType.COMMAND, - text: "set iframe x position to [X]", + text: Scratch.translate("set iframe x position to [X]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -189,7 +190,7 @@ { opcode: "setY", blockType: Scratch.BlockType.COMMAND, - text: "set iframe y position to [Y]", + text: Scratch.translate("set iframe y position to [Y]"), arguments: { Y: { type: Scratch.ArgumentType.NUMBER, @@ -200,7 +201,7 @@ { opcode: "setWidth", blockType: Scratch.BlockType.COMMAND, - text: "set iframe width to [WIDTH]", + text: Scratch.translate("set iframe width to [WIDTH]"), arguments: { WIDTH: { type: Scratch.ArgumentType.NUMBER, @@ -211,7 +212,7 @@ { opcode: "setHeight", blockType: Scratch.BlockType.COMMAND, - text: "set iframe height to [HEIGHT]", + text: Scratch.translate("set iframe height to [HEIGHT]"), arguments: { HEIGHT: { type: Scratch.ArgumentType.NUMBER, @@ -222,7 +223,7 @@ { opcode: "setInteractive", blockType: Scratch.BlockType.COMMAND, - text: "set iframe interactive to [INTERACTIVE]", + text: Scratch.translate("set iframe interactive to [INTERACTIVE]"), arguments: { INTERACTIVE: { type: Scratch.ArgumentType.STRING, @@ -233,7 +234,7 @@ { opcode: "setResize", blockType: Scratch.BlockType.COMMAND, - text: "set iframe resize behavior to [RESIZE]", + text: Scratch.translate("set iframe resize behavior to [RESIZE]"), arguments: { RESIZE: { type: Scratch.ArgumentType.STRING, @@ -246,23 +247,36 @@ getMenu: { acceptReporters: true, items: [ - "url", - "visible", + Scratch.translate("url"), + Scratch.translate("visible"), "x", "y", - "width", - "height", - "interactive", - "resize behavior", + Scratch.translate("width"), + Scratch.translate("height"), + Scratch.translate("interactive"), + Scratch.translate("resize behavior"), ], }, interactiveMenu: { acceptReporters: true, - items: ["true", "false"], + items: [ + // The getter blocks will return English regardless of translating these + "true", + "false", + ], }, resizeMenu: { acceptReporters: true, - items: ["scale", "viewport"], + items: [ + { + text: Scratch.translate("scale"), + value: "scale", + }, + { + text: Scratch.translate("viewport"), + value: "viewport", + }, + ], }, }, }; diff --git a/extensions/lab/text.js b/extensions/lab/text.js index 5793d78c90..3c912fea53 100644 --- a/extensions/lab/text.js +++ b/extensions/lab/text.js @@ -601,25 +601,25 @@ getInfo() { return { id: "text", - name: "Animated Text", + name: Scratch.translate("Animated Text"), color1: "#9966FF", blockIconURI: blockIconURI, blocks: [ { opcode: "setText", blockType: Scratch.BlockType.COMMAND, - text: "show text [TEXT]", + text: Scratch.translate("show text [TEXT]"), arguments: { TEXT: { type: Scratch.ArgumentType.STRING, - defaultValue: "Welcome to my project!", + defaultValue: Scratch.translate("Welcome to my project!"), }, }, }, { opcode: "animateText", blockType: Scratch.BlockType.COMMAND, - text: "[ANIMATE] text [TEXT]", + text: Scratch.translate("[ANIMATE] text [TEXT]"), arguments: { ANIMATE: { type: Scratch.ArgumentType.STRING, @@ -628,20 +628,20 @@ }, TEXT: { type: Scratch.ArgumentType.STRING, - defaultValue: "Here we go!", + defaultValue: Scratch.translate("Here we go!"), }, }, }, { opcode: "clearText", blockType: Scratch.BlockType.COMMAND, - text: "show sprite", + text: Scratch.translate("show sprite"), }, "---", { opcode: "setFont", blockType: Scratch.BlockType.COMMAND, - text: "set font to [FONT]", + text: Scratch.translate("set font to [FONT]"), arguments: { FONT: { type: Scratch.ArgumentType.STRING, @@ -652,7 +652,7 @@ { opcode: "setColor", blockType: Scratch.BlockType.COMMAND, - text: "set text color to [COLOR]", + text: Scratch.translate("set text color to [COLOR]"), arguments: { COLOR: { type: Scratch.ArgumentType.COLOR, @@ -662,7 +662,7 @@ { opcode: "setWidth", blockType: Scratch.BlockType.COMMAND, - text: "set width to [WIDTH] aligned [ALIGN]", + text: Scratch.translate("set width to [WIDTH] aligned [ALIGN]"), arguments: { WIDTH: { type: Scratch.ArgumentType.NUMBER, @@ -684,18 +684,18 @@ { func: "disableCompatibilityMode", blockType: Scratch.BlockType.BUTTON, - text: "Enable Non-Scratch Lab Features", + text: Scratch.translate("Enable Non-Scratch Lab Features"), hideFromPalette: !compatibilityMode, }, { blockType: Scratch.BlockType.LABEL, - text: "Incompatible with Scratch Lab:", + text: Scratch.translate("Incompatible with Scratch Lab:"), hideFromPalette: compatibilityMode, }, { opcode: "setAlignment", blockType: Scratch.BlockType.COMMAND, - text: "align text to [ALIGN]", + text: Scratch.translate("align text to [ALIGN]"), hideFromPalette: compatibilityMode, arguments: { ALIGN: { @@ -708,7 +708,7 @@ // why is the other block called "setWidth" :( opcode: "setWidthValue", blockType: Scratch.BlockType.COMMAND, - text: "set width to [WIDTH]", + text: Scratch.translate("set width to [WIDTH]"), hideFromPalette: compatibilityMode, arguments: { WIDTH: { @@ -720,26 +720,26 @@ { opcode: "resetWidth", blockType: Scratch.BlockType.COMMAND, - text: "reset text width", + text: Scratch.translate("reset text width"), hideFromPalette: compatibilityMode, }, "---", { opcode: "addLine", blockType: Scratch.BlockType.COMMAND, - text: "add line [TEXT]", + text: Scratch.translate("add line [TEXT]"), hideFromPalette: compatibilityMode, arguments: { TEXT: { type: Scratch.ArgumentType.STRING, - defaultValue: "Hello!", + defaultValue: Scratch.translate("Hello!"), }, }, }, { opcode: "getLines", blockType: Scratch.BlockType.REPORTER, - text: "# of lines", + text: Scratch.translate("# of lines"), hideFromPalette: compatibilityMode, disableMonitor: true, }, @@ -747,7 +747,7 @@ { opcode: "startAnimate", blockType: Scratch.BlockType.COMMAND, - text: "start [ANIMATE] animation", + text: Scratch.translate("start [ANIMATE] animation"), hideFromPalette: compatibilityMode, arguments: { ANIMATE: { @@ -760,7 +760,7 @@ { opcode: "animateUntilDone", blockType: Scratch.BlockType.COMMAND, - text: "animate [ANIMATE] until done", + text: Scratch.translate("animate [ANIMATE] until done"), hideFromPalette: compatibilityMode, arguments: { ANIMATE: { @@ -773,7 +773,7 @@ { opcode: "isAnimating", blockType: Scratch.BlockType.BOOLEAN, - text: "is animating?", + text: Scratch.translate("is animating?"), hideFromPalette: compatibilityMode, disableMonitor: true, }, @@ -781,7 +781,7 @@ { opcode: "setAnimateDuration", blockType: Scratch.BlockType.COMMAND, - text: "set [ANIMATE] duration to [NUM] seconds", + text: Scratch.translate("set [ANIMATE] duration to [NUM] seconds"), hideFromPalette: compatibilityMode, arguments: { ANIMATE: { @@ -798,7 +798,7 @@ { opcode: "resetAnimateDuration", blockType: Scratch.BlockType.COMMAND, - text: "reset [ANIMATE] duration", + text: Scratch.translate("reset [ANIMATE] duration"), hideFromPalette: compatibilityMode, arguments: { ANIMATE: { @@ -811,7 +811,7 @@ { opcode: "getAnimateDuration", blockType: Scratch.BlockType.REPORTER, - text: "[ANIMATE] duration", + text: Scratch.translate("[ANIMATE] duration"), hideFromPalette: compatibilityMode, arguments: { ANIMATE: { @@ -825,7 +825,7 @@ { opcode: "setTypeDelay", blockType: Scratch.BlockType.COMMAND, - text: "set typing delay to [NUM] seconds", + text: Scratch.translate("set typing delay to [NUM] seconds"), hideFromPalette: compatibilityMode, arguments: { NUM: { @@ -837,13 +837,13 @@ { opcode: "resetTypeDelay", blockType: Scratch.BlockType.COMMAND, - text: "reset typing delay", + text: Scratch.translate("reset typing delay"), hideFromPalette: compatibilityMode, }, { opcode: "getTypeDelay", blockType: Scratch.BlockType.REPORTER, - text: "typing delay", + text: Scratch.translate("typing delay"), hideFromPalette: compatibilityMode, disableMonitor: true, }, @@ -851,21 +851,21 @@ { opcode: "textActive", blockType: Scratch.BlockType.BOOLEAN, - text: "is showing text?", + text: Scratch.translate("is showing text?"), hideFromPalette: compatibilityMode, disableMonitor: true, }, { opcode: "getDisplayedText", blockType: Scratch.BlockType.REPORTER, - text: "displayed text", + text: Scratch.translate("displayed text"), hideFromPalette: compatibilityMode, disableMonitor: true, }, { opcode: "getTextAttribute", blockType: Scratch.BlockType.REPORTER, - text: "text [ATTRIBUTE]", + text: Scratch.translate("text [ATTRIBUTE]"), arguments: { ATTRIBUTE: { type: Scratch.ArgumentType.STRING, @@ -880,7 +880,20 @@ // These all need acceptReporters: false for parity with the Scratch Labs version. animate: { acceptReporters: false, - items: ["type", "rainbow", "zoom"], + items: [ + { + text: Scratch.translate("type"), + value: "type", + }, + { + text: Scratch.translate("rainbow"), + value: "rainbow", + }, + { + text: Scratch.translate("zoom"), + value: "zoom", + }, + ], }, font: { acceptReporters: false, @@ -888,7 +901,20 @@ }, align: { acceptReporters: false, - items: ["left", "center", "right"], + items: [ + { + text: Scratch.translate("left"), + value: "left", + }, + { + text: Scratch.translate("center"), + value: "center", + }, + { + text: Scratch.translate("right"), + value: "right", + }, + ], }, attribute: { acceptReporters: false, @@ -897,15 +923,50 @@ // TurboWarp menus (acceptReporters: true) twAnimate: { acceptReporters: true, - items: ["type", "rainbow", "zoom"], + items: [ + { + text: Scratch.translate("type"), + value: "type", + }, + { + text: Scratch.translate("rainbow"), + value: "rainbow", + }, + { + text: Scratch.translate("zoom"), + value: "zoom", + }, + ], }, twAnimateDuration: { acceptReporters: true, - items: ["rainbow", "zoom"], + items: [ + { + text: Scratch.translate("rainbow"), + value: "rainbow", + }, + { + text: Scratch.translate("zoom"), + value: "zoom", + }, + ], }, twAlign: { acceptReporters: true, - items: ["left", "center", "right"], + items: [ + { + text: Scratch.translate("left"), + value: "left", + }, + { + text: Scratch.translate("center"), + value: "center", + }, + { + text: Scratch.translate("right"), + value: "right", + }, + ], }, }, }; @@ -923,7 +984,7 @@ ...FONTS, ...customFonts, { - text: "random font", + text: Scratch.translate("random font"), value: "Random", }, ]; @@ -1053,11 +1114,12 @@ */ disableCompatibilityMode() { - let popup = [ - "This will enable new blocks and features that WILL NOT WORK in the offical Scratch Lab.", - "Do you wish to continue?", - ]; - if (confirm(popup.join("\n\n"))) { + const popup = Scratch.translate({ + id: "disableCompatibilityMode", + default: + "This will enable new blocks and features that WILL NOT WORK in the offical Scratch Lab.\n\nDo you wish to continue?", + }); + if (confirm(popup)) { compatibilityMode = false; Scratch.vm.extensionManager.refreshBlocks(); } diff --git a/extensions/local-storage.js b/extensions/local-storage.js index 6d93321696..05cd0be63d 100644 --- a/extensions/local-storage.js +++ b/extensions/local-storage.js @@ -19,7 +19,9 @@ const valid = !!namespace; if (!valid && Date.now() - lastNamespaceWarning > 3000) { alert( - 'Local Storage extension: project must run the "set storage namespace ID" block before it can use other blocks' + Scratch.translate( + 'Local Storage extension: project must run the "set storage namespace ID" block before it can use other blocks' + ) ); lastNamespaceWarning = Date.now(); } @@ -90,39 +92,39 @@ getInfo() { return { id: "localstorage", - name: "Local Storage", + name: Scratch.translate("Local Storage"), docsURI: "https://extensions.turbowarp.org/local-storage", blocks: [ { opcode: "setProjectId", blockType: Scratch.BlockType.COMMAND, - text: "set storage namespace ID to [ID]", + text: Scratch.translate("set storage namespace ID to [ID]"), arguments: { ID: { type: Scratch.ArgumentType.STRING, - defaultValue: "project title", + defaultValue: Scratch.translate("project title"), }, }, }, { opcode: "get", blockType: Scratch.BlockType.REPORTER, - text: "get key [KEY]", + text: Scratch.translate("get key [KEY]"), arguments: { KEY: { type: Scratch.ArgumentType.STRING, - defaultValue: "score", + defaultValue: Scratch.translate("score"), }, }, }, { opcode: "set", blockType: Scratch.BlockType.COMMAND, - text: "set key [KEY] to [VALUE]", + text: Scratch.translate("set key [KEY] to [VALUE]"), arguments: { KEY: { type: Scratch.ArgumentType.STRING, - defaultValue: "score", + defaultValue: Scratch.translate("score"), }, VALUE: { type: Scratch.ArgumentType.STRING, @@ -133,23 +135,23 @@ { opcode: "remove", blockType: Scratch.BlockType.COMMAND, - text: "delete key [KEY]", + text: Scratch.translate("delete key [KEY]"), arguments: { KEY: { type: Scratch.ArgumentType.STRING, - defaultValue: "score", + defaultValue: Scratch.translate("score"), }, }, }, { opcode: "removeAll", blockType: Scratch.BlockType.COMMAND, - text: "delete all keys", + text: Scratch.translate("delete all keys"), }, { opcode: "whenChanged", blockType: Scratch.BlockType.EVENT, - text: "when another window changes storage", + text: Scratch.translate("when another window changes storage"), isEdgeActivated: false, }, ], diff --git a/extensions/navigator.js b/extensions/navigator.js index 0896c20a28..5424cb1976 100644 --- a/extensions/navigator.js +++ b/extensions/navigator.js @@ -1,6 +1,7 @@ // Name: Navigator // ID: navigatorinfo // Description: Details about the user's browser and operating system. +// Context: "Navigator" refers to someone's browser (function (Scratch) { "use strict"; @@ -9,27 +10,27 @@ getInfo() { return { id: "navigatorinfo", - name: "Navigator Info", + name: Scratch.translate("Navigator Info"), blocks: [ { opcode: "getOS", blockType: Scratch.BlockType.REPORTER, - text: "operating system", + text: Scratch.translate("operating system"), }, { opcode: "getBrowser", blockType: Scratch.BlockType.REPORTER, - text: "browser", + text: Scratch.translate("browser"), }, { opcode: "getMemory", blockType: Scratch.BlockType.REPORTER, - text: "device memory in GB", + text: Scratch.translate("device memory in GB"), }, { opcode: "getPreferredColorScheme", blockType: Scratch.BlockType.BOOLEAN, - text: "user prefers [THEME] color scheme?", + text: Scratch.translate("user prefers [THEME] color scheme?"), arguments: { THEME: { type: Scratch.ArgumentType.STRING, @@ -41,18 +42,27 @@ { opcode: "getPreferredReducedMotion", blockType: Scratch.BlockType.BOOLEAN, - text: "user prefers reduced motion?", + text: Scratch.translate("user prefers reduced motion?"), }, { opcode: "getPreferredContrast", blockType: Scratch.BlockType.BOOLEAN, - text: "user prefers more contrast?", + text: Scratch.translate("user prefers more contrast?"), }, ], menus: { THEME: { acceptReporters: true, - items: ["light", "dark"], + items: [ + { + text: Scratch.translate("light"), + value: "light", + }, + { + text: Scratch.translate("dark"), + value: "dark", + }, + ], }, }, }; diff --git a/extensions/pointerlock.js b/extensions/pointerlock.js index f106e6454c..1d5457bfb3 100644 --- a/extensions/pointerlock.js +++ b/extensions/pointerlock.js @@ -108,12 +108,12 @@ getInfo() { return { id: "pointerlock", - name: "Pointerlock", + name: Scratch.translate("Pointerlock"), blocks: [ { opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, - text: "set pointer lock [enabled]", + text: Scratch.translate("set pointer lock [enabled]"), arguments: { enabled: { type: Scratch.ArgumentType.STRING, @@ -125,7 +125,7 @@ { opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, - text: "pointer locked?", + text: Scratch.translate("pointer locked?"), }, ], menus: { @@ -133,11 +133,11 @@ acceptReporters: true, items: [ { - text: "enabled", + text: Scratch.translate("enabled"), value: "true", }, { - text: "disabled", + text: Scratch.translate("disabled"), value: "false", }, ], diff --git a/extensions/runtime-options.js b/extensions/runtime-options.js index 6e5134ad73..11a9d382a9 100644 --- a/extensions/runtime-options.js +++ b/extensions/runtime-options.js @@ -21,14 +21,14 @@ getInfo() { return { id: "runtimeoptions", - name: "Runtime Options", + name: Scratch.translate("Runtime Options"), color1: "#8c9abf", color2: "#7d8aab", color3: "#6f7b99", blocks: [ { opcode: "getEnabled", - text: "[thing] enabled?", + text: Scratch.translate("[thing] enabled?"), blockType: Scratch.BlockType.BOOLEAN, arguments: { thing: { @@ -40,7 +40,7 @@ }, { opcode: "setEnabled", - text: "set [thing] to [enabled]", + text: Scratch.translate("set [thing] to [enabled]"), blockType: Scratch.BlockType.COMMAND, arguments: { thing: { @@ -60,12 +60,12 @@ { opcode: "getFramerate", - text: "framerate limit", + text: Scratch.translate("framerate limit"), blockType: Scratch.BlockType.REPORTER, }, { opcode: "setFramerate", - text: "set framerate limit to [fps]", + text: Scratch.translate("set framerate limit to [fps]"), blockType: Scratch.BlockType.COMMAND, arguments: { fps: { @@ -79,12 +79,12 @@ { opcode: "getCloneLimit", - text: "clone limit", + text: Scratch.translate("clone limit"), blockType: Scratch.BlockType.REPORTER, }, { opcode: "setCloneLimit", - text: "set clone limit to [limit]", + text: Scratch.translate("set clone limit to [limit]"), blockType: Scratch.BlockType.COMMAND, arguments: { limit: { @@ -99,7 +99,10 @@ { opcode: "getDimension", - text: "stage [dimension]", + text: Scratch.translate({ + default: "stage [dimension]", + description: "[dimension] is a dropdown of width and height", + }), blockType: Scratch.BlockType.REPORTER, arguments: { dimension: { @@ -111,7 +114,9 @@ }, { opcode: "setDimensions", - text: "set stage size width: [width] height: [height]", + text: Scratch.translate( + "set stage size width: [width] height: [height]" + ), blockType: Scratch.BlockType.COMMAND, arguments: { width: { @@ -129,7 +134,7 @@ { opcode: "setUsername", - text: "set username to [username]", + text: Scratch.translate("set username to [username]"), blockType: Scratch.BlockType.COMMAND, arguments: { username: { @@ -140,7 +145,7 @@ }, { opcode: "greenFlag", - text: "run green flag [flag]", + text: Scratch.translate("run green flag [flag]"), blockType: Scratch.BlockType.COMMAND, arguments: { flag: { @@ -155,23 +160,23 @@ acceptReporters: true, items: [ { - text: "turbo mode", + text: Scratch.translate("turbo mode"), value: TURBO_MODE, }, { - text: "interpolation", + text: Scratch.translate("interpolation"), value: INTERPOLATION, }, { - text: "remove fencing", + text: Scratch.translate("remove fencing"), value: REMOVE_FENCING, }, { - text: "remove misc limits", + text: Scratch.translate("remove misc limits"), value: REMOVE_MISC_LIMITS, }, { - text: "high quality pen", + text: Scratch.translate("high quality pen"), value: HIGH_QUALITY_PEN, }, ], @@ -181,11 +186,11 @@ acceptReporters: true, items: [ { - text: "enabled", + text: Scratch.translate("enabled"), value: "true", }, { - text: "disabled", + text: Scratch.translate("disabled"), value: "false", }, ], @@ -195,11 +200,13 @@ acceptReporters: true, items: [ { - text: "default (300)", + text: Scratch.translate("default ({n})", { + n: "300", + }), value: "300", }, { - text: "Infinity", + text: Scratch.translate("Infinity"), value: "Infinity", }, ], @@ -209,11 +216,11 @@ acceptReporters: true, items: [ { - text: "width", + text: Scratch.translate("width"), value: "width", }, { - text: "height", + text: Scratch.translate("height"), value: "height", }, ], diff --git a/extensions/sound.js b/extensions/sound.js index f83a6e3341..baf1568ae5 100644 --- a/extensions/sound.js +++ b/extensions/sound.js @@ -167,12 +167,12 @@ return { // 'sound' would conflict with normal Scratch id: "notSound", - name: "Sound", + name: Scratch.translate("Sound"), blocks: [ { opcode: "play", blockType: Scratch.BlockType.COMMAND, - text: "start sound from url: [path]", + text: Scratch.translate("start sound from url: [path]"), arguments: { path: { type: Scratch.ArgumentType.STRING, @@ -183,7 +183,7 @@ { opcode: "playUntilDone", blockType: Scratch.BlockType.COMMAND, - text: "play sound from url: [path] until done", + text: Scratch.translate("play sound from url: [path] until done"), arguments: { path: { type: Scratch.ArgumentType.STRING, diff --git a/extensions/stretch.js b/extensions/stretch.js index 0446e2ceeb..233054f1db 100644 --- a/extensions/stretch.js +++ b/extensions/stretch.js @@ -52,7 +52,7 @@ getInfo() { return { id: "stretch", - name: "Stretch", + name: Scratch.translate("Stretch"), color1: "#4287f5", color2: "#2b62ba", color3: "#204785", @@ -60,7 +60,7 @@ { opcode: "setStretch", blockType: Scratch.BlockType.COMMAND, - text: "set stretch to x: [X] y: [Y]", + text: Scratch.translate("set stretch to x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -76,7 +76,7 @@ { opcode: "changeStretch", blockType: Scratch.BlockType.COMMAND, - text: "change stretch by x: [DX] y: [DY]", + text: Scratch.translate("change stretch by x: [DX] y: [DY]"), arguments: { DX: { type: Scratch.ArgumentType.NUMBER, @@ -94,7 +94,7 @@ { opcode: "setStretchX", blockType: Scratch.BlockType.COMMAND, - text: "set stretch x to [X]", + text: Scratch.translate("set stretch x to [X]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -106,7 +106,7 @@ { opcode: "setStretchY", blockType: Scratch.BlockType.COMMAND, - text: "set stretch y to [Y]", + text: Scratch.translate("set stretch y to [Y]"), arguments: { Y: { type: Scratch.ArgumentType.NUMBER, @@ -118,7 +118,7 @@ { opcode: "changeStretchX", blockType: Scratch.BlockType.COMMAND, - text: "change stretch x by [DX]", + text: Scratch.translate("change stretch x by [DX]"), arguments: { DX: { type: Scratch.ArgumentType.NUMBER, @@ -129,7 +129,7 @@ { opcode: "changeStretchY", blockType: Scratch.BlockType.COMMAND, - text: "change stretch y by [DY]", + text: Scratch.translate("change stretch y by [DY]"), arguments: { DY: { type: Scratch.ArgumentType.NUMBER, @@ -143,14 +143,14 @@ { opcode: "getX", blockType: Scratch.BlockType.REPORTER, - text: "x stretch", + text: Scratch.translate("x stretch"), filter: [Scratch.TargetType.SPRITE], disableMonitor: true, }, { opcode: "getY", blockType: Scratch.BlockType.REPORTER, - text: "y stretch", + text: Scratch.translate("y stretch"), filter: [Scratch.TargetType.SPRITE], disableMonitor: true, }, From e3c64a012b8044e751929416eb16dc8f15b54be0 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Wed, 15 Nov 2023 21:29:01 -0600 Subject: [PATCH 055/196] Update translations (#1151) Soon enough this will be automated --- translations/extension-metadata.json | 32 +++++--- translations/extension-runtime.json | 109 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 13 deletions(-) diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index 10f48de680..162c660f45 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -6,19 +6,33 @@ "runtime-options@name": "Nastavení běhu" }, "de": { - "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools Ihres Browsers integrierten JavaScript-Konsole interagieren", "-SIPC-/time@name": "Zeit", - "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem", - "CubesterYT/TurboHook@description": "Ermöglicht die Benutzung von Webhooks", - "Lily/SoundExpanded@description": "Fügt mehr Blöcke hinzu, die mit Klängen zu tun haben", + "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem.", + "CubesterYT/TurboHook@description": "Ermöglicht die Benutzung von Webhooks.", + "DT/cameracontrols@description": "Bewege den angezeigten Teil der Bühne.", + "Lily/AllMenus@name": "Alle Menüs", + "Lily/CommentBlocks@name": "Kommentar Blöcke", + "Lily/LooksPlus@description": "Erweitert die Kategorie \"Aussehen\", indem es dir ermöglicht, das Anzeigen/Verbergen, Abrufen von Kostümdaten und Bearbeiten von SVG-Skins auf Sprites zu steuern.", + "Lily/MoreEvents@description": "Neue Wege, um deine Skripte zu starten.", + "Lily/MoreEvents@name": "Mehr Ereignisse", + "Lily/MoreTimers@name": "Mehr Stoppuhren", + "Lily/SoundExpanded@description": "Fügt mehr Blöcke hinzu, die mit Klängen zu tun haben.", + "Lily/SoundExpanded@name": "Klänge Erweitert", "Lily/TempVariables2@name": "Temporäre Variablen", + "Lily/Video@description": "Spiele Videos von URLs ab.", + "Longboost/color_channels@description": "Nur bestimmte RGB-Kanäle anzeigen oder hinterlasse Abdruck nur auf bestimmten RGB-Kanälen.", + "Longboost/color_channels@name": "RGB Kanäle", "NOname-awa/more-comparisons@name": "Mehr Vergleiche", + "NexusKitten/sgrab@description": "Erhalte Informationen über Scratch Projekte und Scratch Benutzer.", + "Skyhigh173/bigint@description": "Mathe Blöcke, die mit unendlich großen Ganzzahlen (ohne Dezimalstellen) arbeiten.", "ar@name": "Erweiterte Realität", "battery@name": "Batterie", "clipboard@name": "Zwischenablage", "files@name": "Dateien", "lab/text@name": "Animierter Text", "mdwalters/notifications@name": "Benachrichtigungen", + "obviousAlexC/SensingPlus@description": "Eine Erweiterung der Fühlen Kategorie.", + "obviousAlexC/SensingPlus@name": "Fühlen Plus", "runtime-options@name": "Laufzeit-Optionen", "shreder95ua/resolution@name": "Bildschirmauflösung", "sound@name": "Klänge", @@ -39,7 +53,6 @@ "runtime-options@name": "Lefutási Opciók" }, "it": { - "-SIPC-/consoles@description": "Blocchi che interagiscono con la console Javascript degli strumenti per sviluppatori del browser. ", "-SIPC-/consoles@name": "Console Javascript", "-SIPC-/time@description": "Blocchi per interagire con i timestamp Unix e altre stringhe rappresentanti ora e data.", "-SIPC-/time@name": "Unix Time", @@ -239,7 +252,6 @@ "runtime-options@name": "Параметри виконання" }, "zh-cn": { - "-SIPC-/consoles@description": "存取JS开发者控制台", "-SIPC-/consoles@name": "控制台", "-SIPC-/time@description": "处理UNIX时间戳和日期字符串", "-SIPC-/time@name": "时间", @@ -251,7 +263,7 @@ "Lily/Cast@name": "类型转换", "Lily/ClonesPlus@name": "克隆+", "Lily/CommentBlocks@description": "给代码添加注释
    ", - "Lily/CommentBlocks@name": "注释
    ", + "Lily/CommentBlocks@name": "注释", "Lily/LooksPlus@name": "外观+", "Lily/MoreEvents@name": "更多事件", "Lily/MoreTimers@name": "更多计时器", @@ -272,12 +284,12 @@ "qxsck/data-analysis@name": "数据分析", "qxsck/var-and-list@name": "变量与列表", "runtime-options@name": "运行选项", - "sound@name": "声音
    ", - "stretch@name": "伸缩
    ", + "sound@name": "声音", + "stretch@name": "伸缩", "text@name": "文本", "true-fantom/base@name": "进制转换", "true-fantom/math@name": "数学", - "true-fantom/network@name": "网络
    ", + "true-fantom/network@name": "网络", "true-fantom/regexp@name": "正则表达式", "utilities@name": "工具", "veggiecan/LongmanDictionary@name": "朗文辞典", diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index fae2c56f04..331833be9b 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -1,4 +1,21 @@ { + "ca": { + "files@_Select or drop file": "Selecciona o deixa anar el fitxer", + "runtime-options@_Runtime Options": "Opcions d'execució" + }, + "cs": { + "files@_Select or drop file": "Vyberte nebo přetáhněte soubor", + "runtime-options@_Runtime Options": "Nastavení běhu" + }, + "de": { + "battery@_Battery": "Batterie", + "clipboard@_Clipboard": "Zwischenablage", + "files@_Files": "Dateien", + "files@_Select or drop file": "Datei auswählen oder ziehen", + "lab/text@_Animated Text": "Animierter Text", + "runtime-options@_Runtime Options": "Laufzeit-Optionen", + "sound@_Sound": "Klänge" + }, "es": { "0832/rxFS2@del": "Eliminar [STR]", "0832/rxFS2@folder": "Fijar [STR] a [STR2]", @@ -10,7 +27,17 @@ "NOname-awa/graphics2d@area": "área", "NOname-awa/graphics2d@diameter": "diámetro", "NOname-awa/graphics2d@name": "Gráficos 2D", - "NOname-awa/graphics2d@radius": "radio" + "NOname-awa/graphics2d@radius": "radio", + "files@_Select or drop file": "Selecciona o suelta aquí un archivo", + "runtime-options@_Runtime Options": "Opciones de Runtime" + }, + "fr": { + "files@_Select or drop file": "Sélectionne ou dépose un fichier", + "runtime-options@_Runtime Options": "Options d'exécution" + }, + "hu": { + "files@_Select or drop file": "Válasszon ki, vagy húzzon ide egy fájlt", + "runtime-options@_Runtime Options": "Lefutási Opciók" }, "it": { "0832/rxFS2@clean": "Svuota il file system", @@ -37,6 +64,16 @@ "NOname-awa/graphics2d@round": "[CS] del cerchio [rd][a]", "NOname-awa/graphics2d@triangle": "[CS] del triangolo ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", "NOname-awa/graphics2d@triangle_s": "area del triangolo [s1] [s2] [s3]", + "battery@_Battery": "Batteria", + "clipboard@_Clipboard": "Appunti", + "clouddata-ping@_Ping Cloud Data": "Ping Dati Cloud", + "cursor@_Mouse Cursor": "Puntatore Mouse", + "encoding@_Encoding": "Codifica", + "files@_Files": "File", + "files@_Select or drop file": "Seleziona o trascina qui un file", + "lab/text@_Animated Text": "Testo Animato", + "local-storage@_Local Storage": "Memoria Locale", + "pointerlock@_Pointerlock": "Blocco Puntatore", "qxsck/data-analysis@average": "media di [NUMBERS]", "qxsck/data-analysis@maximum": "massimo di [NUMBERS]", "qxsck/data-analysis@median": "mediana di [NUMBERS]", @@ -59,7 +96,61 @@ "qxsck/var-and-list@replaceOfList": "sostituisci elemento [INDEX] di [LIST] con [VALUE]", "qxsck/var-and-list@seriListsToJson": "converti in json tutte le liste che iniziano con [START] ", "qxsck/var-and-list@seriVarsToJson": "converti in json tutte le variabili che iniziano con [START]", - "qxsck/var-and-list@setVar": "porta il valore di [VAR] a [VALUE]" + "qxsck/var-and-list@setVar": "porta il valore di [VAR] a [VALUE]", + "runtime-options@_Runtime Options": "Opzioni Esecuzione", + "sound@_Sound": "Suoni", + "stretch@_Stretch": "Stira" + }, + "ja": { + "files@_Select or drop file": "選ぶかファイルをドロップする", + "runtime-options@_Runtime Options": "ランタイムのオプション" + }, + "ja-hira": { + "runtime-options@_Runtime Options": "ランタイムのオプション" + }, + "ko": { + "files@_Select or drop file": "선택하거나 끌어다 놓기", + "runtime-options@_Runtime Options": "실행 설정" + }, + "lt": { + "files@_Select or drop file": "Pasirinkite arba numeskite failą", + "runtime-options@_Runtime Options": "Paleidimo laiko parinktys" + }, + "nl": { + "files@_Select or drop file": "Bestand selecteren of neerzetten", + "runtime-options@_Runtime Options": "Looptijdopties" + }, + "pl": { + "files@_Select or drop file": "Wybierz lub upuść plik", + "runtime-options@_Runtime Options": "Opcje Uruchamiania" + }, + "pt": { + "files@_Select or drop file": "Selecione ou arraste um arquivo", + "runtime-options@_Runtime Options": "Opções de Execução" + }, + "pt-br": { + "files@_Select or drop file": "Selecione ou arraste um arquivo", + "runtime-options@_Runtime Options": "Opções de Execução" + }, + "ru": { + "files@_Select or drop file": "Выберите или \"закиньте\" файл", + "runtime-options@_Runtime Options": "Опции Выполнения" + }, + "sl": { + "files@_Select or drop file": "Izberite ali povlecite datoteko", + "runtime-options@_Runtime Options": "Možnosti izvajanja" + }, + "sv": { + "files@_Select or drop file": "Välj eller släpp fil", + "runtime-options@_Runtime Options": "Körtidsalternativ" + }, + "tr": { + "files@_Select or drop file": "Dosyayı şeçin yada buraya bırakın", + "runtime-options@_Runtime Options": "Çalışma Zamanı Seçenekleri" + }, + "uk": { + "files@_Select or drop file": "Виберіть або \"закиньте\" файл", + "runtime-options@_Runtime Options": "Параметри виконання" }, "zh-cn": { "0832/rxFS2@clean": "清空文件系统", @@ -83,8 +174,20 @@ "NOname-awa/graphics2d@round": "[rd] 为 [a] 的圆的 [CS]", "NOname-awa/graphics2d@triangle": "三角形([x1],[y1])([x2],[y2])([x3],[y3])的 [CS]", "NOname-awa/graphics2d@triangle_s": "三角形 [s1] [s2] [s3] 的面积", + "clipboard@_Clipboard": "剪切板", + "encoding@_Encoding": "编码", + "files@_Files": "文件", + "files@_Select or drop file": "选择或拖入文件", + "lab/text@_Animated Text": "动画文字", "qxsck/data-analysis@name": "数据分析", "qxsck/var-and-list@copyList": "复制 [LIST1] 到 [LIST2]", - "qxsck/var-and-list@name": "变量与列表" + "qxsck/var-and-list@name": "变量与列表", + "runtime-options@_Runtime Options": "运行选项", + "sound@_Sound": "声音", + "stretch@_Stretch": "伸缩" + }, + "zh-tw": { + "files@_Select or drop file": "選擇或放入檔案", + "runtime-options@_Runtime Options": "運行選項" } } \ No newline at end of file From 49ae15ae28d1e61b04f4383606dcf1443d66d338 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Thu, 16 Nov 2023 23:39:35 -0600 Subject: [PATCH 056/196] Update l10n nov 16 (#1154) to be automated soon --- development/builder.js | 4 +- translations/extension-metadata.json | 132 ++++++++++- translations/extension-runtime.json | 342 ++++++++++++++++++++++++++- 3 files changed, 474 insertions(+), 4 deletions(-) diff --git a/development/builder.js b/development/builder.js index d96572e154..23a6d9714b 100644 --- a/development/builder.js +++ b/development/builder.js @@ -206,7 +206,9 @@ class ExtensionFile extends BuildFile { if (translations !== null) { return insertAfterCommentsBeforeCode( data, - `Scratch.translate.setup(${JSON.stringify(translations)});` + `/* generated l10n code */Scratch.translate.setup(${JSON.stringify( + translations + )});/* end generated l10n code */` ); } } diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index 162c660f45..ea5fc2910a 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -6,6 +6,7 @@ "runtime-options@name": "Nastavení běhu" }, "de": { + "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools Ihres Browsers integrierten JavaScript-Konsole interagieren.", "-SIPC-/time@name": "Zeit", "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem.", "CubesterYT/TurboHook@description": "Ermöglicht die Benutzung von Webhooks.", @@ -53,6 +54,7 @@ "runtime-options@name": "Lefutási Opciók" }, "it": { + "-SIPC-/consoles@description": "Blocchi che interagiscono con la console Javascript del browser.", "-SIPC-/consoles@name": "Console Javascript", "-SIPC-/time@description": "Blocchi per interagire con i timestamp Unix e altre stringhe rappresentanti ora e data.", "-SIPC-/time@name": "Unix Time", @@ -209,8 +211,12 @@ "-SIPC-/consoles@name": "コンソール", "-SIPC-/time@name": "時間", "Clay/htmlEncode@name": "HTMLエンコード", + "Lily/Video@name": "動画", + "Skyhigh173/bigint@description": "どんな整数 (小数を含まない) も処理する演算ブロック。", + "cursor@name": "マウスカーソル", "runtime-options@name": "ランタイムのオプション", - "text@name": "テキスト" + "text@name": "テキスト", + "true-fantom/math@name": "数学" }, "ja-hira": { "runtime-options@name": "ランタイムのオプション" @@ -223,6 +229,120 @@ "runtime-options@name": "Paleidimo laiko parinktys" }, "nl": { + "-SIPC-/consoles@description": "Maak gebruik van de ingebouwde JavaScript-console van je browser.", + "-SIPC-/time@description": "Maak gebruik van unix-tijden en andere datum-gerelateerde strings.", + "-SIPC-/time@name": "Tijd", + "0832/rxFS2@description": "Blokken waarmee je gebruik kan maken van een virtueel bestandssysteem in het geheugen.", + "Alestore/nfcwarp@description": "Lees gegevens af van NFC (NDEF) apparaten. Werkt alleen in Chrome op Android.", + "CST1229/zip@description": "Creëer en bewerk .zip-bestanden, inclusief .sb3-bestanden.", + "Clay/htmlEncode@description": "Maak strings veilig om gebruikt te worden in HTML.", + "Clay/htmlEncode@name": "HTML-Codering", + "CubesterYT/TurboHook@description": "Maak het gebruik van webhooks mogelijk.", + "CubesterYT/WindowControls@description": "Verander de positie, grootte en naam van het venster, ga naar volledig scherm, vraag de venstergrootte op en nog meer.", + "CubesterYT/WindowControls@name": "Vensterbesturing", + "DNin/wake-lock@description": "Zorg ervoor dat het apparaat niet in de slaapstand gaat.", + "DNin/wake-lock@name": "Wakker Houden", + "DT/cameracontrols@description": "Verplaats het zichtbare deel van het speelveld.", + "DT/cameracontrols@name": "Camerabesturing (Heeft Bugs)", + "JeremyGamer13/tween@description": "Maak je animaties soepeler met geleidelijke functies.", + "JeremyGamer13/tween@name": "Tweening", + "Lily/AllMenus@description": "Gebruik alle bestaande dropdown-menu's als losse blokken, zelfs die van extensies.", + "Lily/AllMenus@name": "Alle Menu's", + "Lily/Cast@description": "Zet waarden om naar verschillende waarde-types.", + "Lily/Cast@name": "Omzetten", + "Lily/ClonesPlus@description": "Gebruik tal van nieuwe kloon-gerelateerde functies.", + "Lily/ClonesPlus@name": "Klonen Uitgebreid", + "Lily/CommentBlocks@description": "Voorzie je scripts van aantekeningen.", + "Lily/CommentBlocks@name": "Commentaarblokken", + "Lily/HackedBlocks@description": "Krijg toegang tot verschillende \"gehackte blokken\" die werken in Scratch maar normaal gesproken niet zichtbaar zijn in het palet.", + "Lily/HackedBlocks@name": "Verborgen Blokken", + "Lily/LooksPlus@description": "Verbeter de uiterlijken-categorie met functies zoals: sprites tonen/verbergen, gegevens van uiterlijken aflezen en SVG-skins toepassen op sprites.", + "Lily/LooksPlus@name": "Uiterlijken Uitgebreid", + "Lily/McUtils@description": "Verbeter je vaardigheden met deze superhandige functies, speciaal voor fastfoodmedewerkers.", + "Lily/MoreEvents@description": "Begin je scripts op nieuwe manieren.", + "Lily/MoreEvents@name": "Meer Gebeurtenissen", + "Lily/MoreTimers@description": "Bestuur meerdere klokken tegelijkertijd.", + "Lily/MoreTimers@name": "Meer Klokken", + "Lily/Skins@description": "Geef je sprites weer als andere afbeeldingen of uiterlijken.", + "Lily/SoundExpanded@description": "Maak gebruik van meer geluid-gerelateerde blokken.", + "Lily/SoundExpanded@name": "Geluid Uitgebreid", + "Lily/TempVariables2@description": "Creëer tijdelijke looptijd of thread-variabelen.", + "Lily/TempVariables2@name": "Tijdelijke Variabelen", + "Lily/Video@description": "Speel video's af vanuit URL's.", + "Lily/lmsutils@description": "Maak gebruik van een hoop handige nieuwe blokken. Deze extensie heette eerst LMS Utilities.", + "Lily/lmsutils@name": "Lily's Hulpmiddelen", + "Longboost/color_channels@description": "Geef alleen specifieke RGB-kanalen weer op het speelveld.", + "Longboost/color_channels@name": "RGB-Kanalen", + "NOname-awa/graphics2d@description": "Bereken trigonometrische dingen zoals lengtes, hoeken en oppervlaktes in twee dimensies.", + "NOname-awa/graphics2d@name": "2D-Trigonometrie", + "NOname-awa/more-comparisons@description": "Maak gebruik van een hoop handige vergelijkingen.", + "NOname-awa/more-comparisons@name": "Meer Vergelijkingen", + "NexusKitten/controlcontrols@description": "Toon en verberg de projectbesturing.", + "NexusKitten/controlcontrols@name": "Projectbesturing-besturing", + "NexusKitten/moremotion@description": "Maak de beweging van je sprites makkelijker met een paar nieuwe beweging-gerelateerde blokken.", + "NexusKitten/moremotion@name": "Beweging Uitgebreid", + "NexusKitten/sgrab@description": "Krijg informatie over Scratch-projecten en gebruikers.", + "Skyhigh173/bigint@description": "Krijg toegang tot oneindig grote integers (getallen zonder komma) met nieuwe speciale rekenblokken.", + "Skyhigh173/bigint@name": "Oneindige Integers", + "Skyhigh173/json@description": "Werk met JSON-strings en arrays.", + "TheShovel/CanvasEffects@description": "Pas visuele effecten toe op het hele speelveld.", + "TheShovel/CanvasEffects@name": "Canvas-Effecten", + "TheShovel/ColorPicker@description": "Krijg toegang tot de ingebouwde kleurenkiezer van je systeem.", + "TheShovel/ColorPicker@name": "Kleurenkiezer", + "TheShovel/CustomStyles@description": "Pas het uiterlijk van variabele-monitoren en de invoer van het vraagblok aan.", + "TheShovel/CustomStyles@name": "Aangepaste Stijlen", + "TheShovel/LZ-String@description": "Comprimeer en decomprimeer tekst met behulp van lz-string.", + "TheShovel/LZ-String@name": "LZ-Compressie", + "TheShovel/ShovelUtils@description": "Gebruik een hoop verschillende blokken.", + "Xeltalliv/clippingblending@description": "Knip sprites buiten een bepaald gebied af en meng sprites met elkaar.", + "Xeltalliv/clippingblending@name": "Knippen & Mengen", + "XeroName/Deltatime@description": "Lees nauwkeurig het tijdsverschil tussen frames af.", + "XeroName/Deltatime@name": "Deltatijd", + "ZXMushroom63/searchApi@description": "Maak gebruik van URL-zoekparameters: het deel van de URL na het vraagteken.", + "ZXMushroom63/searchApi@name": "Zoekparameters", + "ar@description": "Geef afbeeldingen weer op de camera en voer bewegingsinterpretatie uit, waardoor 3D-projecten op de juiste manier virtuele objecten kunnen weergeven op de echte wereld.", + "battery@description": "Krijg toegang tot informatie over de batterij van telefoons of laptops. Werkt niet op sommige apparaten en browsers.", + "battery@name": "Batterij", + "bitwise@description": "Werk met de binaire representatie van getallen in computers.", + "bitwise@name": "Bitsgewijs", + "box2d@description": "Maak gebruik van een twee-dimensionale fysica-simulatie.", + "box2d@name": "Box2D Fysica-Simulatie", + "clipboard@description": "Lees en schrijf van en naar het klembord van het systeem.", + "clipboard@name": "Klembord", + "clouddata-ping@description": "Bepaal of een server voor cloudvariabelen bereikbaar is.", + "clouddata-ping@name": "Cloudservers Pingen", + "cloudlink@description": "Maak verbinding met servers met deze krachtige WebSocket-extensie.", + "cs2627883/numericalencoding@description": "Codeer strings naar getallen voor cloudvariabelen.", + "cs2627883/numericalencoding@name": "Numerieke Codering", + "cursor@description": "Gebruik aangepaste cursors, verberg het of vervang het met een uiterlijk.", + "cursor@name": "Muisaanwijzer", + "encoding@description": "Codeer en decodeer strings naar Unicode, base64 of URL's.", + "encoding@name": "Codering", + "fetch@description": "Maak verzoeken op het brede internet.", + "files@description": "Lees en download bestanden.", + "files@name": "Bestanden", + "gamejolt@description": "Maak interactie met de API van GameJolt mogelijk. Niet officieel.", + "gamepad@description": "Lees direct de signalen van gamepads af in plaats van knoppen met toetsen te verbinden.", + "godslayerakp/http@description": "Maak interactie met externe websites mogelijk met deze uitgebreide extensie.", + "godslayerakp/ws@description": "Verbind handmatig met WebSocket-servers.", + "iframe@description": "Geef webpagina's of HTML weer op het speelveld.", + "itchio@description": "Maak interactie met de website itch.io mogelijk. Niet officieel.", + "lab/text@description": "Toon en animeer tekst op een simpele manier. Compatibel met het experiment Animated Text van Scratch Lab.", + "lab/text@name": "Geanimeerde Tekst", + "local-storage@description": "Sla gegevens aanhoudend op. Zoals cookies, maar dan beter.", + "local-storage@name": "Lokale Opslag", + "mdwalters/notifications@description": "Geef notificaties weer.", + "mdwalters/notifications@name": "Notificaties", + "navigator@description": "Krijg toegang tot details over de browser en het besturingssysteem van de gebruiker.", + "obviousAlexC/SensingPlus@description": "Verbeter de categorie waarnemen met nieuwe blokken.", + "obviousAlexC/SensingPlus@name": "Waarnemen Uitgebreid", + "obviousAlexC/newgroundsIO@description": "Maak interactie met de API van Newgrounds mogelijk. Niet officieel.", + "obviousAlexC/penPlus@description": "Maak gebruik van gevorderde weergavemogelijkheden.", + "obviousAlexC/penPlus@name": "Pen Uitgebreid V6", + "penplus@description": "Vervangen door de extensie Pen Uitgebreid V6.", + "penplus@name": "Pen Uitgebreid V5 (Verouderd)", + "pointerlock@description": "Zet de muisaanwijzer vast. De blokken muis x & muis y geven de verandering in muispositie sinds de vorige frame.", + "pointerlock@name": "Muisaanwijzer-Vergrendeling", "runtime-options@name": "Looptijdopties", "text@name": "Tekst" }, @@ -236,6 +356,12 @@ "runtime-options@name": "Opções de Execução" }, "ru": { + "NOname-awa/graphics2d@name": "Графика 2D", + "battery@name": "Батарея", + "clipboard@name": "Буфер обмена", + "clouddata-ping@name": "Пинг облачных данных", + "cursor@name": "Курсор мыши", + "encoding@name": "Кодировка", "runtime-options@name": "Опции Выполнения" }, "sl": { @@ -252,11 +378,15 @@ "runtime-options@name": "Параметри виконання" }, "zh-cn": { + "-SIPC-/consoles@description": "一个能与内置于浏览器开发工具中的JavaScript控制台交互的积木块。", "-SIPC-/consoles@name": "控制台", "-SIPC-/time@description": "处理UNIX时间戳和日期字符串", "-SIPC-/time@name": "时间", "0832/rxFS2@description": "创建并使用虚拟档案系统", + "Alestore/nfcwarp@description": "允许从NFC(NDFF)硬件读取数据。仅支持Andriod设备上的Chrome浏览器。", "Alestore/nfcwarp@name": "NFC", + "CST1229/zip@description": "创建和编辑.zip格式的文件,包括.sb3的文件。", + "Clay/htmlEncode@name": "HTML编码", "JeremyGamer13/tween@name": "缓动", "Lily/AllMenus@name": "全部菜单", "Lily/Cast@description": "转换Scratch的资料类型", diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 331833be9b..3999ca118f 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -65,15 +65,198 @@ "NOname-awa/graphics2d@triangle": "[CS] del triangolo ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", "NOname-awa/graphics2d@triangle_s": "area del triangolo [s1] [s2] [s3]", "battery@_Battery": "Batteria", + "battery@_battery level": "livello della batteria", + "battery@_charging?": "in carica", + "battery@_seconds until charged": "secondi mancanti a completare la ricarica", + "battery@_seconds until empty": "secondi mancanti a scaricare la batteria", + "battery@_when battery level changed": "quando il livello della batteria cambia", + "battery@_when charging changed": "quando la ricarica cambia", + "battery@_when time until charged changed": "quando il tempo necessario alla ricarica completa cambia", + "battery@_when time until empty changed": "quando il tempo mancante allo scaricamento completo cambia", + "box2d@griffpatch.applyAngForce": "ruota con forza [force]", + "box2d@griffpatch.applyForce": "spingi con forza [force] in direzione [dir]", + "box2d@griffpatch.categoryName": "Fisica", + "box2d@griffpatch.changeScroll": "cambia scorrimento di x: [ox] y: [oy]", + "box2d@griffpatch.changeVelocity": "cambia velocità di vx: [sx] vy: [sy]", + "box2d@griffpatch.doTick": "esegui un passo della simulazione", + "box2d@griffpatch.getAngVelocity": "velocità angolare", + "box2d@griffpatch.getDensity": "densità", + "box2d@griffpatch.getFriction": "attrito", + "box2d@griffpatch.getGravityX": "gravità x", + "box2d@griffpatch.getGravityY": "gravità y", + "box2d@griffpatch.getRestitution": "elasticità", + "box2d@griffpatch.getScrollX": "scorrimento x", + "box2d@griffpatch.getScrollY": "scorrimento y", + "box2d@griffpatch.getStatic": "fisso", + "box2d@griffpatch.getTouching": "sprite a contatto con [where]", + "box2d@griffpatch.getVelocityX": "velocità x", + "box2d@griffpatch.getVelocityY": "velocità y", + "box2d@griffpatch.setAngVelocity": "porta velocità angolare a [force]", + "box2d@griffpatch.setDensity": "imposta densità a [density]", + "box2d@griffpatch.setDensityValue": "imposta densità a [density]", + "box2d@griffpatch.setFriction": "imposta attrito a [friction]", + "box2d@griffpatch.setFrictionValue": "imposta attrito a [friction]", + "box2d@griffpatch.setGravity": "imposta gravità a x: [gx] y: [gy]", + "box2d@griffpatch.setPhysics": "abilita modalità [mode] per le forme [shape]", + "box2d@griffpatch.setPosition": "vai a x: [x] y: [y] di [space]", + "box2d@griffpatch.setProperties": "imposta densità [density] attrito [friction] elasticità [restitution]", + "box2d@griffpatch.setRestitution": "imposta elasticità a [restitution]", + "box2d@griffpatch.setRestitutionValue": "imposta elasticità a [restitution]", + "box2d@griffpatch.setScroll": "imposta scorrimento a x: [ox] y: [oy]", + "box2d@griffpatch.setStage": "imposta i bordi dello stage a [stageType]", + "box2d@griffpatch.setStatic": "usa modalità [static]", + "box2d@griffpatch.setVelocity": "porta velocità a vx: [sx] vy: [sy]", "clipboard@_Clipboard": "Appunti", + "clipboard@_clipboard": "appunti", + "clipboard@_copy to clipboard: [TEXT]": "copia [TEXT] negli appunti", + "clipboard@_last pasted text": "ultimo testo incollato", + "clipboard@_reset clipboard": "svuota gli appunti", + "clipboard@_when something is copied": "quando qualcosa viene copiato", + "clipboard@_when something is pasted": "quando qualcosa viene incollato", "clouddata-ping@_Ping Cloud Data": "Ping Dati Cloud", + "clouddata-ping@_is cloud data server [SERVER] up?": "il server cloud [SERVER] è attivo", "cursor@_Mouse Cursor": "Puntatore Mouse", + "cursor@_bottom left": "angolo sinistra in basso", + "cursor@_bottom right": "angolo destra in basso", + "cursor@_center": "centro", + "cursor@_cursor": "puntatore", + "cursor@_hide cursor": "nascondi puntatore", + "cursor@_set cursor to [cur]": "usa [cur] come puntatore", + "cursor@_set cursor to current costume center: [position] max size: [size]": "usa il costume attuale con centro: [position] dimensione massima: [size] come puntatore", + "cursor@_top left": "angolo sinistra in alto", + "cursor@_top right": "angolo destra in alto", + "cursor@_{size} (unreliable)": "{size} (inaffidabile)", + "encoding@_Convert the character [string] to [CodeList]": "converti carattere [string] in [CodeList]", + "encoding@_Decode [string] with [code]": "decodifica [string] da [code]", + "encoding@_Encode [string] in [code]": "codifica [string] come [code]", "encoding@_Encoding": "Codifica", + "encoding@_Hash [string] with [hash]": "calcola hash [string] usando [hash]", + "encoding@_Randomly generated [position] character string": "stringa di [position] caratteri scelti a caso", + "encoding@_Use [wordbank] to generate a random [position] character string": "genera una stringa di [position] caratteri scelti a caso tra [wordbank]", + "encoding@_[string] corresponding to the [CodeList] character": "carattere [CodeList] corrispondente al valore [string]", + "encoding@_apple": "mela", + "files@_Accepted formats: {formats}": "Formati accettati: {formats}", "files@_Files": "File", + "files@_Hello, world!": "Ciao mondo!", "files@_Select or drop file": "Seleziona o trascina qui un file", + "files@_any": "qualunque", + "files@_download URL [url] as [file]": "scarica da URL [url] come [file]", + "files@_download [text] as [file]": "scarica [text] come [file]", + "files@_only show selector (unreliable)": "mostra soltanto finestra di dialogo per la selezione (non affidabile)", + "files@_open a [extension] file": "apri un file [extension]", + "files@_open a [extension] file as [as]": "apri un file [extension] come [as]", + "files@_open a file": "apri un file", + "files@_open a file as [as]": "apri un file come [as]", + "files@_open selector immediately": "apri subito finestra di dialogo per selezione file", + "files@_save.txt": "salva.txt", + "files@_set open file selector mode to [mode]": "imposta modalità di apertura file a [mode]", + "files@_show modal": "mostra finestra", + "files@_text": "testo", + "gamepad@_D-pad down (14)": "Tasto direzionale giù (14)", + "gamepad@_D-pad left (15)": "Tasto direzionale sinistra (15)", + "gamepad@_D-pad right (16)": "Tasto direzionale destra (16)", + "gamepad@_D-pad up (13)": "Tasto direzionale su (13)", + "gamepad@_Left bumper (5)": "Pulsante dorsale sinistro (5)", + "gamepad@_Left stick (1 & 2)": "Levetta sinistra (1 & 2)", + "gamepad@_Left stick (11)": "Levetta sinistra (11)", + "gamepad@_Left stick horizontal (1)": "Levetta sinistra orizzontale (1)", + "gamepad@_Left stick vertical (2)": "Levetta sinistra verticale (2)", + "gamepad@_Left trigger (7)": "Grilleto sinistro (7)", + "gamepad@_Right bumper (6)": "Pulsante dorsale destro (6)", + "gamepad@_Right stick (12)": "Levetta destra (12)", + "gamepad@_Right stick (3 & 4)": "Levetta destra (3 & 4)", + "gamepad@_Right stick horizontal (3)": "Levetta destra orizzontale (3)", + "gamepad@_Right stick vertical (4)": "Levetta destra verticale (4)", + "gamepad@_Right trigger (8)": "Grilleto destro (8)", + "gamepad@_Select/View (9)": "Seleziona/Visualizza (9)", + "gamepad@_Start/Menu (10)": "Inizia/Menu (10)", + "gamepad@_any": "qualunque", + "gamepad@_button [b] on pad [i] pressed?": "pulsante [b] del pad [i] premuto", + "gamepad@_direction of axes [axis] on pad [pad]": "direzione degli assi [axis] del pad [pad]", + "gamepad@_gamepad [pad] connected?": "gamepad [pad] connesso", + "gamepad@_magnitude of axes [axis] on pad [pad]": "valore degli assi [axis] del pad [pad]", + "gamepad@_rumble strong [s] and weak [w] for [t] sec. on pad [i]": "vibrazione forte [s] e piano [w] per [t] sec. sul pad [i]", + "gamepad@_value of axis [b] on pad [i]": "valore dell'asse [b] del pad [i]", + "gamepad@_value of button [b] on pad [i]": "valore del pulsante [b] del pad [i]", + "iframe@_It works!": "Funziona!", + "iframe@_close iframe": "chiudi iframe", + "iframe@_height": "altezza", + "iframe@_hide iframe": "nascondi iframe", + "iframe@_iframe [MENU]": "[MENU] dell'iframe", + "iframe@_interactive": "interattività", + "iframe@_resize behavior": "comportamento quando ridimensionato", + "iframe@_scale": "scala", + "iframe@_set iframe height to [HEIGHT]": "imposta larghezza dell'iframe a [HEIGHT]", + "iframe@_set iframe interactive to [INTERACTIVE]": "imposta interattività iframe a [INTERACTIVE]", + "iframe@_set iframe resize behavior to [RESIZE]": "imposta comportamento dell'iframe quando ridimensionato a [RESIZE]", + "iframe@_set iframe width to [WIDTH]": "imposta larghezza dell'iframe a [WIDTH]", + "iframe@_set iframe x position to [X]": "imposta posizione x dell'iframe a [X]", + "iframe@_set iframe y position to [Y]": "imposta posizione y dell'iframe a[Y]", + "iframe@_show HTML [HTML]": "mostra HTML [HTML]", + "iframe@_show iframe": "mostra iframe", + "iframe@_show website [URL]": "mostra sito [URL]", + "iframe@_visible": "visibilità", + "iframe@_width": "larghezza", + "lab/text@_# of lines": "numero di righe", "lab/text@_Animated Text": "Testo Animato", + "lab/text@_Enable Non-Scratch Lab Features": "Abilita blocchi non presenti in Scratch Lab", + "lab/text@_Hello!": "Ciao!", + "lab/text@_Here we go!": "Ecco fatto!", + "lab/text@_Incompatible with Scratch Lab:": "Non compatibili con Scratch Lab:", + "lab/text@_Welcome to my project!": "Bevenuti nel mio progetto!", + "lab/text@_[ANIMATE] duration": "durata [ANIMATE]", + "lab/text@_[ANIMATE] text [TEXT]": "testo [TEXT] con effetto [ANIMATE]", + "lab/text@_add line [TEXT]": "aggiugi riga [TEXT]", + "lab/text@_align text to [ALIGN]": "allinea il testo [ALIGN]", + "lab/text@_animate [ANIMATE] until done": "inizia animazione [ANIMATE] e attendi la fine", + "lab/text@_center": "centro", + "lab/text@_displayed text": "testo animato", + "lab/text@_is animating?": "animato", + "lab/text@_is showing text?": "testo visibile", + "lab/text@_left": "a sinistra", + "lab/text@_rainbow": "effetto arcobaleno", + "lab/text@_random font": "scelto a caso", + "lab/text@_reset [ANIMATE] duration": "resetta durata animazione [ANIMATE]", + "lab/text@_reset text width": "resetta larghezza testo", + "lab/text@_reset typing delay": "resetta intervallo effetto digitazione", + "lab/text@_right": "a destra", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "imposta durata [ANIMATE] a [NUM] secondi", + "lab/text@_set font to [FONT]": "usa carattere [FONT]", + "lab/text@_set text color to [COLOR]": "imposta colore del testo a [COLOR]", + "lab/text@_set typing delay to [NUM] seconds": "imposta intervallo effetto digitazione a [NUM] secondi", + "lab/text@_set width to [WIDTH]": "imposta larghezza a [WIDTH]", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "imposta larghezza a [WIDTH] con allineamento [ALIGN]", + "lab/text@_show sprite": "mostra sprite", + "lab/text@_show text [TEXT]": "mostra testo [TEXT]", + "lab/text@_start [ANIMATE] animation": "inizia animazione [ANIMATE]", + "lab/text@_text [ATTRIBUTE]": "[ATTRIBUTE] del testo", + "lab/text@_type": "effetto digitazione", + "lab/text@_typing delay": "intervallo effetto digitazione", + "lab/text@_zoom": "effetto zoom", + "lab/text@disableCompatibilityMode": "Attivando questa funzione verranno mostrati blocchi che NON SONO DISPONIBILI nella versione ufficiale di Scratch Lab.\n\nVuoi proseguire?", "local-storage@_Local Storage": "Memoria Locale", + "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "Estensione Archiviazone Locale: il progetto deve eseguire il blocco \"imposta ID spazio di archiviazione\" prima di usare gli altri blocchi", + "local-storage@_delete all keys": "cancella tutte le chiavi", + "local-storage@_delete key [KEY]": "cancella chiave [KEY]", + "local-storage@_get key [KEY]": "valore della chiave [KEY]", + "local-storage@_project title": "titolo progetto", + "local-storage@_score": "punteggio", + "local-storage@_set key [KEY] to [VALUE]": "imposta valore chiave [KEY] a [VALUE]", + "local-storage@_set storage namespace ID to [ID]": "imposta ID spazio archiviazione a [ID]", + "local-storage@_when another window changes storage": "quando altra finestra cambia spazio di archiviazione", + "navigator@_Navigator Info": "Informazioni Browser e SO", + "navigator@_dark": "scuro", + "navigator@_device memory in GB": "memoria dispositivo in GB", + "navigator@_light": "chiaro", + "navigator@_operating system": "sistema operativo", + "navigator@_user prefers [THEME] color scheme?": "l'utente preferisce il tema [THEME]", + "navigator@_user prefers more contrast?": "l'utente preferisce contrasto alto", + "navigator@_user prefers reduced motion?": "l'utente preferisce movimento ridotto", "pointerlock@_Pointerlock": "Blocco Puntatore", + "pointerlock@_disabled": "sblocca", + "pointerlock@_enabled": "blocca", + "pointerlock@_pointer locked?": "puntatore bloccato", + "pointerlock@_set pointer lock [enabled]": "[enabled] puntatore", "qxsck/data-analysis@average": "media di [NUMBERS]", "qxsck/data-analysis@maximum": "massimo di [NUMBERS]", "qxsck/data-analysis@median": "mediana di [NUMBERS]", @@ -97,13 +280,58 @@ "qxsck/var-and-list@seriListsToJson": "converti in json tutte le liste che iniziano con [START] ", "qxsck/var-and-list@seriVarsToJson": "converti in json tutte le variabili che iniziano con [START]", "qxsck/var-and-list@setVar": "porta il valore di [VAR] a [VALUE]", + "runtime-options@_Infinity": "infinito", "runtime-options@_Runtime Options": "Opzioni Esecuzione", + "runtime-options@_[thing] enabled?": "[thing] abilitato", + "runtime-options@_clone limit": "limite cloni", + "runtime-options@_default ({n})": "predefinito({n})", + "runtime-options@_disabled": "sblocca", + "runtime-options@_enabled": "blocca", + "runtime-options@_framerate limit": "limite framerate", + "runtime-options@_height": "altezza", + "runtime-options@_high quality pen": "penna alta qualità", + "runtime-options@_interpolation": "interpolazione", + "runtime-options@_remove fencing": "rimuovi i limiti dallo Stage", + "runtime-options@_remove misc limits": "rimuovi limiti", + "runtime-options@_run green flag [flag]": "esegui tutti i cappelli bandiera verde [flag]", + "runtime-options@_set [thing] to [enabled]": "imposta [thing] a [enabled]", + "runtime-options@_set clone limit to [limit]": "imposta limite cloni a [limit]", + "runtime-options@_set framerate limit to [fps]": "imposta limite framerate a [fps]", + "runtime-options@_set stage size width: [width] height: [height]": "imposta dimensioni Stage larghezza: [width]altezza: [height]", + "runtime-options@_set username to [username]": "imposta username a [username]", + "runtime-options@_stage [dimension]": "[dimension] dello Stage", + "runtime-options@_turbo mode": "modalità turbo", + "runtime-options@_width": "larghezza", "sound@_Sound": "Suoni", - "stretch@_Stretch": "Stira" + "sound@_play sound from url: [path] until done": "avvia riproduzione suono da url: [path] e attendi la fine", + "sound@_start sound from url: [path]": "riproduci suono da url: [path]", + "stretch@_Stretch": "Stira", + "stretch@_change stretch by x: [DX] y: [DY]": "cambia deformazione di x: [DX] y: [DY]", + "stretch@_change stretch x by [DX]": "cambia deformazione x di [DX]", + "stretch@_change stretch y by [DY]": "cambia deformazione y di [DY]", + "stretch@_set stretch to x: [X] y: [Y]": "imposta deformazione a x: [X] y: [Y]", + "stretch@_set stretch x to [X]": "porta deformazione x a [X]", + "stretch@_set stretch y to [Y]": "porta deformazione y a [Y]", + "stretch@_x stretch": "deformazione x", + "stretch@_y stretch": "deformazione y" }, "ja": { + "0832/rxFS2@del": "[STR]を削除", + "0832/rxFS2@open": "[STR]を開く", + "0832/rxFS2@search": "[STR]を検索", + "0832/rxFS2@start": "[STR]を作成", + "cursor@_Mouse Cursor": "マウスカーソル", + "encoding@_apple": "りんご", "files@_Select or drop file": "選ぶかファイルをドロップする", - "runtime-options@_Runtime Options": "ランタイムのオプション" + "iframe@_url": "URL", + "lab/text@_Hello!": "こんにちは!", + "lab/text@_show sprite": "スプライトを表示", + "local-storage@_get key [KEY]": "キーを取得[KEY]", + "navigator@_browser": "ブラウザ", + "navigator@_dark": "ダーク", + "navigator@_light": "ライト", + "runtime-options@_Runtime Options": "ランタイムのオプション", + "runtime-options@_turbo mode": "ターボモード" }, "ja-hira": { "runtime-options@_Runtime Options": "ランタイムのオプション" @@ -117,7 +345,17 @@ "runtime-options@_Runtime Options": "Paleidimo laiko parinktys" }, "nl": { + "NOname-awa/graphics2d@name": "2D-Trigonometrie", + "battery@_Battery": "Batterij", + "clipboard@_Clipboard": "Klembord", + "clouddata-ping@_Ping Cloud Data": "Cloudservers Pingen", + "cursor@_Mouse Cursor": "Muisaanwijzer", + "encoding@_Encoding": "Codering", + "files@_Files": "Bestanden", "files@_Select or drop file": "Bestand selecteren of neerzetten", + "lab/text@_Animated Text": "Geanimeerde Tekst", + "local-storage@_Local Storage": "Lokale Opslag", + "pointerlock@_Pointerlock": "Muisaanwijzer-Vergrendeling", "runtime-options@_Runtime Options": "Looptijdopties" }, "pl": { @@ -133,7 +371,107 @@ "runtime-options@_Runtime Options": "Opções de Execução" }, "ru": { + "0832/rxFS2@clean": "Очистить файловую систему", + "0832/rxFS2@del": "Удалить [STR]", + "0832/rxFS2@folder": "Задать [STR] значение [STR2]", + "0832/rxFS2@folder_default": "rxFS это хорошо!", + "0832/rxFS2@in": "Импортировать файловую систему из [STR]", + "0832/rxFS2@list": "Список всех файлов из [STR]", + "0832/rxFS2@open": "Открыть [STR]", + "0832/rxFS2@out": "Экспорт файловой системы", + "0832/rxFS2@search": "Найти [STR]", + "0832/rxFS2@start": "Создать [STR]", + "0832/rxFS2@sync": "Изменить расположение [STR] на [STR2]", + "0832/rxFS2@webin": "Загрузить [STR] из сети", + "NOname-awa/graphics2d@area": "площадь", + "NOname-awa/graphics2d@circumference": "длина", + "NOname-awa/graphics2d@diameter": "диаметр", + "NOname-awa/graphics2d@graph": "[CS] графа [graph]", + "NOname-awa/graphics2d@line_section": "длина от ([x1],[y1]) до ([x2],[y2])", + "NOname-awa/graphics2d@name": "Графика 2D", + "NOname-awa/graphics2d@pi": "пи", + "NOname-awa/graphics2d@quadrilateral": "[CS] четырехугольника ([x1],[y1]) ([x2],[y2]) ([x3],[y3]) ([x4],[y4])", + "NOname-awa/graphics2d@radius": "радиус", + "NOname-awa/graphics2d@ray_direction": "направление от ([x1],[y1]) к ([x2],[y2])", + "NOname-awa/graphics2d@round": "[CS] круга с [rd] ом [a]", + "NOname-awa/graphics2d@triangle": "[CS] треугольника ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", + "NOname-awa/graphics2d@triangle_s": "площадь треугольника [s1] [s2] [s3]", + "battery@_Battery": "Батарея", + "battery@_battery level": "заряд батареи", + "battery@_charging?": "заряжается?", + "battery@_seconds until charged": "секунд до полного заряда", + "battery@_seconds until empty": "секунд до конца заряда", + "battery@_when battery level changed": "когда уровень заряда изменился", + "battery@_when charging changed": "когда зарядка изменилась", + "battery@_when time until charged changed": "когда время до полного заряда изменилось", + "battery@_when time until empty changed": "когда время до конца заряда изменилось", + "box2d@griffpatch.applyAngForce": "крутить с силой [force]", + "box2d@griffpatch.applyForce": "приложить силу [force] в направлении [dir]", + "box2d@griffpatch.categoryName": "Физика", + "box2d@griffpatch.changeScroll": "изменить скролл на x:[ox] y:[oy]", + "box2d@griffpatch.changeVelocity": "изменить скорость на sx:[sx]sy:[sy] ", + "box2d@griffpatch.doTick": "шагнуть симуляцию", + "box2d@griffpatch.getAngVelocity": "угловая скорость", + "box2d@griffpatch.getDensity": "плотность", + "box2d@griffpatch.getFriction": "трение", + "box2d@griffpatch.getGravityX": "гравитация x", + "box2d@griffpatch.getGravityY": "гравитация y", + "box2d@griffpatch.getRestitution": "упругость", + "box2d@griffpatch.getScrollX": "скролл x", + "box2d@griffpatch.getScrollY": "скролл y", + "box2d@griffpatch.getStatic": "зафиксирован?", + "box2d@griffpatch.getTouching": "список спрайтов касающихся [where]", + "box2d@griffpatch.getVelocityX": "скорость x", + "box2d@griffpatch.getVelocityY": "скорость y", + "box2d@griffpatch.setAngVelocity": "установить угловую скорость в [force]", + "box2d@griffpatch.setDensity": "установить плотность в [density]", + "box2d@griffpatch.setDensityValue": "установить плотность в [density]", + "box2d@griffpatch.setFriction": "установить трение в [friction]", + "box2d@griffpatch.setFrictionValue": "установить трение в [friction]", + "box2d@griffpatch.setGravity": "установить гравитацию в x:[gx] y:[gy]", + "box2d@griffpatch.setPhysics": "включить для [shape] режим [mode]", + "box2d@griffpatch.setPosition": "перейти в x:[x] y:[y] [space]", + "box2d@griffpatch.setProperties": "установить плотность [density] трение [friction] упругость [restitution]", + "box2d@griffpatch.setRestitution": "установить упругость в [restitution]", + "box2d@griffpatch.setRestitutionValue": "установить упругость в [restitution]", + "box2d@griffpatch.setScroll": "установить скролл в x:[ox] y:[oy]", + "box2d@griffpatch.setStage": "задать границы сцены как [stageType]", + "box2d@griffpatch.setStatic": "установить тип фиксации как [static]", + "box2d@griffpatch.setVelocity": "установить скорость в sx:[sx]sy:[sy]", + "clipboard@_Clipboard": "Буфер обмена", + "clipboard@_clipboard": "буфер обмена", + "clipboard@_copy to clipboard: [TEXT]": "скопировать [TEXT]", + "clipboard@_last pasted text": "последний вставленный текст", + "clipboard@_reset clipboard": "очистить буфер обмена", + "clipboard@_when something is copied": "когда что-либо скопировано", + "clipboard@_when something is pasted": "когда что-либо вставлено", + "clouddata-ping@_Ping Cloud Data": "Пинг облачных данных", + "clouddata-ping@_is cloud data server [SERVER] up?": "Сервер облачных данных [SERVER] в сети?", + "cursor@_Mouse Cursor": "Курсор Мыши", + "cursor@_bottom left": "нижнем левом углу", + "cursor@_bottom right": "нижнем правом углу", + "cursor@_center": "центре", + "cursor@_cursor": "курсор", + "cursor@_hide cursor": "спрятать курсор", + "cursor@_set cursor to [cur]": "изменить курсор на [cur]", + "cursor@_set cursor to current costume center: [position] max size: [size]": "изменить курсор на текущий костюм с центром в: [position] максимальным размером: [size]", + "cursor@_top left": "верхнем левом углу", + "cursor@_top right": "верхнем правом углу", + "cursor@_{size} (unreliable)": "{size} (ненадежно)", + "encoding@_Convert the character [string] to [CodeList]": "Конвертировать символ [string]в [CodeList]", + "encoding@_Decode [string] with [code]": "Раскодировать [string] из [code]", + "encoding@_Encode [string] in [code]": "Закодировать [string] в [code]", + "encoding@_Encoding": "Кодировка", + "encoding@_Hash [string] with [hash]": "Захешировать [string] через [hash]", + "encoding@_Randomly generated [position] character string": "Случайно сгенерированная строка c длиной [position]", + "encoding@_Use [wordbank] to generate a random [position] character string": "Использовать [wordbank] чтобы случайно сгенерировать строку с длиной [position] ", + "encoding@_[string] corresponding to the [CodeList] character": "символ соответствующий [string] в [CodeList]", + "encoding@_apple": "яблоко", "files@_Select or drop file": "Выберите или \"закиньте\" файл", + "iframe@_It works!": "Работает!", + "lab/text@_Hello!": "Привет!", + "lab/text@_Here we go!": "Поехали!", + "lab/text@_center": "центр", "runtime-options@_Runtime Options": "Опции Выполнения" }, "sl": { From 5fe23e7f29883e163bfd271c157eb9b5bc3bcdf1 Mon Sep 17 00:00:00 2001 From: !Ryan <100989385+softedco@users.noreply.github.com> Date: Fri, 17 Nov 2023 20:40:08 +0600 Subject: [PATCH 057/196] Add Scratch.translate to gamejolt, itchio (#1155) --- extensions/gamejolt.js | 349 ++++++++++++++++++++++++----------------- extensions/itchio.js | 115 +++++++++----- 2 files changed, 279 insertions(+), 185 deletions(-) diff --git a/extensions/gamejolt.js b/extensions/gamejolt.js index 418e938cc6..3af83da2b8 100644 --- a/extensions/gamejolt.js +++ b/extensions/gamejolt.js @@ -1402,17 +1402,23 @@ opcode: "gamejoltBool", blockIconURI: icons.GameJolt, blockType: Scratch.BlockType.BOOLEAN, - text: "On Game Jolt?", + text: Scratch.translate({ + id: "GameJoltAPI_gamejoltBool", + default: "On Game Jolt?", + description: 'Keep "Game Jolt" as is.', + }), }, { blockType: Scratch.BlockType.LABEL, - text: "Session Blocks", + text: Scratch.translate("Session Blocks"), }, { opcode: "setGame", blockIconURI: icons.main, blockType: Scratch.BlockType.COMMAND, - text: "Set game ID to [ID] and private key to [key]", + text: Scratch.translate( + "Set game ID to [ID] and private key to [key]" + ), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1420,7 +1426,7 @@ }, key: { type: Scratch.ArgumentType.STRING, - defaultValue: "private key", + defaultValue: Scratch.translate("private key"), }, }, }, @@ -1428,7 +1434,7 @@ opcode: "session", blockIconURI: icons.main, blockType: Scratch.BlockType.COMMAND, - text: "[openOrClose] session", + text: Scratch.translate("[openOrClose] session"), arguments: { openOrClose: { type: Scratch.ArgumentType.STRING, @@ -1447,13 +1453,13 @@ opcode: "sessionPing", blockIconURI: icons.main, blockType: Scratch.BlockType.COMMAND, - text: "Ping session", + text: Scratch.translate("Ping session"), }, { opcode: "sessionSetStatus", blockIconURI: icons.main, blockType: Scratch.BlockType.COMMAND, - text: "Set session status to [status]", + text: Scratch.translate("Set session status to [status]"), arguments: { status: { type: Scratch.ArgumentType.STRING, @@ -1466,26 +1472,26 @@ opcode: "sessionBool", blockIconURI: icons.main, blockType: Scratch.BlockType.BOOLEAN, - text: "Session open?", + text: Scratch.translate("Session open?"), disableMonitor: true, }, { blockType: Scratch.BlockType.LABEL, - text: "User Blocks", + text: Scratch.translate("User Blocks"), }, { opcode: "loginManual", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Login with [username] and [token]", + text: Scratch.translate("Login with [username] and [token]"), arguments: { username: { type: Scratch.ArgumentType.STRING, - defaultValue: "username", + defaultValue: Scratch.translate("username"), }, token: { type: Scratch.ArgumentType.STRING, - defaultValue: "private token", + defaultValue: Scratch.translate("private token"), }, }, }, @@ -1493,41 +1499,43 @@ opcode: "loginAuto", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Login automatically", + text: Scratch.translate("Login automatically"), }, { opcode: "loginAutoBool", blockIconURI: icons.user, blockType: Scratch.BlockType.BOOLEAN, - text: "Autologin available?", + text: Scratch.translate("Autologin available?"), }, { opcode: "logout", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Logout", + text: Scratch.translate("Logout"), }, { opcode: "loginBool", blockIconURI: icons.user, blockType: Scratch.BlockType.BOOLEAN, - text: "Logged in?", + text: Scratch.translate("Logged in?"), }, { opcode: "loginUser", blockIconURI: icons.user, blockType: Scratch.BlockType.REPORTER, - text: "Logged in user's username", + text: Scratch.translate("Logged in user's username"), }, { opcode: "userFetch", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Fetch user's [usernameOrID] by [fetchType]", + text: Scratch.translate( + "Fetch user's [usernameOrID] by [fetchType]" + ), arguments: { usernameOrID: { type: Scratch.ArgumentType.STRING, - defaultValue: "username", + defaultValue: Scratch.translate("username"), }, fetchType: { type: Scratch.ArgumentType.STRING, @@ -1540,13 +1548,13 @@ opcode: "userFetchCurrent", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Fetch logged in user", + text: Scratch.translate("Fetch logged in user"), }, { opcode: "returnUserData", blockIconURI: icons.user, blockType: Scratch.BlockType.REPORTER, - text: "Fetched user's [userDataType]", + text: Scratch.translate("Fetched user's [userDataType]"), arguments: { userDataType: { type: Scratch.ArgumentType.STRING, @@ -1559,7 +1567,7 @@ opcode: "returnUserDataJson", blockIconURI: icons.user, blockType: Scratch.BlockType.REPORTER, - text: "Fetched user's data in JSON", + text: Scratch.translate("Fetched user's data in JSON"), }, { hideFromPalette: true, @@ -1578,13 +1586,13 @@ opcode: "friendsFetchNew", blockIconURI: icons.user, blockType: Scratch.BlockType.COMMAND, - text: "Fetch user's friend IDs", + text: Scratch.translate("Fetch user's friend IDs"), }, { opcode: "friendsReturn", blockIconURI: icons.user, blockType: Scratch.BlockType.REPORTER, - text: "Fetched user's friend ID at index[index]", + text: Scratch.translate("Fetched user's friend ID at index[index]"), arguments: { index: { type: Scratch.ArgumentType.NUMBER, @@ -1596,17 +1604,17 @@ opcode: "friendsReturnJson", blockIconURI: icons.user, blockType: Scratch.BlockType.REPORTER, - text: "Fetched user's friend IDs in JSON", + text: Scratch.translate("Fetched user's friend IDs in JSON"), }, { blockType: Scratch.BlockType.LABEL, - text: "Trophy Blocks", + text: Scratch.translate("Trophy Blocks"), }, { opcode: "trophyAchieve", blockIconURI: icons.trophy, blockType: Scratch.BlockType.COMMAND, - text: "Achieve trophy of ID [ID]", + text: Scratch.translate("Achieve trophy of ID [ID]"), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1618,7 +1626,7 @@ opcode: "trophyRemove", blockIconURI: icons.trophy, blockType: Scratch.BlockType.COMMAND, - text: "Remove trophy of ID [ID]", + text: Scratch.translate("Remove trophy of ID [ID]"), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1653,7 +1661,7 @@ opcode: "trophyFetchId", blockIconURI: icons.trophy, blockType: Scratch.BlockType.COMMAND, - text: "Fetch trophy of ID[ID]", + text: Scratch.translate("Fetch trophy of ID[ID]"), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1665,7 +1673,7 @@ opcode: "trophyFetchAll", blockIconURI: icons.trophy, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [trophyFetchGroup] trophies", + text: Scratch.translate("Fetch [trophyFetchGroup] trophies"), arguments: { trophyFetchGroup: { type: Scratch.ArgumentType.STRING, @@ -1678,7 +1686,9 @@ opcode: "trophyReturn", blockIconURI: icons.trophy, blockType: Scratch.BlockType.REPORTER, - text: "Fetched trophy [trophyDataType] at index [index]", + text: Scratch.translate( + "Fetched trophy [trophyDataType] at index [index]" + ), arguments: { trophyDataType: { type: Scratch.ArgumentType.STRING, @@ -1695,17 +1705,19 @@ opcode: "trophyReturnJson", blockIconURI: icons.trophy, blockType: Scratch.BlockType.REPORTER, - text: "Fetched trophies in JSON", + text: Scratch.translate("Fetched trophies in JSON"), }, { blockType: Scratch.BlockType.LABEL, - text: "Score Blocks", + text: Scratch.translate("Score Blocks"), }, { opcode: "scoreAdd", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Add score [value] in table of ID [ID] with text [text] and comment [extraData]", + text: Scratch.translate( + "Add score [value] in table of ID [ID] with text [text] and comment [extraData]" + ), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1717,11 +1729,11 @@ }, text: { type: Scratch.ArgumentType.STRING, - defaultValue: "1 point", + defaultValue: Scratch.translate("1 point"), }, extraData: { type: Scratch.ArgumentType.STRING, - defaultValue: "optional", + defaultValue: Scratch.translate("optional"), }, }, }, @@ -1729,7 +1741,9 @@ opcode: "scoreAddGuest", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]", + text: Scratch.translate( + "Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]" + ), arguments: { ID: { type: Scratch.ArgumentType.NUMBER, @@ -1741,15 +1755,15 @@ }, text: { type: Scratch.ArgumentType.STRING, - defaultValue: "1 point", + defaultValue: Scratch.translate("1 point"), }, extraData: { type: Scratch.ArgumentType.STRING, - defaultValue: "optional", + defaultValue: Scratch.translate("optional"), }, username: { type: Scratch.ArgumentType.STRING, - defaultValue: "guest", + defaultValue: Scratch.translate("guest"), }, }, }, @@ -1757,7 +1771,9 @@ opcode: "scoreFetchSimple", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]", + text: Scratch.translate( + "Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]" + ), arguments: { amount: { type: Scratch.ArgumentType.NUMBER, @@ -1778,7 +1794,9 @@ opcode: "scoreFetch", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]", + text: Scratch.translate( + "Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]" + ), arguments: { amount: { type: Scratch.ArgumentType.NUMBER, @@ -1808,7 +1826,9 @@ opcode: "scoreFetchGuestSimple", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount] [username] score/s in table of ID [ID]", + text: Scratch.translate( + "Fetch [amount] [username] score/s in table of ID [ID]" + ), arguments: { amount: { type: Scratch.ArgumentType.NUMBER, @@ -1816,7 +1836,7 @@ }, username: { type: Scratch.ArgumentType.STRING, - defaultValue: "guest", + defaultValue: Scratch.translate("guest"), }, ID: { type: Scratch.ArgumentType.NUMBER, @@ -1828,7 +1848,9 @@ opcode: "scoreFetchGuest", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]", + text: Scratch.translate( + "Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]" + ), arguments: { amount: { type: Scratch.ArgumentType.NUMBER, @@ -1836,7 +1858,7 @@ }, username: { type: Scratch.ArgumentType.STRING, - defaultValue: "guest", + defaultValue: Scratch.translate("guest"), }, betterOrWorse: { type: Scratch.ArgumentType.STRING, @@ -1857,7 +1879,9 @@ opcode: "returnScoreData", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched score [scoreDataType] at index [index]", + text: Scratch.translate( + "Fetched score [scoreDataType] at index [index]" + ), arguments: { scoreDataType: { type: Scratch.ArgumentType.STRING, @@ -1874,13 +1898,15 @@ opcode: "returnScoreDataJson", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched score data in JSON", + text: Scratch.translate("Fetched score data in JSON"), }, { opcode: "scoreGetRank", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched rank of [value] in table of ID [ID]", + text: Scratch.translate( + "Fetched rank of [value] in table of ID [ID]" + ), arguments: { value: { type: Scratch.ArgumentType.NUMBER, @@ -1897,7 +1923,9 @@ opcode: "scoreGetTables", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched table [tableDataType] at index[index] (Deprecated)", + text: Scratch.translate( + "Fetched table [tableDataType] at index[index] (Deprecated)" + ), arguments: { tableDataType: { type: Scratch.ArgumentType.STRING, @@ -1914,13 +1942,15 @@ opcode: "scoreFetchTables", blockIconURI: icons.score, blockType: Scratch.BlockType.COMMAND, - text: "Fetch score tables", + text: Scratch.translate("Fetch score tables"), }, { opcode: "scoreReturnTables", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched table [tableDataType] at index [index]", + text: Scratch.translate( + "Fetched table [tableDataType] at index [index]" + ), arguments: { tableDataType: { type: Scratch.ArgumentType.STRING, @@ -1937,17 +1967,19 @@ opcode: "scoreReturnTablesJson", blockIconURI: icons.score, blockType: Scratch.BlockType.REPORTER, - text: "Fetched tables in JSON", + text: Scratch.translate("Fetched tables in JSON"), }, { blockType: Scratch.BlockType.LABEL, - text: "Data Storage Blocks", + text: Scratch.translate("Data Storage Blocks"), }, { opcode: "dataStoreSet", blockIconURI: icons.store, blockType: Scratch.BlockType.COMMAND, - text: "Set [globalOrPerUser] data at [key] to [data]", + text: Scratch.translate( + "Set [globalOrPerUser] data at [key] to [data]" + ), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -1956,11 +1988,11 @@ }, key: { type: Scratch.ArgumentType.STRING, - defaultValue: "key", + defaultValue: Scratch.translate("key"), }, data: { type: Scratch.ArgumentType.STRING, - defaultValue: "data", + defaultValue: Scratch.translate("data"), }, }, }, @@ -1968,7 +2000,7 @@ opcode: "dataStoreFetch", blockIconURI: icons.store, blockType: Scratch.BlockType.REPORTER, - text: "Fetched [globalOrPerUser] data at [key]", + text: Scratch.translate("Fetched [globalOrPerUser] data at [key]"), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -1977,7 +2009,7 @@ }, key: { type: Scratch.ArgumentType.STRING, - defaultValue: "key", + defaultValue: Scratch.translate("key"), }, }, }, @@ -1985,7 +2017,9 @@ opcode: "dataStoreUpdate", blockIconURI: icons.store, blockType: Scratch.BlockType.COMMAND, - text: "Update [globalOrPerUser] data at [key] by [operationType] [value]", + text: Scratch.translate( + "Update [globalOrPerUser] data at [key] by [operationType] [value]" + ), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -1994,7 +2028,7 @@ }, key: { type: Scratch.ArgumentType.STRING, - defaultValue: "key", + defaultValue: Scratch.translate("key"), }, operationType: { type: Scratch.ArgumentType.STRING, @@ -2011,7 +2045,7 @@ opcode: "dataStoreRemove", blockIconURI: icons.store, blockType: Scratch.BlockType.COMMAND, - text: "Remove [globalOrPerUser] data at [key]", + text: Scratch.translate("Remove [globalOrPerUser] data at [key]"), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -2020,7 +2054,7 @@ }, key: { type: Scratch.ArgumentType.STRING, - defaultValue: "key", + defaultValue: Scratch.translate("key"), }, }, }, @@ -2050,7 +2084,7 @@ opcode: "dataStoreFetchKeys", blockIconURI: icons.store, blockType: Scratch.BlockType.COMMAND, - text: "Fetch all [globalOrPerUser] keys", + text: Scratch.translate("Fetch all [globalOrPerUser] keys"), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -2063,7 +2097,9 @@ opcode: "dataStoreFetchPatternKeys", blockIconURI: icons.store, blockType: Scratch.BlockType.COMMAND, - text: "Fetch [globalOrPerUser] keys matching with [pattern]", + text: Scratch.translate( + "Fetch [globalOrPerUser] keys matching with [pattern]" + ), arguments: { globalOrPerUser: { type: Scratch.ArgumentType.STRING, @@ -2080,7 +2116,7 @@ opcode: "dataStoreReturnKeys", blockIconURI: icons.store, blockType: Scratch.BlockType.REPORTER, - text: "Fetched key at index [index]", + text: Scratch.translate("Fetched key at index [index]"), arguments: { index: { type: Scratch.ArgumentType.NUMBER, @@ -2092,11 +2128,11 @@ opcode: "dataStoreReturnKeysJson", blockIconURI: icons.store, blockType: Scratch.BlockType.REPORTER, - text: "Fetched keys in JSON", + text: Scratch.translate("Fetched keys in JSON"), }, { blockType: Scratch.BlockType.LABEL, - text: "Time Blocks", + text: Scratch.translate("Time Blocks"), }, { hideFromPalette: true, @@ -2116,13 +2152,13 @@ opcode: "timeFetchNew", blockIconURI: icons.time, blockType: Scratch.BlockType.COMMAND, - text: "Fetch server's time", + text: Scratch.translate("Fetch server's time"), }, { opcode: "timeReturn", blockIconURI: icons.time, blockType: Scratch.BlockType.REPORTER, - text: "Fetched server's [timeType]", + text: Scratch.translate("Fetched server's [timeType]"), arguments: { timeType: { type: Scratch.ArgumentType.STRING, @@ -2135,7 +2171,7 @@ opcode: "timeReturnJson", blockIconURI: icons.time, blockType: Scratch.BlockType.REPORTER, - text: "Fetched server's time in JSON", + text: Scratch.translate("Fetched server's time in JSON"), }, { blockType: Scratch.BlockType.LABEL, @@ -2145,7 +2181,9 @@ opcode: "batchAdd", blockIconURI: icons.batch, blockType: Scratch.BlockType.COMMAND, - text: "Add [namespace] request with [parameters] to batch", + text: Scratch.translate( + "Add [namespace] request with [parameters] to batch" + ), arguments: { namespace: { type: Scratch.ArgumentType.STRING, @@ -2161,19 +2199,19 @@ opcode: "batchClear", blockIconURI: icons.batch, blockType: Scratch.BlockType.COMMAND, - text: "Clear batch", + text: Scratch.translate("Clear batch"), }, { opcode: "batchJson", blockIconURI: icons.batch, blockType: Scratch.BlockType.REPORTER, - text: "Batch in JSON", + text: Scratch.translate("Batch in JSON"), }, { opcode: "batchCall", blockIconURI: icons.batch, blockType: Scratch.BlockType.COMMAND, - text: "Fetch batch [parameter]", + text: Scratch.translate("Fetch batch [parameter]"), arguments: { parameter: { type: Scratch.ArgumentType.STRING, @@ -2186,17 +2224,17 @@ opcode: "batchReturnJson", blockIconURI: icons.batch, blockType: Scratch.BlockType.REPORTER, - text: "Fetched batch data in JSON", + text: Scratch.translate("Fetched batch data in JSON"), }, { blockType: Scratch.BlockType.LABEL, - text: "Debug Blocks", + text: Scratch.translate("Debug Blocks"), }, { opcode: "debug", blockIconURI: icons.debug, blockType: Scratch.BlockType.COMMAND, - text: "Turn debug mode [toggle]", + text: Scratch.translate("Turn debug mode [toggle]"), arguments: { toggle: { type: Scratch.ArgumentType.STRING, @@ -2209,137 +2247,164 @@ opcode: "debugBool", blockIconURI: icons.debug, blockType: Scratch.BlockType.BOOLEAN, - text: "In debug mode?", + text: Scratch.translate("In debug mode?"), }, { opcode: "debugLastErr", blockIconURI: icons.debug, blockType: Scratch.BlockType.REPORTER, - text: "Last API error", + text: Scratch.translate("Last API error"), }, ], menus: { debug: { items: [ - { text: "on", value: "true" }, - { text: "off", value: "" }, + { text: Scratch.translate("on"), value: "true" }, + { text: Scratch.translate("off"), value: "" }, ], }, status: { - items: ["active", "idle"], + items: [ + { text: Scratch.translate("active"), value: "active" }, + { text: Scratch.translate("idle"), value: "idle" }, + ], }, fetchTypes: { items: [ - { text: "username", value: "true" }, - { text: "ID", value: "" }, + { text: Scratch.translate("username"), value: "true" }, + { text: Scratch.translate("ID"), value: "" }, ], }, userDataTypes: { items: [ - { text: "ID", value: "id" }, - { text: "username", value: "username" }, - { text: "developer username", value: "developer_name" }, - { text: "description", value: "developer_description" }, - { text: "status", value: "status" }, - { text: "type", value: "type" }, - { text: "avatar URL", value: "avatar_url" }, - { text: "website", value: "website" }, - { text: "sign up date", value: "signed_up" }, - { text: "sign up timestamp", value: "signed_up_timestamp" }, - { text: "last login", value: "last_logged_in" }, + { text: Scratch.translate("ID"), value: "id" }, + { text: Scratch.translate("username"), value: "username" }, + { + text: Scratch.translate("developer username"), + value: "developer_name", + }, + { + text: Scratch.translate("description"), + value: "developer_description", + }, + { text: Scratch.translate("status"), value: "status" }, + { text: Scratch.translate("type"), value: "type" }, + { text: Scratch.translate("avatar URL"), value: "avatar_url" }, + { text: Scratch.translate("website"), value: "website" }, + { text: Scratch.translate("sign up date"), value: "signed_up" }, + { + text: Scratch.translate("sign up timestamp"), + value: "signed_up_timestamp", + }, + { + text: Scratch.translate("last login"), + value: "last_logged_in", + }, { - text: "last login timestamp", + text: Scratch.translate("last login timestamp"), value: "last_logged_in_timestamp", }, ], }, operationTypes: { items: [ - { text: "adding", value: "add" }, - { text: "subtracting", value: "subtract" }, - { text: "multiplying by", value: "multiply" }, - { text: "dividing by", value: "divide" }, - { text: "appending", value: "append" }, - { text: "prepending", value: "prepend" }, + { text: Scratch.translate("adding"), value: "add" }, + { text: Scratch.translate("subtracting"), value: "subtract" }, + { text: Scratch.translate("multiplying by"), value: "multiply" }, + { text: Scratch.translate("dividing by"), value: "divide" }, + { text: Scratch.translate("appending"), value: "append" }, + { text: Scratch.translate("prepending"), value: "prepend" }, ], }, scoreDataTypes: { items: [ - { text: "value", value: "sort" }, - { text: "text", value: "score" }, - { text: "comment", value: "extra_data" }, - { text: "username", value: "user" }, - { text: "user ID", value: "user_id" }, - { text: "score date", value: "stored" }, - { text: "score timestamp", value: "stored_timestamp" }, + { text: Scratch.translate("value"), value: "sort" }, + { text: Scratch.translate("text"), value: "score" }, + { text: Scratch.translate("comment"), value: "extra_data" }, + { text: Scratch.translate("username"), value: "user" }, + { text: Scratch.translate("user ID"), value: "user_id" }, + { text: Scratch.translate("score date"), value: "stored" }, + { + text: Scratch.translate("score timestamp"), + value: "stored_timestamp", + }, ], }, trophyDataTypes: { items: [ - { text: "ID", value: "id" }, - { text: "title", value: "title" }, - { text: "description", value: "description" }, - { text: "difficulty", value: "difficulty" }, - { text: "image URL", value: "image_url" }, - { text: "achievement date", value: "achieved" }, + { text: Scratch.translate("ID"), value: "id" }, + { text: Scratch.translate("title"), value: "title" }, + { text: Scratch.translate("description"), value: "description" }, + { text: Scratch.translate("difficulty"), value: "difficulty" }, + { text: Scratch.translate("image URL"), value: "image_url" }, + { + text: Scratch.translate("achievement date"), + value: "achieved", + }, ], }, timeTypes: { items: [ - "timestamp", - "timezone", - "year", - "month", - "day", - "hour", - "minute", - "second", + { text: Scratch.translate("timestamp"), value: "timestamp" }, + { text: Scratch.translate("timezone"), value: "timezone" }, + { text: Scratch.translate("year"), value: "year" }, + { text: Scratch.translate("month"), value: "month" }, + { text: Scratch.translate("day"), value: "day" }, + { text: Scratch.translate("hour"), value: "hour" }, + { text: Scratch.translate("minute"), value: "minute" }, + { text: Scratch.translate("second"), value: "second" }, ], }, tableDataTypes: { items: [ - { text: "ID", value: "id" }, - { text: "name", value: "name" }, - { text: "description", value: "description" }, - { text: "primary", value: "primary" }, + { text: Scratch.translate("ID"), value: "id" }, + { text: Scratch.translate("name"), value: "name" }, + { text: Scratch.translate("description"), value: "description" }, + { text: Scratch.translate("primary"), value: "primary" }, ], }, openOrClose: { items: [ - { text: "Open", value: "true" }, - { text: "Close", value: "" }, + { text: Scratch.translate("Open"), value: "true" }, + { text: Scratch.translate("Close"), value: "" }, ], }, globalOrPerUser: { items: [ - { text: "global", value: "false" }, - { text: "user", value: "true" }, + { text: Scratch.translate("global"), value: "false" }, + { text: Scratch.translate("user"), value: "true" }, ], }, trophyFetchGroup: { items: [ - { text: "all", value: "0" }, - { text: "all achieved", value: "1" }, - { text: "all unachieved", value: "-1" }, + { text: Scratch.translate("all"), value: "0" }, + { text: Scratch.translate("all achieved"), value: "1" }, + { text: Scratch.translate("all unachieved"), value: "-1" }, ], }, indexOrID: { items: [ - { text: "index", value: "true" }, - { text: "ID", value: "" }, + { text: Scratch.translate("index"), value: "true" }, + { text: Scratch.translate("ID"), value: "" }, ], }, betterOrWorse: { items: [ - { text: "better", value: "true" }, - { text: "worse", value: "" }, + { text: Scratch.translate("better"), value: "true" }, + { text: Scratch.translate("worse"), value: "" }, ], }, batchParameters: { items: [ - { text: "sequentially", value: "sequentially" }, - { text: "sequentially, break on error", value: "break_on_error" }, - { text: "in parallel", value: "parallel" }, + { + text: Scratch.translate("sequentially"), + value: "sequentially", + }, + { + text: Scratch.translate("sequentially, break on error"), + value: "break_on_error", + }, + { text: Scratch.translate("in parallel"), value: "parallel" }, ], }, }, diff --git a/extensions/itchio.js b/extensions/itchio.js index 913712d65a..e4cbe420f8 100644 --- a/extensions/itchio.js +++ b/extensions/itchio.js @@ -14,8 +14,10 @@ const getGameData = (user, game, secret, onComplete) => { if (!user || !game) { let callback = { errors: [] }; - if (!user) callback.errors.push("user argument not found"); - if (!game) callback.errors.push("game argument not found"); + if (!user) + callback.errors.push(Scratch.translate("user argument not found")); + if (!game) + callback.errors.push(Scratch.translate("game argument not found")); return onComplete(callback); } const url = @@ -40,8 +42,18 @@ let data = {}; let err = () => { - if (!data.errors) return "Error: Data not found."; - let output = data.errors[1] ? "Errors: " : "Error: "; + if (!data.errors) return Scratch.translate("Error: Data not found."); + let output = data.errors[1] + ? Scratch.translate({ + id: "itchio_errors", + default: "Errors: ", + description: "Don't forget the space at the end.", + }) + : Scratch.translate({ + id: "itchio_error", + default: "Error: ", + description: "Don't forget the space at the end.", + }); output += data.errors[0].charAt(0).toUpperCase() + data.errors[0].slice(1); for (let i = 1; true; i++) if (data.errors[i]) output += ", " + data.errors[i]; @@ -67,20 +79,22 @@ blocks: [ { blockType: Scratch.BlockType.LABEL, - text: "Window", + text: Scratch.translate("Window"), }, { opcode: "openItchWindow", blockType: Scratch.BlockType.COMMAND, - text: "Open [prefix] itch.io [page] window with [width]width and [height]height", + text: Scratch.translate( + "Open [prefix] itch.io [page] window with [width]width and [height]height" + ), arguments: { prefix: { type: Scratch.ArgumentType.STRING, - defaultValue: "user", + defaultValue: Scratch.translate("user"), }, page: { type: Scratch.ArgumentType.STRING, - defaultValue: "game", + defaultValue: Scratch.translate("game"), }, width: { type: Scratch.ArgumentType.NUMBER, @@ -94,12 +108,12 @@ }, { blockType: Scratch.BlockType.LABEL, - text: "Data", + text: Scratch.translate("Data"), }, { opcode: "getGameData", blockType: Scratch.BlockType.COMMAND, - text: "Fetch game data [user][game][secret]", + text: Scratch.translate("Fetch game data [user][game][secret]"), arguments: { user: { type: Scratch.ArgumentType.STRING, @@ -118,7 +132,9 @@ { opcode: "getGameDataJson", blockType: Scratch.BlockType.REPORTER, - text: "Return game data [user][game][secret] in .json", + text: Scratch.translate( + "Return game data [user][game][secret] in .json" + ), arguments: { user: { type: Scratch.ArgumentType.STRING, @@ -137,13 +153,13 @@ { opcode: "returnGameDataJson", blockType: Scratch.BlockType.REPORTER, - text: "Return game data in .json", + text: Scratch.translate("Return game data in .json"), }, "---", { opcode: "returnGameData", blockType: Scratch.BlockType.REPORTER, - text: "Return game [data]", + text: Scratch.translate("Return game [data]"), arguments: { data: { type: Scratch.ArgumentType.STRING, @@ -155,16 +171,18 @@ { opcode: "gameBool", blockType: Scratch.BlockType.BOOLEAN, - text: "Game data?", + text: Scratch.translate("Game data?"), }, { blockType: Scratch.BlockType.LABEL, - text: "Rewards", + text: Scratch.translate("Rewards"), }, { opcode: "returnGameRewards", blockType: Scratch.BlockType.REPORTER, - text: "Return game rewards [rewards] by index:[index]", + text: Scratch.translate( + "Return game rewards [rewards] by index:[index]" + ), arguments: { rewards: { type: Scratch.ArgumentType.STRING, @@ -180,21 +198,23 @@ { opcode: "rewardsLenght", // fixing this typo would break projects blockType: Scratch.BlockType.REPORTER, - text: "Return rewards list length", + text: Scratch.translate("Return rewards list length"), }, { opcode: "rewardsBool", blockType: Scratch.BlockType.BOOLEAN, - text: "Rewards?", + text: Scratch.translate("Rewards?"), }, { blockType: Scratch.BlockType.LABEL, - text: "Sub products", + text: Scratch.translate("Sub products"), }, { opcode: "returnGameSubProducts", blockType: Scratch.BlockType.REPORTER, - text: "Return game sub products [subProducts] by index:[index]", + text: Scratch.translate( + "Return game sub products [subProducts] by index:[index]" + ), arguments: { subProducts: { type: Scratch.ArgumentType.STRING, @@ -210,21 +230,21 @@ { opcode: "subProductsLength", blockType: Scratch.BlockType.REPORTER, - text: "Return sub products list length", + text: Scratch.translate("Return sub products list length"), }, { opcode: "subProductsBool", blockType: Scratch.BlockType.BOOLEAN, - text: "Sub products?", + text: Scratch.translate("Sub products?"), }, { blockType: Scratch.BlockType.LABEL, - text: "Sale", + text: Scratch.translate("Sale"), }, { opcode: "returnGameSale", blockType: Scratch.BlockType.REPORTER, - text: "Return game sale [sale]", + text: Scratch.translate("Return game sale [sale]"), arguments: { sale: { type: Scratch.ArgumentType.STRING, @@ -236,42 +256,51 @@ { opcode: "saleBool", blockType: Scratch.BlockType.BOOLEAN, - text: "Sale?", + text: Scratch.translate("Sale?"), }, ], menus: { menu: { items: [ - { text: "id", value: "id" }, - { text: "title", value: "title" }, - { text: "cover image URL", value: "cover_image" }, - { text: "price", value: "price" }, - { text: "original price", value: "original_price" }, + { text: Scratch.translate("id"), value: "id" }, + { text: Scratch.translate("title"), value: "title" }, + { + text: Scratch.translate("cover image URL"), + value: "cover_image", + }, + { text: Scratch.translate("price"), value: "price" }, + { + text: Scratch.translate("original price"), + value: "original_price", + }, ], }, rewardsMenu: { items: [ - { text: "id", value: "id" }, - { text: "title", value: "title" }, - { text: "price", value: "price" }, - { text: "amount", value: "amount" }, - { text: "amount remaining", value: "amount_remaining" }, - { text: "available", value: "available" }, + { text: Scratch.translate("id"), value: "id" }, + { text: Scratch.translate("title"), value: "title" }, + { text: Scratch.translate("price"), value: "price" }, + { text: Scratch.translate("amount"), value: "amount" }, + { + text: Scratch.translate("amount remaining"), + value: "amount_remaining", + }, + { text: Scratch.translate("available"), value: "available" }, ], }, subProductsMenu: { items: [ - { text: "id", value: "id" }, - { text: "name", value: "name" }, - { text: "price", value: "price" }, + { text: Scratch.translate("id"), value: "id" }, + { text: Scratch.translate("name"), value: "name" }, + { text: Scratch.translate("price"), value: "price" }, ], }, saleMenu: { items: [ - { text: "id", value: "id" }, - { text: "title", value: "title" }, - { text: "end date", value: "end_date" }, - { text: "rate", value: "rate" }, + { text: Scratch.translate("id"), value: "id" }, + { text: Scratch.translate("title"), value: "title" }, + { text: Scratch.translate("end date"), value: "end_date" }, + { text: Scratch.translate("rate"), value: "rate" }, ], }, }, From cec16cb77c5f9330909fcbc35beb5aa54d36171b Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Fri, 17 Nov 2023 09:55:49 -0600 Subject: [PATCH 058/196] L10N - Part 3 of many (#1156) --- extensions/-SIPC-/consoles.js | 50 ++--- extensions/-SIPC-/recording.js | 18 +- extensions/-SIPC-/time.js | 39 +++- extensions/Alestore/nfcwarp.js | 9 +- extensions/CST1229/images.js | 69 ++++--- extensions/CST1229/zip.js | 216 ++++++++++++++++++---- extensions/Clay/htmlEncode.js | 6 +- extensions/CubesterYT/TurboHook.js | 21 ++- extensions/CubesterYT/WindowControls.js | 127 ++++++++----- extensions/DNin/wake-lock.js | 13 +- extensions/DT/cameracontrols.js | 42 ++--- extensions/NexusKitten/controlcontrols.js | 29 ++- extensions/NexusKitten/moremotion.js | 57 ++++-- extensions/NexusKitten/sgrab.js | 101 ++++++++-- extensions/cs2627883/numericalencoding.js | 98 +++++----- 15 files changed, 633 insertions(+), 262 deletions(-) diff --git a/extensions/-SIPC-/consoles.js b/extensions/-SIPC-/consoles.js index 9fca8f1d85..e47a061681 100644 --- a/extensions/-SIPC-/consoles.js +++ b/extensions/-SIPC-/consoles.js @@ -14,7 +14,7 @@ getInfo() { return { id: "sipcconsole", - name: "Consoles", + name: Scratch.translate("Consoles"), color1: "#808080", color2: "#8c8c8c", color3: "#999999", @@ -24,61 +24,61 @@ { opcode: "Emptying", blockType: Scratch.BlockType.COMMAND, - text: "Clear Console", + text: Scratch.translate("Clear Console"), arguments: {}, }, { opcode: "Information", blockType: Scratch.BlockType.COMMAND, - text: "Information [string]", + text: Scratch.translate("Information [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Information", + defaultValue: Scratch.translate("Information"), }, }, }, { opcode: "Journal", blockType: Scratch.BlockType.COMMAND, - text: "Journal [string]", + text: Scratch.translate("Journal [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Journal", + defaultValue: Scratch.translate("Journal"), }, }, }, { opcode: "Warning", blockType: Scratch.BlockType.COMMAND, - text: "Warning [string]", + text: Scratch.translate("Warning [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Warning", + defaultValue: Scratch.translate("Warning"), }, }, }, { opcode: "Error", blockType: Scratch.BlockType.COMMAND, - text: "Error [string]", + text: Scratch.translate("Error [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Error", + defaultValue: Scratch.translate("Error"), }, }, }, { opcode: "debug", blockType: Scratch.BlockType.COMMAND, - text: "Debug [string]", + text: Scratch.translate("Debug [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Debug", + defaultValue: Scratch.translate("Debug"), }, }, }, @@ -87,62 +87,66 @@ { opcode: "group", blockType: Scratch.BlockType.COMMAND, - text: "Create a group named [string]", + text: Scratch.translate("Create a group named [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "group", + defaultValue: Scratch.translate("group"), }, }, }, { opcode: "groupCollapsed", blockType: Scratch.BlockType.COMMAND, - text: "Create a collapsed group named [string]", + text: Scratch.translate("Create a collapsed group named [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "group", + defaultValue: Scratch.translate("group"), }, }, }, { opcode: "groupEnd", blockType: Scratch.BlockType.COMMAND, - text: "Exit the current group", + text: Scratch.translate("Exit the current group"), arguments: {}, }, "---", { opcode: "Timeron", blockType: Scratch.BlockType.COMMAND, - text: "Start a timer named [string]", + text: Scratch.translate("Start a timer named [string]"), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Time", + defaultValue: Scratch.translate("Time"), }, }, }, { opcode: "Timerlog", blockType: Scratch.BlockType.COMMAND, - text: "Print the time run by the timer named [string]", + text: Scratch.translate( + "Print the time run by the timer named [string]" + ), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Time", + defaultValue: Scratch.translate("Time"), }, }, }, { opcode: "Timeroff", blockType: Scratch.BlockType.COMMAND, - text: "End the timer named [string] and print the time elapsed from start to end", + text: Scratch.translate( + "End the timer named [string] and print the time elapsed from start to end" + ), arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: "Time", + defaultValue: Scratch.translate("Time"), }, }, }, diff --git a/extensions/-SIPC-/recording.js b/extensions/-SIPC-/recording.js index 2265654c94..b4bc74f081 100644 --- a/extensions/-SIPC-/recording.js +++ b/extensions/-SIPC-/recording.js @@ -12,39 +12,45 @@ getInfo() { return { id: "sipcrecording", - name: "Recording", + name: Scratch.translate("Recording"), color1: "#696969", blocks: [ { opcode: "startRecording", blockType: Scratch.BlockType.COMMAND, - text: "Start recording", + text: Scratch.translate("Start recording"), blockIconURI: icon, arguments: {}, }, { opcode: "stopRecording", blockType: Scratch.BlockType.COMMAND, - text: "Stop recording", + text: Scratch.translate("Stop recording"), blockIconURI: icon, arguments: {}, }, { opcode: "stopRecordingAndDownload", blockType: Scratch.BlockType.COMMAND, - text: "Stop recording and download with [name] as filename", + text: Scratch.translate( + "Stop recording and download with [name] as filename" + ), blockIconURI: icon, arguments: { name: { type: Scratch.ArgumentType.STRING, - defaultValue: "recording.wav", + defaultValue: + Scratch.translate({ + default: "recording", + description: "Default file name", + }) + ".wav", }, }, }, { opcode: "isRecording", blockType: Scratch.BlockType.BOOLEAN, - text: "Recording?", + text: Scratch.translate("Recording?"), blockIconURI: icon, arguments: {}, }, diff --git a/extensions/-SIPC-/time.js b/extensions/-SIPC-/time.js index 6a8413c821..5690f799b9 100644 --- a/extensions/-SIPC-/time.js +++ b/extensions/-SIPC-/time.js @@ -13,7 +13,7 @@ getInfo() { return { id: "sipctime", - name: "Time", + name: Scratch.translate("Time"), color1: "#ff8000", color2: "#804000", color3: "#804000", @@ -23,19 +23,19 @@ { opcode: "Timestamp", blockType: Scratch.BlockType.REPORTER, - text: "current timestamp", + text: Scratch.translate("current timestamp"), arguments: {}, }, { opcode: "timezone", blockType: Scratch.BlockType.REPORTER, - text: "current time zone", + text: Scratch.translate("current time zone"), arguments: {}, }, { opcode: "Timedata", blockType: Scratch.BlockType.REPORTER, - text: "get [Timedata] from [timestamp]", + text: Scratch.translate("get [Timedata] from [timestamp]"), arguments: { timestamp: { type: Scratch.ArgumentType.NUMBER, @@ -51,7 +51,7 @@ { opcode: "TimestampToTime", blockType: Scratch.BlockType.REPORTER, - text: "convert [timestamp] to datetime", + text: Scratch.translate("convert [timestamp] to datetime"), arguments: { timestamp: { type: Scratch.ArgumentType.NUMBER, @@ -62,7 +62,7 @@ { opcode: "TimeToTimestamp", blockType: Scratch.BlockType.REPORTER, - text: "convert [time] to timestamp", + text: Scratch.translate("convert [time] to timestamp"), arguments: { time: { type: Scratch.ArgumentType.STRING, @@ -74,7 +74,32 @@ menus: { Time: { acceptReporters: true, - items: ["year", "month", "day", "hour", "minute", "second"], + items: [ + { + text: Scratch.translate("year"), + value: "year", + }, + { + text: Scratch.translate("month"), + value: "month", + }, + { + text: Scratch.translate("day"), + value: "day", + }, + { + text: Scratch.translate("hour"), + value: "hour", + }, + { + text: Scratch.translate("minute"), + value: "minute", + }, + { + text: Scratch.translate("second"), + value: "second", + }, + ], }, }, }; diff --git a/extensions/Alestore/nfcwarp.js b/extensions/Alestore/nfcwarp.js index 6e1face62c..e2d29b21b7 100644 --- a/extensions/Alestore/nfcwarp.js +++ b/extensions/Alestore/nfcwarp.js @@ -2,6 +2,7 @@ // ID: alestorenfc // Description: Allows reading data from NFC (NDEF) devices. Only works in Chrome on Android. // By: Alestore Games +// Context: NFC stands for "Near-field communication". Ideally check a real phone in your language to see how they translated it. (function (Scratch) { "use strict"; @@ -17,7 +18,7 @@ getInfo() { return { id: "alestorenfc", - name: "NFCWarp", + name: Scratch.translate("NFCWarp"), color1: "#FF4646", color2: "#FF0000", color3: "#990033", @@ -26,17 +27,17 @@ blocks: [ { blockType: Scratch.BlockType.LABEL, - text: "Only works in Chrome on Android", + text: Scratch.translate("Only works in Chrome on Android"), }, { opcode: "supported", blockType: Scratch.BlockType.BOOLEAN, - text: "NFC supported?", + text: Scratch.translate("NFC supported?"), }, { opcode: "nfcRead", blockType: Scratch.BlockType.REPORTER, - text: "read NFC tag", + text: Scratch.translate("read NFC tag"), disableMonitor: true, }, ], diff --git a/extensions/CST1229/images.js b/extensions/CST1229/images.js index 4f1691ba86..43b33ccd0a 100644 --- a/extensions/CST1229/images.js +++ b/extensions/CST1229/images.js @@ -33,7 +33,7 @@ { opcode: "getImage", blockType: Scratch.BlockType.REPORTER, - text: "new image from URL [IMAGEURL]", + text: Scratch.translate("new image from URL [IMAGEURL]"), arguments: { IMAGEURL: { type: Scratch.ArgumentType.STRING, @@ -47,7 +47,7 @@ { opcode: "penTrailsImage", blockType: Scratch.BlockType.REPORTER, - text: "pen trails as image", + text: Scratch.translate("pen trails as image"), arguments: {}, hideFromPalette: true, }, @@ -55,7 +55,7 @@ { opcode: "queryImage", blockType: Scratch.BlockType.REPORTER, - text: "[QUERY] of image [IMG]", + text: Scratch.translate("[QUERY] of image [IMG]"), arguments: { QUERY: { type: Scratch.ArgumentType.STRING, @@ -75,7 +75,9 @@ { opcode: "drawImage", blockType: Scratch.BlockType.COMMAND, - text: "stamp image [IMG] at x: [X] y: [Y] x scale: [XSCALE] y scale: [YSCALE]", + text: Scratch.translate( + "stamp image [IMG] at x: [X] y: [Y] x scale: [XSCALE] y scale: [YSCALE]" + ), arguments: { IMG: { // Intentional null input to require dropping a block in @@ -104,7 +106,7 @@ { opcode: "switchToImage", blockType: Scratch.BlockType.COMMAND, - text: "switch costume to image [IMG]", + text: Scratch.translate("switch costume to image [IMG]"), arguments: { IMG: { // Intentional null input to require dropping a block in @@ -116,20 +118,20 @@ { opcode: "imageID", blockType: Scratch.BlockType.REPORTER, - text: "current image ID", + text: Scratch.translate("current image ID"), arguments: {}, disableMonitor: true, }, { opcode: "resetCostume", blockType: Scratch.BlockType.COMMAND, - text: "switch back to costume", + text: Scratch.translate("switch back to costume"), arguments: {}, }, { opcode: "deleteImage", blockType: Scratch.BlockType.COMMAND, - text: "delete image [IMG]", + text: Scratch.translate("delete image [IMG]"), arguments: { IMG: { type: null, @@ -140,33 +142,52 @@ { opcode: "deleteAllImages", blockType: Scratch.BlockType.COMMAND, - text: "delete all images", + text: Scratch.translate("delete all images"), arguments: {}, }, ], menus: { queryImage: { acceptReporters: false, - items: this._queryImageMenu(), + items: [ + { + text: Scratch.translate("width"), + value: QueryImage.WIDTH, + }, + { + text: Scratch.translate("height"), + value: QueryImage.HEIGHT, + }, + { + text: Scratch.translate("top"), + value: QueryImage.TOP, + }, + { + text: Scratch.translate("bottom"), + value: QueryImage.BOTTOM, + }, + { + text: Scratch.translate("left"), + value: QueryImage.LEFT, + }, + { + text: Scratch.translate("right"), + value: QueryImage.RIGHT, + }, + { + text: Scratch.translate("rotation center x"), + value: QueryImage.ROTATION_CENTER_X, + }, + { + text: Scratch.translate("rotation center y"), + value: QueryImage.ROTATION_CENTER_Y, + }, + ], }, }, }; } - _queryImageMenu() { - const get = (param) => QueryImage[param]; - return [ - get("WIDTH"), - get("HEIGHT"), - get("TOP"), - get("BOTTOM"), - get("LEFT"), - get("RIGHT"), - get("ROTATION_CENTER_X"), - get("ROTATION_CENTER_Y"), - ]; - } - _createdImage(id) { if (!this.render || id === undefined || !this.render._allSkins[id]) return ""; diff --git a/extensions/CST1229/zip.js b/extensions/CST1229/zip.js index a80062b1ac..a8827db750 100644 --- a/extensions/CST1229/zip.js +++ b/extensions/CST1229/zip.js @@ -27,7 +27,7 @@ getInfo() { return { id: "cst1229zip", - name: "Zip", + name: Scratch.translate("Zip"), docsURI: "https://extensions.turbowarp.org/CST1229/zip", blockIconURI: extIcon, @@ -40,13 +40,13 @@ { opcode: "createEmpty", blockType: Scratch.BlockType.COMMAND, - text: "create empty archive", + text: Scratch.translate("create empty archive"), arguments: {}, }, { opcode: "open", blockType: Scratch.BlockType.COMMAND, - text: "open zip from [TYPE] [DATA]", + text: Scratch.translate("open zip from [TYPE] [DATA]"), arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -63,7 +63,9 @@ { opcode: "getZip", blockType: Scratch.BlockType.REPORTER, - text: "output zip type [TYPE] compression level [COMPRESSION]", + text: Scratch.translate( + "output zip type [TYPE] compression level [COMPRESSION]" + ), arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -80,13 +82,13 @@ { opcode: "close", blockType: Scratch.BlockType.COMMAND, - text: "close archive", + text: Scratch.translate("close archive"), arguments: {}, }, { opcode: "isOpen", blockType: Scratch.BlockType.BOOLEAN, - text: "archive is open?", + text: Scratch.translate("archive is open?"), arguments: {}, }, @@ -95,10 +97,11 @@ { opcode: "exists", blockType: Scratch.BlockType.BOOLEAN, - text: "[OBJECT] exists?", + text: Scratch.translate("[OBJECT] exists?"), arguments: { OBJECT: { type: Scratch.ArgumentType.STRING, + // Don't translate so this matches the default zip defaultValue: "folder/", }, }, @@ -106,11 +109,16 @@ { opcode: "writeFile", blockType: Scratch.BlockType.COMMAND, - text: "write file [FILE] content [CONTENT] type [TYPE]", + text: Scratch.translate( + "write file [FILE] content [CONTENT] type [TYPE]" + ), arguments: { FILE: { type: Scratch.ArgumentType.STRING, - defaultValue: "new file.txt", + defaultValue: `${Scratch.translate({ + default: "new file", + description: "Default file name", + })}.txt`, }, TYPE: { type: Scratch.ArgumentType.STRING, @@ -119,7 +127,7 @@ }, CONTENT: { type: Scratch.ArgumentType.STRING, - defaultValue: "Hello, world?", + defaultValue: Scratch.translate("Hello, world?"), }, }, }, @@ -130,10 +138,12 @@ arguments: { FROM: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "hello.txt", }, TO: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "hello renamed.txt", }, }, @@ -141,10 +151,11 @@ { opcode: "deleteFile", blockType: Scratch.BlockType.COMMAND, - text: "delete [FILE]", + text: Scratch.translate("delete [FILE]"), arguments: { FILE: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "hello.txt", }, }, @@ -152,10 +163,11 @@ { opcode: "getFile", blockType: Scratch.BlockType.REPORTER, - text: "file [FILE] as [TYPE]", + text: Scratch.translate("file [FILE] as [TYPE]"), arguments: { FILE: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "hello.txt", }, TYPE: { @@ -171,7 +183,7 @@ { opcode: "setFileMeta", blockType: Scratch.BlockType.COMMAND, - text: "set [META] of [FILE] to [VALUE]", + text: Scratch.translate("set [META] of [FILE] to [VALUE]"), arguments: { META: { type: Scratch.ArgumentType.STRING, @@ -180,6 +192,7 @@ }, FILE: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "folder/dango.png", }, VALUE: { @@ -191,7 +204,7 @@ { opcode: "getFileMeta", blockType: Scratch.BlockType.REPORTER, - text: "[META] of [FILE]", + text: Scratch.translate("[META] of [FILE]"), arguments: { META: { type: Scratch.ArgumentType.STRING, @@ -200,6 +213,7 @@ }, FILE: { type: Scratch.ArgumentType.STRING, + // Don't translate so matches default zip defaultValue: "folder/dango.png", }, }, @@ -210,18 +224,18 @@ { opcode: "createDir", blockType: Scratch.BlockType.COMMAND, - text: "create directory [DIR]", + text: Scratch.translate("create directory [DIR]"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, - defaultValue: "new folder", + defaultValue: Scratch.translate("new folder"), }, }, }, { opcode: "goToDir", blockType: Scratch.BlockType.COMMAND, - text: "go to directory [DIR]", + text: Scratch.translate("go to directory [DIR]"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -232,7 +246,7 @@ { opcode: "getDir", blockType: Scratch.BlockType.REPORTER, - text: "contents of directory [DIR]", + text: Scratch.translate("contents of directory [DIR]"), arguments: { DIR: { type: Scratch.ArgumentType.STRING, @@ -243,7 +257,7 @@ { opcode: "currentDir", blockType: Scratch.BlockType.REPORTER, - text: "current directory path", + text: Scratch.translate("current directory path"), }, "---", @@ -251,18 +265,18 @@ { opcode: "setComment", blockType: Scratch.BlockType.COMMAND, - text: "set archive comment to [COMMENT]", + text: Scratch.translate("set archive comment to [COMMENT]"), arguments: { COMMENT: { type: Scratch.ArgumentType.STRING, - defaultValue: "any text", + defaultValue: Scratch.translate("any text"), }, }, }, { opcode: "getComment", blockType: Scratch.BlockType.REPORTER, - text: "archive comment", + text: Scratch.translate("archive comment"), arguments: {}, }, @@ -271,7 +285,7 @@ { opcode: "normalizePath", blockType: Scratch.BlockType.REPORTER, - text: "path [PATH] from [ORIGIN]", + text: Scratch.translate("path [PATH] from [ORIGIN]"), arguments: { PATH: { type: Scratch.ArgumentType.STRING, @@ -288,28 +302,115 @@ fileType: { // used in the open zip block acceptReporters: true, - items: ["URL", "base64", "hex", "binary", "string"], + items: [ + { + text: Scratch.translate("URL"), + value: "URL", + }, + { + text: Scratch.translate("base64"), + value: "base64", + }, + { + text: Scratch.translate("hex"), + value: "hex", + }, + { + text: Scratch.translate("binary"), + value: "binary", + }, + { + text: Scratch.translate("string"), + value: "string", + }, + ], }, zipFileType: { // used in the output zip block acceptReporters: true, - items: ["data: URL", "base64", "hex", "binary", "string"], + items: [ + { + text: Scratch.translate("data: URL"), + value: "data: URL", + }, + { + text: Scratch.translate("base64"), + value: "base64", + }, + { + text: Scratch.translate("hex"), + value: "hex", + }, + { + text: Scratch.translate("binary"), + value: "binary", + }, + { + text: Scratch.translate("string"), + value: "string", + }, + ], }, getFileType: { // used in the get file block acceptReporters: true, - items: ["text", "data: URL", "base64", "hex", "binary"], + items: [ + { + text: Scratch.translate("text"), + value: "text", + }, + { + text: Scratch.translate("data: URL"), + value: "data: URL", + }, + { + text: Scratch.translate("base64"), + value: "base64", + }, + { + text: Scratch.translate("hex"), + value: "hex", + }, + { + text: Scratch.translate("binary"), + value: "binary", + }, + ], }, writeFileType: { // used in the write file block acceptReporters: true, - items: ["text", "URL", "base64", "hex", "binary"], + items: [ + { + text: Scratch.translate("text"), + value: "text", + }, + { + text: Scratch.translate("URL"), + value: "URL", + }, + { + text: Scratch.translate("base64"), + value: "base64", + }, + { + text: Scratch.translate("hex"), + value: "hex", + }, + { + text: Scratch.translate("binary"), + value: "binary", + }, + ], }, compressionLevel: { acceptReporters: true, items: [ - { text: "no compression (fastest)", value: "0" }, - { text: "1 (fast, large)", value: "1" }, + { + text: Scratch.translate("no compression (fastest)"), + value: "0", + }, + { text: Scratch.translate("1 (fast, large)"), value: "1" }, { text: "2", value: "2" }, { text: "3", value: "3" }, { text: "4", value: "4" }, @@ -317,28 +418,61 @@ { text: "6", value: "6" }, { text: "7", value: "7" }, { text: "8", value: "8" }, - { text: "9 (slowest, smallest)", value: "9" }, + { text: Scratch.translate("9 (slowest, smallest)"), value: "9" }, ], }, fileMeta: { acceptReporters: true, items: [ - "name", - "path", - "folder", - "modification date", - "long modification date", - "modified days since 2000", - "unix modified timestamp", - "comment", + { + text: Scratch.translate("name"), + value: "name", + }, + { + text: Scratch.translate("path"), + value: "path", + }, + { + text: Scratch.translate("folder"), + value: "folder", + }, + { + text: Scratch.translate("modification date"), + value: "modification date", + }, + { + text: Scratch.translate("long modification date"), + value: "long modification date", + }, + { + text: Scratch.translate("modified days since 2000"), + value: "modified days since 2000", + }, + { + text: Scratch.translate("unix modified timestamp"), + value: "unix modified timestamp", + }, + { + text: Scratch.translate("comment"), + value: "comment", + }, ], }, setFileMeta: { acceptReporters: true, items: [ - "modified days since 2000", - "unix modified timestamp", - "comment", + { + text: Scratch.translate("modified days since 2000"), + value: "modified days since 2000", + }, + { + text: Scratch.translate("unix modified timestamp"), + value: "unix modified timestamp", + }, + { + text: Scratch.translate("comment"), + value: "comment", + }, ], }, }, diff --git a/extensions/Clay/htmlEncode.js b/extensions/Clay/htmlEncode.js index 37ee534e50..ba93b6c902 100644 --- a/extensions/Clay/htmlEncode.js +++ b/extensions/Clay/htmlEncode.js @@ -10,19 +10,19 @@ getInfo() { return { id: "claytonhtmlencode", - name: "HTML Encode", + name: Scratch.translate("HTML Encode"), blocks: [ { opcode: "encode", blockType: Scratch.BlockType.REPORTER, - text: "encode [text] as HTML-safe", + text: Scratch.translate("encode [text] as HTML-safe"), arguments: { text: { type: Scratch.ArgumentType.STRING, // don't use a script tag as the example here as the closing script // tag might break things when this extension gets inlined in packed // projects - defaultValue: "

    Hello!

    ", + defaultValue: `

    ${Scratch.translate("Hello!")}

    `, }, }, }, diff --git a/extensions/CubesterYT/TurboHook.js b/extensions/CubesterYT/TurboHook.js index a87d1192a4..7230166881 100644 --- a/extensions/CubesterYT/TurboHook.js +++ b/extensions/CubesterYT/TurboHook.js @@ -22,7 +22,7 @@ getInfo() { return { id: "cubesterTurboHook", - name: "TurboHook", + name: Scratch.translate("TurboHook"), color1: "#3c48c2", color2: "#2f39a1", color3: "#28318f", @@ -32,7 +32,9 @@ blocks: [ { opcode: "webhook", - text: "webhook data: [hookDATA] webhook url: [hookURL]", + text: Scratch.translate( + "webhook data: [hookDATA] webhook url: [hookURL]" + ), blockType: Scratch.BlockType.COMMAND, arguments: { hookURL: { @@ -63,7 +65,20 @@ menus: { PARAMS: { acceptReporters: true, - items: ["content", "name", "icon"], + items: [ + { + text: Scratch.translate("content"), + value: "content", + }, + { + text: Scratch.translate("name"), + value: "name", + }, + { + text: Scratch.translate("icon"), + value: "icon", + }, + ], }, }, }; diff --git a/extensions/CubesterYT/WindowControls.js b/extensions/CubesterYT/WindowControls.js index 92b0abb25c..08d85d27e2 100644 --- a/extensions/CubesterYT/WindowControls.js +++ b/extensions/CubesterYT/WindowControls.js @@ -22,7 +22,7 @@ getInfo() { return { id: "cubesterWindowControls", - name: "Window Controls", + name: Scratch.translate("Window Controls"), color1: "#359ed4", color2: "#298ec2", color3: "#2081b3", @@ -31,17 +31,17 @@ blocks: [ { - blockType: "label", - text: "May not work in normal browser tabs", + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("May not work in normal browser tabs"), }, { - blockType: "label", - text: "Refer to Documentation for details", + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Refer to Documentation for details"), }, { opcode: "moveTo", blockType: Scratch.BlockType.COMMAND, - text: "move window to x: [X] y: [Y]", + text: Scratch.translate("move window to x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -56,7 +56,7 @@ { opcode: "moveToPresets", blockType: Scratch.BlockType.COMMAND, - text: "move window to the [PRESETS]", + text: Scratch.translate("move window to the [PRESETS]"), arguments: { PRESETS: { type: Scratch.ArgumentType.STRING, @@ -67,7 +67,7 @@ { opcode: "changeX", blockType: Scratch.BlockType.COMMAND, - text: "change window x by [X]", + text: Scratch.translate("change window x by [X]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -78,7 +78,7 @@ { opcode: "setX", blockType: Scratch.BlockType.COMMAND, - text: "set window x to [X]", + text: Scratch.translate("set window x to [X]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -89,7 +89,7 @@ { opcode: "changeY", blockType: Scratch.BlockType.COMMAND, - text: "change window y by [Y]", + text: Scratch.translate("change window y by [Y]"), arguments: { Y: { type: Scratch.ArgumentType.NUMBER, @@ -100,7 +100,7 @@ { opcode: "setY", blockType: Scratch.BlockType.COMMAND, - text: "set window y to [Y]", + text: Scratch.translate("set window y to [Y]"), arguments: { Y: { type: Scratch.ArgumentType.NUMBER, @@ -111,12 +111,12 @@ { opcode: "windowX", blockType: Scratch.BlockType.REPORTER, - text: "window x", + text: Scratch.translate("window x"), }, { opcode: "windowY", blockType: Scratch.BlockType.REPORTER, - text: "window y", + text: Scratch.translate("window y"), }, "---", @@ -124,7 +124,7 @@ { opcode: "resizeTo", blockType: Scratch.BlockType.COMMAND, - text: "resize window to width: [W] height: [H]", + text: Scratch.translate("resize window to width: [W] height: [H]"), arguments: { W: { type: Scratch.ArgumentType.NUMBER, @@ -139,7 +139,7 @@ { opcode: "resizeToPresets", blockType: Scratch.BlockType.COMMAND, - text: "resize window to [PRESETS]", + text: Scratch.translate("resize window to [PRESETS]"), arguments: { PRESETS: { type: Scratch.ArgumentType.STRING, @@ -150,7 +150,7 @@ { opcode: "changeW", blockType: Scratch.BlockType.COMMAND, - text: "change window width by [W]", + text: Scratch.translate("change window width by [W]"), arguments: { W: { type: Scratch.ArgumentType.NUMBER, @@ -161,7 +161,7 @@ { opcode: "setW", blockType: Scratch.BlockType.COMMAND, - text: "set window width to [W]", + text: Scratch.translate("set window width to [W]"), arguments: { W: { type: Scratch.ArgumentType.NUMBER, @@ -172,7 +172,7 @@ { opcode: "changeH", blockType: Scratch.BlockType.COMMAND, - text: "change window height by [H]", + text: Scratch.translate("change window height by [H]"), arguments: { H: { type: Scratch.ArgumentType.NUMBER, @@ -183,7 +183,7 @@ { opcode: "setH", blockType: Scratch.BlockType.COMMAND, - text: "set window height to [H]", + text: Scratch.translate("set window height to [H]"), arguments: { H: { type: Scratch.ArgumentType.NUMBER, @@ -194,17 +194,17 @@ { opcode: "matchStageSize", blockType: Scratch.BlockType.COMMAND, - text: "match stage size", + text: Scratch.translate("match stage size"), }, { opcode: "windowW", blockType: Scratch.BlockType.REPORTER, - text: "window width", + text: Scratch.translate("window width"), }, { opcode: "windowH", blockType: Scratch.BlockType.REPORTER, - text: "window height", + text: Scratch.translate("window height"), }, "---", @@ -212,17 +212,17 @@ { opcode: "isTouchingEdge", blockType: Scratch.BlockType.BOOLEAN, - text: "is window touching screen edge?", + text: Scratch.translate("is window touching screen edge?"), }, { opcode: "screenW", blockType: Scratch.BlockType.REPORTER, - text: "screen width", + text: Scratch.translate("screen width"), }, { opcode: "screenH", blockType: Scratch.BlockType.REPORTER, - text: "screen height", + text: Scratch.translate("screen height"), }, "---", @@ -230,7 +230,7 @@ { opcode: "isFocused", blockType: Scratch.BlockType.BOOLEAN, - text: "is window focused?", + text: Scratch.translate("is window focused?"), }, "---", @@ -238,18 +238,18 @@ { opcode: "changeTitleTo", blockType: Scratch.BlockType.COMMAND, - text: "set window title to [TITLE]", + text: Scratch.translate("set window title to [TITLE]"), arguments: { TITLE: { type: Scratch.ArgumentType.STRING, - defaultValue: "Hello World!", + defaultValue: Scratch.translate("Hello World!"), }, }, }, { opcode: "windowTitle", blockType: Scratch.BlockType.REPORTER, - text: "window title", + text: Scratch.translate("window title"), }, "---", @@ -257,17 +257,17 @@ { opcode: "enterFullscreen", blockType: Scratch.BlockType.COMMAND, - text: "enter fullscreen", + text: Scratch.translate("enter fullscreen"), }, { opcode: "exitFullscreen", blockType: Scratch.BlockType.COMMAND, - text: "exit fullscreen", + text: Scratch.translate("exit fullscreen"), }, { opcode: "isFullscreen", blockType: Scratch.BlockType.BOOLEAN, - text: "is window fullscreen?", + text: Scratch.translate("is window fullscreen?"), }, "---", @@ -276,23 +276,53 @@ opcode: "closeWindow", blockType: Scratch.BlockType.COMMAND, isTerminal: true, - text: "close window", + text: Scratch.translate("close window"), }, ], menus: { MOVE: { acceptReporters: true, items: [ - "center", - "right", - "left", - "top", - "bottom", - "top right", - "top left", - "bottom right", - "bottom left", - "random position", + { + text: Scratch.translate("center"), + value: "center", + }, + { + text: Scratch.translate("right"), + value: "right", + }, + { + text: Scratch.translate("left"), + value: "left", + }, + { + text: Scratch.translate("top"), + value: "top", + }, + { + text: Scratch.translate("bottom"), + value: "bottom", + }, + { + text: Scratch.translate("top right"), + value: "top right", + }, + { + text: Scratch.translate("top left"), + value: "top left", + }, + { + text: Scratch.translate("bottom right"), + value: "bottom right", + }, + { + text: Scratch.translate("bottom left"), + value: "bottom left", + }, + { + text: Scratch.translate("random position"), + value: "random position", + }, ], }, RESIZE: { @@ -497,11 +527,12 @@ return document.fullscreenElement !== null; } closeWindow() { - const editorConfirmation = [ - "Are you sure you want to close this window?", - "", - "(This message will not appear when the project is packaged)", - ].join("\n"); + const editorConfirmation = Scratch.translate({ + id: "editorConfirmation", + default: + "Are you sure you want to close this window?\n\n(This message will not appear when the project is packaged)", + }); + // @ts-expect-error if (typeof ScratchBlocks === "undefined" || confirm(editorConfirmation)) { window.close(); } diff --git a/extensions/DNin/wake-lock.js b/extensions/DNin/wake-lock.js index 41b98c8aa2..d3a9d4f56a 100644 --- a/extensions/DNin/wake-lock.js +++ b/extensions/DNin/wake-lock.js @@ -24,13 +24,16 @@ getInfo() { return { id: "dninwakelock", - name: "Wake Lock", + name: Scratch.translate("Wake Lock"), docsURI: "https://extensions.turbowarp.org/DNin/wake-lock", blocks: [ { opcode: "setWakeLock", blockType: Scratch.BlockType.COMMAND, - text: "turn wake lock [enabled]", + text: Scratch.translate({ + default: "turn wake lock [enabled]", + description: "[enabled] is a drop down with items 'on' and 'off'", + }), arguments: { enabled: { type: Scratch.ArgumentType.STRING, @@ -42,7 +45,7 @@ { opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, - text: "is wake lock active?", + text: Scratch.translate("is wake lock active?"), }, ], menus: { @@ -50,11 +53,11 @@ acceptReporters: true, items: [ { - text: "on", + text: Scratch.translate("on"), value: "true", }, { - text: "off", + text: Scratch.translate("off"), value: "false", }, ], diff --git a/extensions/DT/cameracontrols.js b/extensions/DT/cameracontrols.js index 6d577b51d8..c3eca35460 100644 --- a/extensions/DT/cameracontrols.js +++ b/extensions/DT/cameracontrols.js @@ -234,7 +234,7 @@ getInfo() { return { id: "DTcameracontrols", - name: "Camera (Very Buggy)", + name: Scratch.translate("Camera (Very Buggy)"), color1: "#ff4da7", color2: "#de4391", @@ -246,7 +246,7 @@ { opcode: "moveSteps", blockType: Scratch.BlockType.COMMAND, - text: "move camera [val] steps", + text: Scratch.translate("move camera [val] steps"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -257,7 +257,7 @@ { opcode: "rotateCW", blockType: Scratch.BlockType.COMMAND, - text: "turn camera [image] [val] degrees", + text: Scratch.translate("turn camera [image] [val] degrees"), arguments: { image: { type: Scratch.ArgumentType.IMAGE, @@ -272,7 +272,7 @@ { opcode: "rotateCCW", blockType: Scratch.BlockType.COMMAND, - text: "turn camera [image] [val] degrees", + text: Scratch.translate("turn camera [image] [val] degrees"), arguments: { image: { type: Scratch.ArgumentType.IMAGE, @@ -288,7 +288,7 @@ { opcode: "goTo", blockType: Scratch.BlockType.COMMAND, - text: "move camera to [sprite]", + text: Scratch.translate("move camera to [sprite]"), arguments: { sprite: { type: Scratch.ArgumentType.STRING, @@ -299,7 +299,7 @@ { opcode: "setBoth", blockType: Scratch.BlockType.COMMAND, - text: "set camera to x: [x] y: [y]", + text: Scratch.translate("set camera to x: [x] y: [y]"), arguments: { x: { type: Scratch.ArgumentType.NUMBER, @@ -315,7 +315,7 @@ { opcode: "setDirection", blockType: Scratch.BlockType.COMMAND, - text: "set camera direction to [val]", + text: Scratch.translate("set camera direction to [val]"), arguments: { val: { type: Scratch.ArgumentType.ANGLE, @@ -326,7 +326,7 @@ { opcode: "pointTowards", blockType: Scratch.BlockType.COMMAND, - text: "point camera towards [sprite]", + text: Scratch.translate("point camera towards [sprite]"), arguments: { sprite: { type: Scratch.ArgumentType.STRING, @@ -338,7 +338,7 @@ { opcode: "changeX", blockType: Scratch.BlockType.COMMAND, - text: "change camera x by [val]", + text: Scratch.translate("change camera x by [val]"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -349,7 +349,7 @@ { opcode: "setX", blockType: Scratch.BlockType.COMMAND, - text: "set camera x to [val]", + text: Scratch.translate("set camera x to [val]"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -360,7 +360,7 @@ { opcode: "changeY", blockType: Scratch.BlockType.COMMAND, - text: "change camera y by [val]", + text: Scratch.translate("change camera y by [val]"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -371,7 +371,7 @@ { opcode: "setY", blockType: Scratch.BlockType.COMMAND, - text: "set camera y to [val]", + text: Scratch.translate("set camera y to [val]"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -383,17 +383,17 @@ { opcode: "getX", blockType: Scratch.BlockType.REPORTER, - text: "camera x", + text: Scratch.translate("camera x"), }, { opcode: "getY", blockType: Scratch.BlockType.REPORTER, - text: "camera y", + text: Scratch.translate("camera y"), }, { opcode: "getDirection", blockType: Scratch.BlockType.REPORTER, - text: "camera direction", + text: Scratch.translate("camera direction"), }, /* // debugging blocks @@ -412,7 +412,7 @@ { opcode: "changeZoom", blockType: Scratch.BlockType.COMMAND, - text: "change camera zoom by [val]", + text: Scratch.translate("change camera zoom by [val]"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -423,7 +423,7 @@ { opcode: "setZoom", blockType: Scratch.BlockType.COMMAND, - text: "set camera zoom to [val] %", + text: Scratch.translate("set camera zoom to [val] %"), arguments: { val: { type: Scratch.ArgumentType.NUMBER, @@ -434,13 +434,13 @@ { opcode: "getZoom", blockType: Scratch.BlockType.REPORTER, - text: "camera zoom", + text: Scratch.translate("camera zoom"), }, "---", { opcode: "setCol", blockType: Scratch.BlockType.COMMAND, - text: "set background color to [val]", + text: Scratch.translate("set background color to [val]"), arguments: { val: { type: Scratch.ArgumentType.COLOR, @@ -450,7 +450,7 @@ { opcode: "getCol", blockType: Scratch.BlockType.REPORTER, - text: "background color", + text: Scratch.translate("background color"), }, ], menus: { @@ -475,7 +475,7 @@ if (e.isOriginal && !e.isStage) sprites.push(e.sprite.name); }); if (sprites.length === 0) { - sprites.push("no sprites exist"); + sprites.push(Scratch.translate("no sprites exist")); } return sprites; } diff --git a/extensions/NexusKitten/controlcontrols.js b/extensions/NexusKitten/controlcontrols.js index e4d7351e0b..f0a152450f 100644 --- a/extensions/NexusKitten/controlcontrols.js +++ b/extensions/NexusKitten/controlcontrols.js @@ -56,7 +56,7 @@ getInfo() { return { id: "nkcontrols", - name: "Control Controls", + name: Scratch.translate("Control Controls"), color1: "#ffab19", color2: "#ec9c13", color3: "#b87d17", @@ -64,7 +64,7 @@ { opcode: "showOption", blockType: Scratch.BlockType.COMMAND, - text: "show [OPTION]", + text: Scratch.translate("show [OPTION]"), arguments: { OPTION: { type: Scratch.ArgumentType.STRING, @@ -75,7 +75,7 @@ { opcode: "hideOption", blockType: Scratch.BlockType.COMMAND, - text: "hide [OPTION]", + text: Scratch.translate("hide [OPTION]"), arguments: { OPTION: { type: Scratch.ArgumentType.STRING, @@ -87,7 +87,7 @@ { opcode: "optionShown", blockType: Scratch.BlockType.BOOLEAN, - text: "[OPTION] shown?", + text: Scratch.translate("[OPTION] shown?"), arguments: { OPTION: { type: Scratch.ArgumentType.STRING, @@ -99,7 +99,7 @@ { opcode: "optionExists", blockType: Scratch.BlockType.BOOLEAN, - text: "[OPTION] exists?", + text: Scratch.translate("[OPTION] exists?"), arguments: { OPTION: { type: Scratch.ArgumentType.STRING, @@ -111,7 +111,24 @@ menus: { OPTION: { acceptReporters: true, - items: ["green flag", "pause", "stop", "fullscreen"], + items: [ + { + text: Scratch.translate("green flag"), + value: "green flag", + }, + { + text: Scratch.translate("pause"), + value: "pause", + }, + { + text: Scratch.translate("stop"), + value: "stop", + }, + { + text: Scratch.translate("fullscreen"), + value: "fullscreen", + }, + ], }, }, }; diff --git a/extensions/NexusKitten/moremotion.js b/extensions/NexusKitten/moremotion.js index d3aee71d53..dcaca41096 100644 --- a/extensions/NexusKitten/moremotion.js +++ b/extensions/NexusKitten/moremotion.js @@ -17,20 +17,24 @@ getInfo() { return { id: "nkmoremotion", - name: "More Motion", + name: Scratch.translate("More Motion"), color1: "#4c97ff", color2: "#3373cc", blocks: [ { filter: [Scratch.TargetType.STAGE], blockType: Scratch.BlockType.LABEL, - text: "Stage selected: no motion blocks", + // We can copy this translation from scratch-blocks + text: + typeof ScratchBlocks !== "undefined" + ? ScratchBlocks.Msg["MOTION_STAGE_SELECTED"] + : "Stage selected: no motion blocks", }, { filter: [Scratch.TargetType.SPRITE], opcode: "changexy", blockType: Scratch.BlockType.COMMAND, - text: "change x: [X] y: [Y]", + text: Scratch.translate("change x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -46,7 +50,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "pointto", blockType: Scratch.BlockType.COMMAND, - text: "point towards x: [X] y: [Y]", + text: Scratch.translate("point towards x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -62,7 +66,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "rotationStyle", blockType: Scratch.BlockType.REPORTER, - text: "rotation style", + text: Scratch.translate("rotation style"), disableMonitor: true, }, "---", @@ -70,14 +74,18 @@ filter: [Scratch.TargetType.SPRITE], opcode: "fence", blockType: Scratch.BlockType.COMMAND, - text: "manually fence", + text: Scratch.translate({ + default: "manually fence", + description: + "This blocks forces the sprite to be onscreen if it moved offscreen.", + }), }, "---", { filter: [Scratch.TargetType.SPRITE], opcode: "steptowards", blockType: Scratch.BlockType.COMMAND, - text: "move [STEPS] steps towards x: [X] y: [Y]", + text: Scratch.translate("move [STEPS] steps towards x: [X] y: [Y]"), arguments: { STEPS: { type: Scratch.ArgumentType.NUMBER, @@ -97,7 +105,9 @@ filter: [Scratch.TargetType.SPRITE], opcode: "tweentowards", blockType: Scratch.BlockType.COMMAND, - text: "move [PERCENT]% of the way to x: [X] y: [Y]", + text: Scratch.translate( + "move [PERCENT]% of the way to x: [X] y: [Y]" + ), arguments: { PERCENT: { type: Scratch.ArgumentType.NUMBER, @@ -118,7 +128,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "directionto", blockType: Scratch.BlockType.REPORTER, - text: "direction to x: [X] y: [Y]", + text: Scratch.translate("direction to x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -134,7 +144,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "distanceto", blockType: Scratch.BlockType.REPORTER, - text: "distance from x: [X] y: [Y]", + text: Scratch.translate("distance from x: [X] y: [Y]"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -150,7 +160,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "spritewh", blockType: Scratch.BlockType.REPORTER, - text: "sprite [WHAT]", + text: Scratch.translate("sprite [WHAT]"), disableMonitor: true, arguments: { WHAT: { @@ -164,7 +174,7 @@ filter: [Scratch.TargetType.SPRITE], opcode: "touchingxy", blockType: Scratch.BlockType.BOOLEAN, - text: "touching x: [X] y: [Y]?", + text: Scratch.translate("touching x: [X] y: [Y]?"), arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -180,7 +190,9 @@ filter: [Scratch.TargetType.SPRITE], opcode: "touchingrect", blockType: Scratch.BlockType.BOOLEAN, - text: "touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?", + text: Scratch.translate( + "touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?" + ), arguments: { X1: { type: Scratch.ArgumentType.NUMBER, @@ -204,7 +216,24 @@ menus: { WHAT: { acceptreporters: true, - items: ["width", "height", "costume width", "costume height"], + items: [ + { + text: Scratch.translate("width"), + value: "width", + }, + { + text: Scratch.translate("height"), + value: "height", + }, + { + text: Scratch.translate("costume width"), + value: "costume width", + }, + { + text: Scratch.translate("costume height"), + value: "costume height", + }, + ], }, }, }; diff --git a/extensions/NexusKitten/sgrab.js b/extensions/NexusKitten/sgrab.js index 6066bea0d2..c948cb1e40 100644 --- a/extensions/NexusKitten/sgrab.js +++ b/extensions/NexusKitten/sgrab.js @@ -17,7 +17,7 @@ getInfo() { return { id: "nexuskittensgrab", - name: "S-Grab", + name: Scratch.translate("S-Grab"), menuIconURI: icon, color1: "#ECA90B", color2: "#EBAF00", @@ -25,7 +25,7 @@ { opcode: "usergrab", blockType: Scratch.BlockType.REPORTER, - text: "grab [WHAT] count of user [WHO]", + text: Scratch.translate("grab [WHAT] count of user [WHO]"), arguments: { WHAT: { type: Scratch.ArgumentType.STRING, @@ -40,7 +40,7 @@ { opcode: "rankusergrab", blockType: Scratch.BlockType.REPORTER, - text: "global [WHAT] ranking for [WHO]", + text: Scratch.translate("global [WHAT] ranking for [WHO]"), arguments: { WHAT: { type: Scratch.ArgumentType.STRING, @@ -55,7 +55,7 @@ { opcode: "usergrab2", blockType: Scratch.BlockType.REPORTER, - text: "[WHAT] of user [WHO]", + text: Scratch.translate("[WHAT] of user [WHO]"), arguments: { WHAT: { type: Scratch.ArgumentType.STRING, @@ -71,7 +71,7 @@ { opcode: "projectgrab", blockType: Scratch.BlockType.REPORTER, - text: "grab [WHAT] count of project id [WHO]", + text: Scratch.translate("grab [WHAT] count of project id [WHO]"), arguments: { WHAT: { type: Scratch.ArgumentType.STRING, @@ -86,7 +86,9 @@ { opcode: "rankprojectgrab", blockType: Scratch.BlockType.REPORTER, - text: "global [WHAT] ranking for project id [WHO]", + text: Scratch.translate( + "global [WHAT] ranking for project id [WHO]" + ), arguments: { WHAT: { type: Scratch.ArgumentType.STRING, @@ -101,7 +103,7 @@ { opcode: "idtoname", blockType: Scratch.BlockType.REPORTER, - text: "name of project id [WHO]", + text: Scratch.translate("name of project id [WHO]"), arguments: { WHO: { type: Scratch.ArgumentType.STRING, @@ -112,7 +114,7 @@ { opcode: "idtoowner", blockType: Scratch.BlockType.REPORTER, - text: "creator of project id [WHO]", + text: Scratch.translate("creator of project id [WHO]"), arguments: { WHO: { type: Scratch.ArgumentType.STRING, @@ -124,23 +126,96 @@ menus: { WHAT: { acceptReporters: true, - items: ["follower", "following"], + items: [ + { + text: Scratch.translate("follower"), + value: "follower", + }, + { + text: Scratch.translate("following"), + value: "following", + }, + ], }, WHAT2: { acceptReporters: true, - items: ["follower", "love", "favorite", "view"], + items: [ + { + text: Scratch.translate("follower"), + value: "follower", + }, + { + text: Scratch.translate("love"), + value: "love", + }, + { + text: Scratch.translate("favorite"), + value: "favorite", + }, + { + text: Scratch.translate("view"), + value: "view", + }, + ], }, WHAT3: { acceptReporters: true, - items: ["love", "favorite", "view"], + items: [ + { + text: Scratch.translate("love"), + value: "love", + }, + { + text: Scratch.translate("favorite"), + value: "favorite", + }, + { + text: Scratch.translate("view"), + value: "view", + }, + ], }, WHAT4: { acceptReporters: true, - items: ["love", "favorite", "view"], + items: [ + { + text: Scratch.translate("love"), + value: "love", + }, + { + text: Scratch.translate("favorite"), + value: "favorite", + }, + { + text: Scratch.translate("view"), + value: "view", + }, + ], }, WHAT5: { acceptReporters: true, - items: ["about me", "wiwo", "location", "status"], + items: [ + { + text: Scratch.translate("about me"), + value: "about me", + }, + { + text: Scratch.translate({ + default: "wiwo", + description: + "WIWO stands for 'What I'm Working On', part of the Scratch profile page.", + }), + value: "wiwo", + }, + { + text: Scratch.translate("location"), + value: "location", + }, + { + text: Scratch.translate("status"), + value: "status", + }, + ], }, }, }; diff --git a/extensions/cs2627883/numericalencoding.js b/extensions/cs2627883/numericalencoding.js index 5212283722..57b44c2dd2 100644 --- a/extensions/cs2627883/numericalencoding.js +++ b/extensions/cs2627883/numericalencoding.js @@ -7,80 +7,99 @@ (function (Scratch) { "use strict"; + + // There are 149,186 unicode characters, so the maximum character code length is 6 + const MAX_CHAR_LEN = 6; + + /** + * @param {string} str + * @returns {string} + */ + const encode = (str) => { + let encoded = ""; + for (let i = 0; i < str.length; ++i) { + // Get character + const char = String(str.charCodeAt(i)); + // Pad encodedChar with 0s to ensure all encodedchars are the same length + const encodedChar = "0".repeat(MAX_CHAR_LEN - char.length) + char; + encoded += encodedChar; + } + return encoded; + }; + + /** + * @param {string} str + * @returns {string} + */ + const decode = (str) => { + if (str === "") { + return ""; + } + let decoded = ""; + // Create regex to split by char length + const regex = new RegExp(".{1," + MAX_CHAR_LEN + "}", "g"); + // Split into array of characters + const split = str.match(regex); + for (let i = 0; i < split.length; i++) { + // Get character from char code + const decodedChar = String.fromCharCode(+split[i]); + decoded += decodedChar; + } + return decoded; + }; + class NumericalEncodingExtension { - maxcharlength = 6; // There are 149,186 unicode characters, so the maximum character code length is 6 + /** @type {string|number} */ encoded = 0; + + /** @type {string|number} */ decoded = 0; + getInfo() { return { id: "cs2627883NumericalEncoding", - name: "Numerical Encoding", + name: Scratch.translate("Numerical Encoding"), blocks: [ { opcode: "NumericalEncode", blockType: Scratch.BlockType.COMMAND, - text: "Encode [DATA] to numbers", + text: Scratch.translate("Encode [DATA] to numbers"), arguments: { DATA: { type: Scratch.ArgumentType.STRING, - defaultValue: "Hello!", + defaultValue: Scratch.translate("Hello!"), }, }, }, { opcode: "NumericalDecode", blockType: Scratch.BlockType.COMMAND, - text: "Decode [ENCODED] back to text", + text: Scratch.translate("Decode [ENCODED] back to text"), arguments: { ENCODED: { type: Scratch.ArgumentType.STRING, - defaultValue: "000072000101000108000108000111000033", //Encoded "Hello!" + defaultValue: encode(Scratch.translate("Hello!")), }, }, }, { opcode: "GetNumericalEncoded", blockType: Scratch.BlockType.REPORTER, - text: "encoded", + text: Scratch.translate("encoded"), }, { opcode: "GetNumericalDecoded", blockType: Scratch.BlockType.REPORTER, - text: "decoded", + text: Scratch.translate("decoded"), }, ], }; } NumericalEncode(args) { - const toencode = String(args.DATA); - var encoded = ""; - for (let i = 0; i < toencode.length; ++i) { - // Get char code of character - var encodedchar = String(toencode.charCodeAt(i)); - // Pad encodedchar with 0s to ensure all encodedchars are the same length - encodedchar = - "0".repeat(this.maxcharlength - encodedchar.length) + encodedchar; - encoded += encodedchar; - } - this.encoded = encoded; + this.encoded = encode(Scratch.Cast.toString(args.DATA)); } NumericalDecode(args) { - const todecode = String(args.ENCODED); - if (todecode == "") { - this.decoded = ""; - return; - } - var decoded = ""; - // Create regex to split by char length - const regex = new RegExp(".{1," + this.maxcharlength + "}", "g"); - // Split into array of characters - var encodedchars = todecode.match(regex); - for (let i = 0; i < encodedchars.length; i++) { - // Get character from char code - var decodedchar = String.fromCharCode(encodedchars[i]); - decoded += decodedchar; - } - this.decoded = decoded; + this.decoded = decode(Scratch.Cast.toString(args.ENCODED)); } GetNumericalEncoded(args) { return this.encoded; @@ -90,14 +109,5 @@ } } - // Test Code - /* - encoding = new NumericalEncodingExtension(); - encodingNumericalEncode({"DATA": 'Hello!'}); - console.log(encoding.GetNumericalEncoded()) - encoding.NumericalDecode({"ENCODED": encoding.GetNumericalEncoded()}); - console.log(encoding.GetNumericalDecoded()); - */ - Scratch.extensions.register(new NumericalEncodingExtension()); })(Scratch); From 996a0074de0bf7890790dad5255d94de72a9f5c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:07:30 -0600 Subject: [PATCH 059/196] build(deps-dev): bump eslint from 8.53.0 to 8.54.0 (#1160) --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index a48398b8bb..fec81801a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,9 +60,9 @@ } }, "@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true }, "@humanwhocodes/config-array": { @@ -432,15 +432,15 @@ "dev": true }, "eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -698,9 +698,9 @@ } }, "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "requires": { "flatted": "^3.2.9", @@ -827,9 +827,9 @@ } }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true }, "image-size": { diff --git a/package.json b/package.json index d62d006187..59fdbdfebb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "markdown-it": "^13.0.2" }, "devDependencies": { - "eslint": "^8.53.0", + "eslint": "^8.54.0", "espree": "^9.6.1", "esquery": "^1.5.0", "prettier": "^3.1.0" From 8915ea371038ce47c0538ec1264427ea45f5c156 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sat, 25 Nov 2023 17:31:12 -0600 Subject: [PATCH 060/196] Update translations (#1164) --- translations/extension-metadata.json | 117 ++- translations/extension-runtime.json | 1453 +++++++++++++++++++++++++- 2 files changed, 1556 insertions(+), 14 deletions(-) diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index ea5fc2910a..bf550a2a0f 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -7,25 +7,64 @@ }, "de": { "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools Ihres Browsers integrierten JavaScript-Konsole interagieren.", + "-SIPC-/consoles@name": "Konsolen", + "-SIPC-/time@description": "Blöcke fürs Interagieren mit Unix-Zeitstempeln und andere Datenstrings.", "-SIPC-/time@name": "Zeit", "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem.", + "Alestore/nfcwarp@description": "Erlaubt es, Daten von NFC-Geräten (NDEF) zu erhalten. Funktioniert nur mit Chrome auf Android.", + "CST1229/zip@description": "Erstelle und bearbeite Dateien im .zip-Format, auch .sb3-Dateien.", + "Clay/htmlEncode@description": "Sichere unvertrauenswürdigen Text, sodass er sicher in HTML genutzt werden kann.", + "Clay/htmlEncode@name": "HTML-Sicherung", "CubesterYT/TurboHook@description": "Ermöglicht die Benutzung von Webhooks.", + "CubesterYT/WindowControls@description": "Bewege, ändere die Größe oder benenne das Fenster um, starte den Vollbildmodus, finde die Bildschirmgröße heraus, und mehr.", + "CubesterYT/WindowControls@name": "Fensterkontrolle", + "DNin/wake-lock@description": "Erlaube dem Computer nicht, in den Ruhemodus überzugehen", + "DNin/wake-lock@name": "Bleibe wach", "DT/cameracontrols@description": "Bewege den angezeigten Teil der Bühne.", + "DT/cameracontrols@name": "Kamerakontrolle (Sehr fehlerhaft)", + "JeremyGamer13/tween@description": "Methoden für sanftere Animationen", + "Lily/AllMenus@description": "Besondere Kategorie mit jedem Eingabefeld von jeder Scratchkategorie und -erweiterung.", "Lily/AllMenus@name": "Alle Menüs", + "Lily/Cast@description": "Verändere die Typen von Wertern.", + "Lily/Cast@name": "Typen", + "Lily/ClonesPlus@description": "Erweitert die Klonfunktionen von Scratch.", + "Lily/ClonesPlus@name": "Klone Plus", + "Lily/CommentBlocks@description": "Füge Kommentare zu deinen Skripten hinzu.", "Lily/CommentBlocks@name": "Kommentar Blöcke", + "Lily/HackedBlocks@description": "Verschiedene \"gehackte Blöcke\", die auf Scratch funktionieren, in der Palette allerdings nicht sichtbar sind.", + "Lily/HackedBlocks@name": "Versteckte Blöcke", "Lily/LooksPlus@description": "Erweitert die Kategorie \"Aussehen\", indem es dir ermöglicht, das Anzeigen/Verbergen, Abrufen von Kostümdaten und Bearbeiten von SVG-Skins auf Sprites zu steuern.", + "Lily/LooksPlus@name": "Aussehen Plus", + "Lily/McUtils@description": "Hilfreiche Blöcke für jeden Fast-Food-Angestellten.", + "Lily/McUtils@name": "McBlöcke", "Lily/MoreEvents@description": "Neue Wege, um deine Skripte zu starten.", "Lily/MoreEvents@name": "Mehr Ereignisse", + "Lily/MoreTimers@description": "Kontrolliere mehrere Stoppuhren auf einmal.", "Lily/MoreTimers@name": "Mehr Stoppuhren", + "Lily/Skins@description": "Gebe deinen Figuren andere Bilder als Kostüme.", "Lily/SoundExpanded@description": "Fügt mehr Blöcke hinzu, die mit Klängen zu tun haben.", "Lily/SoundExpanded@name": "Klänge Erweitert", "Lily/TempVariables2@name": "Temporäre Variablen", "Lily/Video@description": "Spiele Videos von URLs ab.", + "Lily/lmsutils@description": "Davor \"LMS Utilities\".", + "Lily/lmsutils@name": "Lily's Werkzeuge", "Longboost/color_channels@description": "Nur bestimmte RGB-Kanäle anzeigen oder hinterlasse Abdruck nur auf bestimmten RGB-Kanälen.", "Longboost/color_channels@name": "RGB Kanäle", + "NOname-awa/graphics2d@description": "Blöcke um Längen, Winkel und Flächen in zwei Dimensionen auszurechnen.", + "NOname-awa/graphics2d@name": "Grafiken 2D", + "NOname-awa/more-comparisons@description": "Mehr Vergleichsblöcke.", "NOname-awa/more-comparisons@name": "Mehr Vergleiche", + "NexusKitten/controlcontrols@description": "Zeige und verstecke Teile der Kontrollleiste eines Projekts.", + "NexusKitten/controlcontrols@name": "Kontrolle der Kontrollleiste", + "NexusKitten/moremotion@description": "Mehr Blöcke über Bewegung.", + "NexusKitten/moremotion@name": "Mehr Bewegung", "NexusKitten/sgrab@description": "Erhalte Informationen über Scratch Projekte und Scratch Benutzer.", "Skyhigh173/bigint@description": "Mathe Blöcke, die mit unendlich großen Ganzzahlen (ohne Dezimalstellen) arbeiten.", + "Skyhigh173/json@description": "Arbeite mit JSON-Strings und Listen.", + "TheShovel/CanvasEffects@description": "Setze Effekte für die ganze Bühne.", + "TheShovel/CanvasEffects@name": "Bühneneffekte", + "TheShovel/ColorPicker@description": "Nutze den Farbwähler des Systems.", + "TheShovel/ColorPicker@name": "Farbwähler", "ar@name": "Erweiterte Realität", "battery@name": "Batterie", "clipboard@name": "Zwischenablage", @@ -232,7 +271,7 @@ "-SIPC-/consoles@description": "Maak gebruik van de ingebouwde JavaScript-console van je browser.", "-SIPC-/time@description": "Maak gebruik van unix-tijden en andere datum-gerelateerde strings.", "-SIPC-/time@name": "Tijd", - "0832/rxFS2@description": "Blokken waarmee je gebruik kan maken van een virtueel bestandssysteem in het geheugen.", + "0832/rxFS2@description": "Maak gebruik van een virtueel bestandssysteem in het geheugen.", "Alestore/nfcwarp@description": "Lees gegevens af van NFC (NDEF) apparaten. Werkt alleen in Chrome op Android.", "CST1229/zip@description": "Creëer en bewerk .zip-bestanden, inclusief .sb3-bestanden.", "Clay/htmlEncode@description": "Maak strings veilig om gebruikt te worden in HTML.", @@ -243,7 +282,7 @@ "DNin/wake-lock@description": "Zorg ervoor dat het apparaat niet in de slaapstand gaat.", "DNin/wake-lock@name": "Wakker Houden", "DT/cameracontrols@description": "Verplaats het zichtbare deel van het speelveld.", - "DT/cameracontrols@name": "Camerabesturing (Heeft Bugs)", + "DT/cameracontrols@name": "Camerabesturing (Veel Bugs)", "JeremyGamer13/tween@description": "Maak je animaties soepeler met geleidelijke functies.", "JeremyGamer13/tween@name": "Tweening", "Lily/AllMenus@description": "Gebruik alle bestaande dropdown-menu's als losse blokken, zelfs die van extensies.", @@ -321,7 +360,7 @@ "fetch@description": "Maak verzoeken op het brede internet.", "files@description": "Lees en download bestanden.", "files@name": "Bestanden", - "gamejolt@description": "Maak interactie met de API van GameJolt mogelijk. Niet officieel.", + "gamejolt@description": "Maak interactie met de API van GameJolt mogelijk. Niet officieel. Opmerking: de vertalingen voor de blokken van deze extensie kunnen inaccuraat zijn.", "gamepad@description": "Lees direct de signalen van gamepads af in plaats van knoppen met toetsen te verbinden.", "godslayerakp/http@description": "Maak interactie met externe websites mogelijk met deze uitgebreide extensie.", "godslayerakp/ws@description": "Verbind handmatig met WebSocket-servers.", @@ -329,7 +368,7 @@ "itchio@description": "Maak interactie met de website itch.io mogelijk. Niet officieel.", "lab/text@description": "Toon en animeer tekst op een simpele manier. Compatibel met het experiment Animated Text van Scratch Lab.", "lab/text@name": "Geanimeerde Tekst", - "local-storage@description": "Sla gegevens aanhoudend op. Zoals cookies, maar dan beter.", + "local-storage@description": "Sla gegevens aanhoudend op. Vergelijkbaar met cookies, maar dan beter.", "local-storage@name": "Lokale Opslag", "mdwalters/notifications@description": "Geef notificaties weer.", "mdwalters/notifications@name": "Notificaties", @@ -343,8 +382,36 @@ "penplus@name": "Pen Uitgebreid V5 (Verouderd)", "pointerlock@description": "Zet de muisaanwijzer vast. De blokken muis x & muis y geven de verandering in muispositie sinds de vorige frame.", "pointerlock@name": "Muisaanwijzer-Vergrendeling", + "qxsck/data-analysis@description": "Bereken gemiddelde, mediaan, maximum, minimum, variantie en modus van een reeks getallen.", + "qxsck/data-analysis@name": "Gegevens Analyseren", + "qxsck/var-and-list@description": "Verwerk je gegevens beter met deze nieuwe blokken gerelateerd aan variabelen en lijsten.", + "qxsck/var-and-list@name": "Variabelen en Lijsten", + "rixxyx@description": "Maak gebruik van diverse nuttige blokken.", + "runtime-options@description": "Lees en verander turbomodus, framerate, interpolatie, kloonlimiet, speelveldgrootte en meer.", "runtime-options@name": "Looptijdopties", - "text@name": "Tekst" + "shreder95ua/resolution@description": "Lees de resolutie van het primaire beeldscherm af.", + "shreder95ua/resolution@name": "Schermresolutie", + "sound@description": "Speel geluiden af vanuit URL's.", + "sound@name": "Geluid", + "stretch@description": "Rek je sprites horizontaal en verticaal uit.", + "stretch@name": "Rekken", + "text@description": "Manipuleer tekens en tekst.", + "text@name": "Tekst", + "true-fantom/base@description": "Zet getallen tussen bases om.", + "true-fantom/couplers@description": "Maak gebruik van een paar adapter-blokken.", + "true-fantom/couplers@name": "Koppelingen", + "true-fantom/math@description": "Maak gebruik van een hoop functieblokken, van machtsverheffen naar trigonometrische functies.", + "true-fantom/math@name": "Wiskunde", + "true-fantom/network@description": "Maak gebruik van het netwerk met een paar netwerk-gerelateerde blokken.", + "true-fantom/network@name": "Netwerk", + "true-fantom/regexp@description": "Werk met Reguliere Expressies met behulp van een volledige interface.", + "utilities@description": "Maak gebruik van een verzameling interessante blokken.", + "utilities@name": "Utiliteiten", + "veggiecan/LongmanDictionary@description": "Krijg de definities van woorden uit het Longman Dictionary in je projecten. LET OP: alleen in het Engels.", + "veggiecan/browserfullscreen@description": "Schakel het volledig scherm van je browser in of uit.", + "veggiecan/browserfullscreen@name": "Browser Volledig Scherm", + "vercte/dictionaries@description": "Gebruik de kracht van JSON-woordenboeken in je project.", + "vercte/dictionaries@name": "JSON-Woordenboeken" }, "pl": { "runtime-options@name": "Opcje Uruchamiania" @@ -357,12 +424,17 @@ }, "ru": { "NOname-awa/graphics2d@name": "Графика 2D", + "NexusKitten/controlcontrols@name": "Настройки управления", "battery@name": "Батарея", "clipboard@name": "Буфер обмена", "clouddata-ping@name": "Пинг облачных данных", "cursor@name": "Курсор мыши", "encoding@name": "Кодировка", - "runtime-options@name": "Опции Выполнения" + "files@name": "Файлы", + "gamepad@name": "Геймпад", + "runtime-options@name": "Опции Выполнения", + "sound@name": "Звук", + "stretch@name": "Растяжение" }, "sl": { "runtime-options@name": "Možnosti izvajanja" @@ -386,35 +458,66 @@ "Alestore/nfcwarp@description": "允许从NFC(NDFF)硬件读取数据。仅支持Andriod设备上的Chrome浏览器。", "Alestore/nfcwarp@name": "NFC", "CST1229/zip@description": "创建和编辑.zip格式的文件,包括.sb3的文件。", + "Clay/htmlEncode@description": "将不受信任的字符转义,使其能安全的包括在HTML中。", "Clay/htmlEncode@name": "HTML编码", + "CubesterYT/TurboHook@description": "允许你使用网络钩子。", + "CubesterYT/TurboHook@name": "Turbo网络钩子", + "CubesterYT/WindowControls@description": "移动、调整大小、重命名窗口、输入全屏、获取屏幕大小等等。", + "CubesterYT/WindowControls@name": "网页控制", + "DNin/wake-lock@description": "防止电脑进入睡眠状态。", + "DNin/wake-lock@name": "保持唤醒", + "DT/cameracontrols@description": "移动舞台上的可见部分。", + "DT/cameracontrols@name": "摄像头控制器(有缺陷)", + "JeremyGamer13/tween@description": "实现简单的平滑动画。", "JeremyGamer13/tween@name": "缓动", + "Lily/AllMenus@description": "将Scratch的拓展和特殊类别用菜单进行分类。", "Lily/AllMenus@name": "全部菜单", "Lily/Cast@description": "转换Scratch的资料类型", "Lily/Cast@name": "类型转换", + "Lily/ClonesPlus@description": "更多的Scratch克隆功能。", "Lily/ClonesPlus@name": "克隆+", "Lily/CommentBlocks@description": "给代码添加注释
    ", "Lily/CommentBlocks@name": "注释", + "Lily/HackedBlocks@name": "隐藏积木块", "Lily/LooksPlus@name": "外观+", "Lily/MoreEvents@name": "更多事件", "Lily/MoreTimers@name": "更多计时器", + "Lily/Skins@name": "造型", + "Lily/TempVariables2@name": "临时变量", "Lily/Video@name": "视频", "Lily/lmsutils@name": "Lily 的工具箱", "NOname-awa/graphics2d@name": "图形 2D", + "NOname-awa/more-comparisons@description": "更多关于比较的积木。", + "NOname-awa/more-comparisons@name": "更多比较", "NexusKitten/controlcontrols@name": "控件控制", "NexusKitten/moremotion@name": "更多运动", + "Skyhigh173/bigint@name": "高精度", "Skyhigh173/json@description": "处理JSON字符串和数组", + "TheShovel/CanvasEffects@name": "画笔特效", + "bitwise@description": "在Scratch使用二进制", + "bitwise@name": "位运算", "box2d@name": "Box2D 物理引擎", "clipboard@name": "剪切板", + "clouddata-ping@name": "云数据检测", + "cloudlink@name": "云链接", + "cursor@name": "鼠标图标", "encoding@name": "编码", + "fetch@name": "请求API", "files@name": "文件", - "lab/text@name": "动画文字", + "iframe@name": "内嵌框架", + "lab/text@name": "艺术字", + "local-storage@name": "临时变量", "obviousAlexC/SensingPlus@name": "侦测+", "obviousAlexC/penPlus@name": "画笔+ V6", "penplus@name": "画笔+ V5(旧)", + "qxsck/data-analysis@description": "关于数据分析的积木,如平均数,最大值,最小值,中位数,众数,方差等。", "qxsck/data-analysis@name": "数据分析", + "qxsck/var-and-list@description": "关于变量与列表的拓展积木。", "qxsck/var-and-list@name": "变量与列表", "runtime-options@name": "运行选项", + "sound@description": "从URL播放声音。", "sound@name": "声音", + "stretch@description": "改变角色的伸缩比例", "stretch@name": "伸缩", "text@name": "文本", "true-fantom/base@name": "进制转换", diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 3999ca118f..c3e26c7fe3 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -1,17 +1,31 @@ { "ca": { "files@_Select or drop file": "Selecciona o deixa anar el fitxer", + "gamejolt@_Close": "Tanca", "runtime-options@_Runtime Options": "Opcions d'execució" }, "cs": { + "-SIPC-/consoles@_Error": "Chyba", "files@_Select or drop file": "Vyberte nebo přetáhněte soubor", + "gamejolt@_Close": "Zavřít", "runtime-options@_Runtime Options": "Nastavení běhu" }, "de": { + "-SIPC-/consoles@_Consoles": "Konsolen", + "-SIPC-/consoles@_Error": "Fehler", + "-SIPC-/consoles@_Time": "Zeit", + "-SIPC-/time@_Time": "Zeit", + "Clay/htmlEncode@_HTML Encode": "HTML-Sicherung", + "CubesterYT/WindowControls@_Window Controls": "Fensterkontrolle", + "DNin/wake-lock@_Wake Lock": "Bleibe wach", + "NOname-awa/graphics2d@name": "Grafiken 2D", + "NexusKitten/controlcontrols@_Control Controls": "Kontrolle der Kontrollleiste", + "NexusKitten/moremotion@_More Motion": "Mehr Bewegung", "battery@_Battery": "Batterie", "clipboard@_Clipboard": "Zwischenablage", "files@_Files": "Dateien", "files@_Select or drop file": "Datei auswählen oder ziehen", + "gamejolt@_Close": "Schließen", "lab/text@_Animated Text": "Animierter Text", "runtime-options@_Runtime Options": "Laufzeit-Optionen", "sound@_Sound": "Klänge" @@ -29,17 +43,50 @@ "NOname-awa/graphics2d@name": "Gráficos 2D", "NOname-awa/graphics2d@radius": "radio", "files@_Select or drop file": "Selecciona o suelta aquí un archivo", + "gamejolt@_Close": "Cerrar", "runtime-options@_Runtime Options": "Opciones de Runtime" }, "fr": { + "-SIPC-/consoles@_Error": "Erreur", "files@_Select or drop file": "Sélectionne ou dépose un fichier", + "gamejolt@_Close": "Fermer", "runtime-options@_Runtime Options": "Options d'exécution" }, "hu": { + "-SIPC-/consoles@_Error": "Hiba", "files@_Select or drop file": "Válasszon ki, vagy húzzon ide egy fájlt", + "gamejolt@_Close": "Bezárás", "runtime-options@_Runtime Options": "Lefutási Opciók" }, "it": { + "-SIPC-/consoles@_Clear Console": "Svuota Console", + "-SIPC-/consoles@_Consoles": "Console Javascript", + "-SIPC-/consoles@_Create a collapsed group named [string]": "Crea il gruppo collassato [string]", + "-SIPC-/consoles@_Create a group named [string]": "Crea il gruppo [string]", + "-SIPC-/consoles@_End the timer named [string] and print the time elapsed from start to end": "Termina il timer [string]e stampa il tempo passato tra l'inizio e la fine", + "-SIPC-/consoles@_Error": "Errore", + "-SIPC-/consoles@_Error [string]": "Errore [string]", + "-SIPC-/consoles@_Exit the current group": "Esci dal gruppo attuale", + "-SIPC-/consoles@_Information": "Informazioni", + "-SIPC-/consoles@_Information [string]": "Inserisci informazione [string]", + "-SIPC-/consoles@_Print the time run by the timer named [string]": "Stampa il tempo indicato dal timer [string]", + "-SIPC-/consoles@_Start a timer named [string]": "Avvia il timer [string]", + "-SIPC-/consoles@_Time": "Unix Time", + "-SIPC-/consoles@_Warning": "Avviso", + "-SIPC-/consoles@_Warning [string]": "Avviso [string]", + "-SIPC-/consoles@_group": "gruppo", + "-SIPC-/time@_Time": "Unix Time", + "-SIPC-/time@_convert [time] to timestamp": "convert [time] in timestamp", + "-SIPC-/time@_convert [timestamp] to datetime": "converti [timestamp] to datetime", + "-SIPC-/time@_current time zone": "fuso orario attuale", + "-SIPC-/time@_current timestamp": "timestamp attuale", + "-SIPC-/time@_day": "giorno", + "-SIPC-/time@_get [Timedata] from [timestamp]": "estrai [Timedata] da [timestamp]", + "-SIPC-/time@_hour": "ora", + "-SIPC-/time@_minute": "minuti", + "-SIPC-/time@_month": "mese", + "-SIPC-/time@_second": "secondi", + "-SIPC-/time@_year": "anno", "0832/rxFS2@clean": "Svuota il file system", "0832/rxFS2@del": "Rimuovi [STR]", "0832/rxFS2@folder": "Imposta [STR] a [STR2]", @@ -52,6 +99,120 @@ "0832/rxFS2@start": "Crea [STR]", "0832/rxFS2@sync": "Cambia la posizione di [STR] in [STR2]", "0832/rxFS2@webin": "Leggi [STR] dal web", + "Alestore/nfcwarp@_NFC supported?": "NFC supportato", + "Alestore/nfcwarp@_NFCWarp": "Sensore di Prossimità (NFC)", + "Alestore/nfcwarp@_Only works in Chrome on Android": "Funziona soltanto in Chrome per Android", + "Alestore/nfcwarp@_read NFC tag": "leggi tag NFC", + "CST1229/zip@_1 (fast, large)": "1 (veloce, grande)", + "CST1229/zip@_9 (slowest, smallest)": "9 (più lento, piccolo)", + "CST1229/zip@_Hello, world?": "Ciao mondo", + "CST1229/zip@_[META] of [FILE]": "[META] di [FILE]", + "CST1229/zip@_[OBJECT] exists?": "[OBJECT] esiste", + "CST1229/zip@_any text": "qualunque testo", + "CST1229/zip@_archive comment": "commento archivio", + "CST1229/zip@_archive is open?": "l'archivio è aperto", + "CST1229/zip@_binary": "binario", + "CST1229/zip@_close archive": "chiudi archivio", + "CST1229/zip@_comment": "commento", + "CST1229/zip@_contents of directory [DIR]": "contenuto della cartella [DIR]", + "CST1229/zip@_create directory [DIR]": "crea cartella [DIR]", + "CST1229/zip@_create empty archive": "crea archivio vuoto", + "CST1229/zip@_current directory path": "percorso cartella attuale", + "CST1229/zip@_delete [FILE]": "cancella [FILE]", + "CST1229/zip@_file [FILE] as [TYPE]": "file [FILE] come [TYPE]", + "CST1229/zip@_folder": "cartella", + "CST1229/zip@_go to directory [DIR]": "vai alla cartella [DIR]", + "CST1229/zip@_long modification date": "date modifica lunga", + "CST1229/zip@_modification date": "data modifica", + "CST1229/zip@_modified days since 2000": "giorni modifica dal 2000", + "CST1229/zip@_name": "nome", + "CST1229/zip@_new file": "nuovo file", + "CST1229/zip@_new folder": "nuova cartella", + "CST1229/zip@_no compression (fastest)": "nessuna compressione (più veloce)", + "CST1229/zip@_open zip from [TYPE] [DATA]": "apri zip da [TYPE] [DATA]", + "CST1229/zip@_output zip type [TYPE] compression level [COMPRESSION]": "crea zip di tipo [TYPE] con livello di compressione [COMPRESSION]", + "CST1229/zip@_path": "percorso", + "CST1229/zip@_path [PATH] from [ORIGIN]": "percorso [PATH] da [ORIGIN]", + "CST1229/zip@_set [META] of [FILE] to [VALUE]": "imposta [META] di [FILE] a [VALUE]", + "CST1229/zip@_set archive comment to [COMMENT]": "imposta commento archivio a [COMMENT]", + "CST1229/zip@_string": "stringa", + "CST1229/zip@_text": "testo", + "CST1229/zip@_unix modified timestamp": "timestamp unix modifica", + "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "scrivi file [FILE] con contenuto [CONTENT] e tipo [TYPE]", + "Clay/htmlEncode@_HTML Encode": "HTML Encoding", + "Clay/htmlEncode@_Hello!": "Ciao!", + "Clay/htmlEncode@_encode [text] as HTML-safe": "codifica [text] come HTML sicuro", + "CubesterYT/TurboHook@_content": "contenuto", + "CubesterYT/TurboHook@_icon": "icona", + "CubesterYT/TurboHook@_name": "nome", + "CubesterYT/TurboHook@_webhook data: [hookDATA] webhook url: [hookURL]": "dati webhook: [hookDATA] url webhook: [hookURL]", + "CubesterYT/WindowControls@_Hello World!": "Ciao Mondo", + "CubesterYT/WindowControls@_May not work in normal browser tabs": "Potrebbe non funzionare nelle normali schede del browser", + "CubesterYT/WindowControls@_Refer to Documentation for details": "Per i dettagli fare riferimento alla Documetazione", + "CubesterYT/WindowControls@_Window Controls": "Controlli Finestra", + "CubesterYT/WindowControls@_bottom": "in fondo", + "CubesterYT/WindowControls@_bottom left": "angolo sinistra in basso", + "CubesterYT/WindowControls@_bottom right": "angolo destra in basso", + "CubesterYT/WindowControls@_center": "centro", + "CubesterYT/WindowControls@_change window height by [H]": "cambia altezza finestra di [H]", + "CubesterYT/WindowControls@_change window width by [W]": "cambia larghezza finestra di [W]", + "CubesterYT/WindowControls@_change window x by [X]": "cambia x finestra di [X]", + "CubesterYT/WindowControls@_change window y by [Y]": "cambia y finestra di [Y]", + "CubesterYT/WindowControls@_close window": "chiudi finestra", + "CubesterYT/WindowControls@_enter fullscreen": "passa a schermo intero", + "CubesterYT/WindowControls@_exit fullscreen": "esci da schermo intero", + "CubesterYT/WindowControls@_is window focused?": "finestra in primo piano", + "CubesterYT/WindowControls@_is window fullscreen?": "la finestra è a schermo intero", + "CubesterYT/WindowControls@_is window touching screen edge?": "la finestra tocca il bordo dello schermo", + "CubesterYT/WindowControls@_left": "a sinistra", + "CubesterYT/WindowControls@_match stage size": "dimensione Stage", + "CubesterYT/WindowControls@_move window to the [PRESETS]": "sposta finestra a [PRESETS]", + "CubesterYT/WindowControls@_move window to x: [X] y: [Y]": "sposta finestra a x: [X] y: [Y]", + "CubesterYT/WindowControls@_random position": "posizione scelta a caso", + "CubesterYT/WindowControls@_resize window to [PRESETS]": "ridimensiona finestra a [PRESETS]", + "CubesterYT/WindowControls@_resize window to width: [W] height: [H]": "ridimensiona finestra a larghezza: [W] altezza: [H]", + "CubesterYT/WindowControls@_right": "a destra", + "CubesterYT/WindowControls@_screen height": "altezza schermo", + "CubesterYT/WindowControls@_screen width": "larghezza schermo", + "CubesterYT/WindowControls@_set window height to [H]": "porta altezza finestra a [H]", + "CubesterYT/WindowControls@_set window title to [TITLE]": "imposta titolo finestra a [TITLE]", + "CubesterYT/WindowControls@_set window width to [W]": "porta larghezza finestra a [W]", + "CubesterYT/WindowControls@_set window x to [X]": "sposta finestra a x [X]", + "CubesterYT/WindowControls@_set window y to [Y]": "sposta finestra a y [Y]", + "CubesterYT/WindowControls@_top": "in cima", + "CubesterYT/WindowControls@_top left": "angolo sinistra in alto", + "CubesterYT/WindowControls@_top right": "angolo destra in alto", + "CubesterYT/WindowControls@_window height": "altezza finestra", + "CubesterYT/WindowControls@_window title": "titolo finestra", + "CubesterYT/WindowControls@_window width": "larghezza finestra", + "CubesterYT/WindowControls@_window x": "x finestra", + "CubesterYT/WindowControls@_window y": "y finestra", + "CubesterYT/WindowControls@editorConfirmation": "Sei sicuro di voler chiudere questa finestra?\n\n(Questo messaggio non apparirà se si usa il packager)", + "DNin/wake-lock@_Wake Lock": "Blocco Standby", + "DNin/wake-lock@_is wake lock active?": "blocco sveglia attivo ", + "DNin/wake-lock@_off": "disabilita", + "DNin/wake-lock@_on": "abilita", + "DNin/wake-lock@_turn wake lock [enabled]": "[enabled] blocco sveglia", + "DT/cameracontrols@_Camera (Very Buggy)": "Camera (Presenti Diversi Bug)", + "DT/cameracontrols@_background color": "colore di sfondo", + "DT/cameracontrols@_camera direction": "direzione camera", + "DT/cameracontrols@_camera x": "x camera", + "DT/cameracontrols@_camera y": "y camera", + "DT/cameracontrols@_camera zoom": "zoom camera", + "DT/cameracontrols@_change camera x by [val]": "cambia camera x di [val]", + "DT/cameracontrols@_change camera y by [val]": "cambia camera y di [val]", + "DT/cameracontrols@_change camera zoom by [val]": "cambia zoom camera di [val]", + "DT/cameracontrols@_move camera [val] steps": "sposta camera di [val]passi", + "DT/cameracontrols@_move camera to [sprite]": "sposta la camera dove si trova [sprite]", + "DT/cameracontrols@_no sprites exist": "non ci sono sprite", + "DT/cameracontrols@_point camera towards [sprite]": "punta camera verso [sprite]", + "DT/cameracontrols@_set background color to [val]": "usa colore di sfondo [val]", + "DT/cameracontrols@_set camera direction to [val]": "porta direzione camera a [val]", + "DT/cameracontrols@_set camera to x: [x] y: [y]": "sposta la camera a x: [x] y: [y]", + "DT/cameracontrols@_set camera x to [val]": "porta camera x a [val]", + "DT/cameracontrols@_set camera y to [val]": "porta camera y a [val]", + "DT/cameracontrols@_set camera zoom to [val] %": "porta zoom camera a [val] %", + "DT/cameracontrols@_turn camera [image] [val] degrees": "gira camera [image] di [val] gradi", "NOname-awa/graphics2d@circumference": "circonferenza", "NOname-awa/graphics2d@diameter": "diametro", "NOname-awa/graphics2d@graph": "[CS] del grafo [graph]", @@ -64,6 +225,44 @@ "NOname-awa/graphics2d@round": "[CS] del cerchio [rd][a]", "NOname-awa/graphics2d@triangle": "[CS] del triangolo ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", "NOname-awa/graphics2d@triangle_s": "area del triangolo [s1] [s2] [s3]", + "NexusKitten/controlcontrols@_Control Controls": "Gestione Pulsanti di Controllo", + "NexusKitten/controlcontrols@_[OPTION] exists?": "[OPTION] esiste", + "NexusKitten/controlcontrols@_[OPTION] shown?": "[OPTION] visibile", + "NexusKitten/controlcontrols@_fullscreen": "schermo intero", + "NexusKitten/controlcontrols@_green flag": "bandiera verde", + "NexusKitten/controlcontrols@_hide [OPTION]": "nascondi [OPTION]", + "NexusKitten/controlcontrols@_pause": "pausa", + "NexusKitten/controlcontrols@_show [OPTION]": "mostra [OPTION]", + "NexusKitten/controlcontrols@_stop": "arresta", + "NexusKitten/moremotion@_More Motion": "Movimento Plus", + "NexusKitten/moremotion@_change x: [X] y: [Y]": "cambia x di [X] y di [Y]", + "NexusKitten/moremotion@_costume height": "altezza costume", + "NexusKitten/moremotion@_costume width": "larghezza costume", + "NexusKitten/moremotion@_direction to x: [X] y: [Y]": "direzione verso x: [X] y: [Y]", + "NexusKitten/moremotion@_distance from x: [X] y: [Y]": "distanza da x: [X] y: [Y]", + "NexusKitten/moremotion@_height": "altezza", + "NexusKitten/moremotion@_manually fence": "impedisci sprite fuori Stage", + "NexusKitten/moremotion@_move [PERCENT]% of the way to x: [X] y: [Y]": "percorri [PERCENT]% della distanza da x: [X] y: [Y]", + "NexusKitten/moremotion@_move [STEPS] steps towards x: [X] y: [Y]": "fai [STEPS] passi verso x: [X] y: [Y]", + "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "punta verso x: [X] y: [Y]", + "NexusKitten/moremotion@_rotation style": "stile rotazione", + "NexusKitten/moremotion@_sprite [WHAT]": "[WHAT] dello sprite", + "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "sta toccando rettangolo x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]", + "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "sta toccando x: [X] y: [Y]", + "NexusKitten/moremotion@_width": "larghezza", + "NexusKitten/sgrab@_[WHAT] of user [WHO]": "[WHAT] dell'utente [WHO]", + "NexusKitten/sgrab@_about me": "info su di me", + "NexusKitten/sgrab@_creator of project id [WHO]": "creatore del progetto con id [WHO]", + "NexusKitten/sgrab@_favorite": "favoriti", + "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "poszione globale per [WHAT] di [WHO] ", + "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "poszione globale per [WHAT] per il progetto con id [WHO] ", + "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "numero di [WHAT] del progetto con id [WHO]", + "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "numero di [WHAT] dell'utente [WHO]", + "NexusKitten/sgrab@_location": "posizione", + "NexusKitten/sgrab@_name of project id [WHO]": "nome del progetto con id [WHO]", + "NexusKitten/sgrab@_status": "stato", + "NexusKitten/sgrab@_view": "visualizzazioni", + "NexusKitten/sgrab@_wiwo": "Su Cosa Sto Lavorando", "battery@_Battery": "Batteria", "battery@_battery level": "livello della batteria", "battery@_charging?": "in carica", @@ -115,6 +314,12 @@ "clipboard@_when something is pasted": "quando qualcosa viene incollato", "clouddata-ping@_Ping Cloud Data": "Ping Dati Cloud", "clouddata-ping@_is cloud data server [SERVER] up?": "il server cloud [SERVER] è attivo", + "cs2627883/numericalencoding@_Decode [ENCODED] back to text": "decodifica [ENCODED] in testo", + "cs2627883/numericalencoding@_Encode [DATA] to numbers": "codifica [DATA] in numeri", + "cs2627883/numericalencoding@_Hello!": "Ciao!", + "cs2627883/numericalencoding@_Numerical Encoding": "Codifica Numerica", + "cs2627883/numericalencoding@_decoded": "decodificato", + "cs2627883/numericalencoding@_encoded": "codificato", "cursor@_Mouse Cursor": "Puntatore Mouse", "cursor@_bottom left": "angolo sinistra in basso", "cursor@_bottom right": "angolo destra in basso", @@ -152,6 +357,113 @@ "files@_set open file selector mode to [mode]": "imposta modalità di apertura file a [mode]", "files@_show modal": "mostra finestra", "files@_text": "testo", + "gamejolt@GameJoltAPI_gamejoltBool": "su Game Jolt", + "gamejolt@_1 point": "1 punto", + "gamejolt@_Achieve trophy of ID [ID]": "ottieni trofeo con ID [ID]", + "gamejolt@_Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]": "aggiungi punteggio [value] a utente [username]nella tabella con ID [ID] con testo [text] e commento [extraData]", + "gamejolt@_Add score [value] in table of ID [ID] with text [text] and comment [extraData]": "aggiungi punteggio [value] nella tabella con ID [ID] con testo [text] e commento [extraData]", + "gamejolt@_Autologin available?": "accesso automatico disponibile", + "gamejolt@_Close": "Chiudi", + "gamejolt@_Data Storage Blocks": "Blocchi Memorizzazione Dati", + "gamejolt@_Debug Blocks": "Blocchi Debug", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]": "recupera [amount][globalOrPerUser] [betterOrWorse] di [value] nella tabella con ID [ID]", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]": "recupera [amount][globalOrPerUser] nella tabella con ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]": "recupera [amount]dell'utente[username] [betterOrWorse] di [value] nella tabella con ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s in table of ID [ID]": "recupera [amount][username]nella tabella con ID [ID]", + "gamejolt@_Fetch [globalOrPerUser] keys matching with [pattern]": "recupera tutte le chiavi [globalOrPerUser] corrispondenti a [pattern]", + "gamejolt@_Fetch [trophyFetchGroup] trophies": "recupera [trophyFetchGroup]", + "gamejolt@_Fetch all [globalOrPerUser] keys": "recupera tutte le chiavi [globalOrPerUser]", + "gamejolt@_Fetch logged in user": "recupera utente collegato", + "gamejolt@_Fetch server's time": "restituisci ora del server", + "gamejolt@_Fetch trophy of ID[ID]": "recupera trofeo con ID [ID]", + "gamejolt@_Fetch user's [usernameOrID] by [fetchType]": "restituisci dati utente con [fetchType] [usernameOrID]", + "gamejolt@_Fetch user's friend IDs": "recupera ID amici dell'utente", + "gamejolt@_Fetched score [scoreDataType] at index [index]": "[trophyDataType] del punteggio recuperato alla posizione [index]", + "gamejolt@_Fetched score data in JSON": "converti in JSON i dati del punteggio recuparato", + "gamejolt@_Fetched trophies in JSON": "converti in JSON trofei recuperati ", + "gamejolt@_Fetched trophy [trophyDataType] at index [index]": "[trophyDataType] del trofeo alla posizione [index]", + "gamejolt@_Fetched user's [userDataType]": "[userDataType] dell'utente collegato", + "gamejolt@_Fetched user's data in JSON": "dati recuperati dell'utente in JSON ", + "gamejolt@_Fetched user's friend ID at index[index]": "ID recuperato in posizione [index] degli amici dell'utente", + "gamejolt@_Fetched user's friend IDs in JSON": "ID recuperati degli amici dell'utente in JSON ", + "gamejolt@_In debug mode?": "modalità debug attiva", + "gamejolt@_Last API error": "ultimo errore API", + "gamejolt@_Logged in user's username": "username dell'utente collegato", + "gamejolt@_Logged in?": "collegato", + "gamejolt@_Login automatically": "accedi automaticamente", + "gamejolt@_Login with [username] and [token]": "collegati con [username] e [token]", + "gamejolt@_Logout": "esci", + "gamejolt@_Open": "Apri", + "gamejolt@_Ping session": "ping sessione", + "gamejolt@_Remove [globalOrPerUser] data at [key]": "rimuovi dati [globalOrPerUser] alla chiave [key]", + "gamejolt@_Remove trophy of ID [ID]": "Rimuovi trofeo con ID [ID]", + "gamejolt@_Score Blocks": "Blocchi Punteggio", + "gamejolt@_Session Blocks": "blocchi sessione", + "gamejolt@_Session open?": "sessione aperta", + "gamejolt@_Set game ID to [ID] and private key to [key]": "imposta ID gioco a [ID] e chiave privata a [key]", + "gamejolt@_Set session status to [status]": "porta stato sessione a [status]", + "gamejolt@_Time Blocks": "Blocchi Tempo", + "gamejolt@_Trophy Blocks": "Blocchi Trofeo", + "gamejolt@_Turn debug mode [toggle]": " [toggle] modalità debug", + "gamejolt@_Update [globalOrPerUser] data at [key] by [operationType] [value]": "aggiorna dati [globalOrPerUser] alla chiave [key] [operationType] [value]", + "gamejolt@_User Blocks": "Blocchi Utente", + "gamejolt@_[openOrClose] session": "[openOrClose] sessione", + "gamejolt@_achievement date": "data risultato", + "gamejolt@_active": "attivo", + "gamejolt@_adding": "aggiungendo", + "gamejolt@_all": "tutti i trofei", + "gamejolt@_all achieved": "tutti i trofei ottenuti", + "gamejolt@_all unachieved": "tutti i trofei non ottenuti", + "gamejolt@_appending": "aggiungendo", + "gamejolt@_avatar URL": "URL avatar", + "gamejolt@_better": "megliori", + "gamejolt@_comment": "commento", + "gamejolt@_data": "dati", + "gamejolt@_day": "giorno", + "gamejolt@_description": "descrizione", + "gamejolt@_developer username": "username svilppatore", + "gamejolt@_difficulty": "difficoltà", + "gamejolt@_dividing by": "dividendo per", + "gamejolt@_global": "globali", + "gamejolt@_guest": "ospite", + "gamejolt@_hour": "ora", + "gamejolt@_idle": "sospeso", + "gamejolt@_image URL": "URL immagine", + "gamejolt@_in parallel": "in parallelo", + "gamejolt@_index": "posizione", + "gamejolt@_key": "chiave", + "gamejolt@_last login": "ultimo accesso", + "gamejolt@_last login timestamp": "timestap ultimo accesso", + "gamejolt@_minute": "minuti", + "gamejolt@_month": "mese", + "gamejolt@_multiplying by": "mutiplicando per", + "gamejolt@_name": "nome", + "gamejolt@_off": "disabilita", + "gamejolt@_on": "abilita", + "gamejolt@_optional": "opzionale", + "gamejolt@_prepending": "inserendo all'inizio", + "gamejolt@_primary": "primario", + "gamejolt@_private key": "chiave privata", + "gamejolt@_private token": "token privato", + "gamejolt@_score date": "data puntaggio", + "gamejolt@_score timestamp": "timestamp punteggio", + "gamejolt@_second": "secondi", + "gamejolt@_sequentially": "sequenzialmente", + "gamejolt@_sequentially, break on error": "sequenzialmente, ma arresta in casa di errore", + "gamejolt@_sign up date": "data registrazione", + "gamejolt@_sign up timestamp": "timestamp registrazione", + "gamejolt@_status": "stato", + "gamejolt@_subtracting": "sottraendo", + "gamejolt@_text": "testo", + "gamejolt@_timezone": "fuso orario", + "gamejolt@_title": "titolo", + "gamejolt@_type": "effetto digitazione", + "gamejolt@_user": "dell'utente", + "gamejolt@_user ID": "ID utente", + "gamejolt@_value": "valore", + "gamejolt@_website": "sito", + "gamejolt@_worse": "peggiori", + "gamejolt@_year": "anno", "gamepad@_D-pad down (14)": "Tasto direzionale giù (14)", "gamepad@_D-pad left (15)": "Tasto direzionale sinistra (15)", "gamepad@_D-pad right (16)": "Tasto direzionale destra (16)", @@ -197,6 +509,38 @@ "iframe@_show website [URL]": "mostra sito [URL]", "iframe@_visible": "visibilità", "iframe@_width": "larghezza", + "itchio@_Data": "Dati", + "itchio@_Error: Data not found.": "Errore: dato non trovato.", + "itchio@_Fetch game data [user][game][secret]": "recupera dati gioco [game] dell'utente [user] con chiave [secret]", + "itchio@_Game data?": "sono stati recuperati dati del gioco", + "itchio@_Open [prefix] itch.io [page] window with [width]width and [height]height": "apri finestra del gioco [page] dell'utente [prefix] di larghezza [width]e altezza [height]", + "itchio@_Return game [data]": "restituisci [data] del gioco ", + "itchio@_Return game data [user][game][secret] in .json": "restituisci json dei dati del gioco [game] dell'utente [user] con chiave [secret]", + "itchio@_Return game data in .json": "restituisci json dei dati del gioco", + "itchio@_Return game sale [sale]": "restituisci [sale] dei giochi in saldo", + "itchio@_Return rewards list length": "restituisci lunghezza lista premi", + "itchio@_Return sub products list length": "restituisci lunghezza lista sottoprodotti", + "itchio@_Rewards": "Premi", + "itchio@_Rewards?": "sono stati recuperati dati sui premi", + "itchio@_Sale": "Saldi", + "itchio@_Sub products": "Sottoprodotti", + "itchio@_Sub products?": "sono stati recuperati dati sui sottoprodotti", + "itchio@_Window": "Finestre", + "itchio@_amount": "quantità", + "itchio@_amount remaining": "quantità rimanente", + "itchio@_available": "disponibile", + "itchio@_cover image URL": "URL copertina", + "itchio@_end date": "data fine saldo", + "itchio@_game": "gioco", + "itchio@_game argument not found": "gioco non trovato", + "itchio@_name": "nome", + "itchio@_original price": "prezzo originale", + "itchio@_price": "prezzo", + "itchio@_title": "titolo", + "itchio@_user": "utente", + "itchio@_user argument not found": "utente non trovato", + "itchio@itchio_error": "Errore: ", + "itchio@itchio_errors": "Errori: ", "lab/text@_# of lines": "numero di righe", "lab/text@_Animated Text": "Testo Animato", "lab/text@_Enable Non-Scratch Lab Features": "Abilita blocchi non presenti in Scratch Lab", @@ -316,13 +660,21 @@ "stretch@_y stretch": "deformazione y" }, "ja": { + "-SIPC-/consoles@_Consoles": "コンソール", + "-SIPC-/consoles@_Error": "エラー", + "-SIPC-/consoles@_Time": "時間", + "-SIPC-/time@_Time": "時間", "0832/rxFS2@del": "[STR]を削除", "0832/rxFS2@open": "[STR]を開く", "0832/rxFS2@search": "[STR]を検索", "0832/rxFS2@start": "[STR]を作成", + "Clay/htmlEncode@_HTML Encode": "HTMLエンコード", + "Clay/htmlEncode@_Hello!": "こんにちは!", + "cs2627883/numericalencoding@_Hello!": "こんにちは!", "cursor@_Mouse Cursor": "マウスカーソル", "encoding@_apple": "りんご", "files@_Select or drop file": "選ぶかファイルをドロップする", + "gamejolt@_Close": "閉じる", "iframe@_url": "URL", "lab/text@_Hello!": "こんにちは!", "lab/text@_show sprite": "スプライトを表示", @@ -334,43 +686,665 @@ "runtime-options@_turbo mode": "ターボモード" }, "ja-hira": { + "-SIPC-/consoles@_Error": "エラー", + "gamejolt@_Close": "とじる", "runtime-options@_Runtime Options": "ランタイムのオプション" }, "ko": { + "-SIPC-/consoles@_Error": "오류", "files@_Select or drop file": "선택하거나 끌어다 놓기", + "gamejolt@_Close": "닫기", "runtime-options@_Runtime Options": "실행 설정" }, "lt": { + "-SIPC-/consoles@_Error": "Klaida", "files@_Select or drop file": "Pasirinkite arba numeskite failą", + "gamejolt@_Close": "Uždaryti", "runtime-options@_Runtime Options": "Paleidimo laiko parinktys" }, "nl": { + "-SIPC-/consoles@_Clear Console": "console wissen", + "-SIPC-/consoles@_Create a collapsed group named [string]": "creëer samengevouwen groep genaamd [string]", + "-SIPC-/consoles@_Create a group named [string]": "creëer groep genaamd [string]", + "-SIPC-/consoles@_Debug": "debug", + "-SIPC-/consoles@_Debug [string]": "debug [string]", + "-SIPC-/consoles@_End the timer named [string] and print the time elapsed from start to end": "beëindig timer genaamd [string] en print totale tijd ", + "-SIPC-/consoles@_Error": "fout", + "-SIPC-/consoles@_Error [string]": "fout [string]", + "-SIPC-/consoles@_Exit the current group": "verlaat huidige groep", + "-SIPC-/consoles@_Information": "informatie", + "-SIPC-/consoles@_Information [string]": "informatie [string]", + "-SIPC-/consoles@_Journal": "log", + "-SIPC-/consoles@_Journal [string]": "log [string]", + "-SIPC-/consoles@_Print the time run by the timer named [string]": "print tijd van timer genaamd [string]", + "-SIPC-/consoles@_Start a timer named [string]": "begin timer genaamd [string]", + "-SIPC-/consoles@_Time": "tijd", + "-SIPC-/consoles@_Warning": "waarschuwing", + "-SIPC-/consoles@_Warning [string]": "waarschuwing [string]", + "-SIPC-/consoles@_group": "groep", + "-SIPC-/time@_Time": "Tijd", + "-SIPC-/time@_convert [time] to timestamp": "[time] in tijdstempel", + "-SIPC-/time@_convert [timestamp] to datetime": "[timestamp] in datum en tijd", + "-SIPC-/time@_current time zone": "huidige tijdzone", + "-SIPC-/time@_current timestamp": "huidige tijdstempel", + "-SIPC-/time@_day": "dag", + "-SIPC-/time@_get [Timedata] from [timestamp]": "[Timedata] van [timestamp]", + "-SIPC-/time@_hour": "uur", + "-SIPC-/time@_minute": "minuut", + "-SIPC-/time@_month": "maand", + "-SIPC-/time@_second": "seconde", + "-SIPC-/time@_year": "jaar", + "0832/rxFS2@clean": "wis het bestandssysteem", + "0832/rxFS2@del": "verwijder [STR]", + "0832/rxFS2@folder": "maak [STR] [STR2]", + "0832/rxFS2@folder_default": "rxFS is geweldig!", + "0832/rxFS2@in": "importeer bestandssysteem van [STR]", + "0832/rxFS2@list": "alle bestanden onder [STR]", + "0832/rxFS2@open": "open [STR]", + "0832/rxFS2@out": "exporteer bestandssysteem", + "0832/rxFS2@search": "zoek [STR]", + "0832/rxFS2@start": "creëer [STR]", + "0832/rxFS2@sync": "verander locatie van [STR] naar [STR2]", + "0832/rxFS2@webin": "laad [STR] van het web", + "Alestore/nfcwarp@_NFC supported?": "NFC ondersteund?", + "Alestore/nfcwarp@_Only works in Chrome on Android": "Werkt alleen in Chrome op Android", + "Alestore/nfcwarp@_read NFC tag": "gegevens van NFC tag", + "CST1229/zip@_1 (fast, large)": "1 (snel, groot)", + "CST1229/zip@_9 (slowest, smallest)": "9 (traagst, kleinst)", + "CST1229/zip@_Hello, world?": "Hallo... wereld?", + "CST1229/zip@_[META] of [FILE]": "[META] van [FILE]", + "CST1229/zip@_[OBJECT] exists?": "[OBJECT] bestaat?", + "CST1229/zip@_any text": "tekst", + "CST1229/zip@_archive comment": "archiefopmerking", + "CST1229/zip@_archive is open?": "archief open?", + "CST1229/zip@_binary": "binair", + "CST1229/zip@_close archive": "sluit archief", + "CST1229/zip@_comment": "opmerking", + "CST1229/zip@_contents of directory [DIR]": "inhoud van map [DIR]", + "CST1229/zip@_create directory [DIR]": "creëer map [DIR]", + "CST1229/zip@_create empty archive": "creëer leeg archief", + "CST1229/zip@_current directory path": "huidig map-pad", + "CST1229/zip@_delete [FILE]": "verwijder [FILE]", + "CST1229/zip@_file [FILE] as [TYPE]": "bestand [FILE] als [TYPE]", + "CST1229/zip@_folder": "map", + "CST1229/zip@_go to directory [DIR]": "ga naar map [DIR]", + "CST1229/zip@_long modification date": "lange wijzigingsdatum", + "CST1229/zip@_modification date": "wijzigingsdatum", + "CST1229/zip@_modified days since 2000": "wijzigingsdatum in dagen sinds 2000", + "CST1229/zip@_name": "naam", + "CST1229/zip@_new file": "bestand", + "CST1229/zip@_new folder": "nieuwe map", + "CST1229/zip@_no compression (fastest)": "geen (snelst)", + "CST1229/zip@_open zip from [TYPE] [DATA]": "open zip van [TYPE] [DATA]", + "CST1229/zip@_output zip type [TYPE] compression level [COMPRESSION]": "zip als [TYPE] met comprimeerniveau [COMPRESSION]", + "CST1229/zip@_path": "pad", + "CST1229/zip@_path [PATH] from [ORIGIN]": "pad [PATH] vanuit [ORIGIN] ", + "CST1229/zip@_set [META] of [FILE] to [VALUE]": "maak [META] van [FILE] [VALUE]", + "CST1229/zip@_set archive comment to [COMMENT]": "maak archiefopmerking [COMMENT]", + "CST1229/zip@_text": "tekst", + "CST1229/zip@_unix modified timestamp": "wijzigingsdatum in unix-tijdstempel", + "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "nieuw bestand [FILE] inhoud [CONTENT] soort [TYPE]", + "Clay/htmlEncode@_HTML Encode": "HTML-Codering", + "Clay/htmlEncode@_Hello!": "Hallo!", + "Clay/htmlEncode@_encode [text] as HTML-safe": "codeer [text] naar HTML-veilig", + "CubesterYT/TurboHook@_content": "inhoud", + "CubesterYT/TurboHook@_icon": "pictogram", + "CubesterYT/TurboHook@_name": "naam", + "CubesterYT/TurboHook@_webhook data: [hookDATA] webhook url: [hookURL]": "stuur gegevens: [hookDATA] naar url: [hookURL] met webhook", + "CubesterYT/WindowControls@_Hello World!": "Hallo Wereld!", + "CubesterYT/WindowControls@_May not work in normal browser tabs": "Werkt misschien niet in browsers", + "CubesterYT/WindowControls@_Refer to Documentation for details": "Lees documentatie voor details", + "CubesterYT/WindowControls@_Window Controls": "Vensterbesturing", + "CubesterYT/WindowControls@_bottom": "onder", + "CubesterYT/WindowControls@_bottom left": "linksonder", + "CubesterYT/WindowControls@_bottom right": "rechtsonder", + "CubesterYT/WindowControls@_center": "midden", + "CubesterYT/WindowControls@_change window height by [H]": "verander vensterhoogte met [H]", + "CubesterYT/WindowControls@_change window width by [W]": "verander vensterbreedte met [W]", + "CubesterYT/WindowControls@_change window x by [X]": "verander venster-x met [X]", + "CubesterYT/WindowControls@_change window y by [Y]": "verander venster-y met [Y]", + "CubesterYT/WindowControls@_close window": "sluit venster", + "CubesterYT/WindowControls@_enter fullscreen": "schakel volledig scherm in", + "CubesterYT/WindowControls@_exit fullscreen": "schakel volledig scherm uit", + "CubesterYT/WindowControls@_is window focused?": "venster gefocust?", + "CubesterYT/WindowControls@_is window fullscreen?": "venster in volledig scherm?", + "CubesterYT/WindowControls@_is window touching screen edge?": "venster raakt schermrand aan?", + "CubesterYT/WindowControls@_left": "links", + "CubesterYT/WindowControls@_match stage size": "maak venstergrootte gelijk aan speelveldgrootte", + "CubesterYT/WindowControls@_move window to the [PRESETS]": "verplaats venster naar [PRESETS]", + "CubesterYT/WindowControls@_move window to x: [X] y: [Y]": "verplaats venster naar x: [X] y: [Y]", + "CubesterYT/WindowControls@_random position": "willekeurige positie", + "CubesterYT/WindowControls@_resize window to [PRESETS]": "maak venstergrootte [PRESETS]", + "CubesterYT/WindowControls@_resize window to width: [W] height: [H]": "maak vensterbreedte [W] en -hoogte [H]", + "CubesterYT/WindowControls@_right": "rechts", + "CubesterYT/WindowControls@_screen height": "schermhoogte", + "CubesterYT/WindowControls@_screen width": "schermbreedte", + "CubesterYT/WindowControls@_set window height to [H]": "maak vensterhoogte [H]", + "CubesterYT/WindowControls@_set window title to [TITLE]": "maak venstertitel [TITLE]", + "CubesterYT/WindowControls@_set window width to [W]": "maak vensterbreedte [W]", + "CubesterYT/WindowControls@_set window x to [X]": "maak venster-x [X]", + "CubesterYT/WindowControls@_set window y to [Y]": "maak venster-y [Y]", + "CubesterYT/WindowControls@_top": "boven", + "CubesterYT/WindowControls@_top left": "linksboven", + "CubesterYT/WindowControls@_top right": "rechtsboven", + "CubesterYT/WindowControls@_window height": "vensterhoogte", + "CubesterYT/WindowControls@_window title": "venstertitel", + "CubesterYT/WindowControls@_window width": "vensterbreedte", + "CubesterYT/WindowControls@_window x": "venster-x", + "CubesterYT/WindowControls@_window y": "venster-y", + "CubesterYT/WindowControls@editorConfirmation": "Weet je zeker dat je dit venster wilt sluiten?\n\n(Dit bericht wordt niet weergegeven wanneer het project gepackaged is)", + "DNin/wake-lock@_Wake Lock": "Wakker Houden", + "DNin/wake-lock@_is wake lock active?": "wakker houden ingeschakeld?", + "DNin/wake-lock@_off": "uit", + "DNin/wake-lock@_on": "in", + "DNin/wake-lock@_turn wake lock [enabled]": "schakel wakker houden [enabled]", + "DT/cameracontrols@_Camera (Very Buggy)": "Camera (Veel Bugs)", + "DT/cameracontrols@_background color": "achtergrondkleur", + "DT/cameracontrols@_camera direction": "camera-richting", + "DT/cameracontrols@_camera x": "camera-x", + "DT/cameracontrols@_camera y": "camera-y", + "DT/cameracontrols@_camera zoom": "camera-zoom", + "DT/cameracontrols@_change camera x by [val]": "verander camera-x met [val]", + "DT/cameracontrols@_change camera y by [val]": "verander camera-y [val]", + "DT/cameracontrols@_change camera zoom by [val]": "verander camera-zoom met [val]", + "DT/cameracontrols@_move camera [val] steps": "verplaats camera [val] stappen", + "DT/cameracontrols@_move camera to [sprite]": "verplaats camera naar [sprite]", + "DT/cameracontrols@_no sprites exist": "...", + "DT/cameracontrols@_point camera towards [sprite]": "richt camera naar [sprite]", + "DT/cameracontrols@_set background color to [val]": "maak achtergrondkleur [val]", + "DT/cameracontrols@_set camera direction to [val]": "richt camera naar [val] graden", + "DT/cameracontrols@_set camera to x: [x] y: [y]": "verplaats camera naar x: [x] y: [y]", + "DT/cameracontrols@_set camera x to [val]": "maak camera-x [val]", + "DT/cameracontrols@_set camera y to [val]": "maak camera-y [val]", + "DT/cameracontrols@_set camera zoom to [val] %": "maak camera-zoom [val] %", + "DT/cameracontrols@_turn camera [image] [val] degrees": "draai camera [image] [val] graden", + "NOname-awa/graphics2d@area": "oppervlakte", + "NOname-awa/graphics2d@circumference": "omtrek", + "NOname-awa/graphics2d@graph": "[CS] van grafiek [graph]", + "NOname-awa/graphics2d@line_section": "lengte van ([x1],[y1]) naar ([x2],[y2])", "NOname-awa/graphics2d@name": "2D-Trigonometrie", + "NOname-awa/graphics2d@quadrilateral": "[CS] van vierhoek ([x1],[y1]) ([x2],[y2]) ([x3],[y3]) ([x4],[y4])", + "NOname-awa/graphics2d@radius": "straal", + "NOname-awa/graphics2d@ray_direction": "richting van ([x1],[y1]) naar ([x2],[y2])", + "NOname-awa/graphics2d@round": "[CS] van cirkel met [rd] [a]", + "NOname-awa/graphics2d@triangle": "[CS] van driehoek ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", + "NOname-awa/graphics2d@triangle_s": "oppervlakte van driehoek [s1] [s2] [s3]", + "NexusKitten/controlcontrols@_Control Controls": "Projectbesturing-besturing", + "NexusKitten/controlcontrols@_[OPTION] exists?": "[OPTION] bestaat?", + "NexusKitten/controlcontrols@_[OPTION] shown?": "[OPTION] getoond?", + "NexusKitten/controlcontrols@_fullscreen": "volledig scherm", + "NexusKitten/controlcontrols@_green flag": "groene vlag", + "NexusKitten/controlcontrols@_hide [OPTION]": "verberg [OPTION]", + "NexusKitten/controlcontrols@_pause": "pauzeer", + "NexusKitten/controlcontrols@_show [OPTION]": "toon [OPTION]", + "NexusKitten/moremotion@_More Motion": "Beweging Uitgebreid", + "NexusKitten/moremotion@_change x: [X] y: [Y]": "verander x: [X] y: [Y]", + "NexusKitten/moremotion@_costume height": "uiterlijkhoogte", + "NexusKitten/moremotion@_costume width": "uiterlijkbreedte", + "NexusKitten/moremotion@_direction to x: [X] y: [Y]": "richting naar x: [X] y: [Y]", + "NexusKitten/moremotion@_distance from x: [X] y: [Y]": "afstand tot x: [X] y: [Y]", + "NexusKitten/moremotion@_height": "hoogte", + "NexusKitten/moremotion@_manually fence": "beweeg naar binnen het scherm", + "NexusKitten/moremotion@_move [PERCENT]% of the way to x: [X] y: [Y]": "beweeg [PERCENT]% richting x: [X] y: [Y]", + "NexusKitten/moremotion@_move [STEPS] steps towards x: [X] y: [Y]": "neem [STEPS] stappen richting x: [X] y: [Y]", + "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "richt naar x: [X] y: [Y]", + "NexusKitten/moremotion@_rotation style": "draaistijl", + "NexusKitten/moremotion@_sprite [WHAT]": "[WHAT] van sprite", + "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "raak ik een punt tussen x1: [X1] y1: [Y1] en x2: [X2] y2: [Y2]?", + "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "raak ik x: [X] y: [Y]?", + "NexusKitten/moremotion@_width": "breedte", + "NexusKitten/sgrab@_[WHAT] of user [WHO]": "[WHAT] van gebruiker [WHO]", + "NexusKitten/sgrab@_about me": "over mij", + "NexusKitten/sgrab@_creator of project id [WHO]": "maker van project-id [WHO]", + "NexusKitten/sgrab@_favorite": "sterretjes", + "NexusKitten/sgrab@_follower": "volgers", + "NexusKitten/sgrab@_following": "volgend", + "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "positie van [WHO] in globale ranglijst voor [WHAT]", + "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "positie van project-id [WHO] in globale ranglijst voor [WHAT]", + "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "aantal [WHAT] van project-id [WHO]", + "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "aantal [WHAT] van gebruiker [WHO]", + "NexusKitten/sgrab@_location": "locatie", + "NexusKitten/sgrab@_love": "hartjes", + "NexusKitten/sgrab@_name of project id [WHO]": "naam van project-id [WHO]", + "NexusKitten/sgrab@_view": "weergaven", + "NexusKitten/sgrab@_wiwo": "waar ik mee bezig ben", "battery@_Battery": "Batterij", + "battery@_battery level": "batterijniveau", + "battery@_charging?": "batterij aan het opladen?", + "battery@_seconds until charged": "seconden tot batterij opgeladen", + "battery@_seconds until empty": "seconden tot batterij leeg", + "battery@_when battery level changed": "wanneer batterijniveau verandert", + "battery@_when charging changed": "wanneer batterij begint of stopt met opladen", + "battery@_when time until charged changed": "wanneer tijd tot batterij opgeladen verandert", + "battery@_when time until empty changed": "wanneer tijd tot batterij leeg verandert", + "box2d@griffpatch.applyAngForce": "draai met kracht [force]", + "box2d@griffpatch.applyForce": "duw met kracht [force] in richting [dir]", + "box2d@griffpatch.categoryName": "Fysica-Simulatie", + "box2d@griffpatch.changeScroll": "verander scroll met x: [ox] y: [oy]", + "box2d@griffpatch.changeVelocity": "verander snelheid met sx: [sx] sy: [sy]", + "box2d@griffpatch.doTick": "voer simulatie één keer uit", + "box2d@griffpatch.getAngVelocity": "hoeksnelheid", + "box2d@griffpatch.getDensity": "dichtheid", + "box2d@griffpatch.getFriction": "wrijving", + "box2d@griffpatch.getGravityX": "zwaartekracht x", + "box2d@griffpatch.getGravityY": "zwaartekracht y", + "box2d@griffpatch.getRestitution": "stuiterkracht", + "box2d@griffpatch.getScrollX": "x-scroll", + "box2d@griffpatch.getScrollY": "y-scroll", + "box2d@griffpatch.getStatic": "vastgezet?", + "box2d@griffpatch.getTouching": "alle sprites die [where] aanraken", + "box2d@griffpatch.getVelocityX": "snelheid x", + "box2d@griffpatch.getVelocityY": "snelheid y", + "box2d@griffpatch.setAngVelocity": "maak hoeksnelheid [force]", + "box2d@griffpatch.setDensity": "maak dichtheid [density]", + "box2d@griffpatch.setDensityValue": "maak dichtheid [density]", + "box2d@griffpatch.setFriction": "maak wrijving [friction]", + "box2d@griffpatch.setFrictionValue": "maak wrijving [friction]", + "box2d@griffpatch.setGravity": "stel zwaartekracht in op x: [gx] y: [gy]", + "box2d@griffpatch.setPhysics": "schakel voor [shape] de stand [mode] in", + "box2d@griffpatch.setPosition": "ga naar x: [x] y: [y] [space]", + "box2d@griffpatch.setProperties": "maak dichtheid [density], wrijving [friction] en stuiterkracht [restitution]", + "box2d@griffpatch.setRestitution": "maak stuiterkracht [restitution]", + "box2d@griffpatch.setRestitutionValue": "maak stuiterkracht [restitution]", + "box2d@griffpatch.setScroll": "stel scroll in op x: [ox] y: [oy]", + "box2d@griffpatch.setStage": "maak grenstype [stageType]", + "box2d@griffpatch.setStatic": "maak vastzettype [static]", + "box2d@griffpatch.setVelocity": "stel snelheid in op sx: [sx] sy: [sy]", "clipboard@_Clipboard": "Klembord", + "clipboard@_clipboard": "klembord", + "clipboard@_copy to clipboard: [TEXT]": "kopieer [TEXT] naar klembord", + "clipboard@_last pasted text": "laatst geplakte tekst", + "clipboard@_reset clipboard": "wis klembord", + "clipboard@_when something is copied": "wanneer iets is gekopieerd", + "clipboard@_when something is pasted": "wanneer iets is geplakt", "clouddata-ping@_Ping Cloud Data": "Cloudservers Pingen", + "clouddata-ping@_is cloud data server [SERVER] up?": "is cloud-gegevensserver [SERVER] bereikbaar?", + "cs2627883/numericalencoding@_Decode [ENCODED] back to text": "decodeer [ENCODED] terug naar tekst", + "cs2627883/numericalencoding@_Encode [DATA] to numbers": "codeer [DATA] naar getallen", + "cs2627883/numericalencoding@_Hello!": "Hallo!", + "cs2627883/numericalencoding@_Numerical Encoding": "Numerieke Codering", + "cs2627883/numericalencoding@_decoded": "gedecodeerd", + "cs2627883/numericalencoding@_encoded": "gecodeerd", "cursor@_Mouse Cursor": "Muisaanwijzer", + "cursor@_bottom left": "linksonder", + "cursor@_bottom right": "rechtsonder", + "cursor@_center": "midden", + "cursor@_hide cursor": "verberg cursor", + "cursor@_set cursor to [cur]": "maak cursor [cur]", + "cursor@_set cursor to current costume center: [position] max size: [size]": "maak cursor huidig uiterlijk met middelpunt: [position] en max. grootte: [size]", + "cursor@_top left": "linksboven", + "cursor@_top right": "rechtsboven", + "cursor@_{size} (unreliable)": "{size} (onbetrouwbaar)", + "encoding@_Convert the character [string] to [CodeList]": "zet teken [string] om naar [CodeList]", + "encoding@_Decode [string] with [code]": "decodeer [string] met [code]", + "encoding@_Encode [string] in [code]": "codeer [string] in [code]", "encoding@_Encoding": "Codering", + "encoding@_Hash [string] with [hash]": "hash [string] met [hash]", + "encoding@_Randomly generated [position] character string": "willekeurige string met [position] tekens", + "encoding@_Use [wordbank] to generate a random [position] character string": "gebruik [wordbank] in een willekeurige string met [position] tekens", + "encoding@_[string] corresponding to the [CodeList] character": "teken nr. [string] in [CodeList]", + "encoding@_apple": "appel", + "files@_Accepted formats: {formats}": "Geaccepteerde formaten: {formats}", "files@_Files": "Bestanden", + "files@_Hello, world!": "Hallo, wereld!", "files@_Select or drop file": "Bestand selecteren of neerzetten", + "files@_any": "willekeurig", + "files@_download URL [url] as [file]": "download URL [url] als [file]", + "files@_download [text] as [file]": "download [text] als [file]", + "files@_only show selector (unreliable)": "alleen bestandskiezer tonen (onbetrouwbaar)", + "files@_open a [extension] file": "open een [extension] bestand", + "files@_open a [extension] file as [as]": "open een [extension] bestand als [as]", + "files@_open a file": "open een bestand", + "files@_open a file as [as]": "open een bestand als [as]", + "files@_open selector immediately": "bestandskiezer meteen openen", + "files@_save.txt": "bestand.txt", + "files@_set open file selector mode to [mode]": "stel wijze van bestandskiezer openen in op: [mode]", + "files@_show modal": "modaal tonen", + "files@_text": "tekst", + "gamejolt@GameJoltAPI_gamejoltBool": "op Game Jolt?", + "gamejolt@_1 point": "1 punt", + "gamejolt@_Achieve trophy of ID [ID]": "geef trofee met ID [ID]", + "gamejolt@_Add [namespace] request with [parameters] to batch": "voeg [namespace] verzoek met [parameters] toe aan batch", + "gamejolt@_Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]": "voeg [username] score [value] toe in tabel [ID] met tekst [text] en opmerking [extraData]", + "gamejolt@_Add score [value] in table of ID [ID] with text [text] and comment [extraData]": "voeg score [value] toe in tabel [ID] met tekst [text] en opmerking [extraData]", + "gamejolt@_Autologin available?": "automatisch inloggen mogelijk?", + "gamejolt@_Batch in JSON": "batch in JSON", + "gamejolt@_Clear batch": "wis batch", + "gamejolt@_Close": "sluit", + "gamejolt@_Data Storage Blocks": "Gegevens Opslaan", + "gamejolt@_Debug Blocks": "Debuggen", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]": "vraag [amount] [globalOrPerUser] scores op in tabel [ID] die [betterOrWorse] zijn dan [value]", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]": "vraag [amount] [globalOrPerUser] scores op in tabel [ID]", + "gamejolt@_Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]": "vraag [amount] [username] scores op in tabel [ID] die [betterOrWorse] zijn dan [value]", + "gamejolt@_Fetch [amount] [username] score/s in table of ID [ID]": "vraag [amount] [username] scores op in tabel [ID]", + "gamejolt@_Fetch [globalOrPerUser] keys matching with [pattern]": "vraag alle [globalOrPerUser] keys op die overeenkomen met [pattern]", + "gamejolt@_Fetch [trophyFetchGroup] trophies": "vraag [trophyFetchGroup] trofeeën op", + "gamejolt@_Fetch all [globalOrPerUser] keys": "vraag alle [globalOrPerUser] keys op", + "gamejolt@_Fetch batch [parameter]": "vraag batch [parameter] op", + "gamejolt@_Fetch logged in user": "vraag ingelogde gebruiker op", + "gamejolt@_Fetch score tables": "vraag score-tabellen op", + "gamejolt@_Fetch server's time": "vraag servertijd op", + "gamejolt@_Fetch trophy of ID[ID]": "vraag trofee met ID [ID] op", + "gamejolt@_Fetch user's [usernameOrID] by [fetchType]": "vraag gebruiker [usernameOrID] op met [fetchType] ", + "gamejolt@_Fetch user's friend IDs": "vriend-ID's van opgevraagde gebruiker", + "gamejolt@_Fetched [globalOrPerUser] data at [key]": "opgevraagde [globalOrPerUser] data bij [key]", + "gamejolt@_Fetched batch data in JSON": "opgevraagde batchgegevens in JSON", + "gamejolt@_Fetched key at index [index]": "opgevraagde key bij index [index]", + "gamejolt@_Fetched keys in JSON": "opgevraagde keys in JSON", + "gamejolt@_Fetched rank of [value] in table of ID [ID]": "opgevraagde rang van [value] in tabel [ID]", + "gamejolt@_Fetched score [scoreDataType] at index [index]": "[scoreDataType] bij index [index] van opgevraagde score", + "gamejolt@_Fetched score data in JSON": "gegevens van opgevraagde score in JSON", + "gamejolt@_Fetched server's [timeType]": "opgevraagde [timeType] van servertijd", + "gamejolt@_Fetched server's time in JSON": "opgevraagde servertijd in JSON", + "gamejolt@_Fetched table [tableDataType] at index [index]": "[tableDataType] van opgevraagde tabel bij index [index]", + "gamejolt@_Fetched table [tableDataType] at index[index] (Deprecated)": "[tableDataType] van opgevraagde tabel bij index [index] (verouderd)", + "gamejolt@_Fetched tables in JSON": "opgevraagde tabellen in JSON", + "gamejolt@_Fetched trophies in JSON": "opgevraagde trofeeën in JSON", + "gamejolt@_Fetched trophy [trophyDataType] at index [index]": "[trophyDataType] van opgevraagde trofee op index [index]", + "gamejolt@_Fetched user's [userDataType]": "[userDataType] van opgevraagde gebruiker", + "gamejolt@_Fetched user's data in JSON": "JSON-gegevens van opgevraagde gebruiker", + "gamejolt@_Fetched user's friend ID at index[index]": "vriend [index] van opgevraagde gebruiker", + "gamejolt@_Fetched user's friend IDs in JSON": "vriend-ID's van opgevraagde gebruiker in JSON", + "gamejolt@_In debug mode?": "debug-modus actief?", + "gamejolt@_Last API error": "recentste API-fout", + "gamejolt@_Logged in user's username": "gebruikersnaam van ingelogd", + "gamejolt@_Logged in?": "ingelogd?", + "gamejolt@_Login automatically": "log automatisch in", + "gamejolt@_Login with [username] and [token]": "log in met gebruikersnaam [username] en token [token]", + "gamejolt@_Logout": "log uit", + "gamejolt@_Open": "open", + "gamejolt@_Ping session": "ping sessie", + "gamejolt@_Remove [globalOrPerUser] data at [key]": "verwijder [globalOrPerUser] data bij [key]", + "gamejolt@_Remove trophy of ID [ID]": "verwijder trofee met ID [ID]", + "gamejolt@_Score Blocks": "Score", + "gamejolt@_Session Blocks": "Sessie", + "gamejolt@_Session open?": "sessie open?", + "gamejolt@_Set [globalOrPerUser] data at [key] to [data]": "maak [globalOrPerUser] data bij [key] [data]", + "gamejolt@_Set game ID to [ID] and private key to [key]": "stel game-ID in op [ID] en privé-key op [key]", + "gamejolt@_Set session status to [status]": "stel sessie-status in op [status]", + "gamejolt@_Time Blocks": "Tijd", + "gamejolt@_Trophy Blocks": "Trofeeën", + "gamejolt@_Turn debug mode [toggle]": "schakel debug-modus [toggle]", + "gamejolt@_Update [globalOrPerUser] data at [key] by [operationType] [value]": "verander [globalOrPerUser] data bij [key] door [operationType] met [value]", + "gamejolt@_User Blocks": "Gebruikers", + "gamejolt@_[openOrClose] session": "[openOrClose] sessie", + "gamejolt@_achievement date": "datum behaald", + "gamejolt@_active": "actief", + "gamejolt@_adding": "optellen", + "gamejolt@_all": "alle", + "gamejolt@_all achieved": "alle behaalde", + "gamejolt@_all unachieved": "alle niet-behaalde", + "gamejolt@_appending": "toevoegen aan eind", + "gamejolt@_avatar URL": "avatar-URL", + "gamejolt@_better": "beter", + "gamejolt@_comment": "opmerking", + "gamejolt@_data": "gegevens", + "gamejolt@_day": "dag", + "gamejolt@_description": "beschrijving", + "gamejolt@_developer username": "ontwikkelaarsnaam", + "gamejolt@_difficulty": "moeilijkheid", + "gamejolt@_dividing by": "delen", + "gamejolt@_global": "globale", + "gamejolt@_guest": "gast", + "gamejolt@_hour": "uur", + "gamejolt@_idle": "inactief", + "gamejolt@_image URL": "afbeelding-URL", + "gamejolt@_in parallel": "parallel", + "gamejolt@_last login": "recentste inlogdatum", + "gamejolt@_last login timestamp": "recentste inlog-tijdstempel", + "gamejolt@_minute": "minuut", + "gamejolt@_month": "maand", + "gamejolt@_multiplying by": "vermenigvuldigen", + "gamejolt@_name": "naam", + "gamejolt@_off": "uit", + "gamejolt@_on": "in", + "gamejolt@_optional": "optioneel", + "gamejolt@_prepending": "toevoegen aan begin", + "gamejolt@_primary": "primair", + "gamejolt@_private key": "privé-key", + "gamejolt@_private token": "privé-token", + "gamejolt@_score date": "scoredatum", + "gamejolt@_score timestamp": "score-tijdstempel", + "gamejolt@_second": "seconde", + "gamejolt@_sequentially": "opeenvolgend", + "gamejolt@_sequentially, break on error": "opeenvolgend (stoppen bij fout)", + "gamejolt@_sign up date": "aanmelddatum", + "gamejolt@_sign up timestamp": "aanmeld-tijdstempel", + "gamejolt@_subtracting": "aftellen", + "gamejolt@_text": "tekst", + "gamejolt@_timestamp": "tijdstempel", + "gamejolt@_timezone": "tijdzone", + "gamejolt@_title": "titel", + "gamejolt@_user": "gebruikers", + "gamejolt@_user ID": "gebruiker-ID", + "gamejolt@_username": "gebruikersnaam", + "gamejolt@_value": "waarde", + "gamejolt@_worse": "slechter", + "gamejolt@_year": "jaar", + "gamepad@_D-pad down (14)": "omlaag (14)", + "gamepad@_D-pad left (15)": "links (15)", + "gamepad@_D-pad right (16)": "rechts (16)", + "gamepad@_D-pad up (13)": "omhoog (13)", + "gamepad@_Left bumper (5)": "linker bumper (5)", + "gamepad@_Left stick (1 & 2)": "linker stick (1 & 2)", + "gamepad@_Left stick (11)": "linker stick (11)", + "gamepad@_Left stick horizontal (1)": "linker stick horizontaal (1)", + "gamepad@_Left stick vertical (2)": "linker stick verticaal (2)", + "gamepad@_Left trigger (7)": "linker trigger (7)", + "gamepad@_Right bumper (6)": "rechter bumper (6)", + "gamepad@_Right stick (12)": "rechter stick (12)", + "gamepad@_Right stick (3 & 4)": "rechter stick (3 & 4)", + "gamepad@_Right stick horizontal (3)": "rechter stick horizontaal (3)", + "gamepad@_Right stick vertical (4)": "rechter stick verticaal (4)", + "gamepad@_Right trigger (8)": "rechter trigger (8)", + "gamepad@_Select/View (9)": "selecteer/bekijk (9)", + "gamepad@_Start/Menu (10)": "start/menu (10)", + "gamepad@_any": "willekeurig", + "gamepad@_button [b] on pad [i] pressed?": "knop [b] op gamepad [i] ingedrukt?", + "gamepad@_direction of axes [axis] on pad [pad]": "richting van assen [axis] op gamepad [pad]", + "gamepad@_gamepad [pad] connected?": "gamepad [pad] verbonden?", + "gamepad@_magnitude of axes [axis] on pad [pad]": "afstand van assen [axis] op gamepad [pad]", + "gamepad@_rumble strong [s] and weak [w] for [t] sec. on pad [i]": "vibreer sterk [s] en zwak [w] voor [t] seconden op gamepad [i]", + "gamepad@_value of axis [b] on pad [i]": "waarde van as [b] op gamepad [i]", + "gamepad@_value of button [b] on pad [i]": "waarde van knop [b] op gamepad [i]", + "iframe@_It works!": "Het werkt!", + "iframe@_close iframe": "sluit iframe", + "iframe@_height": "hoogte", + "iframe@_hide iframe": "verberg iframe", + "iframe@_iframe [MENU]": "[MENU] van iframe", + "iframe@_interactive": "interactief?", + "iframe@_resize behavior": "formaatwijzigingsgedrag", + "iframe@_scale": "speelveld", + "iframe@_set iframe height to [HEIGHT]": "maak hoogte van iframe [HEIGHT]", + "iframe@_set iframe interactive to [INTERACTIVE]": "maak iframe interactief? [INTERACTIVE]", + "iframe@_set iframe resize behavior to [RESIZE]": "maak formaatwijzigingsgedrag van iframe [RESIZE]", + "iframe@_set iframe width to [WIDTH]": "maak breedte van iframe [WIDTH]", + "iframe@_set iframe x position to [X]": "maak x-positie van iframe [X]", + "iframe@_set iframe y position to [Y]": "maak y-positie van iframe [Y]", + "iframe@_show HTML [HTML]": "toon HTML [HTML]", + "iframe@_show iframe": "toon iframe", + "iframe@_show website [URL]": "toon website [URL]", + "iframe@_viewport": "beeldscherm", + "iframe@_visible": "zichtbaar?", + "iframe@_width": "breedte", + "itchio@_Data": "Gegevens", + "itchio@_Error: Data not found.": "Fout: Kon gegevens niet vinden.", + "itchio@_Fetch game data [user][game][secret]": "vraag game-gegevens op [user][game][secret]", + "itchio@_Game data?": "game-gegevens geladen?", + "itchio@_Open [prefix] itch.io [page] window with [width]width and [height]height": "open [prefix] itch.io [page] venster met breedte: [width] en hoogte: [height] ", + "itchio@_Return game [data]": "[data] van game", + "itchio@_Return game data [user][game][secret] in .json": "game-gegevens [user][game][secret] in JSON", + "itchio@_Return game data in .json": "game-gegevens in JSON", + "itchio@_Return game rewards [rewards] by index:[index]": "[rewards] van beloning [index] van game", + "itchio@_Return game sale [sale]": "[sale] van game-uitverkoop", + "itchio@_Return game sub products [subProducts] by index:[index]": "[subProducts] van sub-product [index] van game", + "itchio@_Return rewards list length": "aantal beloningen", + "itchio@_Return sub products list length": "aantal sub-producten", + "itchio@_Rewards": "Beloningen", + "itchio@_Rewards?": "gegevens voor beloningen bestaan?", + "itchio@_Sale": "Uitverkoop", + "itchio@_Sale?": "uitverkoop actief?", + "itchio@_Sub products": "Sub-producten", + "itchio@_Sub products?": "gegevens voor sub-producten bestaan?", + "itchio@_Window": "Venster", + "itchio@_amount": "hoeveelheid", + "itchio@_amount remaining": "hoeveelheid over", + "itchio@_available": "beschikbaar?", + "itchio@_cover image URL": "omslagfoto-URL", + "itchio@_end date": "einddatum", + "itchio@_game argument not found": "kon argumenten voor game niet vinden", + "itchio@_id": "ID", + "itchio@_name": "naam", + "itchio@_original price": "originele prijs", + "itchio@_price": "prijs", + "itchio@_rate": "tarief", + "itchio@_title": "titel", + "itchio@_user": "gebruiker", + "itchio@_user argument not found": "kon argumenten voor gebruiker niet vinden", + "itchio@itchio_error": "Fout: ", + "itchio@itchio_errors": "Fouten: ", + "lab/text@_# of lines": "aantal regels", "lab/text@_Animated Text": "Geanimeerde Tekst", + "lab/text@_Enable Non-Scratch Lab Features": "Niet-Scratch Lab Functies Inschakelen", + "lab/text@_Hello!": "Hallo!", + "lab/text@_Here we go!": "Daar gaan we!", + "lab/text@_Incompatible with Scratch Lab:": "Incompatibel met Scratch Lab:", + "lab/text@_Welcome to my project!": "Welkom bij mijn project!", + "lab/text@_[ANIMATE] duration": "duur van [ANIMATE]", + "lab/text@_[ANIMATE] text [TEXT]": "[ANIMATE] tekst [TEXT]", + "lab/text@_add line [TEXT]": "voeg regel [TEXT] toe", + "lab/text@_align text to [ALIGN]": "tekst [ALIGN] uitlijnen", + "lab/text@_animate [ANIMATE] until done": "animeer [ANIMATE] en wacht", + "lab/text@_center": "midden", + "lab/text@_displayed text": "weergegeven tekst", + "lab/text@_is animating?": "animatie bezig?", + "lab/text@_is showing text?": "tekst aan het tonen?", + "lab/text@_left": "links", + "lab/text@_rainbow": "regenboog", + "lab/text@_random font": "willekeurig lettertype", + "lab/text@_reset [ANIMATE] duration": "reset duur van [ANIMATE]", + "lab/text@_reset text width": "reset tekstbreedte", + "lab/text@_reset typing delay": "reset wachttijd van typ-animatie", + "lab/text@_right": "rechts", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "maak duur van [ANIMATE] [NUM] seconden", + "lab/text@_set font to [FONT]": "maak lettertype [FONT]", + "lab/text@_set text color to [COLOR]": "maak tekstkleur [COLOR]", + "lab/text@_set typing delay to [NUM] seconds": "maak wachttijd van typ-animatie [NUM] seconden", + "lab/text@_set width to [WIDTH]": "maak breedte [WIDTH]", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "maak breedte [WIDTH] [ALIGN] uitgelijnd", + "lab/text@_show sprite": "toon sprite", + "lab/text@_show text [TEXT]": "toon tekst [TEXT]", + "lab/text@_start [ANIMATE] animation": "start [ANIMATE] animatie", + "lab/text@_text [ATTRIBUTE]": "[ATTRIBUTE] van tekst", + "lab/text@_type": "typen", + "lab/text@_typing delay": "wachttijd van typ-animatie", + "lab/text@disableCompatibilityMode": "Dit voegt nieuwe blokken en functies toe die NIET werken in het officiële Scratch Lab.\n\nWil je verdergaan?", "local-storage@_Local Storage": "Lokale Opslag", + "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "Lokale Opslag-extensie: het project moet eerst een opslagnaamruimte-ID toegewezen krijgen voordat de andere blokken kunnen werken.", + "local-storage@_delete all keys": "verwijder alle sleutels", + "local-storage@_delete key [KEY]": "verwijder sleutel [KEY]", + "local-storage@_get key [KEY]": "sleutel [KEY]", + "local-storage@_project title": "projecttitel", + "local-storage@_set key [KEY] to [VALUE]": "maak sleutel [KEY] [VALUE]", + "local-storage@_set storage namespace ID to [ID]": "maak opslagnaamruimte-ID [ID]", + "local-storage@_when another window changes storage": "wanneer een ander venster de opslag aanpast", + "navigator@_Navigator Info": "Navigator-Info", + "navigator@_dark": "donker", + "navigator@_device memory in GB": "apparaatgeheugen in GB", + "navigator@_light": "licht", + "navigator@_operating system": "besturingssysteem", + "navigator@_user prefers [THEME] color scheme?": "gebruik heeft voorkeur voor [THEME] kleurenschema?", + "navigator@_user prefers more contrast?": "gebruiker heeft voorkeur voor meer contrast?", + "navigator@_user prefers reduced motion?": "gebruiker heeft voorkeur voor verminderde beweging?", "pointerlock@_Pointerlock": "Muisaanwijzer-Vergrendeling", - "runtime-options@_Runtime Options": "Looptijdopties" + "pointerlock@_disabled": "ontgrendel", + "pointerlock@_enabled": "vergrendel", + "pointerlock@_pointer locked?": "muisaanwijzer vergrendeld?", + "pointerlock@_set pointer lock [enabled]": "[enabled] muisaanwijzer", + "qxsck/data-analysis@average": "gemiddelde van [NUMBERS]", + "qxsck/data-analysis@maximum": "maximum van [NUMBERS]", + "qxsck/data-analysis@median": "mediaan van [NUMBERS]", + "qxsck/data-analysis@minimum": "minimum van [NUMBERS]", + "qxsck/data-analysis@mode": "modus van [NUMBERS]", + "qxsck/data-analysis@name": "Gegevens Analyseren", + "qxsck/data-analysis@variance": "variantie van [NUMBERS]", + "qxsck/var-and-list@addValueInList": "voeg [VALUE] toe aan lijst [LIST]", + "qxsck/var-and-list@clearList": "verwijder alle van lijst [LIST]", + "qxsck/var-and-list@copyList": "kopieer lijst [LIST1] naar lijst [LIST2]", + "qxsck/var-and-list@deleteOfList": "verwijder [INDEX] van lijst [LIST]", + "qxsck/var-and-list@getIndexOfList": "eerste index van [VALUE] in lijst [LIST]", + "qxsck/var-and-list@getIndexesOfList": "indexen van [VALUE] in lijst [LIST]", + "qxsck/var-and-list@getList": "waarde van lijst [LIST]", + "qxsck/var-and-list@getValueOfList": "item [INDEX] van lijst [LIST]", + "qxsck/var-and-list@getVar": "waarde van variabele [VAR]", + "qxsck/var-and-list@length": "lengte van lijst [LIST]", + "qxsck/var-and-list@listContains": "lijst [LIST] bevat [VALUE] ?", + "qxsck/var-and-list@name": "Gegevens", + "qxsck/var-and-list@replaceOfList": "vervang item [INDEX] van lijst [LIST] door [VALUE]", + "qxsck/var-and-list@seriListsToJson": "zet alle lijsten beginnend met [START] om naar JSON", + "qxsck/var-and-list@seriVarsToJson": "zet alle variabelen beginnend met [START] om naar JSON", + "qxsck/var-and-list@setVar": "maak de waarde van variabele [VAR] [VALUE]", + "runtime-options@_Infinity": "oneindig", + "runtime-options@_Runtime Options": "Looptijdopties", + "runtime-options@_[thing] enabled?": "[thing] ingeschakeld?", + "runtime-options@_clone limit": "kloonlimiet", + "runtime-options@_default ({n})": "standaard ({n})", + "runtime-options@_disabled": "uit", + "runtime-options@_enabled": "in", + "runtime-options@_framerate limit": "framerate-limiet", + "runtime-options@_height": "hoogte", + "runtime-options@_high quality pen": "pen met hoge kwaliteit", + "runtime-options@_interpolation": "interpolatie", + "runtime-options@_remove fencing": "waarde-limieten weghalen", + "runtime-options@_remove misc limits": "diverse limieten weghalen", + "runtime-options@_run green flag [flag]": "voer groene vlag [flag] uit", + "runtime-options@_set [thing] to [enabled]": "schakel [thing] [enabled]", + "runtime-options@_set clone limit to [limit]": "maak kloonlimiet [limit]", + "runtime-options@_set framerate limit to [fps]": "maak framerate-limiet [fps]", + "runtime-options@_set stage size width: [width] height: [height]": "maak speelveldbreedte: [width] en -hoogte: [height]", + "runtime-options@_set username to [username]": "maak gebruikersnaam [username]", + "runtime-options@_stage [dimension]": "[dimension] van speelveld", + "runtime-options@_turbo mode": "turbomodus", + "runtime-options@_width": "breedte", + "sound@_Sound": "Geluid", + "sound@_play sound from url: [path] until done": "start geluid van URL: [path] en wacht", + "sound@_start sound from url: [path]": "start geluid van URL: [path]", + "stretch@_Stretch": "Rekken", + "stretch@_change stretch by x: [DX] y: [DY]": "verander uitrekking met x: [DX] y: [DY]", + "stretch@_change stretch x by [DX]": "verander x-uitrekking met [DX]", + "stretch@_change stretch y by [DY]": "verander y-uitrekking met [DY]", + "stretch@_set stretch to x: [X] y: [Y]": "stel uitrekking in op x: [X] y: [Y]", + "stretch@_set stretch x to [X]": "maak x-uitrekking [X]", + "stretch@_set stretch y to [Y]": "maak y-uitrekking [Y]", + "stretch@_x stretch": "x-uitrekking", + "stretch@_y stretch": "y-uitrekking" }, "pl": { + "-SIPC-/consoles@_Error": "Błąd", "files@_Select or drop file": "Wybierz lub upuść plik", + "gamejolt@_Close": "Zamknij", "runtime-options@_Runtime Options": "Opcje Uruchamiania" }, "pt": { + "-SIPC-/consoles@_Error": "Erro", "files@_Select or drop file": "Selecione ou arraste um arquivo", + "gamejolt@_Close": "Fechar", "runtime-options@_Runtime Options": "Opções de Execução" }, "pt-br": { + "-SIPC-/consoles@_Error": "Erro", "files@_Select or drop file": "Selecione ou arraste um arquivo", + "gamejolt@_Close": "Fechar", "runtime-options@_Runtime Options": "Opções de Execução" }, "ru": { + "-SIPC-/consoles@_Error": "Ошибка", + "-SIPC-/time@_day": "день", + "-SIPC-/time@_hour": "час", + "-SIPC-/time@_minute": "минута", + "-SIPC-/time@_month": "месяц", + "-SIPC-/time@_second": "секунда", + "-SIPC-/time@_year": "год", "0832/rxFS2@clean": "Очистить файловую систему", "0832/rxFS2@del": "Удалить [STR]", "0832/rxFS2@folder": "Задать [STR] значение [STR2]", @@ -383,6 +1357,20 @@ "0832/rxFS2@start": "Создать [STR]", "0832/rxFS2@sync": "Изменить расположение [STR] на [STR2]", "0832/rxFS2@webin": "Загрузить [STR] из сети", + "CST1229/zip@_comment": "комментарий", + "CST1229/zip@_name": "имя", + "CST1229/zip@_text": "текст", + "Clay/htmlEncode@_Hello!": "Привет!", + "CubesterYT/TurboHook@_name": "имя", + "CubesterYT/WindowControls@_bottom left": "нижнем левом углу", + "CubesterYT/WindowControls@_bottom right": "нижнем правом углу", + "CubesterYT/WindowControls@_center": "центру", + "CubesterYT/WindowControls@_left": "левому краю", + "CubesterYT/WindowControls@_right": "правому краю", + "CubesterYT/WindowControls@_top left": "верхнем левом углу", + "CubesterYT/WindowControls@_top right": "верхнем правом углу", + "DNin/wake-lock@_off": "Выключить", + "DNin/wake-lock@_on": "Включить", "NOname-awa/graphics2d@area": "площадь", "NOname-awa/graphics2d@circumference": "длина", "NOname-awa/graphics2d@diameter": "диаметр", @@ -396,6 +1384,10 @@ "NOname-awa/graphics2d@round": "[CS] круга с [rd] ом [a]", "NOname-awa/graphics2d@triangle": "[CS] треугольника ([x1],[y1]) ([x2],[y2]) ([x3],[y3])", "NOname-awa/graphics2d@triangle_s": "площадь треугольника [s1] [s2] [s3]", + "NexusKitten/controlcontrols@_Control Controls": "Настройки Управления", + "NexusKitten/moremotion@_height": "высота", + "NexusKitten/moremotion@_width": "ширина", + "NexusKitten/sgrab@_status": "статус", "battery@_Battery": "Батарея", "battery@_battery level": "заряд батареи", "battery@_charging?": "заряжается?", @@ -447,6 +1439,7 @@ "clipboard@_when something is pasted": "когда что-либо вставлено", "clouddata-ping@_Ping Cloud Data": "Пинг облачных данных", "clouddata-ping@_is cloud data server [SERVER] up?": "Сервер облачных данных [SERVER] в сети?", + "cs2627883/numericalencoding@_Hello!": "Привет!", "cursor@_Mouse Cursor": "Курсор Мыши", "cursor@_bottom left": "нижнем левом углу", "cursor@_bottom right": "нижнем правом углу", @@ -467,30 +1460,303 @@ "encoding@_Use [wordbank] to generate a random [position] character string": "Использовать [wordbank] чтобы случайно сгенерировать строку с длиной [position] ", "encoding@_[string] corresponding to the [CodeList] character": "символ соответствующий [string] в [CodeList]", "encoding@_apple": "яблоко", + "files@_Files": "Файлы", + "files@_Hello, world!": "Привет, мир!", "files@_Select or drop file": "Выберите или \"закиньте\" файл", + "files@_download URL [url] as [file]": "загрузить URL [url] как [file]", + "files@_download [text] as [file]": "открыть [text] как [file]", + "files@_only show selector (unreliable)": "только окно (ненадежно)", + "files@_open a [extension] file": "открыть файл с расширением [extension]", + "files@_open a [extension] file as [as]": "открыть файл с расширением [extension] как [as]", + "files@_open a file": "открыть файл", + "files@_open a file as [as]": "открыть файл как [as]", + "files@_open selector immediately": "оверлэй и окно", + "files@_save.txt": "сохранение.txt", + "files@_set open file selector mode to [mode]": "запрашивать открытие файлов через [mode]", + "files@_show modal": "оверлэй", + "files@_text": "текст", + "gamejolt@GameJoltAPI_gamejoltBool": "На Game Jolt?", + "gamejolt@_1 point": "1 балл", + "gamejolt@_Achieve trophy of ID [ID]": "Получить трофей с ID [ID]", + "gamejolt@_Add [namespace] request with [parameters] to batch": "Добавить [namespace] запрос с [parameters] в пакет", + "gamejolt@_Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]": "Добавить счет [value] от [username] в таблице с ID [ID] с текстом [text] и комментарием [extraData] ", + "gamejolt@_Add score [value] in table of ID [ID] with text [text] and comment [extraData]": "Добавить счет [value] в таблицу с ID [ID] с текстом [text] и комментарием [extraData]", + "gamejolt@_Autologin available?": "Автоматический вход доступен?", + "gamejolt@_Batch in JSON": "Пакет в JSON", + "gamejolt@_Clear batch": " Очистить пакет", + "gamejolt@_Close": "Закрыть", + "gamejolt@_Data Storage Blocks": "Блоки Хранилища Данных", + "gamejolt@_Debug Blocks": "Блоки Отладки", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]": "Найти [amount] [globalOrPerUser] счет/а [betterOrWorse] чем [value] в таблице с ID [ID]", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]": "Найти [amount] [globalOrPerUser] счет/а в таблице с ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]": "Найти [amount] счет/а от [username] [betterOrWorse] чем [value] в таблице с ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s in table of ID [ID]": "Найти [amount] счет/а от [username] в таблице с ID [ID]", + "gamejolt@_Fetch [globalOrPerUser] keys matching with [pattern]": "Найти [globalOrPerUser] ключи по шаблону [pattern]", + "gamejolt@_Fetch [trophyFetchGroup] trophies": "Найти [trophyFetchGroup] трофеи", + "gamejolt@_Fetch all [globalOrPerUser] keys": "Найти все [globalOrPerUser] ключи ", + "gamejolt@_Fetch batch [parameter]": "[parameter] пакетный запрос ", + "gamejolt@_Fetch logged in user": "Найти данные текущего пользователя", + "gamejolt@_Fetch score tables": "Найти таблицы счета", + "gamejolt@_Fetch server's time": "Найти время сервера", + "gamejolt@_Fetch trophy of ID[ID]": "Найти трофей с ID [ID]", + "gamejolt@_Fetch user's [usernameOrID] by [fetchType]": "Найти данные пользователя [usernameOrID] через [fetchType]", + "gamejolt@_Fetch user's friend IDs": "Найти ID друзей пользователя", + "gamejolt@_Fetched [globalOrPerUser] data at [key]": "Найденный [globalOrPerUser] [key]", + "gamejolt@_Fetched batch data in JSON": "Найденные данные пакетного запроса в JSON", + "gamejolt@_Fetched key at index [index]": "Найденный ключ по индексу [index]", + "gamejolt@_Fetched keys in JSON": "Найденные ключи в JSON", + "gamejolt@_Fetched rank of [value] in table of ID [ID]": "Найденное место счета [value] в таблице с ID [ID]", + "gamejolt@_Fetched score [scoreDataType] at index [index]": "Найденные данные [scoreDataType] счета по индексу [index]", + "gamejolt@_Fetched score data in JSON": "Найденные данные счета/ов в JSON", + "gamejolt@_Fetched server's [timeType]": "Найденные данные [timeType] сервера", + "gamejolt@_Fetched server's time in JSON": "Найденное время сервера в JSON", + "gamejolt@_Fetched table [tableDataType] at index [index]": "Найденные данные [tableDataType] таблицы по индексу [index]", + "gamejolt@_Fetched tables in JSON": "Найденные данные таблиц в JSON", + "gamejolt@_Fetched trophies in JSON": "Найденные трофеи в JSON", + "gamejolt@_Fetched trophy [trophyDataType] at index [index]": "Найденные данные [trophyDataType] трофея по индексу [index]", + "gamejolt@_Fetched user's [userDataType]": "Найденные данные [userDataType] пользователя ", + "gamejolt@_Fetched user's data in JSON": "Найденные данные пользователя в JSON", + "gamejolt@_Fetched user's friend ID at index[index]": "Найденный ID друга пользователя по индексу [index]", + "gamejolt@_Fetched user's friend IDs in JSON": "Найденные ID друзей пользователя в JSON", + "gamejolt@_In debug mode?": "В режиме отладки?", + "gamejolt@_Last API error": "Последняя ошибка API", + "gamejolt@_Logged in user's username": "Имя текущего пользователя", + "gamejolt@_Logged in?": "Вход совершен?", + "gamejolt@_Login automatically": "Войти автоматически", + "gamejolt@_Login with [username] and [token]": "Войти в аккаунт как [username] через [token]", + "gamejolt@_Logout": "Выйти из аккаунта", + "gamejolt@_Open": "Открыть", + "gamejolt@_Ping session": "Пинг сессии", + "gamejolt@_Remove [globalOrPerUser] data at [key]": "Удалить [globalOrPerUser] [key]", + "gamejolt@_Remove trophy of ID [ID]": "Убрать трофей с ID [ID]", + "gamejolt@_Score Blocks": "Блоки Счета", + "gamejolt@_Session Blocks": "Блоки Сессии", + "gamejolt@_Session open?": "Сессия открыта?", + "gamejolt@_Set [globalOrPerUser] data at [key] to [data]": "Задать [globalOrPerUser] [key] значение [data]", + "gamejolt@_Set game ID to [ID] and private key to [key]": "Задать ID игры как [ID] и приватный ключ как [key]", + "gamejolt@_Set session status to [status]": "Задать статус сессии в [status]", + "gamejolt@_Time Blocks": "Блоки Времени", + "gamejolt@_Trophy Blocks": "Блоки Трофеев", + "gamejolt@_Turn debug mode [toggle]": "[toggle] режим отладки", + "gamejolt@_Update [globalOrPerUser] data at [key] by [operationType] [value]": "Обновить [globalOrPerUser] [key] [operationType] [value]", + "gamejolt@_User Blocks": "Блоки Пользователей", + "gamejolt@_[openOrClose] session": "[openOrClose] сессию", + "gamejolt@_achievement date": "дата получения", + "gamejolt@_active": "активный", + "gamejolt@_adding": "прибавив", + "gamejolt@_all": "все", + "gamejolt@_all achieved": "все полученные", + "gamejolt@_all unachieved": "все не полученные", + "gamejolt@_appending": "добавив справа", + "gamejolt@_avatar URL": "URL аватара", + "gamejolt@_better": "лучше", + "gamejolt@_comment": "комментарий", + "gamejolt@_data": "данные", + "gamejolt@_day": "день", + "gamejolt@_description": "описание", + "gamejolt@_developer username": "имя разработчика", + "gamejolt@_difficulty": "сложность", + "gamejolt@_dividing by": "делением на", + "gamejolt@_global": "глобальный", + "gamejolt@_guest": "гость", + "gamejolt@_hour": "час", + "gamejolt@_idle": "неактивный", + "gamejolt@_image URL": "URL картинки", + "gamejolt@_in parallel": "Параллельный", + "gamejolt@_index": "индекс", + "gamejolt@_key": "ключ", + "gamejolt@_last login": "последний вход", + "gamejolt@_last login timestamp": "временной штамп последнего входа", + "gamejolt@_minute": "минута", + "gamejolt@_month": "месяц", + "gamejolt@_multiplying by": "умножением на", + "gamejolt@_name": "имя", + "gamejolt@_off": "Выключить", + "gamejolt@_on": "Включить", + "gamejolt@_optional": "опционально", + "gamejolt@_prepending": "добавив слева", + "gamejolt@_primary": "основная", + "gamejolt@_private key": "приватный ключ", + "gamejolt@_private token": "приватный токен", + "gamejolt@_score date": "дата получения счета", + "gamejolt@_score timestamp": "временной штамп получения счета", + "gamejolt@_second": "секунда", + "gamejolt@_sequentially": "Последовательный", + "gamejolt@_sequentially, break on error": "Последовательный, прервать при ошибке", + "gamejolt@_sign up date": "дата регистрации", + "gamejolt@_sign up timestamp": "временной штамп регистрации", + "gamejolt@_status": "статус", + "gamejolt@_subtracting": "убавив", + "gamejolt@_text": "текст", + "gamejolt@_timestamp": "временной штамп", + "gamejolt@_timezone": "часовой пояс", + "gamejolt@_title": "название", + "gamejolt@_type": "тип", + "gamejolt@_user": "пользовательский", + "gamejolt@_user ID": "ID пользователя", + "gamejolt@_username": "имя пользователя", + "gamejolt@_value": "значение", + "gamejolt@_website": "вебсайт", + "gamejolt@_worse": "хуже", + "gamejolt@_year": "год", + "gamepad@_Gamepad": "Геймпад", + "gamepad@_button [b] on pad [i] pressed?": "триггер [b] на геймпаде [i] нажат?", + "gamepad@_direction of axes [axis] on pad [pad]": "направление на оси [axis] на геймпаде [pad]", + "gamepad@_gamepad [pad] connected?": "геймпад [pad] подключен?", + "gamepad@_magnitude of axes [axis] on pad [pad]": "величина на оси [axis] на геймпаде [pad]", + "gamepad@_value of axis [b] on pad [i]": "значение на оси [b] на геймпаде [i]", + "gamepad@_value of button [b] on pad [i]": "значение на триггере [b] на геймпаде [i]", "iframe@_It works!": "Работает!", + "iframe@_close iframe": "закрыть iframe", + "iframe@_height": "высота", + "iframe@_hide iframe": "спрятать iframe", + "iframe@_set iframe x position to [X]": "задать позицию iframe x в [X]", + "iframe@_set iframe y position to [Y]": "задать позицию iframe y в [Y]", + "iframe@_show HTML [HTML]": "показать HTML [HTML]", + "iframe@_show iframe": "показать iframe", + "iframe@_show website [URL]": "показать вебсайт [URL]", + "iframe@_visible": "виден", + "iframe@_width": "ширина", + "itchio@_Data": "Данные", + "itchio@_Fetch game data [user][game][secret]": "Найти данные игры [user][game][secret]", + "itchio@_Game data?": "Данные игры?", + "itchio@_Open [prefix] itch.io [page] window with [width]width and [height]height": "Открыть окно [prefix] itch.io [page] с шириной [width] и высотой [height] ", + "itchio@_Return game [data]": "Вернуть [data] игры", + "itchio@_Return game data [user][game][secret] in .json": "Вернуть данные игры [user][game][secret] в .json", + "itchio@_Return game data in .json": "Вернуть данные игры в .json", + "itchio@_Return game rewards [rewards] by index:[index]": "Вернуть награды игры [rewards] по индексу: [index]", + "itchio@_Return game sale [sale]": "Вернуть скидку игры [sale]", + "itchio@_Return game sub products [subProducts] by index:[index]": "Вернуть подпродукты игры [subProducts] по индексу: [index]", + "itchio@_Return rewards list length": "Вернуть длину списка наград", + "itchio@_Return sub products list length": "Вернуть длину списа подпродуктов", + "itchio@_Rewards": "Награды", + "itchio@_Rewards?": "Награды?", + "itchio@_Sale": "Скидки", + "itchio@_Sale?": "Скидка?", + "itchio@_Sub products": "Подпродукты", + "itchio@_Sub products?": "Подпродукты?", + "itchio@_amount": "количество", + "itchio@_amount remaining": "оставшееся количество", + "itchio@_available": "доступно", + "itchio@_cover image URL": "URL обложки", + "itchio@_end date": "дата конца", + "itchio@_game": "игра", + "itchio@_id": "ID", + "itchio@_name": "имя", + "itchio@_original price": "изначальная цена", + "itchio@_price": "цена", + "itchio@_rate": "оценка", + "itchio@_title": "название", + "itchio@_user": "пользователь", + "lab/text@_# of lines": "кол-во строк", + "lab/text@_Enable Non-Scratch Lab Features": "Включить функции не из Scratch Lab", "lab/text@_Hello!": "Привет!", "lab/text@_Here we go!": "Поехали!", - "lab/text@_center": "центр", - "runtime-options@_Runtime Options": "Опции Выполнения" + "lab/text@_Incompatible with Scratch Lab:": "Несовместимо со Scratch Lab:", + "lab/text@_Welcome to my project!": "Добро пожаловать в мой проект!", + "lab/text@_[ANIMATE] duration": "продолжительность анимации [ANIMATE] ", + "lab/text@_[ANIMATE] text [TEXT]": "[ANIMATE] текст [TEXT]", + "lab/text@_add line [TEXT]": "добавить строку [TEXT]", + "lab/text@_align text to [ALIGN]": "выровнять текст по [ALIGN]", + "lab/text@_animate [ANIMATE] until done": "анимировать [ANIMATE] текст до конца", + "lab/text@_center": "центру", + "lab/text@_displayed text": "показанный текст", + "lab/text@_is animating?": "анимируется?", + "lab/text@_is showing text?": "показывает текст?", + "lab/text@_left": "левому краю", + "lab/text@_rainbow": "радужный", + "lab/text@_random font": "случайный шрифт", + "lab/text@_reset [ANIMATE] duration": "сбросить продолжительность анимации [ANIMATE]", + "lab/text@_reset text width": "сбросить ширину текста", + "lab/text@_reset typing delay": "сбросить задержку печатания", + "lab/text@_right": "правому краю", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "задать продолжительность анимации [ANIMATE] в [NUM] секунд", + "lab/text@_set font to [FONT]": "задать шрифт в [FONT]", + "lab/text@_set text color to [COLOR]": "задать цвет текста в [COLOR]", + "lab/text@_set typing delay to [NUM] seconds": "задать задержку печатания в [NUM] секунд", + "lab/text@_set width to [WIDTH]": "задать ширину в [WIDTH]", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "задать ширину в [WIDTH] выровненную по [ALIGN]", + "lab/text@_show sprite": "показать спрайт", + "lab/text@_start [ANIMATE] animation": "начать анимацию [ANIMATE] текст", + "lab/text@_text [ATTRIBUTE]": "[ATTRIBUTE] текста", + "lab/text@_type": "печатающийся", + "lab/text@_typing delay": "задержка печатания", + "lab/text@_zoom": "вырастающий", + "lab/text@disableCompatibilityMode": "Это включит новые блоки и функции которые НЕ БУДУТ РАБОТАТЬ в оффициальной Scratch Lab.\n\nХотите продолжить?", + "pointerlock@_disabled": "выключен", + "pointerlock@_enabled": "включен", + "runtime-options@_Infinity": "Бесконечно", + "runtime-options@_Runtime Options": "Опции Выполнения", + "runtime-options@_[thing] enabled?": "[thing] включен?", + "runtime-options@_clone limit": "лимит клонов", + "runtime-options@_default ({n})": "по умолчанию ({n})", + "runtime-options@_disabled": "выключен", + "runtime-options@_enabled": "включен", + "runtime-options@_framerate limit": "лимит частоты кадров", + "runtime-options@_height": "высота", + "runtime-options@_high quality pen": "перо в высоком качестве", + "runtime-options@_interpolation": "интерполяция", + "runtime-options@_remove fencing": "убрать рамку", + "runtime-options@_remove misc limits": "убрать разные ограничения", + "runtime-options@_run green flag [flag]": "запустить зеленый флажок [flag]", + "runtime-options@_set [thing] to [enabled]": "установить [thing] в [enabled]", + "runtime-options@_set clone limit to [limit]": "задать лимит клонов в [limit]", + "runtime-options@_set framerate limit to [fps]": "задать лимит частоты кадров в [fps]", + "runtime-options@_set stage size width: [width] height: [height]": "задать ширину: [width] высоту: [height] сцены", + "runtime-options@_set username to [username]": "задать имя пользователя как [username]", + "runtime-options@_stage [dimension]": "[dimension] сцены", + "runtime-options@_turbo mode": "турбо режим", + "runtime-options@_width": "ширина", + "sound@_Sound": "Звук", + "sound@_play sound from url: [path] until done": "играть звук из url: [path] до конца", + "sound@_start sound from url: [path]": "включить звук из url: [path]", + "stretch@_Stretch": "Растяжение", + "stretch@_change stretch by x: [DX] y: [DY]": "изменить растяжение на x: [DX] y: [DY]", + "stretch@_change stretch x by [DX]": "изменить растяжение x на [DX]", + "stretch@_change stretch y by [DY]": "изменить растяжение y на [DY]", + "stretch@_set stretch to x: [X] y: [Y]": "задать растяжение в x: [X] y: [Y]", + "stretch@_set stretch x to [X]": "задать растяжение x в [X]", + "stretch@_set stretch y to [Y]": "задать растяжение y в [Y]", + "stretch@_x stretch": "растяжение x", + "stretch@_y stretch": "растяжение y" }, "sl": { + "-SIPC-/consoles@_Error": "Napaka", "files@_Select or drop file": "Izberite ali povlecite datoteko", + "gamejolt@_Close": "Zapri", "runtime-options@_Runtime Options": "Možnosti izvajanja" }, "sv": { + "-SIPC-/consoles@_Error": "Fel", "files@_Select or drop file": "Välj eller släpp fil", + "gamejolt@_Close": "Stäng", "runtime-options@_Runtime Options": "Körtidsalternativ" }, "tr": { + "-SIPC-/consoles@_Error": "Hata", "files@_Select or drop file": "Dosyayı şeçin yada buraya bırakın", + "gamejolt@_Close": "Kapat", "runtime-options@_Runtime Options": "Çalışma Zamanı Seçenekleri" }, "uk": { + "-SIPC-/consoles@_Error": "Помилка", "files@_Select or drop file": "Виберіть або \"закиньте\" файл", + "gamejolt@_Close": "Закрити", "runtime-options@_Runtime Options": "Параметри виконання" }, "zh-cn": { + "-SIPC-/consoles@_Clear Console": "清空控制台", + "-SIPC-/consoles@_Consoles": "控制台", + "-SIPC-/consoles@_Debug [string]": "Debug[string]", + "-SIPC-/consoles@_Error": "错误", + "-SIPC-/consoles@_Error [string]": "打印错误[string]", + "-SIPC-/consoles@_Information [string]": "打印信息[string]", + "-SIPC-/consoles@_Print the time run by the timer named [string]": "打印计时器[string]运行的时间", + "-SIPC-/consoles@_Start a timer named [string]": "启动计时器[string]", + "-SIPC-/consoles@_Time": "时间", + "-SIPC-/consoles@_Warning [string]": "打印警告[string]", + "-SIPC-/consoles@_group": "群组", + "-SIPC-/time@_Time": "时间", "0832/rxFS2@clean": "清空文件系统", "0832/rxFS2@del": "删除 [STR]", "0832/rxFS2@folder": "设置 [STR] 为 [STR2]", @@ -503,29 +1769,202 @@ "0832/rxFS2@start": "新建 [STR]", "0832/rxFS2@sync": "将 [STR] 的位置改为 [STR2]", "0832/rxFS2@webin": "从网络加载 [STR]", + "Alestore/nfcwarp@_NFCWarp": "NFC", + "Alestore/nfcwarp@_read NFC tag": "读取NFC标签", + "CST1229/zip@_Hello, world?": "你好世界?", + "CST1229/zip@_folder": "文件夹", + "CST1229/zip@_hex": "Hex", + "CST1229/zip@_name": "名字", + "CST1229/zip@_new file": "新文件", + "CST1229/zip@_new folder": "新文件夹", + "CST1229/zip@_path": "路径", + "CST1229/zip@_string": "字符串", + "CST1229/zip@_text": "文本", + "Clay/htmlEncode@_HTML Encode": "HTML编码", + "Clay/htmlEncode@_Hello!": "你好!", + "CubesterYT/TurboHook@_TurboHook": "Turbo网络钩子", + "CubesterYT/TurboHook@_icon": "图标", + "CubesterYT/TurboHook@_name": "名字", + "CubesterYT/WindowControls@_Hello World!": "你好世界!", + "CubesterYT/WindowControls@_Window Controls": "网页控制", + "CubesterYT/WindowControls@_center": "居中", + "CubesterYT/WindowControls@_enter fullscreen": "进入全屏", + "CubesterYT/WindowControls@_exit fullscreen": "退出全屏", + "CubesterYT/WindowControls@_is window fullscreen?": "页面全屏吗?", + "CubesterYT/WindowControls@_left": "居左", + "CubesterYT/WindowControls@_right": "居右", + "CubesterYT/WindowControls@_screen height": "屏幕高度", + "CubesterYT/WindowControls@_screen width": "屏幕宽度", + "CubesterYT/WindowControls@_set window title to [TITLE]": "设置页面标题为[TITLE]", + "CubesterYT/WindowControls@_window height": "页面高度", + "CubesterYT/WindowControls@_window title": "页面标题", + "CubesterYT/WindowControls@_window width": "页面宽度", + "CubesterYT/WindowControls@_window x": "页面中心的x坐标", + "CubesterYT/WindowControls@_window y": "页面中心的y坐标", + "DNin/wake-lock@_Wake Lock": "保持唤醒", + "DNin/wake-lock@_off": "关闭", + "DNin/wake-lock@_on": "打开", + "DT/cameracontrols@_camera direction": "摄像机的方向", + "DT/cameracontrols@_camera x": "摄像机x坐标", + "DT/cameracontrols@_camera y": "摄像机y坐标", + "DT/cameracontrols@_change camera x by [val]": "摄像机的x坐标增加[val]", + "DT/cameracontrols@_change camera y by [val]": "摄像机的y坐标增加[val]", + "DT/cameracontrols@_move camera [val] steps": "摄像机移动[val]步", + "DT/cameracontrols@_move camera to [sprite]": "移动摄像机到角色[sprite]", + "DT/cameracontrols@_set camera direction to [val]": "设置摄像机的方向为[val]", + "DT/cameracontrols@_set camera to x: [x] y: [y]": "设置摄像机的坐标为x[x] y[y]", + "DT/cameracontrols@_set camera x to [val]": "设置摄像机的x坐标为[val]", + "DT/cameracontrols@_set camera y to [val]": "设置摄像机的y坐标为[val]", "NOname-awa/graphics2d@graph": "图形 [graph] 的 [CS]", "NOname-awa/graphics2d@line_section": "([x1],[y1])到([x2],[y2])的长度", "NOname-awa/graphics2d@name": "图形 2D", - "NOname-awa/graphics2d@pi": "派", + "NOname-awa/graphics2d@pi": "π", "NOname-awa/graphics2d@quadrilateral": "矩形([x1],[y1])([x2],[y2])([x3],[y3])([x4],[y4])的 [CS]", "NOname-awa/graphics2d@ray_direction": "([x1],[y1])的([x2],[y2])的距离", "NOname-awa/graphics2d@round": "[rd] 为 [a] 的圆的 [CS]", "NOname-awa/graphics2d@triangle": "三角形([x1],[y1])([x2],[y2])([x3],[y3])的 [CS]", "NOname-awa/graphics2d@triangle_s": "三角形 [s1] [s2] [s3] 的面积", + "NexusKitten/controlcontrols@_Control Controls": "控件控制", + "NexusKitten/controlcontrols@_fullscreen": "全屏", + "NexusKitten/moremotion@_More Motion": "更多运动", + "NexusKitten/moremotion@_costume height": "造型高度", + "NexusKitten/moremotion@_costume width": "造型宽度", + "NexusKitten/moremotion@_height": "高度", + "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "触碰坐标x[X] y[Y]?", + "NexusKitten/moremotion@_width": "宽度", + "box2d@griffpatch.categoryName": "物理引擎", + "box2d@griffpatch.doTick": "逐步模拟", "clipboard@_Clipboard": "剪切板", + "clouddata-ping@_Ping Cloud Data": "检测云数据", + "cs2627883/numericalencoding@_Hello!": "你好!", + "cursor@_Mouse Cursor": "鼠标图标", + "cursor@_center": "居中", "encoding@_Encoding": "编码", + "encoding@_apple": "苹果", + "fetch@_Fetch": "请求API", "files@_Files": "文件", + "files@_Hello, world!": "你好,世界!", "files@_Select or drop file": "选择或拖入文件", - "lab/text@_Animated Text": "动画文字", + "files@_open a file": "打开一个文件", + "files@_save.txt": "保存.txt", + "files@_text": "文本", + "gamejolt@_Close": "关闭", + "gamejolt@_name": "名字", + "gamejolt@_off": "关闭 ", + "gamejolt@_on": "打开", + "gamejolt@_text": "文本", + "gamejolt@_title": "标题", + "gamejolt@_type": "类型", + "gamejolt@_user ID": "用户ID", + "gamejolt@_value": "值", + "iframe@_Iframe": "内嵌框架", + "iframe@_height": "高度", + "iframe@_width": "宽度", + "itchio@_id": "ID", + "itchio@_name": "名字", + "itchio@_title": "标题", + "lab/text@_# of lines": "文本行数", + "lab/text@_Animated Text": "艺术字", + "lab/text@_Enable Non-Scratch Lab Features": "显示与Scratch Lab不兼容的积木", + "lab/text@_Hello!": "你好!", + "lab/text@_Here we go!": "现在出发!", + "lab/text@_Incompatible with Scratch Lab:": "以下积木与Scratch Lab不兼容:", + "lab/text@_Welcome to my project!": "欢迎来到我的作品!", + "lab/text@_[ANIMATE] text [TEXT]": "显示动画样式是[ANIMATE]的文本[TEXT]", + "lab/text@_add line [TEXT]": "增加一行文本[TEXT]", + "lab/text@_align text to [ALIGN]": "设置文本的展示样式为[ALIGN]", + "lab/text@_center": "居中", + "lab/text@_displayed text": "显示的文本", + "lab/text@_is showing text?": "文本显示了?", + "lab/text@_left": "居左", + "lab/text@_rainbow": "彩虹色", + "lab/text@_random font": "随机字体", + "lab/text@_reset text width": "重置文本宽度", + "lab/text@_right": "居右", + "lab/text@_set font to [FONT]": "设置文本的字体为[FONT]", + "lab/text@_set text color to [COLOR]": "设置文本的颜色为[COLOR]", + "lab/text@_set width to [WIDTH]": "设置文本的宽度为[WIDTH]", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "设置[ALIGN]样式的宽度为[WIDTH]", + "lab/text@_show sprite": "显示角色", + "lab/text@_show text [TEXT]": "显示文本[TEXT]", + "lab/text@_text [ATTRIBUTE]": "文本的[ATTRIBUTE]", + "lab/text@_type": "逐字显示", + "lab/text@_zoom": "移动动画", + "local-storage@_Local Storage": "本地存储", + "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "本地存储拓展:请先运行“设置存储命名空间ID”积木才能使用下面的积木", + "local-storage@_delete all keys": "删除所有本地存储变量", + "local-storage@_delete key [KEY]": "删除本地存储变量[KEY]", + "local-storage@_get key [KEY]": "本地存储变量[KEY]的值", + "local-storage@_project title": "作品标题", + "local-storage@_score": "分数", + "local-storage@_set key [KEY] to [VALUE]": "设置本地存储变量[KEY]的值为[VALUE]", + "local-storage@_set storage namespace ID to [ID]": "设置存储命名空间ID为[ID]", + "navigator@_dark": "暗色", + "navigator@_light": "亮色", + "pointerlock@_disabled": "禁用", + "pointerlock@_enabled": "启用", + "qxsck/data-analysis@average": "[NUMBERS]里所有数字的平均数", + "qxsck/data-analysis@maximum": "[NUMBERS]里所有数字的最大数", + "qxsck/data-analysis@median": "[NUMBERS]里所有数字的中位数", + "qxsck/data-analysis@minimum": "[NUMBERS]里所有数字的最小数", + "qxsck/data-analysis@mode": "[NUMBERS]里所有数字的众数", "qxsck/data-analysis@name": "数据分析", - "qxsck/var-and-list@copyList": "复制 [LIST1] 到 [LIST2]", + "qxsck/data-analysis@variance": "[NUMBERS]里所有数字的方差", + "qxsck/var-and-list@addValueInList": "把[VALUE]加入列表[LIST]", + "qxsck/var-and-list@clearList": "删除列表[LIST]的所有值", + "qxsck/var-and-list@copyList": "复制列表 [LIST1] 的数据到列表 [LIST2]", + "qxsck/var-and-list@deleteOfList": "删除列表[LIST]的第[INDEX]项", + "qxsck/var-and-list@getIndexOfList": "列表[LIST]第一个[VALUE]的位置", + "qxsck/var-and-list@getIndexesOfList": "列表[LIST]里所有[VALUE]的位置", + "qxsck/var-and-list@getList": "列表[LIST]的值", + "qxsck/var-and-list@getValueOfList": "列表[LIST]第[INDEX]项的值", + "qxsck/var-and-list@getVar": "变量[VAR]的值", + "qxsck/var-and-list@length": "列表[LIST]的长度", + "qxsck/var-and-list@listContains": "列表[LIST]包括[VALUE]?", "qxsck/var-and-list@name": "变量与列表", + "qxsck/var-and-list@replaceOfList": "把列表[LIST]第[INDEX]项的值替换为[VALUE]", + "qxsck/var-and-list@seriListsToJson": "把所有以[START]开头的列表转换为JSON", + "qxsck/var-and-list@seriVarsToJson": "把所有以[START]开头的变量转换为JSON", + "qxsck/var-and-list@setVar": "把变量[VAR]的值修改为[VALUE]", + "runtime-options@_Infinity": "无限", "runtime-options@_Runtime Options": "运行选项", + "runtime-options@_[thing] enabled?": "启用了[thing]?", + "runtime-options@_clone limit": "克隆限制", + "runtime-options@_default ({n})": "默认值({n})", + "runtime-options@_disabled": "禁用", + "runtime-options@_enabled": "启用", + "runtime-options@_framerate limit": "FPS上限", + "runtime-options@_height": "高度", + "runtime-options@_high quality pen": "高清画笔", + "runtime-options@_interpolation": "补帧", + "runtime-options@_remove fencing": "允许角色移出舞台", + "runtime-options@_remove misc limits": "取消音效范围与画笔大小限制", + "runtime-options@_run green flag [flag]": "运行绿旗[flag]", + "runtime-options@_set [thing] to [enabled]": "设置[thing]为[enabled]", + "runtime-options@_set clone limit to [limit]": "设置克隆体限制为[limit]", + "runtime-options@_set framerate limit to [fps]": "设置FPS上限为[fps]", + "runtime-options@_set stage size width: [width] height: [height]": "把舞台大小设置为宽[width] 高[height]", + "runtime-options@_set username to [username]": "设置用户名称为[username]", + "runtime-options@_stage [dimension]": "舞台的[dimension]", + "runtime-options@_turbo mode": "编译模式", + "runtime-options@_width": "宽度", "sound@_Sound": "声音", - "stretch@_Stretch": "伸缩" + "sound@_play sound from url: [path] until done": "播放URL[path]的声音直到结束", + "sound@_start sound from url: [path]": "播放URL[path]的声音", + "stretch@_Stretch": "伸缩", + "stretch@_change stretch by x: [DX] y: [DY]": "角色的x轴伸缩比例增加[DX],y轴伸缩比例增加[DY]", + "stretch@_change stretch x by [DX]": "x轴伸缩比例增加[DX]", + "stretch@_change stretch y by [DY]": "y轴伸缩比例增加[DY]", + "stretch@_set stretch to x: [X] y: [Y]": "设置角色的x轴伸缩比例为[X],y轴伸缩比例为[Y]", + "stretch@_set stretch x to [X]": "设置x轴伸缩比例为[X]", + "stretch@_set stretch y to [Y]": "设置y轴伸缩比例为[Y]", + "stretch@_x stretch": "x轴伸缩比例", + "stretch@_y stretch": "y轴伸缩比例" }, "zh-tw": { + "-SIPC-/consoles@_Error": "錯誤", "files@_Select or drop file": "選擇或放入檔案", + "gamejolt@_Close": "關閉", "runtime-options@_Runtime Options": "運行選項" } } \ No newline at end of file From b275d8a3fe745e52862ab9a9a25e0322fff91db4 Mon Sep 17 00:00:00 2001 From: Obvious Alex C <76855369+David-Orangemoon@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:35:46 -0500 Subject: [PATCH 061/196] obviousAlexC/penPlus: bug fixes (#1140) Backport the V7 depth buffer Backport tri drawing from V7 Fix colour bug Fix dot and line drawing bug. --- extensions/obviousAlexC/penPlus.js | 581 ++++++++++++----------------- 1 file changed, 235 insertions(+), 346 deletions(-) diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js index 3d6c9fd469..9fdf55db34 100644 --- a/extensions/obviousAlexC/penPlus.js +++ b/extensions/obviousAlexC/penPlus.js @@ -8,6 +8,7 @@ //?some smaller optimizations just store the multiplacation for later const f32_4 = 4 * Float32Array.BYTES_PER_ELEMENT; + const f32_6 = 6 * Float32Array.BYTES_PER_ELEMENT; const f32_8 = 8 * Float32Array.BYTES_PER_ELEMENT; const f32_10 = 10 * Float32Array.BYTES_PER_ELEMENT; const d2r = 0.0174533; @@ -31,7 +32,7 @@ let depthBufferTexture = gl.createTexture(); //?Make a function for updating the depth canvas to fit the scratch stage - const depthFrameBuffer = gl.createFramebuffer(); + const triFrameBuffer = gl.createFramebuffer(); const depthColorBuffer = gl.createRenderbuffer(); const depthDepthBuffer = gl.createRenderbuffer(); @@ -60,7 +61,7 @@ gl.bindTexture(gl.TEXTURE_2D, depthBufferTexture); gl.activeTexture(gl.TEXTURE0); - gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); gl.bindRenderbuffer(gl.RENDERBUFFER, depthColorBuffer); gl.renderbufferStorage( @@ -97,11 +98,12 @@ depthBufferTexture, 0 ); - gl.enable(gl.DEPTH_TEST); gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + let resizeCall = false; + const updateCanvasSize = () => { nativeSize = renderer.useHighQualityRender ? [canvas.width, canvas.height] @@ -109,8 +111,9 @@ lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); + gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT); gl.bindRenderbuffer(gl.RENDERBUFFER, depthColorBuffer); gl.renderbufferStorage( gl.RENDERBUFFER, @@ -154,18 +157,27 @@ window.addEventListener("resize", updateCanvasSize); vm.runtime.on("STAGE_SIZE_CHANGED", () => { updateCanvasSize(); + resizeCall = true; }); + //Turbowarp vm.runtime.on("BEFORE_EXECUTE", () => { - if ( - (renderer.useHighQualityRender + let calcSize = renderer.useHighQualityRender + ? [canvas.width, canvas.height] + : renderer._nativeSize; + if (calcSize[0] != nativeSize[0] || calcSize[1] != nativeSize[1]) { + nativeSize = renderer.useHighQualityRender ? [canvas.width, canvas.height] - : renderer._nativeSize) != nativeSize - ) { + : renderer._nativeSize; + updateCanvasSize(); + } + + if (resizeCall) { nativeSize = renderer.useHighQualityRender ? [canvas.width, canvas.height] : renderer._nativeSize; updateCanvasSize(); + resizeCall = false; } }); @@ -183,9 +195,6 @@ const penPlusCostumeLibrary = {}; let penPlusImportWrapMode = gl.CLAMP_TO_EDGE; - //?Debug for depth - penPlusCostumeLibrary["!Debug_Depth"] = depthBufferTexture; - const checkForPen = (util) => { const curTarget = util.target; const customState = curTarget["_customState"]; @@ -229,43 +238,19 @@ attribute highp vec4 a_position; attribute highp vec4 a_color; varying highp vec4 v_color; - - varying highp float v_depth; void main() { v_color = a_color; - v_depth = a_position.z; - gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); } `, frag: ` varying highp vec4 v_color; - uniform mediump vec2 u_res; - uniform sampler2D u_depthTexture; - - varying highp float v_depth; - void main() { gl_FragColor = v_color; - highp vec4 v_depthPart = texture2D(u_depthTexture,gl_FragCoord.xy/u_res); - highp float v_depthcalc = v_depthPart.r + floor((v_depthPart.g + floor(v_depthPart.b * 100.0 )) * 100.0); - - highp float v_inDepth = v_depth; - - if (v_depth < 0.0 ) { - v_inDepth = 0.0; - } - if (v_depth > 10000.0 ) { - v_inDepth = 10000.0; - } - - if (v_depthcalc < v_inDepth){ - gl_FragColor.a = 0.0; - } - gl_FragColor.rgb *= gl_FragColor.a; } `, @@ -281,15 +266,12 @@ varying highp vec4 v_color; varying highp vec2 v_texCoord; - - varying highp float v_depth; void main() { v_color = a_color; v_texCoord = a_texCoord; - v_depth = a_position.z; - gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); } `, frag: ` @@ -297,31 +279,10 @@ varying highp vec2 v_texCoord; varying highp vec4 v_color; - - uniform mediump vec2 u_res; - uniform sampler2D u_depthTexture; - - varying highp float v_depth; void main() { gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; - highp vec4 v_depthPart = texture2D(u_depthTexture,gl_FragCoord.xy/u_res); - highp float v_depthcalc = v_depthPart.r + floor((v_depthPart.g + floor(v_depthPart.b * 100.0 )) * 100.0); - - highp float v_inDepth = v_depth; - - if (v_depth < 0.0 ) { - v_inDepth = 0.0; - } - if (v_depth > 10000.0 ) { - v_inDepth = 10000.0; - } - - if (v_depthcalc < v_inDepth){ - gl_FragColor.a = 0.0; - } - gl_FragColor.rgb *= gl_FragColor.a; } @@ -329,35 +290,29 @@ }, ProgramInf: null, }, - depth: { + draw: { Shaders: { vert: ` attribute highp vec4 a_position; - - varying highp float v_depth; + + varying highp vec2 v_texCoord; + attribute highp vec2 a_texCoord; void main() { - v_depth = a_position.z; - gl_Position = a_position * vec4(a_position.w,a_position.w,a_position.w * 0.0001,1); + gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + v_texCoord = (a_position.xy / 2.0) + vec2(0.5,0.5); } `, frag: ` - varying highp float v_depth; + varying highp vec2 v_texCoord; + + uniform sampler2D u_drawTex; void main() { - if (v_depth >= 10000.0) { - gl_FragColor = vec4(1,1,1,1); - } - else { - highp float d_100 = floor(v_depth / 100.0); - gl_FragColor = vec4( - mod(v_depth,1.0), - mod( floor( v_depth - mod(v_depth,1.0) )/100.0,1.0), - mod( floor( d_100 - mod(d_100,1.0) )/100.0,1.0), - 1); - } + gl_FragColor = texture2D(u_drawTex, v_texCoord); + gl_FragColor.rgb *= gl_FragColor.a; } `, }, @@ -429,9 +384,9 @@ penPlusShaders.textured.Shaders.frag ); - penPlusShaders.depth.ProgramInf = penPlusShaders.createAndCompileShaders( - penPlusShaders.depth.Shaders.vert, - penPlusShaders.depth.Shaders.frag + penPlusShaders.draw.ProgramInf = penPlusShaders.createAndCompileShaders( + penPlusShaders.draw.Shaders.vert, + penPlusShaders.draw.Shaders.frag ); } @@ -465,30 +420,20 @@ "u_texture" ); - const u_depthTexture_Location_untext = gl.getUniformLocation( - penPlusShaders.untextured.ProgramInf.program, - "u_depthTexture" - ); - - const u_depthTexture_Location_text = gl.getUniformLocation( - penPlusShaders.textured.ProgramInf.program, - "u_depthTexture" + //?Depth + const u_depthTexture_Location_draw = gl.getUniformLocation( + penPlusShaders.draw.ProgramInf.program, + "u_drawTex" ); - const u_res_Location_untext = gl.getUniformLocation( - penPlusShaders.untextured.ProgramInf.program, - "u_res" + const a_position_Location_draw = gl.getAttribLocation( + penPlusShaders.draw.ProgramInf.program, + "a_position" ); - const u_res_Location_text = gl.getUniformLocation( + const a_textCoord_Location_draw = gl.getAttribLocation( penPlusShaders.textured.ProgramInf.program, - "u_res" - ); - - //?Depth - const a_position_Location_depth = gl.getAttribLocation( - penPlusShaders.depth.ProgramInf.program, - "a_position" + "a_texCoord" ); //?Enables Attributes @@ -502,19 +447,30 @@ gl.enableVertexAttribArray(a_position_Location_text); gl.enableVertexAttribArray(a_color_Location_text); gl.enableVertexAttribArray(a_textCoord_Location_text); - gl.enableVertexAttribArray(a_position_Location_depth); + gl.enableVertexAttribArray(a_position_Location_draw); + gl.enableVertexAttribArray(a_textCoord_Location_draw); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, null); gl.bindBuffer(gl.ARRAY_BUFFER, depthVertexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, null); } + //?Link some stuff to the draw region + //?Might be a better way but I've tried many different things and they didn't work. + let drawnFirst = false; + renderer.oldEnterDrawRegion = renderer.enterDrawRegion; + renderer.enterDrawRegion = (region) => { + triFunctions.drawOnScreen(); + renderer.oldEnterDrawRegion(region); + drawnFirst = false; + }; + //?Override pen Clear with pen+ renderer.penClear = (penSkinID) => { const lastCC = gl.getParameter(gl.COLOR_CLEAR_VALUE); lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); //Pen+ Overrides default pen Clearing - gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); gl.clearColor(1, 1, 1, 1); gl.clear(gl.DEPTH_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT); @@ -541,7 +497,10 @@ //?Have this here for ez pz tri drawing on the canvas const triFunctions = { - drawTri: (curProgram, x1, y1, x2, y2, x3, y3, penColor, targetID) => { + drawTri: (x1, y1, x2, y2, x3, y3, penColor, targetID) => { + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); //? get triangle attributes for current sprite. const triAttribs = triangleAttributesOfAllSprites[targetID]; @@ -630,23 +589,21 @@ gl.useProgram(penPlusShaders.untextured.ProgramInf.program); - gl.uniform1i(u_depthTexture_Location_untext, 1); - - gl.uniform2fv(u_res_Location_untext, nativeSize); - gl.drawArrays(gl.TRIANGLES, 0, 3); //? Hacky fix but it works. - if (penPlusAdvancedSettings.useDepthBuffer) { - triFunctions.drawDepthTri(targetID, x1, y1, x2, y2, x3, y3); - } + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + gl.useProgram(penPlusShaders.pen.program); + if (!drawnFirst) triFunctions.drawOnScreen(); }, - drawTextTri: (curProgram, x1, y1, x2, y2, x3, y3, targetID, texture) => { + drawTextTri: (x1, y1, x2, y2, x3, y3, targetID, texture) => { + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); //? get triangle attributes for current sprite. const triAttribs = triangleAttributesOfAllSprites[targetID]; - if (triAttribs) { vertexBufferData = new Float32Array([ x1, @@ -754,79 +711,75 @@ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, currentFilter); gl.uniform1i(u_texture_Location_text, 0); - gl.uniform1i(u_depthTexture_Location_text, 1); + gl.drawArrays(gl.TRIANGLES, 0, 3); - gl.uniform2fv(u_res_Location_text, nativeSize); + gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); - gl.drawArrays(gl.TRIANGLES, 0, 3); - if (penPlusAdvancedSettings.useDepthBuffer) { - triFunctions.drawDepthTri(targetID, x1, y1, x2, y2, x3, y3); - } gl.useProgram(penPlusShaders.pen.program); + if (!drawnFirst) triFunctions.drawOnScreen(); }, //? this is so I don't have to go through the hassle of replacing default scratch shaders //? many of curse words where exchanged between me and a pillow while writing this extension //? but I have previaled! - drawDepthTri: (targetID, x1, y1, x2, y2, x3, y3) => { - lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - const triAttribs = triangleAttributesOfAllSprites[targetID]; - gl.bindFramebuffer(gl.FRAMEBUFFER, depthFrameBuffer); - - if (triAttribs) { - vertexBufferData = new Float32Array([ - x1, - -y1, - triAttribs[5], - triAttribs[6], - - x2, - -y2, - triAttribs[13], - triAttribs[14], - - x3, - -y3, - triAttribs[21], - triAttribs[22], - ]); - } else { - vertexBufferData = new Float32Array([ - x1, - -y1, - 0, - 1, + drawOnScreen: () => { + drawnFirst = true; + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + vertexBufferData = new Float32Array([ + -1, -1, 0, 1, 0, 1, - x2, - -y2, - 0, - 1, + 1, -1, 0, 1, 1, 1, - x3, - -y3, - 0, - 1, - ]); - } + 1, 1, 0, 1, 1, 0, + ]); //? Bind Positional Data gl.bindBuffer(gl.ARRAY_BUFFER, depthVertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.DYNAMIC_DRAW); gl.vertexAttribPointer( - a_position_Location_depth, + a_position_Location_draw, 4, gl.FLOAT, false, - f32_4, + f32_6, 0 ); + gl.vertexAttribPointer( + a_textCoord_Location_draw, + 2, + gl.FLOAT, + false, + f32_6, + f32_4 + ); + + gl.useProgram(penPlusShaders.draw.ProgramInf.program); + + gl.uniform1i(u_depthTexture_Location_draw, 1); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + + vertexBufferData = new Float32Array([ + -1, -1, 0, 1, 0, 1, - gl.useProgram(penPlusShaders.depth.ProgramInf.program); + -1, 1, 0, 1, 0, 0, + + 1, 1, 0, 1, 1, 0, + ]); + + gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.DYNAMIC_DRAW); gl.drawArrays(gl.TRIANGLES, 0, 3); + lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); + let occ = gl.getParameter(gl.COLOR_CLEAR_VALUE); + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.clearColor(occ[0], occ[1], occ[2], occ[3]); gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); + gl.useProgram(penPlusShaders.pen.program); }, setValueAccordingToCaseTriangle: ( @@ -871,14 +824,11 @@ break; } //convert to depth space for best accuracy - valuetoSet = Math.min( - (value * 10000) / penPlusAdvancedSettings._maxDepth, - 10000 - ); + valuetoSet = value; break; } //convert to depth space for best accuracy - valuetoSet = (value * 10000) / penPlusAdvancedSettings._maxDepth; + valuetoSet = value; break; //Clamp to 1 so we don't accidentally clip. @@ -974,7 +924,7 @@ const pixelData = new Uint8Array(width * height * 4); - const decodedColor = colors.hexToRgb(color); + const decodedColor = Scratch.Cast.toRgbColorObject(color); for (let pixelID = 0; pixelID < pixelData.length / 4; pixelID++) { pixelData[pixelID * 4] = decodedColor.r; @@ -1807,25 +1757,18 @@ checkForPen(util); const curTarget = util.target; const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; - - curTarget.runtime.ext_pen.penDown(null, util); - Scratch.vm.renderer.penPoint( Scratch.vm.renderer._penSkinId, attrib, x, y ); - - curTarget.runtime.ext_pen.penUp(null, util); } drawLine({ x1, y1, x2, y2 }, util) { checkForPen(util); const curTarget = util.target; const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; - curTarget.runtime.ext_pen.penDown(null, util); - Scratch.vm.renderer.penLine( Scratch.vm.renderer._penSkinId, attrib, @@ -1834,8 +1777,6 @@ x2, y2 ); - - curTarget.runtime.ext_pen.penUp(null, util); } squareDown(arg, util) { //Just a simple thing to allow for pen drawing @@ -1868,26 +1809,21 @@ //trying my best to reduce memory usage gl.viewport(0, 0, nativeSize[0], nativeSize[1]); - const dWidth = 1 / nativeSize[0]; - const dHeight = 1 / nativeSize[1]; const spritex = curTarget.x; const spritey = curTarget.y; //correction for HQ pen const typSize = renderer._nativeSize; - const mul = renderer.useHighQualityRender - ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) - : 2; //Predifine stuff so there aren't as many calculations - const wMulX = mul * myAttributes[0]; - const wMulY = mul * myAttributes[1]; + const wMulX = myAttributes[0]; + const wMulY = myAttributes[1]; const offDiam = 0.5 * diam; - const sprXoff = spritex * mul; - const sprYoff = spritey * mul; + const sprXoff = spritex; + const sprYoff = spritey; //Paratheses because I know some obscure browser will screw this up. let x1 = Scratch.Cast.toNumber(-offDiam) * wMulX; let x2 = Scratch.Cast.toNumber(offDiam) * wMulX; @@ -1919,28 +1855,14 @@ rotateTheThings(x1, y1, x2, y2, x3, y3, x4, y4); x1 += sprXoff; - y1 += sprYoff; - x2 += sprXoff; - y2 += sprYoff; - x3 += sprXoff; - y3 += sprYoff; - x4 += sprXoff; - y4 += sprYoff; - - x1 *= dWidth; - y1 *= dHeight; - x2 *= dWidth; - y2 *= dHeight; - - x3 *= dWidth; - y3 *= dHeight; - - x4 *= dWidth; - y4 *= dHeight; + y1 += sprYoff; + y2 += sprYoff; + y3 += sprYoff; + y4 += sprYoff; const Attribute_ID = "squareStamp_" + curTarget.id; @@ -1960,51 +1882,36 @@ triangleAttributesOfAllSprites[Attribute_ID][21] = myAttributes[11]; triangleAttributesOfAllSprites[Attribute_ID][23] = myAttributes[10]; - triFunctions.drawTri( - gl.getParameter(gl.CURRENT_PROGRAM), - x1, - y1, - x2, - y2, - x3, - y3, - attrib.color4f, - Attribute_ID + this.drawSolidTri( + { + x1: x1, + y1: y1, + x2: x2, + y2: y2, + x3: x3, + y3: y3, + }, + util, + true ); - triFunctions.drawTri( - gl.getParameter(gl.CURRENT_PROGRAM), - x1, - y1, - x3, - y3, - x4, - y4, - attrib.color4f, - Attribute_ID + this.drawSolidTri( + { + x1: x1, + y1: y1, + x2: x3, + y2: y3, + x3: x4, + y3: y4, + }, + util, + true ); } squareTexDown({ tex }, util) { //Just a simple thing to allow for pen drawing const curTarget = util.target; - let currentTexture = null; - if (penPlusCostumeLibrary[tex]) { - currentTexture = penPlusCostumeLibrary[tex].texture; - } else { - const costIndex = curTarget.getCostumeIndexByName( - Scratch.Cast.toString(tex) - ); - if (costIndex >= 0) { - const curCostume = curTarget.sprite.costumes_[costIndex]; - if (costIndex != curTarget.currentCostume) { - curTarget.setCostume(costIndex); - } - - currentTexture = renderer._allSkins[curCostume.skinId].getTexture(); - } - } - checkForPen(util); const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; @@ -2016,12 +1923,15 @@ lilPenDabble(nativeSize, curTarget, util); // Do this so the renderer doesn't scream at us - if (!triangleAttributesOfAllSprites["squareStamp_" + curTarget.id]) { + if ( + typeof triangleAttributesOfAllSprites["squareStamp_" + curTarget.id] == + "undefined" + ) { triangleAttributesOfAllSprites["squareStamp_" + curTarget.id] = triangleDefaultAttributes; } - if (!squareAttributesOfAllSprites[curTarget.id]) { + if (typeof squareAttributesOfAllSprites[curTarget.id] == "undefined") { squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; } @@ -2029,26 +1939,21 @@ //trying my best to reduce memory usage gl.viewport(0, 0, nativeSize[0], nativeSize[1]); - const dWidth = 1 / nativeSize[0]; - const dHeight = 1 / nativeSize[1]; const spritex = curTarget.x; const spritey = curTarget.y; //correction for HQ pen const typSize = renderer._nativeSize; - const mul = renderer.useHighQualityRender - ? 2 * ((canvas.width + canvas.height) / (typSize[0] + typSize[1])) - : 2; //Predifine stuff so there aren't as many calculations - const wMulX = mul * myAttributes[0]; - const wMulY = mul * myAttributes[1]; + const wMulX = myAttributes[0]; + const wMulY = myAttributes[1]; const offDiam = 0.5 * diam; - const sprXoff = spritex * mul; - const sprYoff = spritey * mul; + const sprXoff = spritex; + const sprYoff = spritey; //Paratheses because I know some obscure browser will screw this up. let x1 = Scratch.Cast.toNumber(-offDiam) * wMulX; let x2 = Scratch.Cast.toNumber(offDiam) * wMulX; @@ -2080,103 +1985,89 @@ rotateTheThings(x1, y1, x2, y2, x3, y3, x4, y4); x1 += sprXoff; - y1 += sprYoff; - x2 += sprXoff; - y2 += sprYoff; - x3 += sprXoff; - y3 += sprYoff; - x4 += sprXoff; - y4 += sprYoff; - - x1 *= dWidth; - y1 *= dHeight; - - x2 *= dWidth; - y2 *= dHeight; - x3 *= dWidth; - y3 *= dHeight; + y1 += sprYoff; + y2 += sprYoff; + y3 += sprYoff; + y4 += sprYoff; + const Attribute_ID = "squareStamp_" + curTarget.id; + triangleAttributesOfAllSprites[Attribute_ID][0] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][1] = + (1 + myAttributes[6]) * myAttributes[5]; - x4 *= dWidth; - y4 *= dHeight; + triangleAttributesOfAllSprites[Attribute_ID][2] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][3] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][4] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][5] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][8] = myAttributes[10]; - if (currentTexture != null && typeof currentTexture != "undefined") { - const Attribute_ID = "squareStamp_" + curTarget.id; - triangleAttributesOfAllSprites[Attribute_ID][0] = - (0 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][1] = - (1 + myAttributes[6]) * myAttributes[5]; - - triangleAttributesOfAllSprites[Attribute_ID][2] = myAttributes[7]; - triangleAttributesOfAllSprites[Attribute_ID][3] = myAttributes[8]; - triangleAttributesOfAllSprites[Attribute_ID][4] = myAttributes[9]; - triangleAttributesOfAllSprites[Attribute_ID][5] = myAttributes[11]; - triangleAttributesOfAllSprites[Attribute_ID][7] = myAttributes[10]; - - triangleAttributesOfAllSprites[Attribute_ID][8] = - (1 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][9] = - (1 + myAttributes[6]) * myAttributes[5]; - - triangleAttributesOfAllSprites[Attribute_ID][10] = myAttributes[7]; - triangleAttributesOfAllSprites[Attribute_ID][11] = myAttributes[8]; - triangleAttributesOfAllSprites[Attribute_ID][12] = myAttributes[9]; - triangleAttributesOfAllSprites[Attribute_ID][13] = myAttributes[11]; - triangleAttributesOfAllSprites[Attribute_ID][15] = myAttributes[10]; - - triangleAttributesOfAllSprites[Attribute_ID][16] = - (1 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][17] = - (0 + myAttributes[6]) * myAttributes[5]; - - triangleAttributesOfAllSprites[Attribute_ID][18] = myAttributes[7]; - triangleAttributesOfAllSprites[Attribute_ID][19] = myAttributes[8]; - triangleAttributesOfAllSprites[Attribute_ID][20] = myAttributes[9]; - triangleAttributesOfAllSprites[Attribute_ID][21] = myAttributes[11]; - triangleAttributesOfAllSprites[Attribute_ID][23] = myAttributes[10]; + triangleAttributesOfAllSprites[Attribute_ID][8] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][9] = + (1 + myAttributes[6]) * myAttributes[5]; - triFunctions.drawTextTri( - gl.getParameter(gl.CURRENT_PROGRAM), - x1, - y1, - x2, - y2, - x3, - y3, - Attribute_ID, - currentTexture - ); - - triangleAttributesOfAllSprites[Attribute_ID][0] = - (0 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][1] = - (1 + myAttributes[6]) * myAttributes[5]; + triangleAttributesOfAllSprites[Attribute_ID][10] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][11] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][12] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][13] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][16] = myAttributes[10]; - triangleAttributesOfAllSprites[Attribute_ID][8] = - (1 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][9] = - (0 + myAttributes[6]) * myAttributes[5]; + triangleAttributesOfAllSprites[Attribute_ID][16] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][17] = + (0 + myAttributes[6]) * myAttributes[5]; - triangleAttributesOfAllSprites[Attribute_ID][16] = - (0 + myAttributes[4]) * myAttributes[3]; - triangleAttributesOfAllSprites[Attribute_ID][17] = - (0 + myAttributes[6]) * myAttributes[5]; + triangleAttributesOfAllSprites[Attribute_ID][18] = myAttributes[7]; + triangleAttributesOfAllSprites[Attribute_ID][19] = myAttributes[8]; + triangleAttributesOfAllSprites[Attribute_ID][20] = myAttributes[9]; + triangleAttributesOfAllSprites[Attribute_ID][21] = myAttributes[11]; + triangleAttributesOfAllSprites[Attribute_ID][24] = myAttributes[10]; + + this.drawTexTri( + { + x1: x1, + y1: y1, + x2: x2, + y2: y2, + x3: x3, + y3: y3, + tex: tex, + }, + util, + true + ); - triFunctions.drawTextTri( - gl.getParameter(gl.CURRENT_PROGRAM), - x1, - y1, - x3, - y3, - x4, - y4, - Attribute_ID, - currentTexture - ); - } + triangleAttributesOfAllSprites[Attribute_ID][0] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][1] = + (1 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][8] = + (1 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][9] = + (0 + myAttributes[6]) * myAttributes[5]; + + triangleAttributesOfAllSprites[Attribute_ID][16] = + (0 + myAttributes[4]) * myAttributes[3]; + triangleAttributesOfAllSprites[Attribute_ID][17] = + (0 + myAttributes[6]) * myAttributes[5]; + this.drawTexTri( + { + x1: x1, + y1: y1, + x2: x3, + y2: y3, + x3: x4, + y3: y4, + tex: tex, + }, + util, + true + ); } setStampAttribute({ target, number }, util) { const curTarget = util.target; @@ -2224,7 +2115,7 @@ squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; } - const calcColor = colors.hexToRgb(color); + const calcColor = Scratch.Cast.toRgbColorObject(color); squareAttributesOfAllSprites[curTarget.id][7] = calcColor.r / 255; squareAttributesOfAllSprites[curTarget.id][8] = calcColor.g / 255; @@ -2280,7 +2171,7 @@ triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; } - const calcColor = colors.hexToRgb(color); + const calcColor = Scratch.Cast.toRgbColorObject(color); triFunctions.setValueAccordingToCaseTriangle( targetId, @@ -2317,7 +2208,7 @@ triangleAttributesOfAllSprites[targetId] = triangleDefaultAttributes; } - const calcColor = colors.hexToRgb(color); + const calcColor = Scratch.Cast.toRgbColorObject(color); triFunctions.setValueAccordingToCaseTriangle( targetId, @@ -2368,7 +2259,7 @@ 1, 1, 1, ]; } - drawSolidTri({ x1, y1, x2, y2, x3, y3 }, util) { + drawSolidTri({ x1, y1, x2, y2, x3, y3 }, util, squareTex) { const curTarget = util.target; checkForPen(util); const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; @@ -2406,7 +2297,6 @@ y3 = Scratch.Cast.toNumber(y3) * dHeight * mul; triFunctions.drawTri( - gl.getParameter(gl.CURRENT_PROGRAM), x1, y1, x2, @@ -2414,10 +2304,10 @@ x3, y3, attrib.color4f, - curTarget.id + squareTex ? "squareStamp_" + curTarget.id : curTarget.id ); } - drawTexTri({ x1, y1, x2, y2, x3, y3, tex }, util) { + drawTexTri({ x1, y1, x2, y2, x3, y3, tex }, util, squareTex) { const curTarget = util.target; let currentTexture = null; if (penPlusCostumeLibrary[tex]) { @@ -2464,14 +2354,13 @@ if (currentTexture != null && typeof currentTexture != "undefined") { triFunctions.drawTextTri( - gl.getParameter(gl.CURRENT_PROGRAM), x1, y1, x2, y2, x3, y3, - curTarget.id, + squareTex ? "squareStamp_" + curTarget.id : curTarget.id, currentTexture ); } @@ -2600,7 +2489,7 @@ x < curCostume.width && x >= 0 ) { - const retColor = colors.hexToRgb(color); + const retColor = Scratch.Cast.toRgbColorObject(color); textureData[colorIndex] = retColor.r; textureData[colorIndex + 1] = retColor.g; textureData[colorIndex + 2] = retColor.b; From 2c2bb1dbf0527900332d9da839fb733046564e42 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 26 Nov 2023 12:21:24 -0600 Subject: [PATCH 062/196] encoding: Don't translate apple (#1167) btoa() errors on Chinese characters. The extension should be modified to actually support those properly, but for now not translating apple will avoid an error for Chinese users https://github.com/TurboWarp/extensions/issues/1166 --- extensions/encoding.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/encoding.js b/extensions/encoding.js index d561267088..7f5699f3aa 100644 --- a/extensions/encoding.js +++ b/extensions/encoding.js @@ -444,7 +444,7 @@ arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: btoa(Scratch.translate("apple")), + defaultValue: btoa("apple"), // don't translate because btoa() will error in Chinese ... }, code: { type: Scratch.ArgumentType.STRING, @@ -460,7 +460,7 @@ arguments: { string: { type: Scratch.ArgumentType.STRING, - defaultValue: Scratch.translate("apple"), + defaultValue: "apple", }, hash: { type: Scratch.ArgumentType.STRING, From e4a7424c2fe94bc767998a26379ca9e486a663fd Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sat, 2 Dec 2023 16:20:35 -0600 Subject: [PATCH 063/196] iframe: fix x/y with viewport resize behavior (#1178) closes https://github.com/TurboWarp/extensions/issues/1174 --- extensions/iframe.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/iframe.js b/extensions/iframe.js index ce87728a2c..0c312d571e 100644 --- a/extensions/iframe.js +++ b/extensions/iframe.js @@ -79,8 +79,12 @@ iframe.style.height = `${(effectiveHeight / stageHeight) * 100}%`; iframe.style.transform = ""; - iframe.style.top = `${(0.5 - effectiveHeight / 2 / stageHeight) * 100}%`; - iframe.style.left = `${(0.5 - effectiveWidth / 2 / stageWidth) * 100}%`; + iframe.style.top = `${ + (0.5 - effectiveHeight / 2 / stageHeight - y / stageHeight) * 100 + }%`; + iframe.style.left = `${ + (0.5 - effectiveWidth / 2 / stageWidth + x / stageWidth) * 100 + }%`; } }; From a3dc68378d219cc4e104aa0bec62990431a01a6f Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:23:18 +0000 Subject: [PATCH 064/196] CONTRIBUTING.md: Discourage extensions that primarily focus on enabling monetization (#1206) https://github.com/TurboWarp/extensions/issues/1195 Including this in the acceptance criteria wouldn't actively disallow the possibility but would actively discourage it. --------- Co-authored-by: GarboMuffin --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1017ab42a..4b1305dbd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,7 @@ Strictly, nothing is banned, but the following are *highly* discouraged: - Broad "Utilities" extensions (break them up into multiple extensions, see https://github.com/TurboWarp/extensions/issues/674) - Extensions that are very similar to existing ones (consider modifying the existing one instead) - One-use personal extensions (load the extension as a local file instead) + - Extensions whose primary purpose is monetization (not in the spirit of an open source project) - Joke extensions (they aren't funny when they cause us to get bug reports) Some extensions were added before these rules existed. That doesn't mean you will be exempted too. From 980cd99917021589c4e888939ad5adb6cbf476f3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:16:50 +0000 Subject: [PATCH 065/196] Lily/CommentBlocks: Allow comment reporter to be dropped anywhere (#1209) Allow the reporter block to be used anywhere to circumvent using couplers. Mild convenience. --- extensions/Lily/CommentBlocks.js | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/Lily/CommentBlocks.js b/extensions/Lily/CommentBlocks.js index bba99b6571..dba33f1653 100644 --- a/extensions/Lily/CommentBlocks.js +++ b/extensions/Lily/CommentBlocks.js @@ -53,6 +53,7 @@ opcode: "commentReporter", blockType: Scratch.BlockType.REPORTER, text: "[INPUT] // [COMMENT]", + allowDropAnywhere: true, arguments: { COMMENT: { type: Scratch.ArgumentType.STRING, From af502babb0244cfe687e5842e1f401d44437de40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:11:13 -0600 Subject: [PATCH 066/196] build(deps-dev): bump eslint from 8.54.0 to 8.56.0 (#1207) --- extensions/ar.js | 2 -- package-lock.json | 28 ++++++++++++++-------------- package.json | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/extensions/ar.js b/extensions/ar.js index a3e59916cf..bce3e12091 100644 --- a/extensions/ar.js +++ b/extensions/ar.js @@ -6,8 +6,6 @@ (function (Scratch) { "use strict"; - /* globals XRWebGLLayer, XRRigidTransform, XRWebGLLayer */ - if (!Scratch.extensions.unsandboxed) { throw new Error("AR extension must be run unsandboxed"); } diff --git a/package-lock.json b/package-lock.json index fec81801a1..92fdd2def4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,9 +26,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -60,9 +60,9 @@ } }, "@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, "@humanwhocodes/config-array": { @@ -432,15 +432,15 @@ "dev": true }, "eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -774,9 +774,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" diff --git a/package.json b/package.json index 59fdbdfebb..ed322d92bf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "markdown-it": "^13.0.2" }, "devDependencies": { - "eslint": "^8.54.0", + "eslint": "^8.56.0", "espree": "^9.6.1", "esquery": "^1.5.0", "prettier": "^3.1.0" From f228dcf9934356abf1a606043d553f6c0cb0698b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:11:22 -0600 Subject: [PATCH 067/196] build(deps): bump markdown-it from 13.0.2 to 14.0.0 (#1193) --- package-lock.json | 46 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92fdd2def4..d7d298bb60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -416,9 +416,9 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, "escape-html": { "version": "1.0.3", @@ -972,11 +972,11 @@ } }, "linkify-it": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", - "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "requires": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "locate-path": { @@ -995,21 +995,22 @@ "dev": true }, "markdown-it": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", - "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", + "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", "requires": { "argparse": "^2.0.1", - "entities": "~3.0.1", - "linkify-it": "^4.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.0.0" } }, "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" }, "media-typer": { "version": "0.3.0", @@ -1196,6 +1197,11 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1422,9 +1428,9 @@ } }, "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz", + "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==" }, "unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index ed322d92bf..937eec5ec5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "ejs": "^3.1.9", "express": "^4.18.2", "image-size": "^1.0.2", - "markdown-it": "^13.0.2" + "markdown-it": "^14.0.0" }, "devDependencies": { "eslint": "^8.56.0", From 340dd46197293666f16797e1bfa00884f5f9426e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:13:21 -0600 Subject: [PATCH 068/196] build(deps-dev): bump prettier from 3.1.0 to 3.1.1 (#1192) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d7d298bb60..c3d640af6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1177,9 +1177,9 @@ "dev": true }, "prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true }, "proxy-addr": { diff --git a/package.json b/package.json index 937eec5ec5..8eafbc6863 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint": "^8.56.0", "espree": "^9.6.1", "esquery": "^1.5.0", - "prettier": "^3.1.0" + "prettier": "^3.1.1" }, "private": true } From 4a014200904c36a213585e9dfc4b0f123d1a30dc Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 20 Dec 2023 21:59:03 -0800 Subject: [PATCH 069/196] runtime-options: add event block for when turbo mode, etc. changed (#1210) Co-authored-by: Muffin --- extensions/runtime-options.js | 108 +++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/extensions/runtime-options.js b/extensions/runtime-options.js index 11a9d382a9..6abd3d4fd1 100644 --- a/extensions/runtime-options.js +++ b/extensions/runtime-options.js @@ -16,6 +16,55 @@ const REMOVE_FENCING = "remove fencing"; const REMOVE_MISC_LIMITS = "remove misc limits"; const HIGH_QUALITY_PEN = "high quality pen"; + const FRAMERATE = "framerate"; + const CLONE_LIMIT = "clone limit"; + const STAGE_SIZE = "stage size"; + const USERNAME = "username"; + + /** @param {string} what */ + const emitChanged = (what) => + Scratch.vm.runtime.startHats("runtimeoptions_whenChange", { + WHAT: what, + }); + + /** + * @template T + * @param {T} obj + * @returns {T} + */ + const shallowCopy = (obj) => Object.assign({}, obj); + + let previousRuntimeOptions = shallowCopy(Scratch.vm.runtime.runtimeOptions); + + Scratch.vm.on("TURBO_MODE_OFF", () => emitChanged(TURBO_MODE)); + Scratch.vm.on("TURBO_MODE_ON", () => emitChanged(TURBO_MODE)); + Scratch.vm.on("INTERPOLATION_CHANGED", () => emitChanged(INTERPOLATION)); + Scratch.vm.on("RUNTIME_OPTIONS_CHANGED", (newOptions) => { + if (newOptions.fencing !== previousRuntimeOptions.fencing) { + emitChanged(REMOVE_FENCING); + } + if (newOptions.miscLimits !== previousRuntimeOptions.miscLimits) { + emitChanged(REMOVE_MISC_LIMITS); + } + if (newOptions.maxClones !== previousRuntimeOptions.maxClones) { + emitChanged(CLONE_LIMIT); + } + previousRuntimeOptions = shallowCopy(newOptions); + }); + Scratch.vm.renderer.on("UseHighQualityRenderChanged", () => + emitChanged(HIGH_QUALITY_PEN) + ); + Scratch.vm.on("FRAMERATE_CHANGED", () => emitChanged(FRAMERATE)); + Scratch.vm.on("STAGE_SIZE_CHANGED", () => emitChanged(STAGE_SIZE)); + + const originalPostData = Scratch.vm.runtime.ioDevices.userData.postData; + Scratch.vm.runtime.ioDevices.userData.postData = function (data) { + const newUsername = data.username !== this._username; + originalPostData.call(this, data); + if (newUsername) { + emitChanged(USERNAME); + } + }; class RuntimeOptions { getInfo() { @@ -154,6 +203,18 @@ }, }, }, + + "---", + + { + opcode: "whenChange", + blockType: Scratch.BlockType.EVENT, + text: "when [WHAT] changed", + isEdgeActivated: false, + arguments: { + WHAT: { type: Scratch.ArgumentType.STRING, menu: "changeable" }, + }, + }, ], menus: { thing: { @@ -182,6 +243,48 @@ ], }, + changeable: { + acceptReporters: false, + items: [ + { + text: Scratch.translate("turbo mode"), + value: TURBO_MODE, + }, + { + text: Scratch.translate("interpolation"), + value: INTERPOLATION, + }, + { + text: Scratch.translate("remove fencing"), + value: REMOVE_FENCING, + }, + { + text: Scratch.translate("remove misc limits"), + value: REMOVE_MISC_LIMITS, + }, + { + text: Scratch.translate("high quality pen"), + value: HIGH_QUALITY_PEN, + }, + { + text: Scratch.translate("framerate"), + value: FRAMERATE, + }, + { + text: Scratch.translate("clone limit"), + value: CLONE_LIMIT, + }, + { + text: Scratch.translate("stage size"), + value: STAGE_SIZE, + }, + { + text: Scratch.translate("username"), + value: USERNAME, + }, + ], + }, + enabled: { acceptReporters: true, items: [ @@ -300,8 +403,9 @@ } setUsername({ username }) { - Scratch.vm.runtime.ioDevices.userData._username = - Scratch.Cast.toString(username); + Scratch.vm.postIOData("userData", { + username: Scratch.Cast.toString(username), + }); } greenFlag() { From aefaa8812f95130d0f9acaf752585c110c3382c9 Mon Sep 17 00:00:00 2001 From: Obvious Alex C <76855369+David-Orangemoon@users.noreply.github.com> Date: Thu, 21 Dec 2023 01:00:52 -0500 Subject: [PATCH 070/196] obviousAlexC/penPlus: lukewarm fix (#1201) --- extensions/obviousAlexC/penPlus.js | 138 ++++++++++++++--------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js index 9fdf55db34..e58df86768 100644 --- a/extensions/obviousAlexC/penPlus.js +++ b/extensions/obviousAlexC/penPlus.js @@ -235,86 +235,86 @@ untextured: { Shaders: { vert: ` - attribute highp vec4 a_position; - attribute highp vec4 a_color; - varying highp vec4 v_color; - - void main() - { - v_color = a_color; - gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); - } - `, + attribute highp vec4 a_position; + attribute highp vec4 a_color; + varying highp vec4 v_color; + + void main() + { + v_color = a_color; + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); + } + `, frag: ` - varying highp vec4 v_color; - - void main() - { - gl_FragColor = v_color; - gl_FragColor.rgb *= gl_FragColor.a; - } - `, + varying highp vec4 v_color; + + void main() + { + gl_FragColor = v_color; + gl_FragColor.rgb *= gl_FragColor.a; + } + `, }, ProgramInf: null, }, textured: { Shaders: { vert: ` - attribute highp vec4 a_position; - attribute highp vec4 a_color; - attribute highp vec2 a_texCoord; - - varying highp vec4 v_color; - varying highp vec2 v_texCoord; - - void main() - { - v_color = a_color; - v_texCoord = a_texCoord; - gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); - } - `, + attribute highp vec4 a_position; + attribute highp vec4 a_color; + attribute highp vec2 a_texCoord; + + varying highp vec4 v_color; + varying highp vec2 v_texCoord; + + void main() + { + v_color = a_color; + v_texCoord = a_texCoord; + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); + } + `, frag: ` - uniform sampler2D u_texture; - - varying highp vec2 v_texCoord; - varying highp vec4 v_color; - - void main() - { - gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; - gl_FragColor.rgb *= gl_FragColor.a; - - } - `, + uniform sampler2D u_texture; + + varying highp vec2 v_texCoord; + varying highp vec4 v_color; + + void main() + { + gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; + gl_FragColor.rgb *= gl_FragColor.a; + + } + `, }, ProgramInf: null, }, draw: { Shaders: { vert: ` - attribute highp vec4 a_position; - - varying highp vec2 v_texCoord; - attribute highp vec2 a_texCoord; - - void main() - { - gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); - v_texCoord = (a_position.xy / 2.0) + vec2(0.5,0.5); - } - `, + attribute highp vec4 a_position; + + varying highp vec2 v_texCoord; + attribute highp vec2 a_texCoord; + + void main() + { + gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + v_texCoord = (a_position.xy / 2.0) + vec2(0.5,0.5); + } + `, frag: ` - varying highp vec2 v_texCoord; - - uniform sampler2D u_drawTex; - - void main() - { - gl_FragColor = texture2D(u_drawTex, v_texCoord); - gl_FragColor.rgb *= gl_FragColor.a; - } - `, + varying highp vec2 v_texCoord; + + uniform sampler2D u_drawTex; + + void main() + { + gl_FragColor = texture2D(u_drawTex, v_texCoord); + gl_FragColor.rgb *= gl_FragColor.a; + } + `, }, ProgramInf: null, }, @@ -2259,7 +2259,7 @@ 1, 1, 1, ]; } - drawSolidTri({ x1, y1, x2, y2, x3, y3 }, util, squareTex) { + drawSolidTri({ x1, y1, x2, y2, x3, y3 }, util) { const curTarget = util.target; checkForPen(util); const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; @@ -2304,10 +2304,10 @@ x3, y3, attrib.color4f, - squareTex ? "squareStamp_" + curTarget.id : curTarget.id + curTarget.id ); } - drawTexTri({ x1, y1, x2, y2, x3, y3, tex }, util, squareTex) { + drawTexTri({ x1, y1, x2, y2, x3, y3, tex }, util) { const curTarget = util.target; let currentTexture = null; if (penPlusCostumeLibrary[tex]) { @@ -2360,7 +2360,7 @@ y2, x3, y3, - squareTex ? "squareStamp_" + curTarget.id : curTarget.id, + curTarget.id, currentTexture ); } From 11ca5435997ee2b88865aeac5cb1044e98bf6878 Mon Sep 17 00:00:00 2001 From: CST1229 <68464103+CST1229@users.noreply.github.com> Date: Thu, 21 Dec 2023 07:08:32 +0100 Subject: [PATCH 071/196] box2d: disable physics and simulation rate blocks (#1190) Resolves #1171, #925 --- docs/box2d.md | 17 ++++++++++++ extensions/box2d.js | 66 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/box2d.md b/docs/box2d.md index c4ac9f5abf..cef9752bcd 100644 --- a/docs/box2d.md +++ b/docs/box2d.md @@ -30,6 +30,15 @@ step simulation :: #0FBD8C Move forward in time by one step. Run this in a loop to keep the physics going. + +```scratch +set simulation rate to (30)/s :: #0FBD8C +``` + +Set how much simulation steps is considered one second. Usually this should be project's framerate, but can also be used to slow down or speed up time. + +You can get the current simulation rate with the (simulation rate) reporter. + ## Sprites Manipulate individual sprites. @@ -48,6 +57,14 @@ Precision mode will make the sprite work extra hard to make sure it doesn't over --- +```scratch +disable physics for this sprite :: #0FBD8C +``` + +Makes physics no longer apply to this sprite. + +--- + ```scratch go to x: [0] y: [0] [in world v] :: #0FBD8C ``` diff --git a/extensions/box2d.js b/extensions/box2d.js index ad14eadc58..f488f3afc0 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12244,6 +12244,14 @@ return body; }; + const _removeBody = function (id) { + if (!bodies[id]) return; + + world.DestroyBody(bodies[id]); + delete bodies[id]; + delete prevPos[id]; + }; + const _applyForce = function (id, ftype, x, y, dir, pow) { const body = bodies[id]; if (!body) { @@ -12444,6 +12452,8 @@ } }; + let tickRate = 30; + const blockIconURI = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQoJIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOmE9Imh0dHA6Ly9ucy5hZG9iZS5jb20vQWRvYmVTVkdWaWV3ZXJFeHRlbnNpb25zLzMuMC8iDQoJIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSItMy43IC0zLjcgNDAgNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgLTMuNyAtMy43IDQwIDQwIg0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxkZWZzPg0KPC9kZWZzPg0KPHJlY3QgeD0iOC45IiB5PSIxLjUiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZT0iIzE2OUZCMCIgc3Ryb2tlLXdpZHRoPSIzIiB3aWR0aD0iMTQuOCIgaGVpZ2h0PSIxNC44Ii8+DQo8cmVjdCB4PSIxLjUiIHk9IjE2LjMiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZT0iIzE2OUZCMCIgc3Ryb2tlLXdpZHRoPSIzIiB3aWR0aD0iMTQuOCIgaGVpZ2h0PSIxNC44Ii8+DQo8cmVjdCB4PSIxNi4zIiB5PSIxNi4zIiBmaWxsPSIjRkZGRkZGIiBzdHJva2U9IiMxNjlGQjAiIHN0cm9rZS13aWR0aD0iMyIgd2lkdGg9IjE0LjgiIGhlaWdodD0iMTQuOCIvPg0KPC9zdmc+"; const menuIconURI = @@ -12487,6 +12497,7 @@ delete bodies[body]; delete prevPos[body]; } + tickRate = 30; // todo: delete joins? } @@ -12593,6 +12604,17 @@ }, filter: [Scratch.TargetType.SPRITE], }, + { + opcode: "disablePhysics", + blockType: BlockType.COMMAND, + text: Scratch.translate({ + id: "griffpatch.disablePhysics", + default: "disable physics for this sprite", + description: "Disable Physics for this Sprite", + }), + arguments: {}, + filter: [Scratch.TargetType.SPRITE], + }, // { // opcode: 'setPhysics', // blockType: BlockType.COMMAND, @@ -12630,6 +12652,32 @@ description: "Run a single tick of the physics simulation", }), }, + { + opcode: "setTickRate", + blockType: BlockType.COMMAND, + text: Scratch.translate({ + id: "griffpatch.setTickRate", + default: "set simulation rate to [rate]/s", + description: + "Set the number of physics simulation steps to run per second", + }), + arguments: { + rate: { + type: ArgumentType.NUMBER, + defaultValue: 30, + }, + }, + }, + { + opcode: "getTickRate", + blockType: BlockType.REPORTER, + text: Scratch.translate({ + id: "griffpatch.getTickRate", + default: "simulation rate", + description: + "Get the number of physics simulation steps to run per second", + }), + }, "---", @@ -13219,7 +13267,7 @@ this._checkMoved(); // world.Step(1 / 30, 10, 10); - world.Step(1 / 30, 10, 10); + world.Step(1 / tickRate, 10, 10); world.ClearForces(); for (const targetID in bodies) { @@ -13248,6 +13296,18 @@ } } + setTickRate(args) { + let rate = Scratch.Cast.toNumber(args.rate); + if (Number.isNaN(rate) || rate === Infinity) rate = 30; + rate = Math.max(rate, 0.01); + + tickRate = rate; + } + + getTickRate() { + return tickRate; + } + _checkMoved() { for (const targetID in bodies) { const body = bodies[targetID]; @@ -13408,6 +13468,10 @@ return body; } + disablePhysics(args, util) { + _removeBody(util.target.id); + } + /** * * @param svg the svg element From a2314e0bd884f8ab8f49786eaa58716a1c1c5a8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 00:26:08 -0600 Subject: [PATCH 072/196] build(deps): bump @turbowarp/types from `39457f3` to `86dc10a` (#1211) --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3d640af6e..56ebc1e56f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -137,8 +137,8 @@ "integrity": "sha512-DUay/UeKoYht03tfcBfp8+m8RSrOtr7eMxFTXujdUXfoiRM7xnQNy6SutufeFmIOdVZU65w3vstLcV3K+6Mhyg==" }, "@turbowarp/types": { - "version": "git+https://github.com/TurboWarp/types-tw.git#39457f33b1f09f6a256ea312d3727fc9cbbfa13b", - "from": "git+https://github.com/TurboWarp/types-tw.git#39457f33b1f09f6a256ea312d3727fc9cbbfa13b" + "version": "git+https://github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19", + "from": "git+https://github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19" }, "@ungap/structured-clone": { "version": "1.2.0", From 158bc297c164a0004a035f1a164b15fb1f2229b2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:26:37 +0000 Subject: [PATCH 073/196] Add Lily/Assets extension (#891) --- extensions/Lily/Assets.js | 652 +++++++++++++++++++++++++++++++++++++ extensions/extensions.json | 1 + images/Lily/Assets.svg | 1 + images/README.md | 6 +- 4 files changed, 659 insertions(+), 1 deletion(-) create mode 100644 extensions/Lily/Assets.js create mode 100644 images/Lily/Assets.svg diff --git a/extensions/Lily/Assets.js b/extensions/Lily/Assets.js new file mode 100644 index 0000000000..ad5e3aaebb --- /dev/null +++ b/extensions/Lily/Assets.js @@ -0,0 +1,652 @@ +// Name: Asset Manager +// ID: lmsAssets +// Description: Add, remove, and get data from various types of assets. + +// TheShovel is so epic and cool and awesome + +(function (Scratch) { + "use strict"; + + const vm = Scratch.vm; + const runtime = vm.runtime; + const Cast = Scratch.Cast; + + class Assets { + getInfo() { + return { + id: "lmsAssets", + color1: "#5779ca", + color2: "#4e6db6", + color3: "#4661a2", + name: "Asset Manager", + blocks: [ + { + opcode: "addSprite", + blockType: Scratch.BlockType.COMMAND, + text: "add sprite from URL [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "addCostume", + blockType: Scratch.BlockType.COMMAND, + text: "add costume from URL [URL] named [NAME]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "costume1", + }, + }, + }, + { + opcode: "addSound", + blockType: Scratch.BlockType.COMMAND, + text: "add sound from URL [URL] named [NAME]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "sound1", + }, + }, + }, + "---", + { + opcode: "renameSprite", + blockType: Scratch.BlockType.COMMAND, + text: "rename sprite [TARGET] to [NAME]", + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targets", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Sprite1", + }, + }, + }, + { + opcode: "renameCostume", + blockType: Scratch.BlockType.COMMAND, + text: "rename costume [COSTUME] to [NAME]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "costume1", + }, + }, + }, + { + opcode: "renameSound", + blockType: Scratch.BlockType.COMMAND, + text: "rename sound [SOUND] to [NAME]", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "sound1", + }, + }, + }, + "---", + { + opcode: "deleteSprite", + blockType: Scratch.BlockType.COMMAND, + text: "delete sprite [TARGET]", + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: "targets", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Sprite1", + }, + }, + }, + { + opcode: "deleteCostume", + blockType: Scratch.BlockType.COMMAND, + text: "delete costume [COSTUME]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "costume1", + }, + }, + }, + { + opcode: "deleteSound", + blockType: Scratch.BlockType.COMMAND, + text: "delete sound [SOUND]", + arguments: { + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "sound1", + }, + }, + }, + "---", + { + opcode: "getAllSprites", + blockType: Scratch.BlockType.REPORTER, + text: "all sprites", + }, + { + opcode: "getAllCostumes", + blockType: Scratch.BlockType.REPORTER, + text: "all costumes", + }, + { + opcode: "getAllSounds", + blockType: Scratch.BlockType.REPORTER, + text: "all sounds", + }, + { + opcode: "getSpriteName", + blockType: Scratch.BlockType.REPORTER, + text: "sprite name", + }, + "---", + { + opcode: "reorderCostume", + blockType: Scratch.BlockType.COMMAND, + text: "reorder costume # [INDEX1] to index [INDEX2]", + arguments: { + INDEX1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + INDEX2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + }, + }, + { + opcode: "reorderSound", + blockType: Scratch.BlockType.COMMAND, + text: "reorder sound # [INDEX1] to index [INDEX2]", + arguments: { + INDEX1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + INDEX2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + }, + }, + "---", + { + opcode: "getSoundData", + blockType: Scratch.BlockType.REPORTER, + text: "[ATTRIBUTE] of [SOUND]", + arguments: { + ATTRIBUTE: { + type: Scratch.ArgumentType.STRING, + menu: "attribute", + }, + SOUND: { + type: Scratch.ArgumentType.SOUND, + }, + }, + }, + { + opcode: "getCostumeData", + blockType: Scratch.BlockType.REPORTER, + text: "[ATTRIBUTE] of [COSTUME]", + arguments: { + ATTRIBUTE: { + type: Scratch.ArgumentType.STRING, + menu: "attribute", + }, + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + }, + }, + "---", + { + opcode: "getCostumeAtIndex", + blockType: Scratch.BlockType.REPORTER, + text: "name of costume # [INDEX]", + arguments: { + INDEX: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "getSoundAtIndex", + blockType: Scratch.BlockType.REPORTER, + text: "name of sound # [INDEX]", + arguments: { + INDEX: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + "---", + { + opcode: "openProject", + blockType: Scratch.BlockType.COMMAND, + text: "open project from URL [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + }, + }, + }, + { + opcode: "getProjectJSON", + blockType: Scratch.BlockType.REPORTER, + text: "project JSON", + }, + "---", + { + opcode: "loadExtension", + blockType: Scratch.BlockType.COMMAND, + text: "load extension from URL [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: + "https://extensions.turbowarp.org/Skyhigh173/json.js", + }, + }, + }, + { + opcode: "getLoadedExtensions", + blockType: Scratch.BlockType.REPORTER, + text: "loaded extensions", + }, + ], + menus: { + targets: { + acceptReporters: true, + items: "_getTargets", + }, + attribute: { + acceptReporters: false, + items: ["index", "dataURI", "format", "header", "asset ID"], + }, + }, + }; + } + + async addSprite(args, util) { + const url = Cast.toString(args.URL); + + const response = await Scratch.fetch(url); + const json = await response.arrayBuffer(); + + try { + await vm.addSprite(json); + } catch (e) { + console.error(e); + } + } + + // Thank you PenguinMod for providing this code. + async addCostume(args, util) { + const targetId = util.target.id; + const assetName = Cast.toString(args.NAME); + + const res = await Scratch.fetch(args.URL); + const blob = await res.blob(); + + if (!(this._typeIsBitmap(blob.type) || blob.type === "image/svg+xml")) { + console.error(`Invalid MIME type: ${blob.type}`); + return; + } + const assetType = this._typeIsBitmap(blob.type) + ? runtime.storage.AssetType.ImageBitmap + : runtime.storage.AssetType.ImageVector; + const dataType = + blob.type === "image/svg+xml" ? "svg" : blob.type.split("/")[1]; + + const arrayBuffer = await new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result); + fr.onerror = () => + reject(new Error(`Failed to read as array buffer: ${fr.error}`)); + fr.readAsArrayBuffer(blob); + }); + + const asset = runtime.storage.createAsset( + assetType, + dataType, + new Uint8Array(arrayBuffer), + null, + true + ); + const md5ext = `${asset.assetId}.${asset.dataFormat}`; + + try { + await vm.addCostume( + md5ext, + { + asset, + md5ext, + name: assetName, + }, + targetId + ); + } catch (e) { + console.error(e); + } + } + + async addSound(args, util) { + const targetId = util.target.id; + const assetName = Cast.toString(args.NAME); + + const res = await Scratch.fetch(args.URL); + const buffer = await res.arrayBuffer(); + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + } catch (e) { + console.error(e); + } + } + // End of PenguinMod + + renameSprite(args, util) { + const target = this._getTargetFromMenu(args.TARGET, util); + if (!target || target.isStage) return; + + const name = Cast.toString(args.NAME); + target.sprite.name = name; + } + + renameCostume(args, util) { + const target = util.target; + const costumeName = Cast.toString(args.COSTUME); + const costumeIndex = target.getCostumeIndexByName(costumeName); + if (costumeIndex < 0) return; + + const name = Cast.toString(args.NAME); + target.renameCostume(costumeIndex, name); + } + + renameSound(args, util) { + const target = util.target; + const soundName = Cast.toString(args.SOUND); + const soundIndex = this._getSoundIndexByName(soundName, util); + if (soundIndex < 0) return; + + const name = Cast.toString(args.NAME); + target.renameSound(soundIndex, name); + } + + deleteSprite(args, util) { + const target = this._getTargetFromMenu(args.TARGET); + if (!target || target.isStage) return; + + Scratch.vm.deleteSprite(target.id); + } + + deleteCostume(args, util) { + const target = util.target; + const costumeName = Cast.toString(args.COSTUME); + const costumeIndex = target.getCostumeIndexByName(costumeName); + if (costumeIndex < 0) return; + + if (target.sprite.costumes.length > 0) { + target.deleteCostume(costumeIndex); + } + } + + deleteSound(args, util) { + const target = util.target; + const soundName = Cast.toString(args.SOUND); + const soundIndex = this._getSoundIndexByName(soundName, util); + if (soundIndex < 0) return; + + if (target.sprite.sounds.length > 0) { + target.deleteSound(soundIndex); + } + } + + getAllSprites() { + const spriteNames = []; + const targets = Scratch.vm.runtime.targets; + for (const target of targets) { + // People reckoned the stage shouldn't be included + if (target.isOriginal && !target.isStage) { + spriteNames.push(target.sprite.name); + } + } + return JSON.stringify(spriteNames); + } + + getAllCostumes(args, util) { + const costumeNames = []; + const costumes = util.target.sprite.costumes; + for (const costume of costumes) { + costumeNames.push(costume.name); + } + return JSON.stringify(costumeNames); + } + + getAllSounds(args, util) { + const soundNames = []; + const sounds = util.target.sprite.sounds; + for (const sound of sounds) { + soundNames.push(sound.name); + } + return JSON.stringify(soundNames); + } + + getSpriteName(args, util) { + return util.target.sprite.name ?? ""; + } + + reorderCostume(args, util) { + const target = util.target; + const index1 = Cast.toNumber(args.INDEX1) - 1; + const index2 = Cast.toNumber(args.INDEX2) - 1; + const costumes = target.sprite.costumes; + + if (index1 < 0 || index1 >= costumes.length) return; + if (index2 < 0 || index2 >= costumes.length) return; + + target.reorderCostume(index1, index2); + } + + reorderSound(args, util) { + const target = util.target; + const index1 = Cast.toNumber(args.INDEX1) - 1; + const index2 = Cast.toNumber(args.INDEX2) - 1; + const sounds = target.sprite.sounds; + + if (index1 < 0 || index1 >= sounds.length) return; + if (index2 < 0 || index2 >= sounds.length) return; + + target.reorderSound(index1, index2); + } + + getCostumeData(args, util) { + const target = util.target; + const attribute = Cast.toString(args.ATTRIBUTE); + const costumeName = Cast.toString(args.COSTUME); + const costumeIndex = target.getCostumeIndexByName(costumeName); + if (costumeIndex < 0) return ""; + + const costume = target.sprite.costumes[costumeIndex]; + switch (attribute) { + case "dataURI": + return costume.asset.encodeDataURI(); + case "index": + return costumeIndex + 1; + case "format": + return costume.asset.assetType.runtimeFormat; + case "header": + return costume.asset.assetType.contentType; + case "asset ID": + return costume.asset.assetId; + default: + return ""; + } + } + + getSoundData(args, util) { + const target = util.target; + const attribute = Cast.toString(args.ATTRIBUTE); + const soundName = Cast.toString(args.SOUND); + const soundIndex = this._getSoundIndexByName(soundName, util); + if (soundIndex < 0) return ""; + + const sound = target.sprite.sounds[soundIndex]; + switch (attribute) { + case "dataURI": + return sound.asset.encodeDataURI(); + case "index": + return soundIndex + 1; + case "format": + return sound.asset.assetType.runtimeFormat; + case "header": + return sound.asset.assetType.contentType; + case "asset ID": + return sound.asset.assetId; + default: + return ""; + } + } + + getCostumeAtIndex(args, util) { + const target = util.target; + const index = Math.round(Cast.toNumber(args.INDEX - 1)); + const costumes = target.sprite.costumes; + if (index < 0 || index >= costumes.length) return ""; + + return costumes[index].name; + } + + getSoundAtIndex(args, util) { + const target = util.target; + const index = Math.round(Cast.toNumber(args.INDEX - 1)); + const sounds = target.sprite.sounds; + if (index < 0 || index >= sounds.length) return ""; + + return sounds[index].name; + } + + openProject(args) { + const url = Cast.toString(args.URL); + Scratch.fetch(url) + .then((r) => r.arrayBuffer()) + .then((buffer) => vm.loadProject(buffer)); + } + + getProjectJSON() { + return Scratch.vm.toJSON(); + } + + async loadExtension(args) { + const url = Cast.toString(args.URL); + await vm.extensionManager.loadExtensionURL(url); + } + + getLoadedExtensions(args) { + return JSON.stringify( + Array.from(vm.extensionManager._loadedExtensions.keys()) + ); + } + + /* Utility Functions */ + + _getSoundIndexByName(soundName, util) { + const sounds = util.target.sprite.sounds; + for (let i = 0; i < sounds.length; i++) { + if (sounds[i].name === soundName) { + return i; + } + } + return -1; + } + + // PenguinMod + _typeIsBitmap(type) { + return ( + type === "image/png" || + type === "image/bmp" || + type === "image/jpg" || + type === "image/jpeg" || + type === "image/jfif" || + type === "image/webp" || + type === "image/gif" + ); + } + + _getTargetFromMenu(targetName, util) { + let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); + if (targetName === "_myself_") target = util.target.sprite.clones[0]; + return target; + } + + _getTargets() { + const spriteNames = []; + if (Scratch.vm.editingTarget && !Scratch.vm.editingTarget.isStage) { + spriteNames.push({ + text: "myself", + value: "_myself_", + }); + } + const targets = Scratch.vm.runtime.targets; + for (let index = 1; index < targets.length; index++) { + const target = targets[index]; + if (target.isOriginal) { + spriteNames.push(target.getName()); + } + } + if (spriteNames.length > 0) { + return spriteNames; + } else { + return [""]; + } + } + } + Scratch.extensions.register(new Assets()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 06b95e22b9..5b983b75b8 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -56,6 +56,7 @@ "-SIPC-/consoles", "ZXMushroom63/searchApi", "TheShovel/ShovelUtils", + "Lily/Assets", "DNin/wake-lock", "Skyhigh173/json", "cs2627883/numericalencoding", diff --git a/images/Lily/Assets.svg b/images/Lily/Assets.svg new file mode 100644 index 0000000000..56e67cdaa6 --- /dev/null +++ b/images/Lily/Assets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index 069ffb31ca..6e7e485d79 100644 --- a/images/README.md +++ b/images/README.md @@ -285,8 +285,12 @@ All images in this folder are licensed under the [GNU General Public License ver ## Lily/SoundExpanded.svg - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694410464 +## Lily/Assets.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings). + - Dango based on dango from [Twemoji](https://twemoji.twitter.com/) under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). + ## iframe.svg - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694716263 ## Lily/Video.svg - - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 \ No newline at end of file From 28f8f4e581c52f6cb591711f21c65e528fb0c022 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 24 Dec 2023 23:51:21 -0600 Subject: [PATCH 074/196] Update translations (#1214) --- translations/extension-metadata.json | 183 +++++++++++++++++-- translations/extension-runtime.json | 251 ++++++++++++++++++++++++++- 2 files changed, 415 insertions(+), 19 deletions(-) diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index bf550a2a0f..0d39efe072 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -6,7 +6,7 @@ "runtime-options@name": "Nastavení běhu" }, "de": { - "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools Ihres Browsers integrierten JavaScript-Konsole interagieren.", + "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools deines Browsers integrierten JavaScript-Konsole interagieren.", "-SIPC-/consoles@name": "Konsolen", "-SIPC-/time@description": "Blöcke fürs Interagieren mit Unix-Zeitstempeln und andere Datenstrings.", "-SIPC-/time@name": "Zeit", @@ -44,6 +44,7 @@ "Lily/Skins@description": "Gebe deinen Figuren andere Bilder als Kostüme.", "Lily/SoundExpanded@description": "Fügt mehr Blöcke hinzu, die mit Klängen zu tun haben.", "Lily/SoundExpanded@name": "Klänge Erweitert", + "Lily/TempVariables2@description": "Erstelle temporäre Laufzeit- oder Thread-Variablen.", "Lily/TempVariables2@name": "Temporäre Variablen", "Lily/Video@description": "Spiele Videos von URLs ab.", "Lily/lmsutils@description": "Davor \"LMS Utilities\".", @@ -65,19 +66,93 @@ "TheShovel/CanvasEffects@name": "Bühneneffekte", "TheShovel/ColorPicker@description": "Nutze den Farbwähler des Systems.", "TheShovel/ColorPicker@name": "Farbwähler", + "TheShovel/CustomStyles@description": "Passe das Aussehen von Variablen und Fragen in deinem Projekt an.", + "TheShovel/CustomStyles@name": "Benutzerdefiniertes Aussehen", + "TheShovel/LZ-String@description": "Komprimiere und dekomprimiere Text mit lz-string.", + "TheShovel/LZ-String@name": "LZ-Kompression", + "TheShovel/ShovelUtils@description": "Ein paar verschiedene Blöcke.", + "TheShovel/ShovelUtils@name": "SchaufelUtils", + "Xeltalliv/clippingblending@description": "Verändere die Ränder Figur in einer rechteckigen Fläche und nutze verschiedene Farbmischungsmodi.", + "Xeltalliv/clippingblending@name": "Ränder verändern & Farben mischen", + "XeroName/Deltatime@description": "Präzise Deltatimingblöcke.", + "XeroName/Deltatime@name": "Deltazeit", + "ZXMushroom63/searchApi@description": "Interagiert mit URL-Parametern - der Teil der URL nach dem Fragezeichen.", + "ZXMushroom63/searchApi@name": "URL-Parameter", + "ar@description": "Zeigt Bilder von der Kamera und führt Bewegungstracking aus, sodass 3D-Projekte korrekt virtuelle Objekte auf die echte Welt projezieren können.", "ar@name": "Erweiterte Realität", + "battery@description": "Sehe Informationen über den Akku von Handys oder Laptops ein. Funktioniert möglicherweise nicht auf allen Geräten und Browsern.", "battery@name": "Batterie", + "bitwise@description": "Blöcke, die mit der Binärepresentation von Zahlen in Computern arbeiten.", + "bitwise@name": "Bitweise", + "box2d@description": "Zweidimensionale Physik.", + "box2d@name": "Box2D-Physik", + "clipboard@description": "Lese und schreibe aus der Systemzwischenablage.", "clipboard@name": "Zwischenablage", + "clouddata-ping@description": "Stelle fest, ob ein Cloudvariablenserver momentan funktioniert.", + "clouddata-ping@name": "Ping Clouddaten", + "cloudlink@description": "Eine mächtige WebSocket-Erweiterung für Scratch.", + "cs2627883/numericalencoding@description": "Kodiere Strings als Zahlen für Cloudvariablen.", + "cs2627883/numericalencoding@name": "Numerisches Kodieren", + "cursor@description": "Benutzt eigene Mauszeiger oder versteckt den Cursor. Erlaubt auch, den Cursor mit jedem Kostümbild zu ersetzen.", + "cursor@name": "Mauszeiger", + "encoding@description": "Kodiere und dekodiere Strings in ihre Unicodezahlen, Base 64, oder URLs.", + "encoding@name": "Kodieren", + "fetch@description": "Mache Requests zum ganzen Internet.", + "fetch@name": "Internetquests", + "files@description": "Lese und lade Dateien herunter.", "files@name": "Dateien", + "gamejolt@description": "Blöcke, die mit der GameJolt-API interagieren. Inoffiziell.", + "gamepad@description": "Greife direkt auf Gamepads zu, statt nur Knöpfe zu Tasten zu machen.", + "godslayerakp/http@description": "Volle Erweiterung, um mit Websites außerhalb von Turbowarp zu kommunizieren.", + "godslayerakp/ws@description": "Verbinde dich manuell mit Websocket-Servern.", + "iframe@description": "Zeige Webseiten oder HTML über der Bühne.", + "itchio@description": "Blöcke, die mit der itch.io-Website interagieren. Inoffiziell.", + "lab/text@description": "Eine einfache Art, Text anzuzeigen und zu animieren. Mit dem Animated Text-Experiment in Scratch Lab kompatibel.", "lab/text@name": "Animierter Text", + "local-storage@description": "Speichert Daten für immer. Wie Cookies, aber besser.", + "local-storage@name": "Speicherplatz", + "mdwalters/notifications@description": "Zeigt Benachrichtigungen an.", "mdwalters/notifications@name": "Benachrichtigungen", + "navigator@description": "Details über den Browser und das Betriebssystem des Nutzers.", + "navigator@name": "Browser", "obviousAlexC/SensingPlus@description": "Eine Erweiterung der Fühlen Kategorie.", "obviousAlexC/SensingPlus@name": "Fühlen Plus", + "obviousAlexC/newgroundsIO@description": "Blöcke, die mit der Newgrounds-API interagieren. Inoffiziell.", + "obviousAlexC/penPlus@description": "Fortgeschrittene Renderingmöglichkeiten.", + "penplus@description": "Von Pen Plus V6 ersetzt.", + "penplus@name": "Pen Plus V5 (Alt)", + "pointerlock@description": "Fügt Blöcke zum Zeigersperren hinzu. Die Maus-X- und Maus-Y-Blöcke werden den Unterschied von der vorherigen Frame anzeigen, wenn der Zeiger gesperrt ist. Ersetzt das Pointerlock-Experiment.", + "pointerlock@name": "Zeigersperren", + "qxsck/data-analysis@description": "Blöcke um Durchschnitte, Mediane, Maxima, Minimia, Varianzen und Modi auszurechnen.", + "qxsck/data-analysis@name": "Datenanalyse", + "qxsck/var-and-list@description": "Mehr Blöcke mit Variablen und Listen.", + "qxsck/var-and-list@name": "Variablen und Listen", + "rixxyx@description": "Verschiedene Hilfsblöcke.", + "runtime-options@description": "Erhalte und bearbeite Turbo Mode, Bildrate, Interpolation, die Klonbegrenzung, die Bühnengröße, und mehr.", "runtime-options@name": "Laufzeit-Optionen", + "shreder95ua/resolution@description": "Erhalte die Auflösung des Hauptbildschirms.", "shreder95ua/resolution@name": "Bildschirmauflösung", + "sound@description": "Spiele Klänge von URLs.", "sound@name": "Klänge", + "stretch@description": "Strecke Figuren horizontal oder vertikal.", + "stretch@name": "Strecken", + "text@description": "Manipuliere Zeichen und Text.", + "true-fantom/base@description": "Wandle die Basen von Zahlen um.", + "true-fantom/base@name": "Basis", + "true-fantom/couplers@description": "Ein paar Adapterblöcke", + "true-fantom/couplers@name": "Coupler", + "true-fantom/math@description": "Viele Operatorblöcke, von Potenzen zu trigonometrischen Funktionen.", "true-fantom/math@name": "Mathe", - "veggiecan/LongmanDictionary@name": "Longman Wörterbuch" + "true-fantom/network@description": "Verschiedene Blöcke, um mit dem Netzwerk zu interagieren.", + "true-fantom/network@name": "Netzwerk", + "true-fantom/regexp@description": "Volle Erweiterung um mit Regular Expressions zu arbeiten.", + "utilities@description": "Ein paar nützliche Blöcke.", + "utilities@name": "Verschiedene Blöcke", + "veggiecan/LongmanDictionary@description": "Bekomme die Definitionen von Worten des Longman-Wörterbuches.", + "veggiecan/LongmanDictionary@name": "Longman Wörterbuch", + "veggiecan/browserfullscreen@description": "Vollbildmodus betreten und verlassen.", + "veggiecan/browserfullscreen@name": "Browser Vollbildmodus", + "vercte/dictionaries@description": "Nutze die Macht von Dictionaries in deinem Projekt." }, "es": { "CST1229/zip@description": "Crea y edita archivos de formato .zip, incluyendo archivos .sb3.", @@ -252,7 +327,15 @@ "Clay/htmlEncode@name": "HTMLエンコード", "Lily/Video@name": "動画", "Skyhigh173/bigint@description": "どんな整数 (小数を含まない) も処理する演算ブロック。", + "clipboard@description": "システムのクリップボードから読み込んだり書き込んだりする", + "clipboard@name": "クリップボード", "cursor@name": "マウスカーソル", + "encoding@description": "文字列をunicode、base64、URLにエンコード、デコードする", + "encoding@name": "エンコーディング", + "files@name": "ファイル", + "godslayerakp/http@description": "外部のウェブサイトとやりとりをするための総合的な拡張機能。", + "iframe@name": "埋め込み", + "itchio@description": "itch.ioウェブサイトと連動するブロック。非公式。", "runtime-options@name": "ランタイムのオプション", "text@name": "テキスト", "true-fantom/math@name": "数学" @@ -467,66 +550,132 @@ "DNin/wake-lock@description": "防止电脑进入睡眠状态。", "DNin/wake-lock@name": "保持唤醒", "DT/cameracontrols@description": "移动舞台上的可见部分。", - "DT/cameracontrols@name": "摄像头控制器(有缺陷)", + "DT/cameracontrols@name": "舞台摄像机(有Bug)", "JeremyGamer13/tween@description": "实现简单的平滑动画。", "JeremyGamer13/tween@name": "缓动", "Lily/AllMenus@description": "将Scratch的拓展和特殊类别用菜单进行分类。", "Lily/AllMenus@name": "全部菜单", - "Lily/Cast@description": "转换Scratch的资料类型", + "Lily/Cast@description": "转换Scratch的数据类型。", "Lily/Cast@name": "类型转换", "Lily/ClonesPlus@description": "更多的Scratch克隆功能。", - "Lily/ClonesPlus@name": "克隆+", - "Lily/CommentBlocks@description": "给代码添加注释
    ", + "Lily/ClonesPlus@name": "克隆 +", + "Lily/CommentBlocks@description": "给代码添加注释。", "Lily/CommentBlocks@name": "注释", + "Lily/HackedBlocks@description": "一些在Scratch被隐藏的积木。", "Lily/HackedBlocks@name": "隐藏积木块", - "Lily/LooksPlus@name": "外观+", - "Lily/MoreEvents@name": "更多事件", + "Lily/LooksPlus@description": "拓展外观积木,实现隐藏/显示角色,获取造型信息,用SVG修改造型等操作。", + "Lily/LooksPlus@name": "外观 +", + "Lily/McUtils@description": "为一些人提供的实用积木", + "Lily/MoreEvents@description": "启动作品的更多方式。", + "Lily/MoreEvents@name": "事件 +", + "Lily/MoreTimers@description": "更多的计时器。", "Lily/MoreTimers@name": "更多计时器", + "Lily/Skins@description": "把角色造型修改为其他图像。", "Lily/Skins@name": "造型", + "Lily/SoundExpanded@description": "更多关于声音的积木。", + "Lily/SoundExpanded@name": "声音 +", + "Lily/TempVariables2@description": "创建了局部变量和临时变量。", "Lily/TempVariables2@name": "临时变量", + "Lily/Video@description": "从URL播放视频", "Lily/Video@name": "视频", + "Lily/lmsutils@description": "以前被称为LMS实用程序,现在是Lily的工具箱。", "Lily/lmsutils@name": "Lily 的工具箱", + "Longboost/color_channels@description": "仅展示或者标记某些RGB滤镜。", + "Longboost/color_channels@name": "RGB滤镜", + "NOname-awa/graphics2d@description": "计算长度、角度、面积的积木。", "NOname-awa/graphics2d@name": "图形 2D", "NOname-awa/more-comparisons@description": "更多关于比较的积木。", "NOname-awa/more-comparisons@name": "更多比较", + "NexusKitten/controlcontrols@description": "显示与隐藏页面按钮。", "NexusKitten/controlcontrols@name": "控件控制", - "NexusKitten/moremotion@name": "更多运动", + "NexusKitten/moremotion@description": "更多运动积木。", + "NexusKitten/moremotion@name": "运动 +", + "NexusKitten/sgrab@description": "获取有关于Scratch作品以及角色的信息。", + "Skyhigh173/bigint@description": "高精度数学计算,比较,以及位运算。(不支持小数)", "Skyhigh173/bigint@name": "高精度", - "Skyhigh173/json@description": "处理JSON字符串和数组", + "Skyhigh173/json@description": "处理JSON字符串和数组。", + "TheShovel/CanvasEffects@description": "把画笔特效显示到整个舞台。", "TheShovel/CanvasEffects@name": "画笔特效", - "bitwise@description": "在Scratch使用二进制", + "TheShovel/ColorPicker@description": "使用颜色选择器。", + "TheShovel/ColorPicker@name": "颜色选择器", + "TheShovel/CustomStyles@description": "自定义项目中变量与提示的样式。", + "TheShovel/CustomStyles@name": "更多样式", + "TheShovel/LZ-String@description": "使用LZ压缩与解压字符串。", + "TheShovel/LZ-String@name": "LZ压缩", + "Xeltalliv/clippingblending@description": "在指定的区域与颜色混合模式进行裁剪。", + "Xeltalliv/clippingblending@name": "裁剪与颜色混合", + "XeroName/Deltatime@description": "精确的计算时间增量。", + "XeroName/Deltatime@name": "时间增量", + "ZXMushroom63/searchApi@description": "与URL的搜索参数交互,搜索参数是URL中问号后面的部分。", + "ZXMushroom63/searchApi@name": "搜索参数", + "ar@description": "显示来自相机的图像并执行运动跟踪,使3D项目能够在现实世界中正确地覆盖虚拟对象。", + "ar@name": "增强现实", + "battery@description": "访问有关电池的信息,可能无法在所有设备上运行。", + "battery@name": "电池", + "bitwise@description": "在Scratch使用二进制。", "bitwise@name": "位运算", - "box2d@name": "Box2D 物理引擎", + "box2d@description": "2D物理引擎。", + "box2d@name": "Box2D", + "clipboard@description": "读取与写入系统剪切板。", "clipboard@name": "剪切板", + "clouddata-ping@description": "检测云变量服务器是否已启动。", "clouddata-ping@name": "云数据检测", "cloudlink@name": "云链接", + "cs2627883/numericalencoding@description": "把字符串编码为数字以方便使用云变量。", + "cs2627883/numericalencoding@name": "数字编码", + "cursor@description": "设置和隐藏光标造型,可以用造型自定义光标。", "cursor@name": "鼠标图标", + "encoding@description": "把字符串解码或编码为Unicode,Base64,或者URL。", "encoding@name": "编码", + "fetch@description": "向广泛的互联网进行请求。", "fetch@name": "请求API", + "files@description": "读取或下载文件。", "files@name": "文件", + "iframe@description": "在舞台嵌入网页。", "iframe@name": "内嵌框架", + "lab/text@description": "显示与设置艺术字,与Scratch Lab实验兼容。", "lab/text@name": "艺术字", - "local-storage@name": "临时变量", - "obviousAlexC/SensingPlus@name": "侦测+", - "obviousAlexC/penPlus@name": "画笔+ V6", - "penplus@name": "画笔+ V5(旧)", + "local-storage@description": "持久的存储数据。像Cookies,但更好。", + "local-storage@name": "本地存储", + "mdwalters/notifications@description": "显示通知。", + "mdwalters/notifications@name": "通知", + "obviousAlexC/SensingPlus@description": "拓展了检测积木。", + "obviousAlexC/SensingPlus@name": "侦测 +", + "obviousAlexC/penPlus@name": "画笔 + V6", + "penplus@description": "被画笔 + V6所替代。", + "penplus@name": "画笔 + V5(旧)", + "pointerlock@description": "锁定监测鼠标的x,y坐标,将一直保持上一帧时的变化。", "qxsck/data-analysis@description": "关于数据分析的积木,如平均数,最大值,最小值,中位数,众数,方差等。", "qxsck/data-analysis@name": "数据分析", "qxsck/var-and-list@description": "关于变量与列表的拓展积木。", "qxsck/var-and-list@name": "变量与列表", + "runtime-options@description": "获取或修改关于编译模式,帧限制,补帧,克隆上限,舞台大小以及其他的设置。", "runtime-options@name": "运行选项", + "shreder95ua/resolution@description": "获取主屏幕的分辨率。", + "shreder95ua/resolution@name": "屏幕分辨率", "sound@description": "从URL播放声音。", "sound@name": "声音", "stretch@description": "改变角色的伸缩比例", "stretch@name": "伸缩", + "text@description": "处理字符与文本。", "text@name": "文本", + "true-fantom/base@description": "转换为不同进制的数据。", "true-fantom/base@name": "进制转换", + "true-fantom/couplers@description": "几个适配性的积木。", + "true-fantom/couplers@name": "耦合器", + "true-fantom/math@description": "很多计算的积木,从求幂到三角函数。", "true-fantom/math@name": "数学", "true-fantom/network@name": "网络", + "true-fantom/regexp@description": "完美地使用正则表达式。", "true-fantom/regexp@name": "正则表达式", + "utilities@description": "一些有趣的积木集合。", "utilities@name": "工具", + "veggiecan/LongmanDictionary@description": "从朗文词典获取单词的定义。", "veggiecan/LongmanDictionary@name": "朗文辞典", - "veggiecan/browserfullscreen@name": "全荧幕" + "veggiecan/browserfullscreen@description": "进入和退出全屏模式。", + "veggiecan/browserfullscreen@name": "全屏模式", + "vercte/dictionaries@description": "在你的作品使用字典。", + "vercte/dictionaries@name": "字典" }, "zh-tw": { "runtime-options@name": "運行選項" diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index c3e26c7fe3..08dc0231af 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -23,12 +23,22 @@ "NexusKitten/moremotion@_More Motion": "Mehr Bewegung", "battery@_Battery": "Batterie", "clipboard@_Clipboard": "Zwischenablage", + "clouddata-ping@_Ping Cloud Data": "Ping Clouddaten", + "cs2627883/numericalencoding@_Numerical Encoding": "Numerisches Enkodieren", + "cursor@_Mouse Cursor": "Mauszeiger", + "encoding@_Encoding": "Kodieren", + "fetch@_Fetch": "Internetquests", "files@_Files": "Dateien", "files@_Select or drop file": "Datei auswählen oder ziehen", "gamejolt@_Close": "Schließen", "lab/text@_Animated Text": "Animierter Text", + "local-storage@_Local Storage": "Speicherplatz", + "pointerlock@_Pointerlock": "Zeigersperren", + "qxsck/data-analysis@name": "Datenanalyse", + "qxsck/var-and-list@name": "Variablen und Listen", "runtime-options@_Runtime Options": "Laufzeit-Optionen", - "sound@_Sound": "Klänge" + "sound@_Sound": "Klänge", + "stretch@_Stretch": "Strecken" }, "es": { "0832/rxFS2@del": "Eliminar [STR]", @@ -661,29 +671,110 @@ }, "ja": { "-SIPC-/consoles@_Consoles": "コンソール", + "-SIPC-/consoles@_Create a group named [string]": "[string]という名前のグループを作る", "-SIPC-/consoles@_Error": "エラー", + "-SIPC-/consoles@_Start a timer named [string]": "[string]という名前のタイマーをスタートする", "-SIPC-/consoles@_Time": "時間", + "-SIPC-/consoles@_group": "グループ", "-SIPC-/time@_Time": "時間", + "-SIPC-/time@_convert [time] to timestamp": "[time]をタイムスタンプに変換する", + "-SIPC-/time@_convert [timestamp] to datetime": "[timestamp]を日時に変換する", + "-SIPC-/time@_current time zone": "現在のタイムゾーン", + "-SIPC-/time@_current timestamp": "現在のタイムスタンプ", + "-SIPC-/time@_day": "日", + "-SIPC-/time@_get [Timedata] from [timestamp]": "[timestamp]から[Timedata]を取得する", + "-SIPC-/time@_hour": "時", + "-SIPC-/time@_minute": "分", + "-SIPC-/time@_month": "月", + "-SIPC-/time@_second": "秒", + "-SIPC-/time@_year": "年", "0832/rxFS2@del": "[STR]を削除", "0832/rxFS2@open": "[STR]を開く", "0832/rxFS2@search": "[STR]を検索", "0832/rxFS2@start": "[STR]を作成", + "CST1229/zip@_[META] of [FILE]": "[FILE]の[META]", + "CST1229/zip@_binary": "バイナリ", + "CST1229/zip@_delete [FILE]": "[FILE]を消す", + "CST1229/zip@_name": "名前", + "CST1229/zip@_new folder": "新しいフォルダ", + "CST1229/zip@_string": "文字列", "Clay/htmlEncode@_HTML Encode": "HTMLエンコード", "Clay/htmlEncode@_Hello!": "こんにちは!", + "CubesterYT/TurboHook@_icon": "アイコン", + "CubesterYT/TurboHook@_name": "名前", + "CubesterYT/WindowControls@_left": "左", + "CubesterYT/WindowControls@_right": "右", + "CubesterYT/WindowControls@_window x": "ウィンドウ x", + "CubesterYT/WindowControls@_window y": "ウィンドウ y", + "NexusKitten/controlcontrols@_fullscreen": "フルスクリーン", + "NexusKitten/controlcontrols@_green flag": "緑の旗", + "NexusKitten/controlcontrols@_pause": "一時停止", + "NexusKitten/controlcontrols@_stop": "止める", + "NexusKitten/moremotion@_height": "高さ", + "NexusKitten/moremotion@_width": "横幅", + "NexusKitten/sgrab@_about me": "私について", + "NexusKitten/sgrab@_favorite": "お気に入り", + "NexusKitten/sgrab@_follower": "フォロワー", + "NexusKitten/sgrab@_following": "フォロー", + "NexusKitten/sgrab@_love": "好き", + "NexusKitten/sgrab@_wiwo": "私が取り組んでいること", + "clipboard@_Clipboard": "クリップボード", "cs2627883/numericalencoding@_Hello!": "こんにちは!", "cursor@_Mouse Cursor": "マウスカーソル", + "encoding@_Encoding": "エンコーディング", "encoding@_apple": "りんご", + "files@_Files": "ファイル", "files@_Select or drop file": "選ぶかファイルをドロップする", + "files@_open a [extension] file": "[extension]ファイルを開く", + "files@_open a [extension] file as [as]": "[extension]ファイルを[as]として開く", + "files@_open a file": "ファイルを開く", + "files@_open a file as [as]": "[as]としてファイルを開く", "gamejolt@_Close": "閉じる", + "gamejolt@_day": "日", + "gamejolt@_guest": "ゲスト", + "gamejolt@_hour": "時", + "gamejolt@_minute": "分", + "gamejolt@_month": "月", + "gamejolt@_name": "名前", + "gamejolt@_second": "秒", + "gamejolt@_username": "ユーザー名", + "gamejolt@_year": "年", + "iframe@_Iframe": "埋め込み", + "iframe@_height": "高さ", + "iframe@_show HTML [HTML]": "[HTML]のHTMLを表示する", + "iframe@_show website [URL]": "[URL]のウェブサイトを表示する", "iframe@_url": "URL", + "iframe@_width": "横幅", + "itchio@_name": "名前", "lab/text@_Hello!": "こんにちは!", + "lab/text@_left": "左", + "lab/text@_right": "右", "lab/text@_show sprite": "スプライトを表示", "local-storage@_get key [KEY]": "キーを取得[KEY]", "navigator@_browser": "ブラウザ", "navigator@_dark": "ダーク", "navigator@_light": "ライト", + "pointerlock@_disabled": "無効", + "pointerlock@_enabled": "有効", + "runtime-options@_Infinity": "無限", "runtime-options@_Runtime Options": "ランタイムのオプション", - "runtime-options@_turbo mode": "ターボモード" + "runtime-options@_[thing] enabled?": "[thing]が有効", + "runtime-options@_clone limit": "クローンの制限", + "runtime-options@_default ({n})": "デフォルト({n})", + "runtime-options@_disabled": "無効", + "runtime-options@_enabled": "有効", + "runtime-options@_framerate limit": "フレームレートの制限", + "runtime-options@_height": "高さ", + "runtime-options@_interpolation": "補完機能", + "runtime-options@_run green flag [flag]": "緑の旗[flag]を実行する", + "runtime-options@_set [thing] to [enabled]": "[thing]を[enabled]にする", + "runtime-options@_set clone limit to [limit]": "クローンの制限を[limit]にする", + "runtime-options@_set framerate limit to [fps]": "フレームレートの制限を[fps]にする", + "runtime-options@_set stage size width: [width] height: [height]": "ステージの横幅を[width]高さを[height]にする", + "runtime-options@_set username to [username]": "ユーザー名を[username]にする", + "runtime-options@_stage [dimension]": "ステージの[dimension]", + "runtime-options@_turbo mode": "ターボモード", + "runtime-options@_width": "横幅" }, "ja-hira": { "-SIPC-/consoles@_Error": "エラー", @@ -1747,16 +1838,35 @@ "zh-cn": { "-SIPC-/consoles@_Clear Console": "清空控制台", "-SIPC-/consoles@_Consoles": "控制台", + "-SIPC-/consoles@_Create a collapsed group named [string]": "创建名叫[string]的折叠群组", + "-SIPC-/consoles@_Create a group named [string]": "创建名叫[string]的群组", "-SIPC-/consoles@_Debug [string]": "Debug[string]", + "-SIPC-/consoles@_End the timer named [string] and print the time elapsed from start to end": "结束名为[string]的计时器并打印从开始到结束所用的时间", "-SIPC-/consoles@_Error": "错误", "-SIPC-/consoles@_Error [string]": "打印错误[string]", + "-SIPC-/consoles@_Exit the current group": "退出群组", + "-SIPC-/consoles@_Information": "信息", "-SIPC-/consoles@_Information [string]": "打印信息[string]", + "-SIPC-/consoles@_Journal": "日志", + "-SIPC-/consoles@_Journal [string]": "输出日志[string]", "-SIPC-/consoles@_Print the time run by the timer named [string]": "打印计时器[string]运行的时间", "-SIPC-/consoles@_Start a timer named [string]": "启动计时器[string]", "-SIPC-/consoles@_Time": "时间", + "-SIPC-/consoles@_Warning": "警告", "-SIPC-/consoles@_Warning [string]": "打印警告[string]", "-SIPC-/consoles@_group": "群组", "-SIPC-/time@_Time": "时间", + "-SIPC-/time@_convert [time] to timestamp": "把时间[time]转换为时间戳", + "-SIPC-/time@_convert [timestamp] to datetime": "把时间戳[timestamp]转换为时间", + "-SIPC-/time@_current time zone": "时区", + "-SIPC-/time@_current timestamp": "时间戳", + "-SIPC-/time@_day": "日", + "-SIPC-/time@_get [Timedata] from [timestamp]": "从时间戳[timestamp]获取[Timedata]", + "-SIPC-/time@_hour": "时", + "-SIPC-/time@_minute": "分", + "-SIPC-/time@_month": "月", + "-SIPC-/time@_second": "秒", + "-SIPC-/time@_year": "年", "0832/rxFS2@clean": "清空文件系统", "0832/rxFS2@del": "删除 [STR]", "0832/rxFS2@folder": "设置 [STR] 为 [STR2]", @@ -1769,41 +1879,88 @@ "0832/rxFS2@start": "新建 [STR]", "0832/rxFS2@sync": "将 [STR] 的位置改为 [STR2]", "0832/rxFS2@webin": "从网络加载 [STR]", + "Alestore/nfcwarp@_NFC supported?": "支持NFC?", "Alestore/nfcwarp@_NFCWarp": "NFC", + "Alestore/nfcwarp@_Only works in Chrome on Android": "只能在安卓的Chrome运行", "Alestore/nfcwarp@_read NFC tag": "读取NFC标签", + "CST1229/zip@_1 (fast, large)": "1(快,文件大)", + "CST1229/zip@_9 (slowest, smallest)": "9(慢,文件小)", "CST1229/zip@_Hello, world?": "你好世界?", + "CST1229/zip@_[OBJECT] exists?": "压缩包[OBJECT]存在?", + "CST1229/zip@_any text": "任意文本", + "CST1229/zip@_archive is open?": "打开压缩包?", + "CST1229/zip@_binary": "二进制", + "CST1229/zip@_close archive": "退出压缩包", + "CST1229/zip@_create directory [DIR]": "创建目录[DIR]", + "CST1229/zip@_create empty archive": "创建空的压缩包", + "CST1229/zip@_delete [FILE]": "删除文件[FILE]", + "CST1229/zip@_file [FILE] as [TYPE]": "获取文件[FILE]的内容,类型是[TYPE]", "CST1229/zip@_folder": "文件夹", + "CST1229/zip@_go to directory [DIR]": "到目录[DIR]", "CST1229/zip@_hex": "Hex", "CST1229/zip@_name": "名字", "CST1229/zip@_new file": "新文件", "CST1229/zip@_new folder": "新文件夹", + "CST1229/zip@_no compression (fastest)": "没有压缩(最快)", + "CST1229/zip@_open zip from [TYPE] [DATA]": "打开类型是[TYPE],数据是[DATA]的压缩包", + "CST1229/zip@_output zip type [TYPE] compression level [COMPRESSION]": "输出类型是[TYPE],压缩等级是[COMPRESSION]的压缩数据", "CST1229/zip@_path": "路径", "CST1229/zip@_string": "字符串", "CST1229/zip@_text": "文本", + "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "写入数据[CONTENT],文件名是[FILE],类型是[TYPE]", "Clay/htmlEncode@_HTML Encode": "HTML编码", "Clay/htmlEncode@_Hello!": "你好!", "CubesterYT/TurboHook@_TurboHook": "Turbo网络钩子", "CubesterYT/TurboHook@_icon": "图标", "CubesterYT/TurboHook@_name": "名字", "CubesterYT/WindowControls@_Hello World!": "你好世界!", + "CubesterYT/WindowControls@_May not work in normal browser tabs": "可能无法正常运行", + "CubesterYT/WindowControls@_Refer to Documentation for details": "具体信息参考文档", "CubesterYT/WindowControls@_Window Controls": "网页控制", + "CubesterYT/WindowControls@_bottom": "底部", + "CubesterYT/WindowControls@_bottom left": "底部左侧", + "CubesterYT/WindowControls@_bottom right": "底部右侧", "CubesterYT/WindowControls@_center": "居中", + "CubesterYT/WindowControls@_change window height by [H]": "页面的高增加[H]", + "CubesterYT/WindowControls@_change window width by [W]": "页面的宽增加[W]", + "CubesterYT/WindowControls@_change window x by [X]": "页面的x坐标增加[X]", + "CubesterYT/WindowControls@_change window y by [Y]": "页面的y坐标增加[Y]", + "CubesterYT/WindowControls@_close window": "关闭窗口", "CubesterYT/WindowControls@_enter fullscreen": "进入全屏", "CubesterYT/WindowControls@_exit fullscreen": "退出全屏", + "CubesterYT/WindowControls@_is window focused?": "页面在被使用吗?", "CubesterYT/WindowControls@_is window fullscreen?": "页面全屏吗?", + "CubesterYT/WindowControls@_is window touching screen edge?": "页面接触到屏幕边缘吗?", "CubesterYT/WindowControls@_left": "居左", + "CubesterYT/WindowControls@_match stage size": "匹配舞台大小", + "CubesterYT/WindowControls@_move window to the [PRESETS]": "移动页面到[PRESETS]", + "CubesterYT/WindowControls@_move window to x: [X] y: [Y]": "移动页面到x[X] y[Y]", + "CubesterYT/WindowControls@_random position": "随机位置", + "CubesterYT/WindowControls@_resize window to [PRESETS]": "改变页面的大小为[PRESETS]", + "CubesterYT/WindowControls@_resize window to width: [W] height: [H]": "改变页面的大小为宽[W]高[H]", "CubesterYT/WindowControls@_right": "居右", "CubesterYT/WindowControls@_screen height": "屏幕高度", "CubesterYT/WindowControls@_screen width": "屏幕宽度", + "CubesterYT/WindowControls@_set window height to [H]": "设置页面的高为[H]", "CubesterYT/WindowControls@_set window title to [TITLE]": "设置页面标题为[TITLE]", + "CubesterYT/WindowControls@_set window width to [W]": "设置页面的宽为[W]", + "CubesterYT/WindowControls@_set window x to [X]": "设置页面的x坐标为[X]", + "CubesterYT/WindowControls@_set window y to [Y]": "设置页面的y坐标为[Y]", + "CubesterYT/WindowControls@_top": "顶部", + "CubesterYT/WindowControls@_top left": "顶部左侧", + "CubesterYT/WindowControls@_top right": "顶部右侧", "CubesterYT/WindowControls@_window height": "页面高度", "CubesterYT/WindowControls@_window title": "页面标题", "CubesterYT/WindowControls@_window width": "页面宽度", "CubesterYT/WindowControls@_window x": "页面中心的x坐标", "CubesterYT/WindowControls@_window y": "页面中心的y坐标", + "CubesterYT/WindowControls@editorConfirmation": "你确定关闭页面吗?\n\n(打包作品并不会显示此消息)", "DNin/wake-lock@_Wake Lock": "保持唤醒", + "DNin/wake-lock@_is wake lock active?": "唤醒锁激活了?", "DNin/wake-lock@_off": "关闭", "DNin/wake-lock@_on": "打开", + "DNin/wake-lock@_turn wake lock [enabled]": "设置唤醒锁状态为[enabled]", + "DT/cameracontrols@_Camera (Very Buggy)": "舞台摄像机(有Bug)", "DT/cameracontrols@_camera direction": "摄像机的方向", "DT/cameracontrols@_camera x": "摄像机x坐标", "DT/cameracontrols@_camera y": "摄像机y坐标", @@ -1815,31 +1972,105 @@ "DT/cameracontrols@_set camera to x: [x] y: [y]": "设置摄像机的坐标为x[x] y[y]", "DT/cameracontrols@_set camera x to [val]": "设置摄像机的x坐标为[val]", "DT/cameracontrols@_set camera y to [val]": "设置摄像机的y坐标为[val]", + "NOname-awa/graphics2d@area": "面积", + "NOname-awa/graphics2d@circumference": "周长", + "NOname-awa/graphics2d@diameter": "直径", "NOname-awa/graphics2d@graph": "图形 [graph] 的 [CS]", "NOname-awa/graphics2d@line_section": "([x1],[y1])到([x2],[y2])的长度", "NOname-awa/graphics2d@name": "图形 2D", "NOname-awa/graphics2d@pi": "π", "NOname-awa/graphics2d@quadrilateral": "矩形([x1],[y1])([x2],[y2])([x3],[y3])([x4],[y4])的 [CS]", + "NOname-awa/graphics2d@radius": "半径", "NOname-awa/graphics2d@ray_direction": "([x1],[y1])的([x2],[y2])的距离", "NOname-awa/graphics2d@round": "[rd] 为 [a] 的圆的 [CS]", "NOname-awa/graphics2d@triangle": "三角形([x1],[y1])([x2],[y2])([x3],[y3])的 [CS]", "NOname-awa/graphics2d@triangle_s": "三角形 [s1] [s2] [s3] 的面积", "NexusKitten/controlcontrols@_Control Controls": "控件控制", + "NexusKitten/controlcontrols@_[OPTION] exists?": "存在[OPTION]?", + "NexusKitten/controlcontrols@_[OPTION] shown?": "显示[OPTION]了?", "NexusKitten/controlcontrols@_fullscreen": "全屏", + "NexusKitten/controlcontrols@_green flag": "绿旗", + "NexusKitten/controlcontrols@_hide [OPTION]": "隐藏[OPTION]", + "NexusKitten/controlcontrols@_pause": "暂停", + "NexusKitten/controlcontrols@_show [OPTION]": "显示[OPTION]", + "NexusKitten/controlcontrols@_stop": "停止", "NexusKitten/moremotion@_More Motion": "更多运动", + "NexusKitten/moremotion@_change x: [X] y: [Y]": "x坐标增加[X],y坐标增加[Y]", "NexusKitten/moremotion@_costume height": "造型高度", "NexusKitten/moremotion@_costume width": "造型宽度", + "NexusKitten/moremotion@_direction to x: [X] y: [Y]": "角色位置到x[X] y[Y]的方向", + "NexusKitten/moremotion@_distance from x: [X] y: [Y]": "角色位置到x[X] y[Y]的距离", "NexusKitten/moremotion@_height": "高度", + "NexusKitten/moremotion@_move [PERCENT]% of the way to x: [X] y: [Y]": "向x[X] y[Y]移动[PERCENT]%的路程", + "NexusKitten/moremotion@_move [STEPS] steps towards x: [X] y: [Y]": "向x[X] y[Y]移动[STEPS]步", + "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "面向x[X] y[Y]", + "NexusKitten/moremotion@_rotation style": "旋转模式", + "NexusKitten/moremotion@_sprite [WHAT]": "角色的[WHAT]", + "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "位于从x[X1] y[Y1] 到x[X2] y[Y2]的区域内?", "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "触碰坐标x[X] y[Y]?", "NexusKitten/moremotion@_width": "宽度", + "NexusKitten/sgrab@_[WHAT] of user [WHO]": "获取Scratch用户[WHO]的[WHAT]", + "NexusKitten/sgrab@_about me": "简介", + "NexusKitten/sgrab@_creator of project id [WHO]": "获取Scratch作品ID为[WHO]的制作者", + "NexusKitten/sgrab@_favorite": "收藏", + "NexusKitten/sgrab@_follower": "粉丝", + "NexusKitten/sgrab@_following": "关注者", + "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "获取Scratch用户[WHO]在[WHAT]的排名", + "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "获取Scratch作品ID为[WHO]在[WHAT]的屏幕", + "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "获取Scratch作品ID为[WHO]的[WHAT]数量", + "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "获取Scratch用户[WHO]的[WHAT]数量", + "NexusKitten/sgrab@_location": "居住地", + "NexusKitten/sgrab@_love": "点赞", + "NexusKitten/sgrab@_name of project id [WHO]": "获取Scratch作品ID为[WHO]的名称", + "NexusKitten/sgrab@_status": "用户类型", + "NexusKitten/sgrab@_view": "观看数", + "NexusKitten/sgrab@_wiwo": "工作", + "battery@_Battery": "电池", + "battery@_battery level": "电量", + "battery@_charging?": "正在充电?", + "battery@_seconds until charged": "最近一次充电的时间", + "battery@_seconds until empty": "用完电的时间", + "battery@_when battery level changed": "当电量变化时", + "battery@_when charging changed": "当充电状态变化时", + "battery@_when time until charged changed": "当最近一次充电的时间变化时", + "battery@_when time until empty changed": "当用完电的时间变化时", "box2d@griffpatch.categoryName": "物理引擎", "box2d@griffpatch.doTick": "逐步模拟", + "box2d@griffpatch.getGravityX": "重力方向x坐标", + "box2d@griffpatch.getGravityY": "重力方向y坐标", + "box2d@griffpatch.setGravity": "设置重力方向为x[gx] y[gy]", + "box2d@griffpatch.setPhysics": "设置[shape]的物理模式为[mode]", + "box2d@griffpatch.setPosition": "在[space]前往x[x] y[y]", + "box2d@griffpatch.setStage": "设置舞台类型为[stageType]", "clipboard@_Clipboard": "剪切板", + "clipboard@_clipboard": "最新复制的文本", + "clipboard@_copy to clipboard: [TEXT]": "把文本[TEXT]复制到剪切板", + "clipboard@_last pasted text": "最新粘贴的文本", + "clipboard@_reset clipboard": "清除最新复制的文本", + "clipboard@_when something is copied": "当有文本被复制时", + "clipboard@_when something is pasted": "当有文本被粘贴时", "clouddata-ping@_Ping Cloud Data": "检测云数据", + "clouddata-ping@_is cloud data server [SERVER] up?": "云服务器[SERVER]可以使用?", + "cs2627883/numericalencoding@_Decode [ENCODED] back to text": "解码数字[ENCODED]为文本", + "cs2627883/numericalencoding@_Encode [DATA] to numbers": "编码文本[DATA]为数字", "cs2627883/numericalencoding@_Hello!": "你好!", + "cs2627883/numericalencoding@_Numerical Encoding": "数字编码", + "cs2627883/numericalencoding@_decoded": "解码数据", + "cs2627883/numericalencoding@_encoded": "编码数据", "cursor@_Mouse Cursor": "鼠标图标", + "cursor@_bottom left": "底部左侧", + "cursor@_bottom right": "底部右侧", "cursor@_center": "居中", + "cursor@_top left": "顶部左侧", + "cursor@_top right": "顶部右侧", + "encoding@_Convert the character [string] to [CodeList]": "字符[string]在[CodeList]对应的的ID", + "encoding@_Decode [string] with [code]": "以[code]解码文本[string]", + "encoding@_Encode [string] in [code]": "以[code]编码文本[string]", "encoding@_Encoding": "编码", + "encoding@_Hash [string] with [hash]": "以[hash]生成[string]的哈希字符串", + "encoding@_Randomly generated [position] character string": "生成长度为[position]的随机字符串", + "encoding@_Use [wordbank] to generate a random [position] character string": "用源字符串[wordbank]生成长度为[position]的随机字符串", + "encoding@_[string] corresponding to the [CodeList] character": "ID[string]在[CodeList]对应的字符", "encoding@_apple": "苹果", "fetch@_Fetch": "请求API", "files@_Files": "文件", @@ -1849,14 +2080,21 @@ "files@_save.txt": "保存.txt", "files@_text": "文本", "gamejolt@_Close": "关闭", + "gamejolt@_day": "日", + "gamejolt@_hour": "小时", + "gamejolt@_minute": "分", + "gamejolt@_month": "月份", "gamejolt@_name": "名字", "gamejolt@_off": "关闭 ", "gamejolt@_on": "打开", + "gamejolt@_second": "秒", + "gamejolt@_status": "用户类型", "gamejolt@_text": "文本", "gamejolt@_title": "标题", "gamejolt@_type": "类型", "gamejolt@_user ID": "用户ID", "gamejolt@_value": "值", + "gamejolt@_year": "年份", "iframe@_Iframe": "内嵌框架", "iframe@_height": "高度", "iframe@_width": "宽度", @@ -1870,25 +2108,34 @@ "lab/text@_Here we go!": "现在出发!", "lab/text@_Incompatible with Scratch Lab:": "以下积木与Scratch Lab不兼容:", "lab/text@_Welcome to my project!": "欢迎来到我的作品!", + "lab/text@_[ANIMATE] duration": "动画样式[ANIMATE]的完成时间", "lab/text@_[ANIMATE] text [TEXT]": "显示动画样式是[ANIMATE]的文本[TEXT]", "lab/text@_add line [TEXT]": "增加一行文本[TEXT]", "lab/text@_align text to [ALIGN]": "设置文本的展示样式为[ALIGN]", + "lab/text@_animate [ANIMATE] until done": "显示动画样式[ANIMATE]并等待", "lab/text@_center": "居中", "lab/text@_displayed text": "显示的文本", + "lab/text@_is animating?": "正在显示艺术字?", "lab/text@_is showing text?": "文本显示了?", "lab/text@_left": "居左", "lab/text@_rainbow": "彩虹色", "lab/text@_random font": "随机字体", + "lab/text@_reset [ANIMATE] duration": "重置动画样式[ANIMATE]的完成时间", "lab/text@_reset text width": "重置文本宽度", + "lab/text@_reset typing delay": "重置逐字显示速度", "lab/text@_right": "居右", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "设置动画样式[ANIMATE]的完成时间是[NUM]秒", "lab/text@_set font to [FONT]": "设置文本的字体为[FONT]", "lab/text@_set text color to [COLOR]": "设置文本的颜色为[COLOR]", + "lab/text@_set typing delay to [NUM] seconds": "设置柱子显示速度为[NUM]秒/字", "lab/text@_set width to [WIDTH]": "设置文本的宽度为[WIDTH]", "lab/text@_set width to [WIDTH] aligned [ALIGN]": "设置[ALIGN]样式的宽度为[WIDTH]", "lab/text@_show sprite": "显示角色", "lab/text@_show text [TEXT]": "显示文本[TEXT]", + "lab/text@_start [ANIMATE] animation": "显示动画样式[ANIMATE]", "lab/text@_text [ATTRIBUTE]": "文本的[ATTRIBUTE]", "lab/text@_type": "逐字显示", + "lab/text@_typing delay": "逐字显示速度", "lab/text@_zoom": "移动动画", "local-storage@_Local Storage": "本地存储", "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "本地存储拓展:请先运行“设置存储命名空间ID”积木才能使用下面的积木", From 1bf1d05f0529c4304c3c8789c8436c2709d9ebc8 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Fri, 29 Dec 2023 15:57:51 -0600 Subject: [PATCH 075/196] Add a script for generating credits for scratch-gui (#1216) Will be used for updating https://turbowarp.org/credits.html --- development/builder.js | 47 +----------- development/fs-utils.js | 53 +++++++++++++ development/get-credits-for-gui.js | 118 +++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 46 deletions(-) create mode 100644 development/fs-utils.js create mode 100644 development/get-credits-for-gui.js diff --git a/development/builder.js b/development/builder.js index 23a6d9714b..f2dfb46648 100644 --- a/development/builder.js +++ b/development/builder.js @@ -3,6 +3,7 @@ const AdmZip = require("adm-zip"); const pathUtil = require("path"); const compatibilityAliases = require("./compatibility-aliases"); const parseMetadata = require("./parse-extension-metadata"); +const { mkdirp, recursiveReadDirectory } = require("./fs-utils"); /** * @typedef {'development'|'production'|'desktop'} Mode @@ -14,52 +15,6 @@ const parseMetadata = require("./parse-extension-metadata"); * @property {string} developer_comment Helper text to help translators */ -/** - * Recursively read a directory. - * @param {string} directory - * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. - * The return result includes files in subdirectories, but not the subdirectories themselves. - */ -const recursiveReadDirectory = (directory) => { - const result = []; - for (const name of fs.readdirSync(directory)) { - if (name.startsWith(".")) { - // Ignore .eslintrc.js, .DS_Store, etc. - continue; - } - const absolutePath = pathUtil.join(directory, name); - const stat = fs.statSync(absolutePath); - if (stat.isDirectory()) { - for (const [ - relativeToChildName, - childAbsolutePath, - ] of recursiveReadDirectory(absolutePath)) { - // This always needs to use / on all systems - result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); - } - } else { - result.push([name, absolutePath]); - } - } - return result; -}; - -/** - * Synchronous create a directory and any parents. Does nothing if the folder already exists. - * @param {string} directory - */ -const mkdirp = (directory) => { - try { - fs.mkdirSync(directory, { - recursive: true, - }); - } catch (e) { - if (e.code !== "ENOENT") { - throw e; - } - } -}; - /** * @param {Record>} allTranslations * @param {string} idPrefix diff --git a/development/fs-utils.js b/development/fs-utils.js new file mode 100644 index 0000000000..020a1e9d98 --- /dev/null +++ b/development/fs-utils.js @@ -0,0 +1,53 @@ +const fs = require("fs"); +const pathUtil = require("path"); + +/** + * Recursively read a directory. + * @param {string} directory + * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. + * The return result includes files in subdirectories, but not the subdirectories themselves. + */ +const recursiveReadDirectory = (directory) => { + const result = []; + for (const name of fs.readdirSync(directory)) { + if (name.startsWith(".")) { + // Ignore .eslintrc.js, .DS_Store, etc. + continue; + } + const absolutePath = pathUtil.join(directory, name); + const stat = fs.statSync(absolutePath); + if (stat.isDirectory()) { + for (const [ + relativeToChildName, + childAbsolutePath, + ] of recursiveReadDirectory(absolutePath)) { + // This always needs to use / on all systems + result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); + } + } else { + result.push([name, absolutePath]); + } + } + return result; +}; + +/** + * Synchronous create a directory and any parents. Does nothing if the folder already exists. + * @param {string} directory + */ +const mkdirp = (directory) => { + try { + fs.mkdirSync(directory, { + recursive: true, + }); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } +}; + +module.exports = { + recursiveReadDirectory, + mkdirp, +}; diff --git a/development/get-credits-for-gui.js b/development/get-credits-for-gui.js new file mode 100644 index 0000000000..22465e525c --- /dev/null +++ b/development/get-credits-for-gui.js @@ -0,0 +1,118 @@ +const fs = require("fs"); +const path = require("path"); +const https = require("https"); +const fsUtils = require("./fs-utils"); +const parseMetadata = require("./parse-extension-metadata"); + +class AggregatePersonInfo { + /** @param {Person} person */ + constructor(person) { + this.name = person.name; + + /** @type {Set} */ + this.links = new Set(); + } + + /** @param {string} link */ + addLink(link) { + this.links.add(link); + } +} + +/** + * @param {string} username + * @returns {Promise} + */ +const getUserID = (username) => + new Promise((resolve, reject) => { + process.stdout.write(`Getting user ID for ${username}... `); + const request = https.get(`https://api.scratch.mit.edu/users/${username}`); + + request.on("response", (response) => { + const data = []; + response.on("data", (newData) => { + data.push(newData); + }); + + response.on("end", () => { + const allData = Buffer.concat(data); + const json = JSON.parse(allData.toString("utf-8")); + const userID = String(json.id); + process.stdout.write(`${userID}\n`); + resolve(userID); + }); + + response.on("error", (error) => { + process.stdout.write("error\n"); + reject(error); + }); + }); + + request.on("error", (error) => { + process.stdout.write("error\n"); + reject(error); + }); + + request.end(); + }); + +const run = async () => { + /** + * @type {Map} + */ + const aggregate = new Map(); + + const extensionRoot = path.resolve(__dirname, "../extensions/"); + for (const [name, absolutePath] of fsUtils.recursiveReadDirectory( + extensionRoot + )) { + if (!name.endsWith(".js")) { + continue; + } + + const code = fs.readFileSync(absolutePath, "utf-8"); + const metadata = parseMetadata(code); + + for (const person of [...metadata.by, ...metadata.original]) { + const personID = person.name.toLowerCase(); + if (!aggregate.has(personID)) { + aggregate.set(personID, new AggregatePersonInfo(person)); + } + + if (person.link) { + aggregate.get(personID).addLink(person.link); + } + } + } + + const result = []; + + for (const id of [...aggregate.keys()].sort()) { + const info = aggregate.get(id); + + if (info.links.size > 0) { + const link = [...info.links.values()].sort()[0]; + const username = link.match(/users\/(.+?)\/?$/)[1]; + const userID = await getUserID(username); + result.push({ + userID, + username, + }); + } else { + result.push({ + username: info.name, + }); + } + } + + return result; +}; + +run() + .then((result) => { + console.log(JSON.stringify(result, null, 4)); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); From be9c8919faec2fb27f46fd27c2744af7ddfd6acd Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Fri, 29 Dec 2023 23:09:20 -0600 Subject: [PATCH 076/196] Node 20 (#1218) --- .github/workflows/deploy.yml | 6 +- .github/workflows/validate.yml | 24 +- package-lock.json | 1567 ++++++++++++++++++++++---------- 3 files changed, 1124 insertions(+), 473 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d4e69ab358..f341519f05 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,11 +19,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20.x - name: Install dependencies run: npm ci - name: Build for production diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2db07a1930..c02a81ea48 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '14.x' - cache: 'npm' + node-version: 20.x + cache: npm - name: Install dependencies run: npm ci - name: Validate @@ -23,12 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '14.x' - cache: 'npm' + node-version: 20.x + cache: npm - name: Install dependencies run: npm ci - name: Validate @@ -38,12 +38,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '14.x' - cache: 'npm' + node-version: 20.x + cache: npm - name: Install dependencies run: npm ci - name: Validate diff --git a/package-lock.json b/package-lock.json index 56ebc1e56f..533b64a811 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,69 @@ { "name": "@turbowarp/extensions", "version": "0.0.1", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@aashutoshrathi/word-wrap": { + "packages": { + "": { + "name": "@turbowarp/extensions", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@turbowarp/scratchblocks": "^3.6.4", + "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", + "adm-zip": "^0.5.10", + "chokidar": "^3.5.3", + "ejs": "^3.1.9", + "express": "^4.18.2", + "image-size": "^1.0.2", + "markdown-it": "^14.0.0" + }, + "devDependencies": { + "eslint": "^8.56.0", + "espree": "^9.6.1", + "esquery": "^1.5.0", + "prettier": "^3.1.1" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "@eslint-community/eslint-utils": { + "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, - "requires": { + "dependencies": { "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "@eslint-community/regexpp": { + "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "@eslint/eslintrc": { + "node_modules/@eslint/eslintrc": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "requires": { + "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", @@ -41,202 +74,231 @@ "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "@eslint/js": { + "node_modules/@eslint/js": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } }, - "@humanwhocodes/config-array": { + "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, - "requires": { + "dependencies": { "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "engines": { + "node": ">=10.10.0" } }, - "@humanwhocodes/module-importer": { + "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "@humanwhocodes/object-schema": { + "node_modules/@humanwhocodes/object-schema": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, - "@nodelib/fs.scandir": { + "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "requires": { + "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "@nodelib/fs.stat": { + "node_modules/@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "dev": true, + "engines": { + "node": ">= 8" + } }, - "@nodelib/fs.walk": { + "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "requires": { + "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "@turbowarp/scratchblocks": { + "node_modules/@turbowarp/scratchblocks": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/@turbowarp/scratchblocks/-/scratchblocks-3.6.4.tgz", "integrity": "sha512-DUay/UeKoYht03tfcBfp8+m8RSrOtr7eMxFTXujdUXfoiRM7xnQNy6SutufeFmIOdVZU65w3vstLcV3K+6Mhyg==" }, - "@turbowarp/types": { - "version": "git+https://github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19", - "from": "git+https://github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19" + "node_modules/@turbowarp/types": { + "version": "0.0.12", + "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19", + "license": "Apache-2.0" }, - "@ungap/structured-clone": { + "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "accepts": { + "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { + "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, - "acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "acorn-jsx": { + "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "adm-zip": { + "node_modules/adm-zip": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", - "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==" + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "engines": { + "node": ">=6.0" + } }, - "ajv": { + "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "requires": { + "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "ansi-regex": { + "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { + "dependencies": { "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "anymatch": { + "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "requires": { + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "argparse": { + "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-flatten": { + "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, - "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "binary-extensions": { + "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } }, - "body-parser": { + "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { + "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", @@ -249,194 +311,309 @@ "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" } }, - "brace-expansion": { + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { + "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { + "dependencies": { "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "bytes": { + "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "callsites": { + "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "chalk": { + "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { + "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "chokidar": { + "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "color-convert": { + "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { + "dependencies": { "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "color-name": { + "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "content-disposition": { + "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { + "dependencies": { "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } }, - "cookie": { + "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } }, - "cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "cross-spawn": { + "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "requires": { + "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "deep-is": { + "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "depd": { + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } }, - "destroy": { + "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "doctrine": { + "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "requires": { + "dependencies": { "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" } }, - "ee-first": { + "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, - "ejs": { + "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", - "requires": { + "dependencies": { "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "encodeurl": { + "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } }, - "entities": { + "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "escape-html": { + "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "eslint": { + "node_modules/eslint": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, - "requires": { + "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", @@ -476,100 +653,128 @@ "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "eslint-scope": { + "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "requires": { + "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "eslint-visitor-keys": { + "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } }, - "espree": { + "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "requires": { + "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "esquery": { + "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" } }, - "esrecurse": { + "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "requires": { + "dependencies": { "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "esutils": { + "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "etag": { + "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } }, - "express": { + "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { + "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.1", @@ -601,83 +806,106 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" } }, - "fast-deep-equal": { + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-json-stable-stringify": { + "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "fast-levenshtein": { + "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, - "requires": { + "dependencies": { "reusify": "^1.0.4" } }, - "file-entry-cache": { + "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "requires": { + "dependencies": { "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "filelist": { + "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "requires": { + "dependencies": { "minimatch": "^5.0.1" - }, + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - } + "balanced-match": "^1.0.0" } }, - "fill-range": { + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "finalhandler": { + "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { + "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -685,614 +913,943 @@ "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" } }, - "find-up": { + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "requires": { + "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "flat-cache": { + "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, - "requires": { + "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "flatted": { + "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "forwarded": { + "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } }, - "fresh": { + "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "glob": { + "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, - "requires": { + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "glob-parent": { + "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { + "dependencies": { "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "globals": { + "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "requires": { + "dependencies": { "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "graphemer": { + "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { + "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "has-symbols": { + "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "http-errors": { + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { + "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" } }, - "iconv-lite": { + "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { + "dependencies": { "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "ignore": { + "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true + "dev": true, + "engines": { + "node": ">= 4" + } }, - "image-size": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", - "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "requires": { + "node_modules/image-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.0.tgz", + "integrity": "sha512-asnTHw2K8OlqT5kVnQwX+AGKQqpvLo95LbNzQ/C0ln3yzentZmAdd0ygoD004VC4Kkd4PV7J2iaPQkqwp9yuTw==", + "dependencies": { "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=18.0.0" } }, - "import-fresh": { + "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "requires": { + "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "imurmurhash": { + "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.19" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, - "requires": { + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ipaddr.js": { + "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } }, - "is-binary-path": { + "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-extglob": { + "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } }, - "is-glob": { + "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } }, - "is-path-inside": { + "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "requires": { + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" } }, - "js-yaml": { + "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "requires": { + "dependencies": { "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "json-buffer": { + "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-schema-traverse": { + "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "json-stable-stringify-without-jsonify": { + "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "keyv": { + "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "requires": { + "dependencies": { "json-buffer": "3.0.1" } }, - "levn": { + "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "linkify-it": { + "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "requires": { + "dependencies": { "uc.micro": "^2.0.0" } }, - "locate-path": { + "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "requires": { + "dependencies": { "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash.merge": { + "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "markdown-it": { + "node_modules/markdown-it": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", - "requires": { + "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" } }, - "mdurl": { + "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" }, - "media-typer": { + "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } }, - "merge-descriptors": { + "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, - "methods": { + "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } }, - "mime": { + "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } }, - "mime-db": { + "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { + "dependencies": { "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "minimatch": { + "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, - "natural-compare": { + "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "negotiator": { + "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } }, - "normalize-path": { + "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "on-finished": { + "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { + "dependencies": { "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, - "requires": { + "dependencies": { "wrappy": "1" } }, - "optionator": { + "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, - "requires": { + "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "p-limit": { + "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "requires": { + "dependencies": { "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "p-locate": { + "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "requires": { + "dependencies": { "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "parent-module": { + "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "requires": { + "dependencies": { "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "parseurl": { + "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } }, - "path-exists": { + "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "path-key": { + "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "path-to-regexp": { + "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "prelude-ls": { + "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.8.0" + } }, - "prettier": { + "node_modules/prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", - "dev": true + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } }, - "proxy-addr": { + "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { + "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "punycode": { + "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true + "dev": true, + "engines": { + "node": ">=6" + } }, - "punycode.js": { + "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } }, - "qs": { + "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { + "dependencies": { "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "queue": { + "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "requires": { + "dependencies": { "inherits": "~2.0.3" } }, - "queue-microtask": { + "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "range-parser": { + "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } }, - "raw-body": { + "node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { + "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "readdirp": { + "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "resolve-from": { + "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "reusify": { + "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } }, - "rimraf": { + "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "requires": { + "dependencies": { "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "run-parallel": { + "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "requires": { + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { "queue-microtask": "^1.2.2" } }, - "safe-buffer": { + "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "safer-buffer": { + "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "send": { + "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { + "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1307,175 +1864,269 @@ "range-parser": "~1.2.1", "statuses": "2.0.1" }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } + "ms": "2.0.0" } }, - "serve-static": { + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { + "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "setprototypeof": { + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "shebang-command": { + "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "requires": { + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + } }, - "side-channel": { + "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { + "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "statuses": { + "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } }, - "strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "strip-json-comments": { + "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "supports-color": { + "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { + "dependencies": { "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "text-table": { + "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "toidentifier": { + "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } }, - "type-check": { + "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { + "dependencies": { "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "type-fest": { + "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "type-is": { + "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { + "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "uc.micro": { + "node_modules/uc.micro": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz", "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==" }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } }, - "uri-js": { + "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } }, - "vary": { + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } }, - "which": { + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } From 39a0e47f1ea9457b738033d6789ceca47c1f1db5 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Fri, 29 Dec 2023 23:47:06 -0600 Subject: [PATCH 077/196] Update translations (#1219) --- translations/extension-runtime.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 08dc0231af..18338f6053 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -2096,7 +2096,20 @@ "gamejolt@_value": "值", "gamejolt@_year": "年份", "iframe@_Iframe": "内嵌框架", + "iframe@_It works!": "它在工作!", + "iframe@_close iframe": "退出内嵌网页", "iframe@_height": "高度", + "iframe@_hide iframe": "隐藏内嵌网页", + "iframe@_iframe [MENU]": "内嵌网页的[MENU]", + "iframe@_set iframe height to [HEIGHT]": "设置内嵌网页的高度为[HEIGHT]", + "iframe@_set iframe width to [WIDTH]": "设置内嵌网页的宽度为[WIDTH]", + "iframe@_set iframe x position to [X]": "设置内嵌网页的x坐标为[X]", + "iframe@_set iframe y position to [Y]": "设置内嵌网页的y坐标为[Y]", + "iframe@_show HTML [HTML]": "显示来自文本[HTML]的网页", + "iframe@_show iframe": "显示内嵌网页", + "iframe@_show website [URL]": "显示来自URL[URL]的网页", + "iframe@_url": "URL", + "iframe@_visible": "显示状态", "iframe@_width": "宽度", "itchio@_id": "ID", "itchio@_name": "名字", @@ -2127,7 +2140,7 @@ "lab/text@_set [ANIMATE] duration to [NUM] seconds": "设置动画样式[ANIMATE]的完成时间是[NUM]秒", "lab/text@_set font to [FONT]": "设置文本的字体为[FONT]", "lab/text@_set text color to [COLOR]": "设置文本的颜色为[COLOR]", - "lab/text@_set typing delay to [NUM] seconds": "设置柱子显示速度为[NUM]秒/字", + "lab/text@_set typing delay to [NUM] seconds": "设置逐字显示速度为[NUM]秒/字", "lab/text@_set width to [WIDTH]": "设置文本的宽度为[WIDTH]", "lab/text@_set width to [WIDTH] aligned [ALIGN]": "设置[ALIGN]样式的宽度为[WIDTH]", "lab/text@_show sprite": "显示角色", From df42d96e8f907bd39078081830456e4132ee8f49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jan 2024 23:51:55 -0600 Subject: [PATCH 078/196] build(deps): bump image-size from 1.1.0 to 1.1.1 (#1230) --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 533b64a811..24e9d10371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "chokidar": "^3.5.3", "ejs": "^3.1.9", "express": "^4.18.2", - "image-size": "^1.0.2", + "image-size": "^1.1.1", "markdown-it": "^14.0.0" }, "devDependencies": { @@ -1175,9 +1175,9 @@ } }, "node_modules/image-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.0.tgz", - "integrity": "sha512-asnTHw2K8OlqT5kVnQwX+AGKQqpvLo95LbNzQ/C0ln3yzentZmAdd0ygoD004VC4Kkd4PV7J2iaPQkqwp9yuTw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", "dependencies": { "queue": "6.0.2" }, @@ -1185,7 +1185,7 @@ "image-size": "bin/image-size.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=16.x" } }, "node_modules/import-fresh": { diff --git a/package.json b/package.json index 8eafbc6863..b18ab42b4c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "chokidar": "^3.5.3", "ejs": "^3.1.9", "express": "^4.18.2", - "image-size": "^1.0.2", + "image-size": "^1.1.1", "markdown-it": "^14.0.0" }, "devDependencies": { From 95d37cb22b58319dad365bfca18f875bb5e67fdb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:19:40 +0000 Subject: [PATCH 079/196] merge-upstream: use actual category colours when applicable (#1231) With the introduction of High Contrast mode, extensions like Looks Plus should use the actual category colours by default so their high contrast colours match. --- extensions/Lily/ClonesPlus.js | 19 +++++++++++++++ extensions/Lily/LooksPlus.js | 14 ++++++++++++ extensions/Lily/MoreEvents.js | 17 ++++++++++++++ extensions/Lily/SoundExpanded.js | 12 ++++++++++ extensions/NexusKitten/controlcontrols.js | 4 ++++ extensions/NexusKitten/moremotion.js | 10 ++++++++ extensions/Xeltalliv/clippingblending.js | 7 ++++++ extensions/lab/text.js | 25 ++++++++++++++++++++ extensions/obviousAlexC/SensingPlus.js | 28 +++++++++++++++++++++++ extensions/true-fantom/math.js | 28 +++++++++++++++++++++++ 10 files changed, 164 insertions(+) diff --git a/extensions/Lily/ClonesPlus.js b/extensions/Lily/ClonesPlus.js index cfd7b901f7..8a50260b27 100644 --- a/extensions/Lily/ClonesPlus.js +++ b/extensions/Lily/ClonesPlus.js @@ -68,6 +68,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "createCloneWithVar", @@ -84,6 +85,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, "---", @@ -103,6 +105,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "touchingMainSprite", @@ -110,6 +113,7 @@ text: "touching main sprite?", filter: [Scratch.TargetType.SPRITE], disableMonitor: true, + extensions: ["colours_control"], }, "---", @@ -137,6 +141,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "getVariableOfClone", @@ -158,6 +163,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "setVariableOfMainSprite", @@ -174,6 +180,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "getVariableOfMainSprite", @@ -187,6 +194,7 @@ menu: "variablesMenu", }, }, + extensions: ["colours_control"], }, "---", @@ -206,6 +214,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "getThingOfClone", @@ -228,6 +237,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "getThingOfMainSprite", @@ -242,6 +252,7 @@ menu: "thingOfMenu", }, }, + extensions: ["colours_control"], }, "---", @@ -256,6 +267,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_control"], }, { opcode: "stopScriptsInClone", @@ -272,12 +284,14 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, { opcode: "stopScriptsInMainSprite", blockType: Scratch.BlockType.COMMAND, text: "stop scripts in main sprite", filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_control"], }, "---", @@ -292,6 +306,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_control"], }, { opcode: "deleteCloneWithVar", @@ -308,6 +323,7 @@ defaultValue: "1", }, }, + extensions: ["colours_control"], }, "---", @@ -318,6 +334,7 @@ text: "is clone?", filter: [Scratch.TargetType.SPRITE], disableMonitor: true, + extensions: ["colours_control"], }, "---", @@ -326,6 +343,7 @@ opcode: "cloneCount", blockType: Scratch.BlockType.REPORTER, text: "clone count", + extensions: ["colours_control"], }, { opcode: "spriteCloneCount", @@ -338,6 +356,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_control"], }, ], menus: { diff --git a/extensions/Lily/LooksPlus.js b/extensions/Lily/LooksPlus.js index 49e17834f9..1b540995f3 100644 --- a/extensions/Lily/LooksPlus.js +++ b/extensions/Lily/LooksPlus.js @@ -52,6 +52,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, { opcode: "hideSprite", @@ -63,6 +64,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, { opcode: "spriteVisible", @@ -74,6 +76,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, "---", @@ -92,6 +95,7 @@ defaultValue: "1", }, }, + extensions: ["colours_looks"], }, { opcode: "spriteLayerNumber", @@ -103,6 +107,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, { opcode: "effectValue", @@ -119,6 +124,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, "---", @@ -133,6 +139,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, { opcode: "costumeAttribute", @@ -147,6 +154,7 @@ type: Scratch.ArgumentType.COSTUME, }, }, + extensions: ["colours_looks"], }, "---", @@ -156,6 +164,7 @@ blockType: Scratch.BlockType.REPORTER, text: "snapshot stage", disableMonitor: true, + extensions: ["colours_looks"], }, "---", @@ -178,6 +187,7 @@ defaultValue: "", }, }, + extensions: ["colours_looks"], }, { opcode: "restoreCostumeContent", @@ -188,6 +198,7 @@ type: Scratch.ArgumentType.COSTUME, }, }, + extensions: ["colours_looks"], }, { opcode: "costumeContent", @@ -208,6 +219,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_looks"], }, "---", @@ -230,6 +242,7 @@ defaultValue: "", }, }, + extensions: ["colours_looks"], }, { opcode: "colorHex", @@ -241,6 +254,7 @@ defaultValue: "#FFD983", }, }, + extensions: ["colours_looks"], }, ], menus: { diff --git a/extensions/Lily/MoreEvents.js b/extensions/Lily/MoreEvents.js index d0d40dbe65..ce10c85094 100644 --- a/extensions/Lily/MoreEvents.js +++ b/extensions/Lily/MoreEvents.js @@ -225,12 +225,14 @@ dataURI: stopIcon, }, }, + extensions: ["colours_event"], }, { opcode: "forever", blockType: Scratch.BlockType.EVENT, text: "forever", isEdgeActivated: false, + extensions: ["colours_event"], }, "---", @@ -249,6 +251,7 @@ menu: "boolean", }, }, + extensions: ["colours_event"], }, { opcode: "whileTrueFalse", @@ -264,6 +267,7 @@ menu: "boolean", }, }, + extensions: ["colours_event"], }, "---", @@ -281,6 +285,7 @@ type: null, }, }, + extensions: ["colours_event"], }, { opcode: "everyDuration", @@ -293,6 +298,7 @@ defaultValue: 3, }, }, + extensions: ["colours_event"], }, "---", @@ -313,6 +319,7 @@ menu: "action", }, }, + extensions: ["colours_event"], }, { opcode: "whileKeyPressed", @@ -326,6 +333,7 @@ menu: "keyboardButtons", }, }, + extensions: ["colours_event"], }, "---", @@ -344,6 +352,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, { opcode: "broadcastToTargetAndWait", @@ -359,6 +368,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, "---", @@ -376,6 +386,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, { opcode: "broadcastDataAndWait", @@ -390,6 +401,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, { blockType: Scratch.BlockType.XML, @@ -401,6 +413,7 @@ text: "received data", disableMonitor: true, allowDropAnywhere: true, + extensions: ["colours_event"], }, "---", @@ -423,6 +436,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, { opcode: "broadcastDataToTargetAndWait", @@ -442,6 +456,7 @@ }, }, hideFromPalette: true, + extensions: ["colours_event"], }, { blockType: Scratch.BlockType.XML, @@ -454,6 +469,7 @@ text: "before project saves", shouldRestartExistingThreads: true, isEdgeActivated: false, + extensions: ["colours_event"], }, { blockType: Scratch.BlockType.EVENT, @@ -461,6 +477,7 @@ text: "after project saves", shouldRestartExistingThreads: true, isEdgeActivated: false, + extensions: ["colours_event"], }, ], menus: { diff --git a/extensions/Lily/SoundExpanded.js b/extensions/Lily/SoundExpanded.js index aefe95072b..cd3d151c53 100644 --- a/extensions/Lily/SoundExpanded.js +++ b/extensions/Lily/SoundExpanded.js @@ -32,6 +32,7 @@ defaultValue: 0, }, }, + extensions: ["colours_sound"], }, { opcode: "stopLooping", @@ -42,6 +43,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, { opcode: "isLooping", @@ -52,6 +54,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, "---", @@ -65,6 +68,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, { opcode: "pauseSounds", @@ -75,6 +79,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, { opcode: "resumeSounds", @@ -85,6 +90,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, "---", @@ -98,6 +104,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, { opcode: "attributeOfSound", @@ -112,6 +119,7 @@ type: Scratch.ArgumentType.SOUND, }, }, + extensions: ["colours_sound"], }, { opcode: "getSoundEffect", @@ -127,6 +135,7 @@ menu: "targets", }, }, + extensions: ["colours_sound"], }, "---", { @@ -139,6 +148,7 @@ defaultValue: 100, }, }, + extensions: ["colours_sound"], }, { opcode: "changeProjectVolume", @@ -150,11 +160,13 @@ defaultValue: -10, }, }, + extensions: ["colours_sound"], }, { opcode: "getProjectVolume", blockType: Scratch.BlockType.REPORTER, text: "project volume", + extensions: ["colours_sound"], }, ], menus: { diff --git a/extensions/NexusKitten/controlcontrols.js b/extensions/NexusKitten/controlcontrols.js index f0a152450f..cc5446bbaf 100644 --- a/extensions/NexusKitten/controlcontrols.js +++ b/extensions/NexusKitten/controlcontrols.js @@ -71,6 +71,7 @@ menu: "OPTION", }, }, + extensions: ["colours_control"], }, { opcode: "hideOption", @@ -82,6 +83,7 @@ menu: "OPTION", }, }, + extensions: ["colours_control"], }, "---", { @@ -94,6 +96,7 @@ menu: "OPTION", }, }, + extensions: ["colours_control"], }, "---", { @@ -106,6 +109,7 @@ menu: "OPTION", }, }, + extensions: ["colours_control"], }, ], menus: { diff --git a/extensions/NexusKitten/moremotion.js b/extensions/NexusKitten/moremotion.js index dcaca41096..dc3fe6a357 100644 --- a/extensions/NexusKitten/moremotion.js +++ b/extensions/NexusKitten/moremotion.js @@ -45,6 +45,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -61,6 +62,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -68,6 +70,7 @@ blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("rotation style"), disableMonitor: true, + extensions: ["colours_motion"], }, "---", { @@ -79,6 +82,7 @@ description: "This blocks forces the sprite to be onscreen if it moved offscreen.", }), + extensions: ["colours_motion"], }, "---", { @@ -100,6 +104,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -122,6 +127,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, "---", { @@ -139,6 +145,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -155,6 +162,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -185,6 +193,7 @@ defaultValue: "0", }, }, + extensions: ["colours_motion"], }, { filter: [Scratch.TargetType.SPRITE], @@ -211,6 +220,7 @@ defaultValue: "100", }, }, + extensions: ["colours_motion"], }, ], menus: { diff --git a/extensions/Xeltalliv/clippingblending.js b/extensions/Xeltalliv/clippingblending.js index 4f8505155b..4b6d2649b1 100644 --- a/extensions/Xeltalliv/clippingblending.js +++ b/extensions/Xeltalliv/clippingblending.js @@ -308,12 +308,14 @@ }, }, filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_looks"], }, { opcode: "clearClipbox", blockType: Scratch.BlockType.COMMAND, text: "clear clipping box", filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_looks"], }, { opcode: "getClipbox", @@ -327,6 +329,7 @@ }, }, filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_looks"], }, "---", { @@ -341,6 +344,7 @@ }, }, filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_looks"], }, { opcode: "getBlend", @@ -348,6 +352,7 @@ text: "blending", filter: [Scratch.TargetType.SPRITE], disableMonitor: true, + extensions: ["colours_looks"], }, "---", { @@ -363,6 +368,7 @@ }, filter: [Scratch.TargetType.SPRITE], hideFromPalette: true, + extensions: ["colours_looks"], }, { opcode: "getAdditiveBlend", @@ -371,6 +377,7 @@ filter: [Scratch.TargetType.SPRITE], hideFromPalette: true, disableMonitor: true, + extensions: ["colours_looks"], }, ], menus: { diff --git a/extensions/lab/text.js b/extensions/lab/text.js index 3c912fea53..faa33fcbeb 100644 --- a/extensions/lab/text.js +++ b/extensions/lab/text.js @@ -615,6 +615,7 @@ defaultValue: Scratch.translate("Welcome to my project!"), }, }, + extensions: ["colours_looks"], }, { opcode: "animateText", @@ -631,11 +632,13 @@ defaultValue: Scratch.translate("Here we go!"), }, }, + extensions: ["colours_looks"], }, { opcode: "clearText", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("show sprite"), + extensions: ["colours_looks"], }, "---", { @@ -648,6 +651,7 @@ menu: "font", }, }, + extensions: ["colours_looks"], }, { opcode: "setColor", @@ -658,6 +662,7 @@ type: Scratch.ArgumentType.COLOR, }, }, + extensions: ["colours_looks"], }, { opcode: "setWidth", @@ -673,6 +678,7 @@ menu: "align", }, }, + extensions: ["colours_looks"], }, "---", @@ -686,11 +692,13 @@ blockType: Scratch.BlockType.BUTTON, text: Scratch.translate("Enable Non-Scratch Lab Features"), hideFromPalette: !compatibilityMode, + extensions: ["colours_looks"], }, { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Incompatible with Scratch Lab:"), hideFromPalette: compatibilityMode, + extensions: ["colours_looks"], }, { opcode: "setAlignment", @@ -703,6 +711,7 @@ menu: "twAlign", }, }, + extensions: ["colours_looks"], }, { // why is the other block called "setWidth" :( @@ -716,12 +725,14 @@ defaultValue: 200, }, }, + extensions: ["colours_looks"], }, { opcode: "resetWidth", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("reset text width"), hideFromPalette: compatibilityMode, + extensions: ["colours_looks"], }, "---", { @@ -735,6 +746,7 @@ defaultValue: Scratch.translate("Hello!"), }, }, + extensions: ["colours_looks"], }, { opcode: "getLines", @@ -742,6 +754,7 @@ text: Scratch.translate("# of lines"), hideFromPalette: compatibilityMode, disableMonitor: true, + extensions: ["colours_looks"], }, "---", { @@ -756,6 +769,7 @@ defaultValue: "rainbow", }, }, + extensions: ["colours_looks"], }, { opcode: "animateUntilDone", @@ -769,6 +783,7 @@ defaultValue: "rainbow", }, }, + extensions: ["colours_looks"], }, { opcode: "isAnimating", @@ -776,6 +791,7 @@ text: Scratch.translate("is animating?"), hideFromPalette: compatibilityMode, disableMonitor: true, + extensions: ["colours_looks"], }, "---", { @@ -794,6 +810,7 @@ defaultValue: 3, }, }, + extensions: ["colours_looks"], }, { opcode: "resetAnimateDuration", @@ -807,6 +824,7 @@ defaultValue: "rainbow", }, }, + extensions: ["colours_looks"], }, { opcode: "getAnimateDuration", @@ -820,6 +838,7 @@ defaultValue: "rainbow", }, }, + extensions: ["colours_looks"], }, "---", { @@ -833,12 +852,14 @@ defaultValue: 0.1, }, }, + extensions: ["colours_looks"], }, { opcode: "resetTypeDelay", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("reset typing delay"), hideFromPalette: compatibilityMode, + extensions: ["colours_looks"], }, { opcode: "getTypeDelay", @@ -846,6 +867,7 @@ text: Scratch.translate("typing delay"), hideFromPalette: compatibilityMode, disableMonitor: true, + extensions: ["colours_looks"], }, "---", { @@ -854,6 +876,7 @@ text: Scratch.translate("is showing text?"), hideFromPalette: compatibilityMode, disableMonitor: true, + extensions: ["colours_looks"], }, { opcode: "getDisplayedText", @@ -861,6 +884,7 @@ text: Scratch.translate("displayed text"), hideFromPalette: compatibilityMode, disableMonitor: true, + extensions: ["colours_looks"], }, { opcode: "getTextAttribute", @@ -874,6 +898,7 @@ }, disableMonitor: true, hideFromPalette: compatibilityMode, + extensions: ["colours_looks"], }, ], menus: { diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index d84eea22c5..4ff646a8b6 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -304,6 +304,7 @@ text: "Supports touches?", blockIconURI: touchIco, arguments: {}, + extensions: ["colours_sensing"], }, { opcode: "getMaxTouches", @@ -311,6 +312,7 @@ text: "# of simultaneous possible", blockIconURI: touchIco, arguments: {}, + extensions: ["colours_sensing"], }, { opcode: "getFingersTouching", @@ -318,6 +320,7 @@ text: "# of fingers down", blockIconURI: touchIco, arguments: {}, + extensions: ["colours_sensing"], }, { opcode: "isFingerDown", @@ -330,6 +333,7 @@ menu: "fingerIDMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "touchingFinger", @@ -339,6 +343,7 @@ filter: [Scratch.TargetType.SPRITE], arguments: {}, disableMonitor: true, + extensions: ["colours_sensing"], }, { opcode: "touchingSpecificFinger", @@ -352,6 +357,7 @@ menu: "fingerIDMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "getTouchingFingerID", @@ -361,6 +367,7 @@ filter: [Scratch.TargetType.SPRITE], blockIconURI: touchIco, arguments: {}, + extensions: ["colours_sensing"], }, { opcode: "fingerPosition", @@ -377,6 +384,7 @@ menu: "coordmenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "getFingerSpeed", @@ -389,6 +397,7 @@ menu: "fingerIDMenu", }, }, + extensions: ["colours_sensing"], }, "---", { @@ -406,6 +415,7 @@ menu: "listMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "lengthOfListInSprite", @@ -419,6 +429,7 @@ menu: "listMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "listContains", @@ -435,6 +446,7 @@ menu: "listMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "itemNumberInList", @@ -451,6 +463,7 @@ menu: "listMenu", }, }, + extensions: ["colours_sensing"], }, "---", { @@ -465,6 +478,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "touchingClone", @@ -478,6 +492,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "clonesOfSprite", @@ -491,6 +506,7 @@ menu: "spriteMenu", }, }, + extensions: ["colours_sensing"], }, "---", { @@ -505,6 +521,7 @@ menu: "effectMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "isHidden", @@ -513,6 +530,7 @@ blockIconURI: effectIco, filter: [Scratch.TargetType.SPRITE], disableMonitor: true, + extensions: ["colours_sensing"], }, { opcode: "getRotationStyle", @@ -521,6 +539,7 @@ blockIconURI: rotationIco, disableMonitor: true, filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_sensing"], }, { opcode: "getSpriteLayer", @@ -529,6 +548,7 @@ blockIconURI: layerIco, disableMonitor: true, filter: [Scratch.TargetType.SPRITE], + extensions: ["colours_sensing"], }, "---", { @@ -537,6 +557,7 @@ text: "Copied Contents", blockIconURI: clipboardIco, disableMonitor: true, + extensions: ["colours_sensing"], }, { opcode: "setClipBoard", @@ -549,6 +570,7 @@ defaultValue: "", }, }, + extensions: ["colours_sensing"], }, "---", { @@ -556,6 +578,7 @@ blockIconURI: packagedIco, blockType: Scratch.BlockType.BOOLEAN, text: "Is Packaged?", + extensions: ["colours_sensing"], }, "---", { @@ -573,18 +596,21 @@ menu: "toggleMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "returnWords", blockType: Scratch.BlockType.REPORTER, text: "Recognized Words", blockIconURI: speechIco, + extensions: ["colours_sensing"], }, { opcode: "isrecording", blockType: Scratch.BlockType.BOOLEAN, text: "Recording?", blockIconURI: speechIco, + extensions: ["colours_sensing"], }, "---", { @@ -602,6 +628,7 @@ menu: "deviceMenu", }, }, + extensions: ["colours_sensing"], }, { opcode: "getDeviceSpeed", @@ -619,6 +646,7 @@ menu: "axismenu", }, }, + extensions: ["colours_sensing"], }, ], menus: { diff --git a/extensions/true-fantom/math.js b/extensions/true-fantom/math.js index 7d5f2753e6..a732d6cbbf 100644 --- a/extensions/true-fantom/math.js +++ b/extensions/true-fantom/math.js @@ -122,6 +122,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, { opcode: "root_block", @@ -137,6 +138,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, { opcode: "negative_block", @@ -148,6 +150,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, "---", { @@ -164,6 +167,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "less_or_equal_block", @@ -179,6 +183,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "not_equal_block", @@ -194,6 +199,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "exactly_equal_block", @@ -209,6 +215,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "not_exactly_equal_block", @@ -224,6 +231,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "almost_equal_block", @@ -239,6 +247,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, { opcode: "not_almost_equal_block", @@ -254,6 +263,7 @@ defaultValue: 50, }, }, + extensions: ["colours_operators"], }, "---", { @@ -268,6 +278,7 @@ type: Scratch.ArgumentType.BOOLEAN, }, }, + extensions: ["colours_operators"], }, { opcode: "nor_block", @@ -281,6 +292,7 @@ type: Scratch.ArgumentType.BOOLEAN, }, }, + extensions: ["colours_operators"], }, { opcode: "xor_block", @@ -294,6 +306,7 @@ type: Scratch.ArgumentType.BOOLEAN, }, }, + extensions: ["colours_operators"], }, { opcode: "xnor_block", @@ -307,6 +320,7 @@ type: Scratch.ArgumentType.BOOLEAN, }, }, + extensions: ["colours_operators"], }, "---", { @@ -323,6 +337,7 @@ defaultValue: "a", }, }, + extensions: ["colours_operators"], }, "---", { @@ -343,6 +358,7 @@ defaultValue: "100", }, }, + extensions: ["colours_operators"], }, { opcode: "scale_block", @@ -370,6 +386,7 @@ defaultValue: "1", }, }, + extensions: ["colours_operators"], }, "---", { @@ -386,6 +403,7 @@ defaultValue: "1", }, }, + extensions: ["colours_operators"], }, { opcode: "trunc_block", @@ -397,6 +415,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, "---", { @@ -413,6 +432,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, "---", { @@ -429,22 +449,26 @@ defaultValue: 10, }, }, + extensions: ["colours_operators"], }, "---", { opcode: "pi_block", blockType: Scratch.BlockType.REPORTER, text: "𝜋", + extensions: ["colours_operators"], }, { opcode: "e_block", blockType: Scratch.BlockType.REPORTER, text: "𝘦", + extensions: ["colours_operators"], }, { opcode: "infinity_block", blockType: Scratch.BlockType.REPORTER, text: "∞", + extensions: ["colours_operators"], }, "---", { @@ -457,6 +481,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, "---", { @@ -469,6 +494,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, { opcode: "is_int_block", @@ -480,6 +506,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, { opcode: "is_float_block", @@ -491,6 +518,7 @@ defaultValue: "", }, }, + extensions: ["colours_operators"], }, ], }; From c1528ab96e90404fdaad41646408e2de4a21ac83 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Tue, 9 Jan 2024 13:19:52 -0600 Subject: [PATCH 080/196] Lily/Assets: alert user for packaged runtime (#1232) --- extensions/Lily/Assets.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/extensions/Lily/Assets.js b/extensions/Lily/Assets.js index ad5e3aaebb..4bc3f19adf 100644 --- a/extensions/Lily/Assets.js +++ b/extensions/Lily/Assets.js @@ -11,6 +11,16 @@ const runtime = vm.runtime; const Cast = Scratch.Cast; + const requireNonPackagedRuntime = (blockName) => { + if (vm.runtime.isPackaged) { + alert( + `To use the Asset Manager ${blockName} block, the creator of the packaged project must uncheck "Remove raw asset data after loading to save RAM" under advanced settings in the packager.` + ); + return false; + } + return true; + }; + class Assets { getInfo() { return { @@ -517,14 +527,18 @@ const costume = target.sprite.costumes[costumeIndex]; switch (attribute) { case "dataURI": + if (!requireNonPackagedRuntime("dataURI of costume")) return ""; return costume.asset.encodeDataURI(); case "index": return costumeIndex + 1; case "format": + if (!requireNonPackagedRuntime("format of costume")) return ""; return costume.asset.assetType.runtimeFormat; case "header": + if (!requireNonPackagedRuntime("header of costume")) return ""; return costume.asset.assetType.contentType; case "asset ID": + if (!requireNonPackagedRuntime("asset ID of costume")) return ""; return costume.asset.assetId; default: return ""; @@ -541,14 +555,18 @@ const sound = target.sprite.sounds[soundIndex]; switch (attribute) { case "dataURI": + if (!requireNonPackagedRuntime("dataURI of sound")) return ""; return sound.asset.encodeDataURI(); case "index": return soundIndex + 1; case "format": + if (!requireNonPackagedRuntime("format of sound")) return ""; return sound.asset.assetType.runtimeFormat; case "header": + if (!requireNonPackagedRuntime("header of sound")) return ""; return sound.asset.assetType.contentType; case "asset ID": + if (!requireNonPackagedRuntime("asset ID of sound")) return ""; return sound.asset.assetId; default: return ""; From dd4595e96fec047352b8cc00fba45ab382ed7d3d Mon Sep 17 00:00:00 2001 From: CST1229 <68464103+CST1229@users.noreply.github.com> Date: Wed, 10 Jan 2024 03:48:06 +0100 Subject: [PATCH 081/196] CST1229/zip: turn icon text into path (#1233) This fixes the icon on devices which don't have the font used by it. (also fixes some almost unnoticeable seams between the zippers and the outline, and compresses the icon a bit) --- extensions/CST1229/zip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/CST1229/zip.js b/extensions/CST1229/zip.js index a8827db750..1eef209146 100644 --- a/extensions/CST1229/zip.js +++ b/extensions/CST1229/zip.js @@ -10,7 +10,7 @@ const JSZip = Scratch.vm.exports.JSZip; const extIcon = - "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PHJlY3Qgd2lkdGg9IjIzIiBoZWlnaHQ9IjIwIiB4PSI2IiB5PSIzIiByeD0iNCIgcnk9IjQiIHN0eWxlPSJmb250LXZhcmlhdGlvbi1zZXR0aW5nczpub3JtYWw7ZmlsbDojZDhkODZjO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojN2Q3ZDIzO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTtwYWludC1vcmRlcjpzdHJva2UgbWFya2VycyBmaWxsO3N0b3AtY29sb3I6IzAwMCIvPjxyZWN0IHdpZHRoPSIyOCIgaGVpZ2h0PSIyMCIgeD0iMSIgeT0iOCIgcng9IjQiIHJ5PSI0IiBzdHlsZT0iZm9udC12YXJpYXRpb24tc2V0dGluZ3M6bm9ybWFsO29wYWNpdHk6MTtmaWxsOiNkOGQ4NmM7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOiM3ZDdkMjM7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3BhaW50LW9yZGVyOnN0cm9rZSBtYXJrZXJzIGZpbGw7c3RvcC1jb2xvcjojMDAwO3N0b3Atb3BhY2l0eToxIi8+PHBhdGggZmlsbD0iIzdkN2QyMyIgZD0iTTUgMTJWOGg0djRabTMgNHYtNGg0djRabS0zIDR2LTRoNHY0Wm0zIDR2LTRoNHY0em0tMyA0di00aDR2NFoiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpub3JtYWwiLz48cGF0aCBmaWxsPSIjN2Q3ZDIzIiBzdHJva2U9IiM3ZDdkMjMiIHN0cm9rZS13aWR0aD0iLjEiIGQ9Ik0xMCA3VjVoMnYyem0xLTJWM2gydjJ6IiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6bm9ybWFsIi8+PHRleHQgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeD0iMTQuNSIgeT0iMjQuOCIgc3R5bGU9ImZvbnQtd2VpZ2h0OjcwMDtmb250LXNpemU6MTkuNDMwNHB4O2ZvbnQtZmFtaWx5OkNvbnNvbGFzOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246JnF1b3Q7Q29uc29sYXMsIEJvbGQmcXVvdDs7Zm9udC12YXJpYXRpb24tc2V0dGluZ3M6bm9ybWFsO29wYWNpdHk6MTtmaWxsOiNmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOiM3ZDdkMjM7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO3BhaW50LW9yZGVyOnN0cm9rZSBtYXJrZXJzIGZpbGw7c3RvcC1jb2xvcjojMDAwO3N0b3Atb3BhY2l0eToxIiB0cmFuc2Zvcm09InNjYWxlKDEuMDMzIC45NjgpIj48dHNwYW4geD0iMTQuNSIgeT0iMjQuOCIgc3R5bGU9InN0cm9rZS13aWR0aDoyIj5aPC90c3Bhbj48L3RleHQ+PC9zdmc+"; + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAzMCI+PHJlY3Qgd2lkdGg9IjIzIiBoZWlnaHQ9IjIwIiB4PSI2IiB5PSIzIiBmaWxsPSIjZDhkODZjIiBzdHJva2U9IiM3ZDdkMjMiIHN0cm9rZS13aWR0aD0iMiIgcGFpbnQtb3JkZXI9InN0cm9rZSBtYXJrZXJzIGZpbGwiIHJ4PSI0IiByeT0iNCIgc3R5bGU9ImZvbnQtdmFyaWF0aW9uLXNldHRpbmdzOm5vcm1hbCIvPjxyZWN0IHdpZHRoPSIyOCIgaGVpZ2h0PSIyMCIgeD0iMSIgeT0iOCIgZmlsbD0iI2Q4ZDg2YyIgc3Ryb2tlPSIjN2Q3ZDIzIiBzdHJva2Utd2lkdGg9IjIiIHBhaW50LW9yZGVyPSJzdHJva2UgbWFya2VycyBmaWxsIiByeD0iNCIgcnk9IjQiIHN0eWxlPSJmb250LXZhcmlhdGlvbi1zZXR0aW5nczpub3JtYWwiLz48cGF0aCBmaWxsPSIjN2Q3ZDIzIiBkPSJNNSAxMlY3LjU1bDQtLjAyNlYxMlptMyA0di00aDR2NHptLTMgNHYtNGg0djR6bTMgNHYtNGg0djR6bS0zIDQuMTgxVjI0aDR2NC4xNzV6IiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6bm9ybWFsIi8+PHBhdGggZmlsbD0iIzdkN2QyMyIgc3Ryb2tlPSIjN2Q3ZDIzIiBzdHJva2Utd2lkdGg9Ii4xIiBkPSJNMTAgNy4xMjNWNWgydjIuMTM2Wk0xMSA1VjIuNTYybDItLjE2MlY1WiIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOm5vcm1hbCIvPjxwYXRoIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzdkN2QyMyIgc3Ryb2tlLXdpZHRoPSIyIiBkPSJNMTUuNDg3IDI0Ljh2LTEuNzY1bDUuNjczLTguNTJoLTUuNDkzVjEyLjRoOC40NTN2MS44OTdsLTUuNzExIDguMzg3aDUuNzg3VjI0Ljh6IiBhcmlhLWxhYmVsPSJaIiBmb250LWZhbWlseT0iQ29uc29sYXMiIGZvbnQtc2l6ZT0iMTkuNDMiIGZvbnQtd2VpZ2h0PSI3MDAiIHBhaW50LW9yZGVyPSJzdHJva2UgbWFya2VycyBmaWxsIiBzdHlsZT0iLWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjomcXVvdDtDb25zb2xhcywgQm9sZCZxdW90OyIgdHJhbnNmb3JtPSJzY2FsZSgxLjAzMyAuOTY4KSIvPjwvc3ZnPg=="; class ZipExt { constructor() { From 0d0e10a50f072ca8e17d5be911b4a0d654eefbbd Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:58:26 +0000 Subject: [PATCH 082/196] Lily/SoundExpanded: oops (#1235) --- extensions/Lily/SoundExpanded.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/extensions/Lily/SoundExpanded.js b/extensions/Lily/SoundExpanded.js index cd3d151c53..7fd5d11e3e 100644 --- a/extensions/Lily/SoundExpanded.js +++ b/extensions/Lily/SoundExpanded.js @@ -32,7 +32,7 @@ defaultValue: 0, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "stopLooping", @@ -43,7 +43,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "isLooping", @@ -54,7 +54,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, "---", @@ -68,7 +68,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "pauseSounds", @@ -79,7 +79,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "resumeSounds", @@ -90,7 +90,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, "---", @@ -104,7 +104,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "attributeOfSound", @@ -119,7 +119,7 @@ type: Scratch.ArgumentType.SOUND, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "getSoundEffect", @@ -135,7 +135,7 @@ menu: "targets", }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, "---", { @@ -148,7 +148,7 @@ defaultValue: 100, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "changeProjectVolume", @@ -160,13 +160,13 @@ defaultValue: -10, }, }, - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, { opcode: "getProjectVolume", blockType: Scratch.BlockType.REPORTER, text: "project volume", - extensions: ["colours_sound"], + extensions: ["colours_sounds"], }, ], menus: { From bba813871a8879715d3459d714b3208b88570f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=87=8C?= Date: Sat, 27 Jan 2024 13:31:00 +0800 Subject: [PATCH 083/196] files: fix downloads in some older browsers (#1257) Co-authored-by: Muffin --- extensions/files.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/files.js b/extensions/files.js index be3b6fd0b5..bc7244a62c 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -232,7 +232,10 @@ const downloadBlob = (blob, file) => { const url = URL.createObjectURL(blob); downloadURL(url, file); - URL.revokeObjectURL(url); + // Some old browsers process Blob URLs asynchronously + setTimeout(() => { + URL.revokeObjectURL(url); + }, 1000); }; /** From 99456984b9f46aa92379fd28c6c7d9fadcfb695b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:31:17 -0600 Subject: [PATCH 084/196] build(deps-dev): bump prettier from 3.1.1 to 3.2.4 (#1252) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24e9d10371..8fffdb806a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "eslint": "^8.56.0", "espree": "^9.6.1", "esquery": "^1.5.0", - "prettier": "^3.1.1" + "prettier": "^3.2.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1646,9 +1646,9 @@ } }, "node_modules/prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/package.json b/package.json index b18ab42b4c..8146486a8d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint": "^8.56.0", "espree": "^9.6.1", "esquery": "^1.5.0", - "prettier": "^3.1.1" + "prettier": "^3.2.4" }, "private": true } From a4db23dcde14f4554010a98bef97998d52dd17f2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 27 Jan 2024 05:31:54 +0000 Subject: [PATCH 085/196] Lily/TempVariables2: Minor tweaks (#1236) - Changed the wording of some blocks - Allowed reporters to be dropped anywhere - Removed icon setup --- extensions/Lily/TempVariables2.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/Lily/TempVariables2.js b/extensions/Lily/TempVariables2.js index a7dfb04db3..57bbd95925 100644 --- a/extensions/Lily/TempVariables2.js +++ b/extensions/Lily/TempVariables2.js @@ -39,7 +39,6 @@ name: "Temporary Variables", color1: "#FF791A", color2: "#E15D00", - menuIconURI: menuIconURI, // I intend on making one later blocks: [ label("Thread Variables", false), @@ -80,6 +79,8 @@ opcode: "getThreadVariable", blockType: Scratch.BlockType.REPORTER, text: "thread var [VAR]", + disableMonitor: true, + allowDropAnywhere: true, arguments: { VAR: { type: Scratch.ArgumentType.STRING, @@ -104,7 +105,7 @@ { opcode: "forEachThreadVariable", blockType: Scratch.BlockType.LOOP, - text: "for each [VAR] in [NUM]", + text: "for [VAR] in [NUM]", arguments: { VAR: { type: Scratch.ArgumentType.STRING, @@ -119,7 +120,7 @@ { opcode: "listThreadVariables", blockType: Scratch.BlockType.REPORTER, - text: "list active thread variables", + text: "active thread variables", disableMonitor: true, }, @@ -165,6 +166,7 @@ blockType: Scratch.BlockType.REPORTER, text: "runtime var [VAR]", disableMonitor: true, + allowDropAnywhere: true, arguments: { VAR: { type: Scratch.ArgumentType.STRING, @@ -205,7 +207,7 @@ { opcode: "listRuntimeVariables", blockType: Scratch.BlockType.REPORTER, - text: "list active runtime variables", + text: "active runtime variables", }, ], }; From 195d3e45ee895451df8c58409ad9091cc3cf9326 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:30:00 -0800 Subject: [PATCH 086/196] -SIPC-/time: some bug fixes and new blocks (#928) Co-authored-by: Muffin --- extensions/-SIPC-/time.js | 320 +++++++++++++++++++++++++++++++++----- 1 file changed, 284 insertions(+), 36 deletions(-) diff --git a/extensions/-SIPC-/time.js b/extensions/-SIPC-/time.js index 5690f799b9..e789b517b2 100644 --- a/extensions/-SIPC-/time.js +++ b/extensions/-SIPC-/time.js @@ -1,14 +1,30 @@ // Name: Time // ID: sipctime -// Description: Blocks for interacting with unix timestamps and other date strings. +// Description: Blocks for times, dates, and time zones. // By: -SIPC- +// By: SharkPool + +// If you're curious, the default dates are from the first commits of forkphorus & TurboWarp: +// https://github.com/forkphorus/forkphorus/commit/632d3432a8a98abd627b1309f6c85f47dcc6d428 +// https://github.com/TurboWarp/scratch-vm/commit/4a93dab4fa3704ab7a1374b9794026b3330f3433 (function (Scratch) { "use strict"; - const icon = + + const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI4MS44ODU0IiBoZWlnaHQ9IjgwLjYwMzA4IiB2aWV3Qm94PSIwLDAsODEuODg1NCw4MC42MDMwOCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE5OS4wNTczLC0xMzkuNjk4NDYpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTI4MC45NDI3LDE4MGMwLDIyLjI1NzkyIC0xOC4zMzA2Nyw0MC4zMDE1NCAtNDAuOTQyNyw0MC4zMDE1NGMtMjIuNjEyMDMsMCAtNDAuOTQyNywtMTguMDQzNjEgLTQwLjk0MjcsLTQwLjMwMTU0YzAsLTIyLjI1NzkyIDE4LjMzMDY3LC00MC4zMDE1NCA0MC45NDI3LC00MC4zMDE1NGMyMi42MTIwMywwIDQwLjk0MjcsMTguMDQzNjEgNDAuOTQyNyw0MC4zMDE1NHoiIGZpbGw9IiNmZjgwMDAiIHN0cm9rZS13aWR0aD0iMCIvPjxwYXRoIGQ9Ik0yNjYuNTM0MzcsMTgwYzAsMTQuNjQxMjkgLTExLjg5MzA4LDI2LjUzNDM3IC0yNi41MzQzNywyNi41MzQzN2MtMTQuNjQxMjksMCAtMjYuNTM0MzcsLTExLjg5MzA4IC0yNi41MzQzNywtMjYuNTM0MzdjMCwtMTQuNjQxMjkgMTEuODkzMDgsLTI2LjUzNDM3IDI2LjUzNDM3LC0yNi41MzQzN2MxNC42NDEyOSwwIDI2LjUzNDM3LDExLjg5MzA4IDI2LjUzNDM3LDI2LjUzNDM3ek0yNTMuMjE5OCwxODUuOTcwMjNsLTExLjMyNDQ5LC02LjUzODgzdi0xNC41OTM5aC0zLjc5MDYydjE3LjA1NzgxaDAuNTIxMjFsMTIuNjk4NTksNy4zNDQzM3oiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjQwLjk0MjY5NjA1MzgwMTE0OjQwLjMwMTUzNTI2NTQ4NjcwNi0tPg=="; - const icon2 = + + const blockIconURI = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxNzUiIGhlaWdodD0iMTc1IiB2aWV3Qm94PSIwLDAsMTc1LDE3NSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE1Mi41LC05Mi41KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjZmZmZmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTMyNy41LDE4MGMwLDQ4LjI4MTI1IC0zOS4yMTg3NSw4Ny41IC04Ny41LDg3LjVjLTQ4LjI4MTI1LDAgLTg3LjUsLTM5LjIxODc1IC04Ny41LC04Ny41YzAsLTQ4LjI4MTI1IDM5LjIxODc1LC04Ny41IDg3LjUsLTg3LjVjNDguMjgxMjUsMCA4Ny41LDM5LjIxODc1IDg3LjUsODcuNXpNMjgzLjU5Mzc1LDE5OS42ODc1bC0zNy4zNDM3NSwtMjEuNTYyNXYtNDguMTI1aC0xMi41djU2LjI1aDEuNzE4NzVsNDEuODc1LDI0LjIxODc1eiIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjg3LjU6ODcuNS0tPg=="; + + const parseDate = (str) => { + // TODO: support standalone times here, interpret as today + if (!isNaN(str)) { + return new Date(Scratch.Cast.toNumber(str)); + } + return new Date(Scratch.Cast.toString(str)); + }; + class Time { getInfo() { return { @@ -17,20 +33,18 @@ color1: "#ff8000", color2: "#804000", color3: "#804000", - menuIconURI: icon, - blockIconURI: icon2, + menuIconURI, + blockIconURI, blocks: [ { opcode: "Timestamp", blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("current timestamp"), - arguments: {}, }, { opcode: "timezone", blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("current time zone"), - arguments: {}, }, { opcode: "Timedata", @@ -39,23 +53,24 @@ arguments: { timestamp: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "1145141980000", + defaultValue: "1591657163000", }, Timedata: { type: Scratch.ArgumentType.STRING, menu: "Time", - defaultValue: "year", }, }, }, { opcode: "TimestampToTime", blockType: Scratch.BlockType.REPORTER, - text: Scratch.translate("convert [timestamp] to datetime"), + text: Scratch.translate( + "convert [timestamp] to YYYY-MM-DD HH:MM:SS" + ), arguments: { timestamp: { type: Scratch.ArgumentType.NUMBER, - defaultValue: "1145141980000", + defaultValue: "1591657163000", }, }, }, @@ -66,7 +81,77 @@ arguments: { time: { type: Scratch.ArgumentType.STRING, - defaultValue: "2006-04-16 06:59:40", + defaultValue: "2020-06-08 17:59:23", + }, + }, + }, + "---", + { + opcode: "differenceBetweenDateAndNow", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate( + "difference between [DATE] and now in [TIME_MENU]" + ), + arguments: { + DATE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "2020-06-08 17:59:23", + }, + TIME_MENU: { + type: Scratch.ArgumentType.STRING, + menu: "DurationUnit", + }, + }, + }, + { + opcode: "differenceBetweenDates", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate( + "difference between [START] and [END] in [TIME_MENU]" + ), + arguments: { + START: { + type: Scratch.ArgumentType.STRING, + defaultValue: "2019-01-04 18:41:04", + }, + END: { + type: Scratch.ArgumentType.STRING, + defaultValue: "2020-06-08 17:59:23", + }, + TIME_MENU: { + type: Scratch.ArgumentType.STRING, + menu: "DurationUnit", + }, + }, + }, + "---", + { + opcode: "formatTime", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("format [VALUE] seconds as [ROUND] time"), + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "3883.2", // no hidden meaning in this one + }, + ROUND: { + type: Scratch.ArgumentType.NUMBER, + menu: "TimeFormat", + }, + }, + }, + { + opcode: "daysInMonth", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("number of days in [MONTH] [YEAR]"), + arguments: { + MONTH: { + type: Scratch.ArgumentType.STRING, + menu: "Months", + }, + YEAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2000", }, }, }, @@ -101,18 +186,115 @@ }, ], }, + DurationUnit: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("years"), + value: "years", + }, + { + text: Scratch.translate("months"), + value: "months", + }, + { + text: Scratch.translate("days"), + value: "days", + }, + { + text: Scratch.translate("hours"), + value: "hours", + }, + { + text: Scratch.translate("minutes"), + value: "minutes", + }, + { + text: Scratch.translate("seconds"), + value: "seconds", + }, + ], + }, + TimeFormat: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("rounded"), + value: "rounded", + }, + { + text: Scratch.translate("exact"), + value: "exact", + }, + ], + }, + Months: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("January"), + value: "1", + }, + { + text: Scratch.translate("February"), + value: "2", + }, + { + text: Scratch.translate("March"), + value: "3", + }, + { + text: Scratch.translate("April"), + value: "4", + }, + { + text: Scratch.translate("May"), + value: "5", + }, + { + text: Scratch.translate("June"), + value: "6", + }, + { + text: Scratch.translate("July"), + value: "7", + }, + { + text: Scratch.translate("August"), + value: "8", + }, + { + text: Scratch.translate("September"), + value: "9", + }, + { + text: Scratch.translate("October"), + value: "10", + }, + { + text: Scratch.translate("November"), + value: "11", + }, + { + text: Scratch.translate("December"), + value: "12", + }, + ], + }, }, }; } + Timestamp() { return Date.now(); } + timezone() { return "UTC+" + new Date().getTimezoneOffset() / -60; } + Timedata(args) { - args.timestamp = args.timestamp ? args.timestamp : null; - let date1 = new Date(Scratch.Cast.toNumber(args.timestamp)); + const date1 = parseDate(args.timestamp); switch (args.Timedata) { case "year": return date1.getFullYear(); @@ -137,31 +319,97 @@ } return 0; } + TimestampToTime({ timestamp }) { - timestamp = timestamp ? timestamp : null; - let date2 = new Date(timestamp); - let Y = date2.getFullYear() + "-"; - let M = - (date2.getMonth() + 1 < 10 - ? "0" + (date2.getMonth() + 1) - : date2.getMonth() + 1) + "-"; - let D = - (date2.getDate() < 10 ? "0" + date2.getDate() : date2.getDate()) + " "; - let h = - (date2.getHours() < 10 ? "0" + date2.getHours() : date2.getHours()) + - ":"; - let m = - (date2.getMinutes() < 10 - ? "0" + date2.getMinutes() - : date2.getMinutes()) + ":"; - let s = - date2.getSeconds() < 10 ? "0" + date2.getSeconds() : date2.getSeconds(); - return Y + M + D + h + m + s; + const date = parseDate(timestamp); + const Y = date.getFullYear(); + const M = + date.getMonth() + 1 < 10 + ? "0" + (date.getMonth() + 1) + : date.getMonth() + 1; + const D = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); + const h = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(); + const m = + date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(); + const s = + date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); + return `${Y}-${M}-${D} ${h}:${m}:${s}`; } + TimeToTimestamp({ time }) { - let data3 = time; - let timestamp = Date.parse(data3); - return timestamp; + return parseDate(time).getTime(); + } + + /** + * @param {Date} startDate + * @param {Date} endDate + * @param {string} timeMenu + * @returns {number} + */ + _calculateTimeDifference(startDate, endDate, timeMenu) { + const timeDiff = endDate.getTime() - startDate.getTime(); + switch (Scratch.Cast.toString(timeMenu)) { + case "years": + return timeDiff / (1000 * 60 * 60 * 24 * 365); + case "months": + return timeDiff / (1000 * 60 * 60 * 24 * 30.436875); // average month length from https://en.wikipedia.org/wiki/Month + case "days": + return timeDiff / (1000 * 60 * 60 * 24); + case "hours": + return timeDiff / (1000 * 60 * 60); + case "minutes": + return timeDiff / (1000 * 60); + case "seconds": + return timeDiff / 1000; + default: + return 0; + } + } + + differenceBetweenDateAndNow(args) { + return this._calculateTimeDifference( + parseDate(args.DATE), + new Date(), + args.TIME_MENU + ); + } + + differenceBetweenDates(args) { + return this._calculateTimeDifference( + parseDate(args.START), + parseDate(args.END), + args.TIME_MENU + ); + } + + formatTime(args) { + const totalSeconds = Scratch.Cast.toNumber(args.VALUE); + const seconds = + args.ROUND === "rounded" + ? Math.round(totalSeconds % 60) + .toString() + .padStart(2, "0") + : (totalSeconds % 60).toFixed(3); + const minutes = Math.round((totalSeconds / 60) % 60) + .toString() + .padStart(2, "0"); + const hours = Math.round(totalSeconds / 3600) + .toString() + .padStart(2, "0"); + return `${hours}:${minutes}:${seconds}`; + } + + daysInMonth(args) { + const year = Math.round(Scratch.Cast.toNumber(args.YEAR)); + if (year <= 0) { + return 0; + } + const monthIndex = Math.round(Scratch.Cast.toNumber(args.MONTH)); + if (monthIndex < 0 || monthIndex >= 12) { + return 0; + } + const date = new Date(year, monthIndex, 0); + return date.getDate(); } } Scratch.extensions.register(new Time()); From 93fa5a3a919296d530f7e4af1d43f6050eb7cbfc Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 28 Jan 2024 00:27:39 -0600 Subject: [PATCH 087/196] TheShovel/CanvasEffects: separate x & y scale, add border, add change effect (#1262) Co-authored-by: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> --- extensions/TheShovel/CanvasEffects.js | 197 ++++++++++++++++++++------ 1 file changed, 157 insertions(+), 40 deletions(-) diff --git a/extensions/TheShovel/CanvasEffects.js b/extensions/TheShovel/CanvasEffects.js index 4bbd31ef46..ec2e736f0a 100644 --- a/extensions/TheShovel/CanvasEffects.js +++ b/extensions/TheShovel/CanvasEffects.js @@ -2,6 +2,7 @@ // ID: theshovelcanvaseffects // Description: Apply visual effects to the entire stage. // By: TheShovel +// By: SharkPool (function (Scratch) { "use strict"; @@ -13,7 +14,7 @@ const updateStyle = () => { // Gotta keep the translation to % because of the stage size, window size and so on - const transform = `rotate(${rotation}deg) scale(${scale}%) skew(${skewX}deg, ${skewY}deg) translate(${offsetX}%, ${ + const transform = `rotate(${rotation}deg) scale(${scaleX}%, ${scaleY}%) skew(${skewX}deg, ${skewY}deg) translate(${offsetX}%, ${ 0 - offsetY }%)`; if (canvas.style.transform !== transform) { @@ -27,15 +28,27 @@ if (canvas.style.filter !== filter) { canvas.style.filter = filter; } + const cssBorderRadius = borderRadius === 0 ? "" : `${borderRadius}%`; if (canvas.style.borderRadius !== cssBorderRadius) { canvas.style.borderRadius = cssBorderRadius; } + const imageRendering = resizeMode === "pixelated" ? "pixelated" : ""; if (canvas.style.imageRendering !== imageRendering) { canvas.style.imageRendering = imageRendering; } + + const border = `${borderWidth}px ${borderStyle} ${borderColor}`; + if (canvas.style.border !== border) { + canvas.style.border = border; + } + + if (canvas.style.backgroundColor !== backgroundColor) { + canvas.style.backgroundColor = backgroundColor; + } }; + // scratch-gui may reset canvas styles when resizing the window or going in/out of fullscreen new MutationObserver(updateStyle).observe(canvas, { attributeFilter: ["style"], @@ -48,7 +61,8 @@ let offsetX = 0; let skewY = 0; let skewX = 0; - let scale = 100; + let scaleX = 100; + let scaleY = 100; // Thanks SharkPool for telling me about these let transparency = 0; let sepia = 0; @@ -59,6 +73,10 @@ let brightness = 100; let invert = 0; let resizeMode = "default"; + let borderStyle = "solid"; + let borderWidth = 0; + let borderColor = "#000000"; + let backgroundColor = "transparent"; const resetStyles = () => { borderRadius = 0; @@ -67,7 +85,8 @@ offsetX = 0; skewY = 0; skewX = 0; - scale = 100; + scaleX = 100; + scaleY = 100; transparency = 0; sepia = 0; blur = 0; @@ -77,6 +96,10 @@ brightness = 100; invert = 0; resizeMode = "default"; + borderStyle = "solid"; + borderWidth = 0; + borderColor = "#000000"; + backgroundColor = "transparent"; updateStyle(); }; @@ -102,6 +125,21 @@ }, }, }, + { + opcode: "changeEffect", + blockType: Scratch.BlockType.COMMAND, + text: "change canvas [EFFECT] by [NUMBER]", + arguments: { + EFFECT: { + type: Scratch.ArgumentType.STRING, + menu: "EFFECTMENU", + }, + NUMBER: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + }, + }, { opcode: "geteffect", blockType: Scratch.BlockType.REPORTER, @@ -118,10 +156,34 @@ blockType: Scratch.BlockType.COMMAND, text: "clear canvas effects", }, + "---", + { + opcode: "setBorder", + blockType: Scratch.BlockType.COMMAND, + text: "set canvas border to [WIDTH] pixels [STYLE] with color [COLOR1] and background [COLOR2]", + arguments: { + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "borderTypes", + }, + WIDTH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + COLOR1: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#ff0000", + }, + COLOR2: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#0000ff", + }, + }, + }, { opcode: "renderscale", blockType: Scratch.BlockType.COMMAND, - text: "set canvas render size to width:[X] height:[Y]", + text: "set canvas render size to width: [X] height: [Y]", arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -148,23 +210,7 @@ menus: { EFFECTMENU: { acceptReporters: true, - items: [ - "blur", - "contrast", - "saturation", - "color shift", - "brightness", - "invert", - "sepia", - "transparency", - "scale", - "skew X", - "skew Y", - "offset X", - "offset Y", - "rotation", - "border radius", - ], + items: this._getMenuItems(false), }, RENDERMODE: { acceptReporters: true, @@ -172,29 +218,52 @@ }, EFFECTGETMENU: { acceptReporters: true, - // this contains 'resize rendering mode', EFFECTMENU does not + items: this._getMenuItems(true), + }, + borderTypes: { + acceptReporters: true, items: [ - "blur", - "contrast", - "saturation", - "color shift", - "brightness", - "invert", - "resize rendering mode", - "sepia", - "transparency", - "scale", - "skew X", - "skew Y", - "offset X", - "offset Y", - "rotation", - "border radius", + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", ], }, }, }; } + + _getMenuItems(isGetter) { + return [ + "blur", + "contrast", + "saturation", + "color shift", + "brightness", + "invert", + ...(isGetter ? ["resize rendering mode"] : []), + "sepia", + "transparency", + ...(isGetter ? [] : ["scale"]), + "scale X", + "scale Y", + "skew X", + "skew Y", + "offset X", + "offset Y", + "rotation", + "border radius", + ...(isGetter + ? ["border width", "border style", "border color", "background"] + : []), + ]; + } + geteffect({ EFFECT }) { if (EFFECT === "blur") { return blur; @@ -215,7 +284,12 @@ } else if (EFFECT === "transparency") { return transparency; } else if (EFFECT === "scale") { - return scale; + // old extension compatibility + return scaleX; + } else if (EFFECT === "scale X") { + return scaleX; + } else if (EFFECT === "scale Y") { + return scaleY; } else if (EFFECT === "skew X") { return skewX; } else if (EFFECT === "skew Y") { @@ -228,6 +302,14 @@ return rotation; } else if (EFFECT === "border radius") { return borderRadius; + } else if (EFFECT === "border width") { + return borderWidth; + } else if (EFFECT === "border style") { + return borderStyle; + } else if (EFFECT === "border color") { + return borderColor; + } else if (EFFECT === "background") { + return backgroundColor; } return ""; } @@ -250,7 +332,12 @@ } else if (EFFECT === "transparency") { transparency = NUMBER; } else if (EFFECT === "scale") { - scale = NUMBER; + scaleX = NUMBER; + scaleY = NUMBER; + } else if (EFFECT === "scale X") { + scaleX = NUMBER; + } else if (EFFECT === "scale Y") { + scaleY = NUMBER; } else if (EFFECT === "skew X") { skewX = NUMBER; } else if (EFFECT === "skew Y") { @@ -266,6 +353,23 @@ } updateStyle(); } + changeEffect(args) { + // Scale needs some special treatment to change x & y separately + if (args.EFFECT === "scale") { + scaleX = scaleX + Scratch.Cast.toNumber(args.NUMBER); + scaleY = scaleY + Scratch.Cast.toNumber(args.NUMBER); + updateStyle(); + return; + } + + // Everything else is really generic + const currentEffect = Scratch.Cast.toNumber(this.geteffect(args)); + const newValue = Scratch.Cast.toNumber(args.NUMBER) + currentEffect; + this.seteffect({ + EFFECT: args.EFFECT, + NUMBER: newValue, + }); + } cleareffects() { resetStyles(); } @@ -276,6 +380,19 @@ renderscale({ X, Y }) { Scratch.vm.renderer.resize(X, Y); } + setBorder(args) { + borderWidth = Scratch.Cast.toNumber(args.WIDTH); + borderStyle = Scratch.Cast.toString(args.STYLE).replace(/[^a-z]/gi, ""); + borderColor = Scratch.Cast.toString(args.COLOR1).replace( + /[^#0-9a-z]/gi, + "" + ); + backgroundColor = Scratch.Cast.toString(args.COLOR2).replace( + /[^#0-9a-z]/gi, + "" + ); + updateStyle(); + } } Scratch.extensions.register(new CanvasEffects()); })(Scratch); From 86d4068b5ad9aba2ba39a23b649df7dc9cd429c4 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:36:35 -0800 Subject: [PATCH 088/196] TheShovel/CustomStyles: question label color, allow more custom gradients (#1250) Co-authored-by: Muffin --- extensions/TheShovel/CustomStyles.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/extensions/TheShovel/CustomStyles.js b/extensions/TheShovel/CustomStyles.js index 2a8b8f2fdb..4def809c7d 100644 --- a/extensions/TheShovel/CustomStyles.js +++ b/extensions/TheShovel/CustomStyles.js @@ -32,6 +32,7 @@ let askInputBorderWidth = -1; let askBoxIcon = ""; let askInputText = ""; + let askQuestionText = ""; let askButtonImage = ""; let askInputBorder = ""; @@ -48,6 +49,7 @@ let askBoxBG; let askBoxButton; let askBoxInner; + let askBoxText; let askBoxBorderMain; let askBoxBorderOuter; if (typeof scaffolding !== "undefined") { @@ -63,6 +65,8 @@ askBoxBG = ".sc-question-inner"; askBoxButton = ".sc-question-submit-button"; askBoxInner = ".sc-question-input"; + askBoxText = + '[class^="question_question-container_"] input[class^="question_question-label_"]'; askBoxBorderMain = ".sc-question-input:hover"; askBoxBorderOuter = ".sc-question-input:focus"; } else { @@ -80,6 +84,8 @@ askBoxButton = 'button[class^="question_question-submit-button_"]'; askBoxInner = '[class^="question_question-container_"] input[class^="input_input-form_"]'; + askBoxText = + '[class^="question_question-container_"] div[class^="question_question-label_"]'; askBoxIcon = 'img[class^="question_question-submit-button-icon_"]'; askBoxBorderMain = '[class^="question_question-input_"] input:focus, [class^="question_question-input_"] input:hover'; @@ -177,6 +183,9 @@ if (askInputText) { css += `${askBoxInner} { color: ${askInputText} !important; }`; } + if (askQuestionText) { + css += `${askBoxText} { color: ${askQuestionText} !important; }`; + } if (askInputRoundness >= 0) { css += `${askBoxInner} { border-radius: ${askInputRoundness}px !important; }`; } @@ -222,6 +231,7 @@ askInputBorderWidth = -1; askBoxIcon = ""; askInputText = ""; + askQuestionText = ""; askButtonImage = ""; askInputBorder = ""; @@ -280,8 +290,8 @@ return; } - // Simple linear gradient - if (/^linear-gradient\(\d+deg,#?[a-z0-9]+,#?[a-z0-9]+\)$/.test(color)) { + // General gradient pattern + if (/^[a-z-]+-gradient\([a-z0-9,#%. ]+\)$/i.test(color)) { callback(color); return; } @@ -508,6 +518,7 @@ "ask prompt background", "ask prompt button background", "ask prompt input background", + "ask prompt question text", "ask prompt input text", "ask prompt input border", ], @@ -550,6 +561,7 @@ "ask prompt background", "ask prompt button background", "ask prompt input background", + "ask prompt question text", "ask prompt input text", "ask prompt input border", "monitor background border width", @@ -595,6 +607,8 @@ askButtonBackground = color; } else if (args.COLORABLE === "ask prompt input background") { askInputBackground = color; + } else if (args.COLORABLE === "ask prompt question text") { + askQuestionText = color; } else if (args.COLORABLE === "ask prompt input text") { askInputText = color; } else if (args.COLORABLE === "ask prompt input border") { @@ -719,6 +733,8 @@ return askButtonBackground; } else if (args.ITEM === "ask prompt input background") { return askInputBackground; + } else if (args.ITEM === "ask prompt question text") { + return askQuestionText; } else if (args.ITEM === "ask prompt input text") { return askInputText; } else if (args.ITEM === "ask prompt input border") { From 9e620cb25b33a76e081879926163c3b1c0b04d99 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 28 Jan 2024 06:38:56 +0000 Subject: [PATCH 089/196] Lily/McUtils: Add a special block (#1227) --- extensions/Lily/McUtils.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extensions/Lily/McUtils.js b/extensions/Lily/McUtils.js index 8cf8177d8d..2b82f84be2 100644 --- a/extensions/Lily/McUtils.js +++ b/extensions/Lily/McUtils.js @@ -66,6 +66,13 @@ }, }, }, + { + opcode: "grimaceBlock", + blockType: Scratch.BlockType.REPORTER, + text: "🎂", + extensions: ["colours_looks"], + hideFromPalette: new Date().getMonth() !== 5, + }, ], menus: { iceCreamMenu: { @@ -113,6 +120,10 @@ return args.INPUT; } } + + grimaceBlock(args, util) { + return "All good things are purple, including Scratch <3"; + } } Scratch.extensions.register(new lmsmcutils()); })(Scratch); From db49c3737379b29c4b93a9ffdffaaf6653fac5eb Mon Sep 17 00:00:00 2001 From: DNin01 <106490990+DNin01@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:49:47 -0800 Subject: [PATCH 090/196] DNin/wake-lock: Reacquire wake lock as needed (#1158) Resolves #1120 Currently, if you request a wake lock with the Wake Lock extension and then navigate away from the page, the wake lock gets released in the background because wake locks are automatically released when the document gets hidden, plus, the extension isn't aware of when this happens, so you have no way of knowing. This change makes the extension automatically request another wake lock when the document regains visibility after having been hidden. It still reports to the project that the wake lock is enabled when this happens, therefore projects will act like nothing happened but the wake lock will still be there as long as you come back. I also considered the possibility of projects enabling or disabling wake lock when the document is hidden and now handle it properly. I tested these changes in Edge to the best of my ability. --- extensions/DNin/wake-lock.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/extensions/DNin/wake-lock.js b/extensions/DNin/wake-lock.js index d3a9d4f56a..e4550a24e4 100644 --- a/extensions/DNin/wake-lock.js +++ b/extensions/DNin/wake-lock.js @@ -19,6 +19,16 @@ constructor(runtime) { this.runtime = runtime; this.runtime.on("PROJECT_STOP_ALL", this.stopAll.bind(this)); + + document.addEventListener("visibilitychange", () => { + // If enabled, reacquire wake lock when document becomes visible again + if (wakeLock !== null && document.visibilityState === "visible") { + latestEnabled = false; + this.setWakeLock({ + enabled: true, + }); + } + }); } getInfo() { @@ -77,14 +87,26 @@ // Not supported in this browser. return; } + const enable = Scratch.Cast.toBoolean(args.enabled); + if (enable && document.visibilityState === "hidden") { + // Can't request wake lock while document is hidden. + return; + } const previousEnabled = latestEnabled; - latestEnabled = Scratch.Cast.toBoolean(args.enabled); + latestEnabled = enable; if (latestEnabled && !previousEnabled) { promise = promise .then(() => navigator.wakeLock.request("screen")) .then((sentinel) => { wakeLock = sentinel; + wakeLock.addEventListener("release", () => { + if (document.visibilityState === "visible") { + // If the document is hidden, wake lock should be reacquired when it's visible again. + wakeLock = null; + latestEnabled = false; + } + }); }) .catch((error) => { console.error(error); From 1b9cbe0c502abd8fa99e550822b0717b10dc7e17 Mon Sep 17 00:00:00 2001 From: Jomar Milan Date: Sat, 27 Jan 2024 22:56:01 -0800 Subject: [PATCH 091/196] box2d: support custom stage size (#1224) This PR makes the Box2D extension create boundaries around the stage size when the stage size is (re)set to boxed, instead of using set sizes and positions for a 480 pixel by 360 pixel stage. For the floor stage type, it scales the floor width, since the floor was originally always 5000 units in width which could make stages 5000+ pixels in width not have a full-width floor. It also adds comments where the stage type is set to explain the code and numbers. I think it could be useful since it took me many moments to figure out what the magic numbers meant. **New Constants** ```patch - bodyDef.position.Set(0, 1000 / zoom); + bodyDef.position.Set(0, (stageBounds.top + 820) / zoom); ... - fixDef.shape.SetAsBox(10 / zoom, 800 / zoom); + fixDef.shape.SetAsBox(10 / zoom, (stageHeight + 820) / zoom); ``` Originally the ceiling was positioned at 1000 units, so to get 820 I subtracted 180 (default top bound) from 1000. ```patch + const floorY = (stageBounds.bottom - 100) / zoom; ``` Originally the floor boxes were positioned at `-280 / zoom`. I think this is from a y of -180 (bottom stage bound) with 100 units subtracted because the boxes have a height of 100. ```patch - fixDef.shape.SetAsBox(5000 / zoom, 100 / zoom); + fixDef.shape.SetAsBox((stageWidth + 4520) / zoom, 100 / zoom); ``` I just subtracted 480 from 5000 since the stage is 480 pixels wide by default and the boxes were originally 5000 units wide. ___ This PR resolves suggestion 1 from #236. --------- Co-authored-by: Muffin --- extensions/box2d.js | 47 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/extensions/box2d.js b/extensions/box2d.js index f488f3afc0..6ab907be4c 100644 --- a/extensions/box2d.js +++ b/extensions/box2d.js @@ -12420,28 +12420,51 @@ fixDef.shape = new b2PolygonShape(); bodyDef.angle = 0; + const { stageWidth, stageHeight } = Scratch.vm.runtime; + const stageBounds = { + left: -stageWidth / 2, + right: stageWidth / 2, + top: stageHeight / 2, + bottom: -stageHeight / 2, + }; + if (type === STAGE_TYPE_OPTIONS.BOXED) { - fixDef.shape.SetAsBox(250 / zoom, 10 / zoom); - bodyDef.position.Set(0, -190 / zoom); + // For the ceiling boxes... + // use a width equivalent to the stage width + 10, with a thickness of 10 + fixDef.shape.SetAsBox((stageWidth + 10) / zoom, 10 / zoom); + // create one such box at the bottom of the stage, accounting for thickness... + bodyDef.position.Set(0, (stageBounds.bottom - 10) / zoom); createStageBody(); - bodyDef.position.Set(0, 1000 / zoom); + // and one 820 units above the top of the stage. + bodyDef.position.Set(0, (stageBounds.top + 820) / zoom); createStageBody(); - fixDef.shape.SetAsBox(10 / zoom, 800 / zoom); - bodyDef.position.Set(-250 / zoom, 540 / zoom); + // For the left & right wall boxes... + // use a height equivalent to the stage height + 820, with a thickness of 10 + fixDef.shape.SetAsBox(10 / zoom, (stageHeight + 820) / zoom); + // create a box at the left of the stage... + bodyDef.position.Set((stageBounds.left - 10) / zoom, 0); createStageBody(); - bodyDef.position.Set(250 / zoom, 540 / zoom); + // and one at the right of the stage. + bodyDef.position.Set((stageBounds.right + 10) / zoom, 0); createStageBody(); } else if (type === STAGE_TYPE_OPTIONS.FLOOR) { - fixDef.shape.SetAsBox(5000 / zoom, 100 / zoom); - bodyDef.position.Set(0, -280 / zoom); + // All floor boxes are positioned at the bottom of the stage, accounting for + // the thickness of 100. + const floorY = (stageBounds.bottom - 100) / zoom; + + // The floor boxes have a width of the stage width + 4520 units, and a + // thickness of 100 units. + fixDef.shape.SetAsBox((stageWidth + 4520) / zoom, 100 / zoom); + // Floor boxes are created at different intervals throughout the bottom of the stage. + bodyDef.position.Set(0, floorY); createStageBody(); - bodyDef.position.Set(-10000, -280 / zoom); + bodyDef.position.Set(stageBounds.left - 5000, floorY); createStageBody(); - bodyDef.position.Set(10000, -280 / zoom); + bodyDef.position.Set(stageBounds.right + 5000, floorY); createStageBody(); - bodyDef.position.Set(-20000, -280 / zoom); + bodyDef.position.Set(stageBounds.left - 15000, floorY); createStageBody(); - bodyDef.position.Set(20000, -280 / zoom); + bodyDef.position.Set(stageBounds.right + 15000, floorY); createStageBody(); } From 9b03e28b18ea53f4b77728f1cfcde9bf92699769 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 28 Jan 2024 01:06:28 -0600 Subject: [PATCH 092/196] Update SharkPool credits (#1263) --- extensions/-SIPC-/time.js | 2 +- extensions/TheShovel/CanvasEffects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/-SIPC-/time.js b/extensions/-SIPC-/time.js index e789b517b2..481a813287 100644 --- a/extensions/-SIPC-/time.js +++ b/extensions/-SIPC-/time.js @@ -2,7 +2,7 @@ // ID: sipctime // Description: Blocks for times, dates, and time zones. // By: -SIPC- -// By: SharkPool +// By: SharkPool // If you're curious, the default dates are from the first commits of forkphorus & TurboWarp: // https://github.com/forkphorus/forkphorus/commit/632d3432a8a98abd627b1309f6c85f47dcc6d428 diff --git a/extensions/TheShovel/CanvasEffects.js b/extensions/TheShovel/CanvasEffects.js index ec2e736f0a..a1225cba81 100644 --- a/extensions/TheShovel/CanvasEffects.js +++ b/extensions/TheShovel/CanvasEffects.js @@ -2,7 +2,7 @@ // ID: theshovelcanvaseffects // Description: Apply visual effects to the entire stage. // By: TheShovel -// By: SharkPool +// By: SharkPool (function (Scratch) { "use strict"; From aeecde5fa3051a5ef471d3ed2495742e53a37adb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:19:58 +0000 Subject: [PATCH 093/196] Various: allowDropAnywhere on some ternary operators. (#1268) I've seen multiple people want this now. --- extensions/true-fantom/couplers.js | 1 + extensions/utilities.js | 1 + 2 files changed, 2 insertions(+) diff --git a/extensions/true-fantom/couplers.js b/extensions/true-fantom/couplers.js index 702bafe878..8213a08871 100644 --- a/extensions/true-fantom/couplers.js +++ b/extensions/true-fantom/couplers.js @@ -39,6 +39,7 @@ defaultValue: "banana", }, }, + allowDropAnywhere: true, }, { opcode: "boolean_block", diff --git a/extensions/utilities.js b/extensions/utilities.js index dd156b4057..58692fd039 100644 --- a/extensions/utilities.js +++ b/extensions/utilities.js @@ -150,6 +150,7 @@ defaultValue: "apple", }, }, + allowDropAnywhere: true, }, { opcode: "letters", From e746d762ce2acc571a65be75c2fe6c07fc57fc86 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Fri, 2 Feb 2024 17:16:37 -0600 Subject: [PATCH 094/196] gamepad: Make the deadzone circular and customizable, remember the previous direction (#1280) closes https://github.com/TurboWarp/extensions/issues/324 closes https://github.com/TurboWarp/extensions/issues/1011 closes https://github.com/TurboWarp/extensions/issues/1279 --- extensions/gamepad.js | 210 ++++++++++++++++++++++++++++++++---------- 1 file changed, 163 insertions(+), 47 deletions(-) diff --git a/extensions/gamepad.js b/extensions/gamepad.js index 3b2e7a8ba0..1c2d729063 100644 --- a/extensions/gamepad.js +++ b/extensions/gamepad.js @@ -8,18 +8,108 @@ (function (Scratch) { "use strict"; - const AXIS_DEADZONE = 0.1; + // For joysticks + const DEFAULT_AXIS_DEADZONE = 0.1; + let axisDeadzone = DEFAULT_AXIS_DEADZONE; + + // For triggers. Drift isn't so big of an issue with these. const BUTTON_DEADZONE = 0.05; /** - * @param {number|'any'} index 1-indexed index - * @returns {Gamepad[]} + * @typedef InternalGamepadState + * @property {string} id + * @property {Gamepad} realGamepad + * @property {number} timestamp + * @property {number[]} axisDirections + * @property {number[]} axisMagnitudes + * @property {number[]} axisValues + * @property {number[]} buttonValues + * @property {boolean[]} buttonPressed + */ + + /** @type {Array} */ + let gamepadState = []; + + const updateState = () => { + // In Firefox, the objects returned by getGamepads() change in the background, but in Chrome + // we have to call getGamepads() each frame. Easiest for us to just always call it. + // But because Firefox changes the objects in the background, we need to track old values + // ourselves. + const gamepads = navigator.getGamepads(); + + const oldState = gamepadState; + + gamepadState = gamepads.map((gamepad) => { + if (!gamepad) { + return null; + } + + /** @type {InternalGamepadState} */ + const result = { + id: gamepad.id, + realGamepad: gamepad, + timestamp: gamepad.timestamp, + axisDirections: [], + axisMagnitudes: [], + axisValues: [], + buttonValues: [], + buttonPressed: [], + }; + + const oldResult = oldState.find((i) => i !== null && i.id === gamepad.id); + + // Each pair of axes is given a circular deadzone. + for (let i = 0; i < gamepad.axes.length; i += 2) { + const x = gamepad.axes[i]; + const y = i + 1 >= gamepad.axes.length ? 0 : gamepad.axes[i + 1]; + const magnitude = Math.sqrt(x ** 2 + y ** 2); + + if (magnitude > axisDeadzone) { + let direction = (Math.atan2(y, x) * 180) / Math.PI + 90; + if (direction < 0) { + direction += 360; + } + + result.axisDirections.push(direction, direction); + result.axisMagnitudes.push(magnitude, magnitude); + result.axisValues.push(x, y); + } else { + // Set both axes to 0. Use the old direction state, if it exists, so that using the direction + // inside of something like "point in direction" won't reset when no inputs. + // If we have no information at all, default to 90 degrees, like new sprites. + const oldDirection = oldResult ? oldResult.axisDirections[i] : 90; + result.axisDirections.push(oldDirection, oldDirection); + result.axisMagnitudes.push(0, 0); + result.axisValues.push(0, 0); + } + } + + for (let i = 0; i < gamepad.buttons.length; i++) { + let value = gamepad.buttons[i].value; + if (value < BUTTON_DEADZONE) { + value = 0; + } + result.buttonValues.push(value); + result.buttonPressed.push(gamepad.buttons[i].pressed); + } + + return result; + }); + }; + + Scratch.vm.runtime.on("BEFORE_EXECUTE", () => { + updateState(); + }); + + /** + * @param {unknown} index 1-indexed index or 'any' + * @returns {InternalGamepadState[]} */ const getGamepads = (index) => { if (index === "any") { - return navigator.getGamepads().filter((i) => i); + return gamepadState.filter((i) => i); } - const gamepad = navigator.getGamepads()[index - 1]; + const gamepad = gamepadState[Scratch.Cast.toNumber(index) - 1]; if (gamepad) { return [gamepad]; } @@ -27,52 +117,55 @@ }; /** - * @param {Gamepad} gamepad - * @param {number|'any'} buttonIndex 1-indexed index + * @param {InternalGamepadState} gamepad + * @param {unknown} buttonIndex 1-indexed index or 'any' * @returns {boolean} false if button does not exist */ const isButtonPressed = (gamepad, buttonIndex) => { if (buttonIndex === "any") { - return gamepad.buttons.some((i) => i.pressed); - } - const button = gamepad.buttons[buttonIndex - 1]; - if (!button) { - return false; + return gamepad.buttonPressed.some((i) => i); } - return button.pressed; + return !!gamepad.buttonPressed[Scratch.Cast.toNumber(buttonIndex) - 1]; }; /** - * @param {Gamepad} gamepad - * @param {number} buttonIndex 1-indexed index + * @param {InternalGamepadState} gamepad + * @param {unknown} buttonIndex 1-indexed index * @returns {number} 0 if button does not exist */ const getButtonValue = (gamepad, buttonIndex) => { - const button = gamepad.buttons[buttonIndex - 1]; - if (!button) { - return 0; - } - const value = button.value; - if (value < BUTTON_DEADZONE) { - return 0; - } - return value; + const value = gamepad.buttonValues[Scratch.Cast.toNumber(buttonIndex) - 1]; + return value || 0; }; /** - * @param {Gamepad} gamepad - * @param {number} axisIndex 1-indexed index + * @param {InternalGamepadState} gamepad + * @param {unknown} axisIndex 1-indexed index * @returns {number} 0 if axis does not exist */ const getAxisValue = (gamepad, axisIndex) => { - const axisValue = gamepad.axes[axisIndex - 1]; - if (typeof axisValue !== "number") { - return 0; - } - if (Math.abs(axisValue) < AXIS_DEADZONE) { - return 0; - } - return axisValue; + const axisValue = gamepad.axisValues[Scratch.Cast.toNumber(axisIndex) - 1]; + return axisValue || 0; + }; + + /** + * @param {InternalGamepadState} gamepad + * @param {unknown} startIndex + */ + const getAxisPairMagnitude = (gamepad, startIndex) => { + const magnitude = + gamepad.axisMagnitudes[Scratch.Cast.toNumber(startIndex) - 1]; + return magnitude || 0; + }; + + /** + * @param {InternalGamepadState} gamepad + * @param {unknown} startIndex + */ + const getAxisPairDirection = (gamepad, startIndex) => { + const direction = + gamepad.axisDirections[Scratch.Cast.toNumber(startIndex) - 1]; + return direction || 0; }; class GamepadExtension { @@ -251,6 +344,20 @@ }, }, }, + + "---", + + { + opcode: "setAxisDeadzone", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set axis deadzone to [DEADZONE]"), + arguments: { + DEADZONE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: DEFAULT_AXIS_DEADZONE.toString(), + }, + }, + }, ], menus: { padMenu: { @@ -441,20 +548,24 @@ axisDirection({ axis, pad }) { let greatestMagnitude = 0; + // by default sprites have direction 90 degrees, so that's a reasonable default let direction = 90; - for (const gamepad of getGamepads(pad)) { - const horizontalAxis = getAxisValue(gamepad, axis); - const verticalAxis = getAxisValue(gamepad, +axis + 1); - const magnitude = Math.sqrt(horizontalAxis ** 2 + verticalAxis ** 2); + + const gamepads = getGamepads(pad); + for (const gamepad of gamepads) { + const magnitude = getAxisPairMagnitude(gamepad, axis); if (magnitude > greatestMagnitude) { - greatestMagnitude = magnitude; - direction = - (Math.atan2(verticalAxis, horizontalAxis) * 180) / Math.PI + 90; - if (direction < 0) { - direction += 360; - } + direction = getAxisPairDirection(gamepad, axis); } } + + // if no sticks are far enough out, instead we'll return the last direction + // of the most recently modified gamepad + if (greatestMagnitude === 0 && gamepads.length > 0) { + gamepads.sort((a, b) => b.timestamp - a.timestamp); + direction = getAxisPairDirection(gamepads[0], axis); + } + return direction; } @@ -473,11 +584,11 @@ rumble({ s, w, t, i }) { const gamepads = getGamepads(i); - for (const gamepad of gamepads) { + for (const { realGamepad } of gamepads) { // @ts-ignore - if (gamepad.vibrationActuator) { + if (realGamepad.vibrationActuator) { // @ts-ignore - gamepad.vibrationActuator.playEffect("dual-rumble", { + realGamepad.vibrationActuator.playEffect("dual-rumble", { startDelay: 0, duration: t * 1000, weakMagnitude: w, @@ -486,6 +597,11 @@ } } } + + setAxisDeadzone({ DEADZONE }) { + axisDeadzone = Scratch.Cast.toNumber(DEADZONE); + updateState(); + } } Scratch.extensions.register(new GamepadExtension()); From 6f09cb151dc69bec6eb878e79f9642f3da2d169c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=28=20=CD=A1=C2=B0=20=CD=9C=CA=96=20=CD=A1=C2=B0=29=20Fs?= =?UTF-8?q?=20unlimited?= Date: Fri, 2 Feb 2024 18:22:00 -0500 Subject: [PATCH 095/196] qxsck/var-and-list: add color (#1271) Co-authored-by: Muffin --- extensions/qxsck/var-and-list.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/extensions/qxsck/var-and-list.js b/extensions/qxsck/var-and-list.js index 68779b7866..bbd42e72cf 100644 --- a/extensions/qxsck/var-and-list.js +++ b/extensions/qxsck/var-and-list.js @@ -10,6 +10,8 @@ return { id: "qxsckvarandlist", name: Scratch.translate({ id: "name", default: "Variable and list" }), + color1: "#FF661A", + color2: "#EE6521", blocks: [ { opcode: "getVar", @@ -24,6 +26,7 @@ defaultValue: "variable", }, }, + extensions: ["colours_data"], }, { opcode: "seriVarsToJson", @@ -38,6 +41,7 @@ defaultValue: "variable", }, }, + extensions: ["colours_data"], }, { opcode: "setVar", @@ -56,6 +60,7 @@ defaultValue: "value", }, }, + extensions: ["colours_data"], }, { opcode: "getList", @@ -70,6 +75,7 @@ defaultValue: "list", }, }, + extensions: ["colours_data_lists"], }, { opcode: "getValueOfList", @@ -88,6 +94,7 @@ defaultValue: "1", }, }, + extensions: ["colours_data_lists"], }, { opcode: "seriListsToJson", @@ -102,6 +109,7 @@ defaultValue: "list", }, }, + extensions: ["colours_data_lists"], }, { opcode: "clearList", @@ -116,6 +124,7 @@ defaultValue: "list", }, }, + extensions: ["colours_data_lists"], }, { opcode: "deleteOfList", @@ -134,6 +143,7 @@ defaultValue: "1", }, }, + extensions: ["colours_data_lists"], }, { opcode: "addValueInList", @@ -152,6 +162,7 @@ defaultValue: "value", }, }, + extensions: ["colours_data_lists"], }, { opcode: "replaceOfList", @@ -174,6 +185,7 @@ defaultValue: "thing", }, }, + extensions: ["colours_data_lists"], }, { opcode: "getIndexOfList", @@ -192,6 +204,7 @@ defaultValue: "thing", }, }, + extensions: ["colours_data_lists"], }, { opcode: "getIndexesOfList", @@ -210,6 +223,7 @@ defaultValue: "thing", }, }, + extensions: ["colours_data_lists"], }, { opcode: "length", @@ -224,6 +238,7 @@ defaultValue: "list", }, }, + extensions: ["colours_data_lists"], }, { opcode: "listContains", @@ -242,6 +257,7 @@ defaultValue: "thing", }, }, + extensions: ["colours_data_lists"], }, { opcode: "copyList", @@ -260,6 +276,7 @@ defaultValue: "list2", }, }, + extensions: ["colours_data_lists"], }, ], }; From 4f5fa45407486c27414e2c7eecb1bd3d5fbee41b Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sun, 4 Feb 2024 16:34:37 -0600 Subject: [PATCH 096/196] Update translations (#1282) --- translations/extension-metadata.json | 169 +++++- translations/extension-runtime.json | 766 ++++++++++++++++++++++++++- 2 files changed, 918 insertions(+), 17 deletions(-) diff --git a/translations/extension-metadata.json b/translations/extension-metadata.json index 0d39efe072..53a9ad9d0e 100644 --- a/translations/extension-metadata.json +++ b/translations/extension-metadata.json @@ -8,7 +8,7 @@ "de": { "-SIPC-/consoles@description": "Blöcke, die mit der in die Entwicklertools deines Browsers integrierten JavaScript-Konsole interagieren.", "-SIPC-/consoles@name": "Konsolen", - "-SIPC-/time@description": "Blöcke fürs Interagieren mit Unix-Zeitstempeln und andere Datenstrings.", + "-SIPC-/time@description": "Blöcke für Zeiten, Datum und Zeitzonen.", "-SIPC-/time@name": "Zeit", "0832/rxFS2@description": "Blöcke für das Interagieren mit einem virtuellen im Arbeitsspeicher befindlichem Dateisystem.", "Alestore/nfcwarp@description": "Erlaubt es, Daten von NFC-Geräten (NDEF) zu erhalten. Funktioniert nur mit Chrome auf Android.", @@ -25,6 +25,7 @@ "JeremyGamer13/tween@description": "Methoden für sanftere Animationen", "Lily/AllMenus@description": "Besondere Kategorie mit jedem Eingabefeld von jeder Scratchkategorie und -erweiterung.", "Lily/AllMenus@name": "Alle Menüs", + "Lily/Assets@name": "Ressourcenverwaltung", "Lily/Cast@description": "Verändere die Typen von Wertern.", "Lily/Cast@name": "Typen", "Lily/ClonesPlus@description": "Erweitert die Klonfunktionen von Scratch.", @@ -170,7 +171,7 @@ "it": { "-SIPC-/consoles@description": "Blocchi che interagiscono con la console Javascript del browser.", "-SIPC-/consoles@name": "Console Javascript", - "-SIPC-/time@description": "Blocchi per interagire con i timestamp Unix e altre stringhe rappresentanti ora e data.", + "-SIPC-/time@description": "Blocchi per ora, data e fuso orario", "-SIPC-/time@name": "Unix Time", "0832/rxFS2@description": "Blocchi per gestire un filesystem vituale in memoria.", "Alestore/nfcwarp@description": "Permette di leggere i dati da dispositivi NFC (NDEF). Funziona solo in Chrome e Android.", @@ -189,6 +190,8 @@ "JeremyGamer13/tween@name": "Animazioni Interpolate", "Lily/AllMenus@description": "Categoria speciale che contiene tutti i menu di tutte le categorie e tutte le estensioni di Scratch.", "Lily/AllMenus@name": "Tutti i Menu", + "Lily/Assets@description": "Aggiunge, rimuove e fornisce informazioni sui diversi tipi di risorse", + "Lily/Assets@name": "Gestore Risorse", "Lily/Cast@description": "Converte i valori da un tipo all'altro.", "Lily/Cast@name": "Conversione", "Lily/ClonesPlus@description": "Estensione delle possibilità dei cloni di Scratch.", @@ -350,9 +353,163 @@ "lt": { "runtime-options@name": "Paleidimo laiko parinktys" }, + "nb": { + "-SIPC-/consoles@description": "Blokkeringer som samhandler med JavaScript-konsollen som er innebygd i nettleserens utviklerverktøy.", + "-SIPC-/consoles@name": "Konsoller", + "-SIPC-/time@description": "Blokker for tider, datoer og tidssoner.", + "-SIPC-/time@name": "Tid", + "0832/rxFS2@description": "Blokker for samhandling med et virtuelt filsystem i minnet.", + "Alestore/nfcwarp@description": "Tillater lesing av data fra NFC-enheter (NDEF). Fungerer bare i Chrome på Android.", + "Alestore/nfcwarp@name": "NFCFordeling", + "CST1229/zip@description": "Opprett og rediger .zip-formatfiler, inkludert .sb3-filer.", + "Clay/htmlEncode@description": "Escape uanstolpen tekst for å trygt inkludere i HTML.", + "CubesterYT/TurboHook@description": "Lar deg bruke webhooks.", + "CubesterYT/WindowControls@description": "Flytt, endre størrelse, gi nytt navn til vinduet, gå i fullskjerm, få skjermstørrelse og mer.", + "CubesterYT/WindowControls@name": "Vinduskontroller", + "DNin/wake-lock@description": "Forhindre at datamaskinen sovner.", + "DNin/wake-lock@name": "Vekkelås", + "DT/cameracontrols@description": "Flytt den synlige delen av scenen.", + "DT/cameracontrols@name": "Kamerakontroller (Veldig Ustabil)", + "JeremyGamer13/tween@description": "Easing metoder for jevne animasjoner.", + "Lily/AllMenus@description": "Spesiell kategori med alle menyer fra hver Scratch-kategori og utvidelser.", + "Lily/AllMenus@name": "Alle Menyer", + "Lily/Assets@description": "Legg til, fjern og få data fra ulike typer eiendeler.", + "Lily/Assets@name": "Eiendelsforvalter", + "Lily/Cast@description": "Konverter verdier mellom typer.", + "Lily/Cast@name": "Kast", + "Lily/ClonesPlus@description": "Utvidelse av Scratchs klonfunksjoner.", + "Lily/ClonesPlus@name": "Kloner Pluss", + "Lily/CommentBlocks@description": "Annoter skriptene dine.", + "Lily/CommentBlocks@name": "Kommentarblokker", + "Lily/HackedBlocks@description": "Forskjellige \"hackede blokker\" som fungerer i Scratch, men som ikke er synlige i paletten.", + "Lily/HackedBlocks@name": "Skjult Blokksamling", + "Lily/LooksPlus@description": "Utvider kategorien for utseende, slik at du kan vise/skjule, få kostymedata og redigere SVG-skinn på figurer.", + "Lily/LooksPlus@name": "Seer Pluss", + "Lily/McUtils@description": "Hjelpsomme verktøy for enhver ansatt i hurtigmat.", + "Lily/MoreEvents@description": "Start dine skript på nye måter.", + "Lily/MoreEvents@name": "Meir Hendelser", + "Lily/MoreTimers@description": "Kontroller flere tidtakere samtidig.", + "Lily/MoreTimers@name": "Flere tidtakere", + "Lily/Skins@description": "La dine sprites vises som andre bilder eller kostymer.", + "Lily/Skins@name": "Skinner", + "Lily/SoundExpanded@description": "Legger til flere lydrelaterte blokker.", + "Lily/SoundExpanded@name": "Lyd Utvidet", + "Lily/TempVariables2@description": "Opprett midlertidige kjøretids- eller tråd variabler.", + "Lily/TempVariables2@name": "Midlertidige variabler", + "Lily/Video@description": "Spill videoer fra URL-er.", + "Lily/lmsutils@description": "Tidligere kalt LMS-verktøy.", + "Lily/lmsutils@name": "Lilys Verktøykasse", + "Longboost/color_channels@description": "Bare gjengi eller stemple visse RGB-kanaler.", + "Longboost/color_channels@name": "RGB-kanaler", + "NOname-awa/graphics2d@description": "Blokker for å beregne lengder, vinkler og områder i to dimensjoner.", + "NOname-awa/graphics2d@name": "Grafikk 2D", + "NOname-awa/more-comparisons@description": "Flere sammenligningsblokker.", + "NOname-awa/more-comparisons@name": "Flere sammenligninger", + "NexusKitten/controlcontrols@description": "Vis og skjul prosjektets kontroller.", + "NexusKitten/controlcontrols@name": "Kontroll Kontroller", + "NexusKitten/moremotion@description": "Flere bevegelsesrelaterte blokker.", + "NexusKitten/moremotion@name": "Mer Bevegelse", + "NexusKitten/sgrab@description": "Få informasjon om Scratch-prosjekter og Scratch-brukere.", + "NexusKitten/sgrab@name": "S-Gripe", + "Skyhigh173/bigint@description": "Matematiske blokker som fungerer med uendelig store heltall (ingen desimaler).", + "Skyhigh173/json@description": "Behandle JSON-strenger og matriser.", + "TheShovel/CanvasEffects@description": "Bruk visuelle effekter på hele scenen.", + "TheShovel/CanvasEffects@name": "Canvas effekter", + "TheShovel/ColorPicker@description": "Åpne fargesvelgeren på systemet ditt.", + "TheShovel/ColorPicker@name": "Fargevelger", + "TheShovel/CustomStyles@description": "Tilpass utseendet til variabelovervåkere og forespørsler i prosjektet ditt.", + "TheShovel/CustomStyles@name": "Tilpassede stiler", + "TheShovel/LZ-String@description": "Komprimer og dekomprimer tekst ved hjelp av lz-string.", + "TheShovel/LZ-String@name": "LZ Komprimer", + "TheShovel/ShovelUtils@description": "En haug med forskjellige blokker.", + "Xeltalliv/clippingblending@description": "Klipping utenfor et spesifisert rektangulært område og forskjellige fargeblandingsmoduser.", + "Xeltalliv/clippingblending@name": "Klipping og blanding", + "XeroName/Deltatime@description": "Presise delta timingblokker.", + "XeroName/Deltatime@name": "Deltatid", + "ZXMushroom63/searchApi@description": "Interager med URL-søkeparametere: delen av URL-en etter et spørsmålstegn.", + "ZXMushroom63/searchApi@name": "Søkeparametere", + "ar@description": "Viser bilde fra kameraet og utfører bevegelsessporing, slik at 3D-prosjekter kan riktig legge virtuelle objekter over virkeligheten.", + "ar@name": "Utvidet virkelighet", + "battery@description": "Få tilgang til informasjon om batteriet til telefoner eller bærbare datamaskiner. Kan ikke fungere på alle enheter og nettlesere.", + "battery@name": "Batteri", + "bitwise@description": "Blokker som opererer på den binære representasjonen av tall i datamaskiner.", + "bitwise@name": "Bitvis", + "box2d@description": "To-dimensjonal fysikk.", + "box2d@name": "Box2D Fysikk", + "clipboard@description": "Les og skriv fra systemutklippstavlen.", + "clipboard@name": "Utklippstavle", + "clouddata-ping@description": "Bestem om en skyvariabelserver sannsynligvis er oppe.", + "cloudlink@description": "En kraftig WebSocket-utvidelse for Scratch.", + "cs2627883/numericalencoding@description": "Kodestrengene skal kodes som tall for skyvariabler.", + "cs2627883/numericalencoding@name": "Numerisk koding", + "cursor@description": "Bruk egendefinerte markører eller skjul markøren. Tillater også å erstatte markøren med et hvilket som helst kostyme bilde.", + "cursor@name": "Mus Pekkeren", + "encoding@description": "Kod og dekod strenger til deres unicode-numre, base 64 eller URLer.", + "encoding@name": "Koding", + "fetch@description": "Gjør forespørsler til det bredere internettet.", + "fetch@name": "Hent", + "files@description": "Les og last ned filer.", + "files@name": "Filer", + "gamejolt@description": "Blokker som tillater spill å samhandle med GameJolt API. Uoffisiell.", + "gamepad@description": "Direkte tilgang til spillkontrollere i stedet for bare å kartlegge knapper til taster.", + "gamepad@name": "Spillkontroller", + "godslayerakp/http@description": "Omfattende utvidelse for samhandling med eksterne nettsteder.", + "godslayerakp/ws@description": "Manuelt koble til WebSocket-servere.", + "iframe@description": "Vis nettsider eller HTML over scenen.", + "iframe@name": "IFrame", + "itchio@description": "Blokker som samhandler med itch.io-nettstedet. Uoffisiell.", + "lab/text@description": "En enkel måte å vise og animere tekst på. Kompatibel med Scratch Lab's Animated Text eksperiment.", + "lab/text@name": "Animert Tekst", + "local-storage@description": "Lagre data varig. Som informasjonskapsler, men bedre.", + "local-storage@name": "Lokal lagring", + "mdwalters/notifications@description": "Vis varsler.", + "mdwalters/notifications@name": "Varsler", + "navigator@description": "Detaljer om brukerens nettleser og operativsystem.", + "navigator@name": "Navigatør", + "obviousAlexC/SensingPlus@description": "En utvidelse til sensorkategorien.", + "obviousAlexC/SensingPlus@name": "Sensing Pluss", + "obviousAlexC/newgroundsIO@description": "Blokker som tillater spill å samhandle med Newgrounds API. Uoffisiell.", + "obviousAlexC/penPlus@description": "Avanserte renderingfunksjoner.", + "obviousAlexC/penPlus@name": "Penn Pluss V6", + "penplus@description": "Replasert av Pen Plus V6.", + "penplus@name": "Penn Pluss V5 (Gammel)", + "pointerlock@description": "Legger til blokker for muselåsing. Mus x- og y-blokker vil rapportere endringen siden forrige bilde mens pekeren er låst. Erstatter pekerlåseeksperimentet.", + "pointerlock@name": "Prikkerlås", + "qxsck/data-analysis@description": "Blokker for å beregne gjennomsnitt, medianer, maksimum, minimum, varians og moduser.", + "qxsck/data-analysis@name": "Dataanalyse", + "qxsck/var-and-list@description": "Flere blokker relatert til variabler og lister.", + "qxsck/var-and-list@name": "Variabel og liste", + "rixxyx@description": "Forskjellige verktøyblokker.", + "runtime-options@description": "Få og endre turbo-modus, bildefrekvens, interpolasjon, klon-grense, scenestørrelse og mer.", + "runtime-options@name": "Kjøretidsalternativer", + "shreder95ua/resolution@description": "Få oppløsningen på hovedskjermen.", + "shreder95ua/resolution@name": "Skjermoppløsning", + "sound@description": "Spill lyder fra URL-er.", + "sound@name": "Lyd", + "stretch@description": "Strekke sprites horisontalt eller vertikalt.", + "stretch@name": "Strekke", + "text@description": "Manipulere tegn og tekst.", + "text@name": "Tekst", + "true-fantom/base@description": "Konverter tall mellom baser.", + "true-fantom/couplers@description": "Noen koblinger-blokker.", + "true-fantom/couplers@name": "Koblinger", + "true-fantom/math@description": "Mange operatører blokker, fra eksponentiering til trigonometriske funksjoner.", + "true-fantom/math@name": "Matte", + "true-fantom/network@description": "Forskjellige blokker for å samhandle med nettverket.", + "true-fantom/network@name": "Nettverk", + "true-fantom/regexp@description": "Fullt grensesnitt for å jobbe med regulære uttrykk.", + "utilities@description": "En haug med interessante blokker.", + "utilities@name": "Verktøy", + "veggiecan/LongmanDictionary@description": "Få definisjonene av ord fra Longman Dictionary i prosjektene dine.", + "veggiecan/LongmanDictionary@name": "Longman Ordbok", + "veggiecan/browserfullscreen@description": "Gå inn og ut av fullskjermsmodus.", + "veggiecan/browserfullscreen@name": "Nettleser Fullskjerm", + "vercte/dictionaries@description": "Bruk kraften til ordbøker i prosjektet ditt.", + "vercte/dictionaries@name": "Ordbøker" + }, "nl": { "-SIPC-/consoles@description": "Maak gebruik van de ingebouwde JavaScript-console van je browser.", - "-SIPC-/time@description": "Maak gebruik van unix-tijden en andere datum-gerelateerde strings.", + "-SIPC-/time@description": "Lees de tijd, datum en tijdzones af.", "-SIPC-/time@name": "Tijd", "0832/rxFS2@description": "Maak gebruik van een virtueel bestandssysteem in het geheugen.", "Alestore/nfcwarp@description": "Lees gegevens af van NFC (NDEF) apparaten. Werkt alleen in Chrome op Android.", @@ -370,6 +527,8 @@ "JeremyGamer13/tween@name": "Tweening", "Lily/AllMenus@description": "Gebruik alle bestaande dropdown-menu's als losse blokken, zelfs die van extensies.", "Lily/AllMenus@name": "Alle Menu's", + "Lily/Assets@description": "Beheer de gegevens van verschillende soorten onderdelen.", + "Lily/Assets@name": "Onderdelen Beheren", "Lily/Cast@description": "Zet waarden om naar verschillende waarde-types.", "Lily/Cast@name": "Omzetten", "Lily/ClonesPlus@description": "Gebruik tal van nieuwe kloon-gerelateerde functies.", @@ -503,7 +662,8 @@ "runtime-options@name": "Opções de Execução" }, "pt-br": { - "runtime-options@name": "Opções de Execução" + "runtime-options@name": "Opções de Execução", + "text@name": "Texto" }, "ru": { "NOname-awa/graphics2d@name": "Графика 2D", @@ -535,7 +695,6 @@ "zh-cn": { "-SIPC-/consoles@description": "一个能与内置于浏览器开发工具中的JavaScript控制台交互的积木块。", "-SIPC-/consoles@name": "控制台", - "-SIPC-/time@description": "处理UNIX时间戳和日期字符串", "-SIPC-/time@name": "时间", "0832/rxFS2@description": "创建并使用虚拟档案系统", "Alestore/nfcwarp@description": "允许从NFC(NDFF)硬件读取数据。仅支持Andriod设备上的Chrome浏览器。", diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 18338f6053..7d995835e9 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -85,18 +85,41 @@ "-SIPC-/consoles@_Warning": "Avviso", "-SIPC-/consoles@_Warning [string]": "Avviso [string]", "-SIPC-/consoles@_group": "gruppo", + "-SIPC-/time@_April": "Aprile", + "-SIPC-/time@_August": "Agosto", + "-SIPC-/time@_December": "Dicembre", + "-SIPC-/time@_February": "Febbraio", + "-SIPC-/time@_July": "Luglio", + "-SIPC-/time@_June": "Giugno", + "-SIPC-/time@_March": "Marzo", + "-SIPC-/time@_May": "Maggio", + "-SIPC-/time@_November": "Novembre", + "-SIPC-/time@_October": "Ottobre", + "-SIPC-/time@_September": "Settembre", "-SIPC-/time@_Time": "Unix Time", "-SIPC-/time@_convert [time] to timestamp": "convert [time] in timestamp", - "-SIPC-/time@_convert [timestamp] to datetime": "converti [timestamp] to datetime", + "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "converti [timestamp] in YYYY-MM-DD HH:MM:SS", "-SIPC-/time@_current time zone": "fuso orario attuale", "-SIPC-/time@_current timestamp": "timestamp attuale", "-SIPC-/time@_day": "giorno", + "-SIPC-/time@_days": "giorni", + "-SIPC-/time@_difference between [DATE] and now in [TIME_MENU]": "differenza tra [DATE] e adesso in [TIME_MENU]", + "-SIPC-/time@_difference between [START] and [END] in [TIME_MENU]": "differenza tra [START] e [END] in [TIME_MENU]", + "-SIPC-/time@_exact": "senza arrotondare", + "-SIPC-/time@_format [VALUE] seconds as [ROUND] time": "formatta [VALUE] secondi come ora [ROUND]", "-SIPC-/time@_get [Timedata] from [timestamp]": "estrai [Timedata] da [timestamp]", "-SIPC-/time@_hour": "ora", + "-SIPC-/time@_hours": "ore", "-SIPC-/time@_minute": "minuti", + "-SIPC-/time@_minutes": "minuti", "-SIPC-/time@_month": "mese", + "-SIPC-/time@_months": "mesi", + "-SIPC-/time@_number of days in [MONTH] [YEAR]": "numero di giorni di [MONTH] [YEAR]", + "-SIPC-/time@_rounded": "arrotondando", "-SIPC-/time@_second": "secondi", + "-SIPC-/time@_seconds": "secondi", "-SIPC-/time@_year": "anno", + "-SIPC-/time@_years": "anni", "0832/rxFS2@clean": "Svuota il file system", "0832/rxFS2@del": "Rimuovi [STR]", "0832/rxFS2@folder": "Imposta [STR] a [STR2]", @@ -287,6 +310,7 @@ "box2d@griffpatch.categoryName": "Fisica", "box2d@griffpatch.changeScroll": "cambia scorrimento di x: [ox] y: [oy]", "box2d@griffpatch.changeVelocity": "cambia velocità di vx: [sx] vy: [sy]", + "box2d@griffpatch.disablePhysics": "disattiva gestione della fisica per questo sprite", "box2d@griffpatch.doTick": "esegui un passo della simulazione", "box2d@griffpatch.getAngVelocity": "velocità angolare", "box2d@griffpatch.getDensity": "densità", @@ -297,6 +321,7 @@ "box2d@griffpatch.getScrollX": "scorrimento x", "box2d@griffpatch.getScrollY": "scorrimento y", "box2d@griffpatch.getStatic": "fisso", + "box2d@griffpatch.getTickRate": "passi simulazione", "box2d@griffpatch.getTouching": "sprite a contatto con [where]", "box2d@griffpatch.getVelocityX": "velocità x", "box2d@griffpatch.getVelocityY": "velocità y", @@ -314,6 +339,7 @@ "box2d@griffpatch.setScroll": "imposta scorrimento a x: [ox] y: [oy]", "box2d@griffpatch.setStage": "imposta i bordi dello stage a [stageType]", "box2d@griffpatch.setStatic": "usa modalità [static]", + "box2d@griffpatch.setTickRate": "imposta passi simulazione a [rate]/s", "box2d@griffpatch.setVelocity": "porta velocità a vx: [sx] vy: [sy]", "clipboard@_Clipboard": "Appunti", "clipboard@_clipboard": "appunti", @@ -349,7 +375,6 @@ "encoding@_Randomly generated [position] character string": "stringa di [position] caratteri scelti a caso", "encoding@_Use [wordbank] to generate a random [position] character string": "genera una stringa di [position] caratteri scelti a caso tra [wordbank]", "encoding@_[string] corresponding to the [CodeList] character": "carattere [CodeList] corrispondente al valore [string]", - "encoding@_apple": "mela", "files@_Accepted formats: {formats}": "Formati accettati: {formats}", "files@_Files": "File", "files@_Hello, world!": "Ciao mondo!", @@ -641,6 +666,7 @@ "runtime-options@_default ({n})": "predefinito({n})", "runtime-options@_disabled": "sblocca", "runtime-options@_enabled": "blocca", + "runtime-options@_framerate": "frequenza", "runtime-options@_framerate limit": "limite framerate", "runtime-options@_height": "altezza", "runtime-options@_high quality pen": "penna alta qualità", @@ -654,6 +680,7 @@ "runtime-options@_set stage size width: [width] height: [height]": "imposta dimensioni Stage larghezza: [width]altezza: [height]", "runtime-options@_set username to [username]": "imposta username a [username]", "runtime-options@_stage [dimension]": "[dimension] dello Stage", + "runtime-options@_stage size": "dimensioni Stage", "runtime-options@_turbo mode": "modalità turbo", "runtime-options@_width": "larghezza", "sound@_Sound": "Suoni", @@ -670,15 +697,29 @@ "stretch@_y stretch": "deformazione y" }, "ja": { + "-SIPC-/consoles@_Clear Console": "コンソールを消去", "-SIPC-/consoles@_Consoles": "コンソール", + "-SIPC-/consoles@_Create a collapsed group named [string]": "[string]という名前の折りたたみグループを作る", "-SIPC-/consoles@_Create a group named [string]": "[string]という名前のグループを作る", + "-SIPC-/consoles@_Debug": "デバッグ", + "-SIPC-/consoles@_Debug [string]": "デバッグ [string]", + "-SIPC-/consoles@_End the timer named [string] and print the time elapsed from start to end": "[string]という名前のついたタイマーを停止させ、開始時間と終了時間からかかった時間を表示する", "-SIPC-/consoles@_Error": "エラー", + "-SIPC-/consoles@_Error [string]": "エラー [string]", + "-SIPC-/consoles@_Exit the current group": "現在のグループを終了する", + "-SIPC-/consoles@_Information": "情報", + "-SIPC-/consoles@_Information [string]": "情報 [string]", + "-SIPC-/consoles@_Journal": "ジャーナル", + "-SIPC-/consoles@_Journal [string]": "ジャーナル [string]", + "-SIPC-/consoles@_Print the time run by the timer named [string]": "[string]という名前のついたタイマーのタイムを表示", "-SIPC-/consoles@_Start a timer named [string]": "[string]という名前のタイマーをスタートする", "-SIPC-/consoles@_Time": "時間", + "-SIPC-/consoles@_Warning": "警告", + "-SIPC-/consoles@_Warning [string]": "警告 [string]", "-SIPC-/consoles@_group": "グループ", "-SIPC-/time@_Time": "時間", "-SIPC-/time@_convert [time] to timestamp": "[time]をタイムスタンプに変換する", - "-SIPC-/time@_convert [timestamp] to datetime": "[timestamp]を日時に変換する", + "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "[timestamp]をYYYY-MM-DD HH:MM:SSに変換する", "-SIPC-/time@_current time zone": "現在のタイムゾーン", "-SIPC-/time@_current timestamp": "現在のタイムスタンプ", "-SIPC-/time@_day": "日", @@ -688,16 +729,54 @@ "-SIPC-/time@_month": "月", "-SIPC-/time@_second": "秒", "-SIPC-/time@_year": "年", + "0832/rxFS2@clean": "ファイルシステムを削除する", "0832/rxFS2@del": "[STR]を削除", + "0832/rxFS2@folder": "[STR]を[STR2]にセットする", + "0832/rxFS2@folder_default": "rxFSは良い!", + "0832/rxFS2@in": "[STR]からファイルシステムをインポートする", + "0832/rxFS2@list": "[STR]直下のファイルをリスト化する", "0832/rxFS2@open": "[STR]を開く", + "0832/rxFS2@out": "ファイルシステムをエクスポートする", "0832/rxFS2@search": "[STR]を検索", "0832/rxFS2@start": "[STR]を作成", + "0832/rxFS2@sync": "[STR]のロケーションを[STR2]に変更する", + "0832/rxFS2@webin": "[STR]をウェブから読み込む", + "Alestore/nfcwarp@_NFC supported?": "NFCはサポートされていますか?", + "Alestore/nfcwarp@_Only works in Chrome on Android": "Android上のChromeのみで動作", + "Alestore/nfcwarp@_read NFC tag": "NFCタグを読み取る", + "CST1229/zip@_1 (fast, large)": "1(高速、大きい)", + "CST1229/zip@_9 (slowest, smallest)": "9(低速、小さい)", + "CST1229/zip@_Hello, world?": "こんにちは、世界?", "CST1229/zip@_[META] of [FILE]": "[FILE]の[META]", + "CST1229/zip@_[OBJECT] exists?": "[OBJECT]は存在するか?", + "CST1229/zip@_any text": "何かのテキスト", + "CST1229/zip@_archive comment": "コメントをアーカイブする", + "CST1229/zip@_archive is open?": "アーカイブは開かれているか?", "CST1229/zip@_binary": "バイナリ", + "CST1229/zip@_close archive": "アーカイブを閉じる", + "CST1229/zip@_contents of directory [DIR]": "[DIR]ディレクトリの内容", + "CST1229/zip@_create directory [DIR]": "[DIR] ディレクトリを作成", + "CST1229/zip@_create empty archive": "空のアーカイブを作成", + "CST1229/zip@_current directory path": "現在のディレクトリパス", + "CST1229/zip@_data: URL": "データ: URL", "CST1229/zip@_delete [FILE]": "[FILE]を消す", + "CST1229/zip@_file [FILE] as [TYPE]": "ファイル[FILE]を[TYPE]として", + "CST1229/zip@_folder": "フォルダー", + "CST1229/zip@_go to directory [DIR]": "[DIR]ディレクトリへ行く", + "CST1229/zip@_modification date": "データ修正", "CST1229/zip@_name": "名前", + "CST1229/zip@_new file": "新しいファイル", "CST1229/zip@_new folder": "新しいフォルダ", + "CST1229/zip@_no compression (fastest)": "圧縮なし(最速)", + "CST1229/zip@_open zip from [TYPE] [DATA]": "Zipを[TYPE] [DATA]から開く", + "CST1229/zip@_output zip type [TYPE] compression level [COMPRESSION]": "出力ZIPタイプ[TYPE]の圧縮レベル[COMPRESSION]", + "CST1229/zip@_path": "パス", + "CST1229/zip@_path [PATH] from [ORIGIN]": "[ORIGIN]のパス[PATH]", + "CST1229/zip@_set [META] of [FILE] to [VALUE]": "[FILE]の[META]を[VALUE]にセットする", + "CST1229/zip@_set archive comment to [COMMENT]": "アーカイブコメントを[COMMENT]にセットする", "CST1229/zip@_string": "文字列", + "CST1229/zip@_text": "テキスト", + "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "ファイル[FILE]にコンテンツ[CONTENT]を[TYPE]型で書き込む", "Clay/htmlEncode@_HTML Encode": "HTMLエンコード", "Clay/htmlEncode@_Hello!": "こんにちは!", "CubesterYT/TurboHook@_icon": "アイコン", @@ -722,13 +801,13 @@ "cs2627883/numericalencoding@_Hello!": "こんにちは!", "cursor@_Mouse Cursor": "マウスカーソル", "encoding@_Encoding": "エンコーディング", - "encoding@_apple": "りんご", "files@_Files": "ファイル", "files@_Select or drop file": "選ぶかファイルをドロップする", "files@_open a [extension] file": "[extension]ファイルを開く", "files@_open a [extension] file as [as]": "[extension]ファイルを[as]として開く", "files@_open a file": "ファイルを開く", "files@_open a file as [as]": "[as]としてファイルを開く", + "files@_text": "テキスト", "gamejolt@_Close": "閉じる", "gamejolt@_day": "日", "gamejolt@_guest": "ゲスト", @@ -737,6 +816,7 @@ "gamejolt@_month": "月", "gamejolt@_name": "名前", "gamejolt@_second": "秒", + "gamejolt@_text": "テキスト", "gamejolt@_username": "ユーザー名", "gamejolt@_year": "年", "iframe@_Iframe": "埋め込み", @@ -774,6 +854,7 @@ "runtime-options@_set username to [username]": "ユーザー名を[username]にする", "runtime-options@_stage [dimension]": "ステージの[dimension]", "runtime-options@_turbo mode": "ターボモード", + "runtime-options@_username": "ユーザー名", "runtime-options@_width": "横幅" }, "ja-hira": { @@ -793,6 +874,641 @@ "gamejolt@_Close": "Uždaryti", "runtime-options@_Runtime Options": "Paleidimo laiko parinktys" }, + "nb": { + "-SIPC-/consoles@_Clear Console": "Tøm Konsoll", + "-SIPC-/consoles@_Consoles": "Konsoller", + "-SIPC-/consoles@_Create a collapsed group named [string]": "Opprett en sammenfoldet gruppe med navnet [string]", + "-SIPC-/consoles@_Create a group named [string]": "Opprett en gruppe med navnet [string]", + "-SIPC-/consoles@_End the timer named [string] and print the time elapsed from start to end": "Avslutt tidtakeren med navnet [string] og skriv ut tiden som har gått fra start til slutt.", + "-SIPC-/consoles@_Error": "Feil", + "-SIPC-/consoles@_Error [string]": "Feil [string]", + "-SIPC-/consoles@_Exit the current group": "Avslutt gjeldende gruppe", + "-SIPC-/consoles@_Information": "Informasjon", + "-SIPC-/consoles@_Information [string]": "Informasjon [string]", + "-SIPC-/consoles@_Print the time run by the timer named [string]": "Skriv ut tiden som kjøres av timeren med navnet [string]", + "-SIPC-/consoles@_Start a timer named [string]": "Start en timer med navnet [string]", + "-SIPC-/consoles@_Time": "Tid", + "-SIPC-/consoles@_Warning": "Advarsel", + "-SIPC-/consoles@_Warning [string]": "Advarsel [string]", + "-SIPC-/consoles@_group": "gruppe", + "-SIPC-/time@_April": "april", + "-SIPC-/time@_August": "august", + "-SIPC-/time@_December": "Desember", + "-SIPC-/time@_February": "februar", + "-SIPC-/time@_January": "januar", + "-SIPC-/time@_July": "Juli", + "-SIPC-/time@_June": "Juni", + "-SIPC-/time@_March": "Mars", + "-SIPC-/time@_May": "Mai", + "-SIPC-/time@_October": "Oktober", + "-SIPC-/time@_September": "september", + "-SIPC-/time@_Time": "Tid", + "-SIPC-/time@_convert [time] to timestamp": "konverter [time] til tidsstempel", + "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "konverter [timestamp] til YYYY-MM-DD HH:MM:SS", + "-SIPC-/time@_current time zone": "nåværende tidssone", + "-SIPC-/time@_current timestamp": "nåværende tidsstempel", + "-SIPC-/time@_day": "dag", + "-SIPC-/time@_days": "dager", + "-SIPC-/time@_difference between [DATE] and now in [TIME_MENU]": "forskjellen mellom [DATE] og nå i [TIME_MENU]", + "-SIPC-/time@_difference between [START] and [END] in [TIME_MENU]": "forskjellen mellom [START] og [END] i [TIME_MENU]", + "-SIPC-/time@_exact": "nøyaktig", + "-SIPC-/time@_format [VALUE] seconds as [ROUND] time": "format [VALUE] sekunder som [ROUND] tid", + "-SIPC-/time@_get [Timedata] from [timestamp]": "hent [Timedata] fra [timestamp]", + "-SIPC-/time@_hour": "time", + "-SIPC-/time@_hours": "timer", + "-SIPC-/time@_minute": "minutt", + "-SIPC-/time@_minutes": "minutter", + "-SIPC-/time@_month": "måned", + "-SIPC-/time@_months": "måneder", + "-SIPC-/time@_number of days in [MONTH] [YEAR]": "antall dager i [MONTH] [YEAR]", + "-SIPC-/time@_rounded": "avrundet", + "-SIPC-/time@_second": "sekund", + "-SIPC-/time@_seconds": "sekunder", + "-SIPC-/time@_year": "år", + "-SIPC-/time@_years": "år", + "0832/rxFS2@clean": "Tøm filsystemet", + "0832/rxFS2@del": "Slett [STR]", + "0832/rxFS2@folder": "Sett [STR] til [STR2]", + "0832/rxFS2@folder_default": "rxFS er bra!", + "0832/rxFS2@in": "Import filsystem fra [STR]", + "0832/rxFS2@list": "List alle filer under [STR]", + "0832/rxFS2@open": "Åpne [STR]", + "0832/rxFS2@out": "Eksporter filsystemet", + "0832/rxFS2@search": "Søk [STR]", + "0832/rxFS2@start": "Opprett [STR]", + "0832/rxFS2@sync": "Endre plasseringen av [STR] til [STR2]", + "0832/rxFS2@webin": "Last [STR] fra nettet", + "Alestore/nfcwarp@_NFC supported?": "NFC støttet?", + "Alestore/nfcwarp@_NFCWarp": "NFCFordeling", + "Alestore/nfcwarp@_Only works in Chrome on Android": "Bare fungerer i Chrome på Android", + "Alestore/nfcwarp@_read NFC tag": "les NFC-taggen", + "CST1229/zip@_1 (fast, large)": "1 (rask, stor)", + "CST1229/zip@_9 (slowest, smallest)": "9 (tregeste, minste)", + "CST1229/zip@_Hello, world?": "Hei, verden?", + "CST1229/zip@_[META] of [FILE]": "[META] av [FILE]", + "CST1229/zip@_[OBJECT] exists?": "[OBJECT] eksisterer?", + "CST1229/zip@_any text": "all tekst", + "CST1229/zip@_archive comment": "arkiver kommentar", + "CST1229/zip@_archive is open?": "arkivet er åpent?", + "CST1229/zip@_binary": "binær", + "CST1229/zip@_close archive": "lukk arkiv", + "CST1229/zip@_comment": "kommentar", + "CST1229/zip@_contents of directory [DIR]": "innholdet i katalogen [DIR]", + "CST1229/zip@_create directory [DIR]": "opprett katalog [DIR]", + "CST1229/zip@_create empty archive": "opprett tom arkiv", + "CST1229/zip@_current directory path": "gjeldende katalogbane", + "CST1229/zip@_delete [FILE]": "slett [FILE]", + "CST1229/zip@_file [FILE] as [TYPE]": "fil [FILE] som [TYPE]", + "CST1229/zip@_folder": "mappe", + "CST1229/zip@_go to directory [DIR]": "gå til katalogen [DIR]", + "CST1229/zip@_long modification date": "lang modifikasjonsdato", + "CST1229/zip@_modification date": "modifikasjonsdato", + "CST1229/zip@_modified days since 2000": "modifiserte dager siden 2000", + "CST1229/zip@_name": "navn", + "CST1229/zip@_new file": "ny fil", + "CST1229/zip@_new folder": "ny mappe", + "CST1229/zip@_no compression (fastest)": "ingen komprimering (raskest)", + "CST1229/zip@_open zip from [TYPE] [DATA]": "åpne zip fra [TYPE] [DATA]", + "CST1229/zip@_path": "sti", + "CST1229/zip@_path [PATH] from [ORIGIN]": "stien [PATH] fra [ORIGIN]", + "CST1229/zip@_set [META] of [FILE] to [VALUE]": "sett [META] av [FILE] til [VALUE]", + "CST1229/zip@_set archive comment to [COMMENT]": "sett arkivkommentar til [COMMENT]", + "CST1229/zip@_text": "tekst", + "CST1229/zip@_unix modified timestamp": "unix endret tidsstempel", + "CST1229/zip@_write file [FILE] content [CONTENT] type [TYPE]": "skriv fil [FILE] innhold [CONTENT] type [TYPE]", + "Clay/htmlEncode@_Hello!": "Hei!", + "Clay/htmlEncode@_encode [text] as HTML-safe": "enkoder [text] som HTML-sikker", + "CubesterYT/TurboHook@_content": "innhold", + "CubesterYT/TurboHook@_icon": "ikon", + "CubesterYT/TurboHook@_name": "navn", + "CubesterYT/WindowControls@_Hello World!": "Hei verden!", + "CubesterYT/WindowControls@_May not work in normal browser tabs": "Kan ikke fungere i vanlige nettlesertabeller", + "CubesterYT/WindowControls@_Refer to Documentation for details": "Se Dokumentasjonen for detaljer", + "CubesterYT/WindowControls@_Window Controls": "Vinduskontroller", + "CubesterYT/WindowControls@_bottom": "bunn", + "CubesterYT/WindowControls@_bottom left": "nederst til venstre", + "CubesterYT/WindowControls@_bottom right": "nederst til høyre", + "CubesterYT/WindowControls@_center": "senter", + "CubesterYT/WindowControls@_change window height by [H]": "endre vindushøyden med [H]", + "CubesterYT/WindowControls@_change window width by [W]": "endre vindusbredde med [W]", + "CubesterYT/WindowControls@_change window x by [X]": "endre vinduet x med [X]", + "CubesterYT/WindowControls@_change window y by [Y]": "endre vinduet y med [Y]", + "CubesterYT/WindowControls@_close window": "Lukk vindu", + "CubesterYT/WindowControls@_enter fullscreen": "gå til fullskjerm", + "CubesterYT/WindowControls@_exit fullscreen": "avslutt fullskjerm", + "CubesterYT/WindowControls@_is window focused?": "er vinduet fokusert?", + "CubesterYT/WindowControls@_is window fullscreen?": "er vinduet i fullskjerm?", + "CubesterYT/WindowControls@_is window touching screen edge?": "er vinduet i kontakt med skjermkanten?", + "CubesterYT/WindowControls@_left": "venstre", + "CubesterYT/WindowControls@_match stage size": "kamp scenestørrelse", + "CubesterYT/WindowControls@_move window to the [PRESETS]": "flytt vinduet til [PRESETS]", + "CubesterYT/WindowControls@_move window to x: [X] y: [Y]": "flytt vinduet til x: [X] y: [Y]", + "CubesterYT/WindowControls@_random position": "tilfeldig posisjon", + "CubesterYT/WindowControls@_resize window to [PRESETS]": "endre vinduet til [PRESETS]", + "CubesterYT/WindowControls@_resize window to width: [W] height: [H]": "endre vinduet til bredde: [W] høyde: [H]", + "CubesterYT/WindowControls@_right": "høyre", + "CubesterYT/WindowControls@_screen height": "skjerm høyde", + "CubesterYT/WindowControls@_screen width": "skjerm bredde", + "CubesterYT/WindowControls@_set window height to [H]": "sett vindushøyden til [H]", + "CubesterYT/WindowControls@_set window title to [TITLE]": "sett vindustittel til [TITLE]", + "CubesterYT/WindowControls@_set window width to [W]": "sett vindusbredde til [W]", + "CubesterYT/WindowControls@_set window x to [X]": "sett vindu x til [X]", + "CubesterYT/WindowControls@_set window y to [Y]": "sett vindu y til [Y]", + "CubesterYT/WindowControls@_top": "topp", + "CubesterYT/WindowControls@_top left": "øverst til venstre", + "CubesterYT/WindowControls@_top right": "øverst til høyre", + "CubesterYT/WindowControls@_window height": "vindushøyde", + "CubesterYT/WindowControls@_window title": "vindustittel", + "CubesterYT/WindowControls@_window width": "vindusbredde", + "CubesterYT/WindowControls@_window x": "vindu x", + "CubesterYT/WindowControls@_window y": "vindu y", + "CubesterYT/WindowControls@editorConfirmation": "Er du sikker på at du vil lukke dette vinduet?\n\n(Denne meldingen vil ikke vises når prosjektet er pakket)", + "DNin/wake-lock@_Wake Lock": "Vekkelås", + "DNin/wake-lock@_is wake lock active?": "er vekkelåsen aktiv?", + "DNin/wake-lock@_off": "av", + "DNin/wake-lock@_on": "på", + "DNin/wake-lock@_turn wake lock [enabled]": "slå på vekkelås [enabled]", + "DT/cameracontrols@_Camera (Very Buggy)": "Kamera (Veldig Ustabil)", + "DT/cameracontrols@_background color": "Bakgrunnsfarge", + "DT/cameracontrols@_camera direction": "kamera retning", + "DT/cameracontrols@_camera x": "kamera x", + "DT/cameracontrols@_camera y": "kamera y", + "DT/cameracontrols@_camera zoom": "kamera zoom", + "DT/cameracontrols@_change camera x by [val]": "endre kamera x med [val]", + "DT/cameracontrols@_change camera y by [val]": "endre kamera y med [val]", + "DT/cameracontrols@_change camera zoom by [val]": "endre kamera zoom med [val]", + "DT/cameracontrols@_move camera [val] steps": "flytt kamera [val] trinn", + "DT/cameracontrols@_move camera to [sprite]": "flytt kamera til [sprite]", + "DT/cameracontrols@_no sprites exist": "ingen sprites eksisterer", + "DT/cameracontrols@_point camera towards [sprite]": "peker kameraet mot [sprite]", + "DT/cameracontrols@_set background color to [val]": "sett bakgrunnsfarge til [val]", + "DT/cameracontrols@_set camera direction to [val]": "sett kamera retning til [val]", + "DT/cameracontrols@_set camera to x: [x] y: [y]": "sett kamera til x: [x] y: [y]", + "DT/cameracontrols@_set camera x to [val]": "sett kamera x til [val]", + "DT/cameracontrols@_set camera y to [val]": "sett kamera y til [val]", + "DT/cameracontrols@_set camera zoom to [val] %": "sett kamera zoom til [val] %", + "DT/cameracontrols@_turn camera [image] [val] degrees": "snu kamera [image] [val] grader", + "NOname-awa/graphics2d@area": "område", + "NOname-awa/graphics2d@circumference": "omkrets", + "NOname-awa/graphics2d@graph": "graf [graph] 's [CS]", + "NOname-awa/graphics2d@line_section": "lengde fra ([x1],[y1]) til ([x2],[y2])", + "NOname-awa/graphics2d@name": "Grafikk 2D", + "NOname-awa/graphics2d@quadrilateral": "firkant ([x1],[y1]) ([x2],[y2]) ([x3],[y3]) ([x4],[y4]) 's [CS]", + "NOname-awa/graphics2d@ray_direction": "retning av ([x1],[y1]) til ([x2],[y2])", + "NOname-awa/graphics2d@round": "sirkel av [rd][a]'s [CS]", + "NOname-awa/graphics2d@triangle": "trekant ([x1],[y1]) ([x2],[y2]) ([x3],[y3]) 's [CS]", + "NOname-awa/graphics2d@triangle_s": "trekant [s1] [s2] [s3] 's areal", + "NexusKitten/controlcontrols@_Control Controls": "Kontroll Kontroller", + "NexusKitten/controlcontrols@_[OPTION] exists?": "[OPTION] finnes?", + "NexusKitten/controlcontrols@_[OPTION] shown?": "[OPTION] vist?", + "NexusKitten/controlcontrols@_fullscreen": "fullskjerm", + "NexusKitten/controlcontrols@_green flag": "grønt flagg", + "NexusKitten/controlcontrols@_hide [OPTION]": "skjul [OPTION]", + "NexusKitten/controlcontrols@_show [OPTION]": "vis [OPTION]", + "NexusKitten/controlcontrols@_stop": "stopp", + "NexusKitten/moremotion@_More Motion": "Mer Bevegelse", + "NexusKitten/moremotion@_change x: [X] y: [Y]": "endre x: [X] y: [Y]", + "NexusKitten/moremotion@_costume height": "kostyme høyde", + "NexusKitten/moremotion@_costume width": "kostymebredde", + "NexusKitten/moremotion@_direction to x: [X] y: [Y]": "retning til x: [X] y: [Y]", + "NexusKitten/moremotion@_distance from x: [X] y: [Y]": "avstand fra x: [X] y: [Y]", + "NexusKitten/moremotion@_height": "høyde", + "NexusKitten/moremotion@_manually fence": "manuelt gjerde", + "NexusKitten/moremotion@_move [PERCENT]% of the way to x: [X] y: [Y]": "flytt [PERCENT]% av veien til x: [X] y: [Y]", + "NexusKitten/moremotion@_move [STEPS] steps towards x: [X] y: [Y]": "flytt [STEPS] trinn mot x: [X] y: [Y]", + "NexusKitten/moremotion@_point towards x: [X] y: [Y]": "peker mot x: [X] y: [Y]", + "NexusKitten/moremotion@_rotation style": "rotasjonsstil", + "NexusKitten/moremotion@_touching rectangle x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?": "berøring rektangel x1: [X1] y1: [Y1] x2: [X2] y2: [Y2]?", + "NexusKitten/moremotion@_touching x: [X] y: [Y]?": "berøring x: [X] y: [Y]?", + "NexusKitten/moremotion@_width": "bredde", + "NexusKitten/sgrab@_S-Grab": "S-Gripe", + "NexusKitten/sgrab@_[WHAT] of user [WHO]": "[WHAT] av bruker [WHO]", + "NexusKitten/sgrab@_about me": "om meg", + "NexusKitten/sgrab@_creator of project id [WHO]": "skaper av prosjekt id [WHO]", + "NexusKitten/sgrab@_favorite": "favoritt", + "NexusKitten/sgrab@_follower": "følger", + "NexusKitten/sgrab@_following": "følgende", + "NexusKitten/sgrab@_global [WHAT] ranking for [WHO]": "global [WHAT] rangering for [WHO]", + "NexusKitten/sgrab@_global [WHAT] ranking for project id [WHO]": "global [WHAT] rangering for prosjekt-id [WHO]", + "NexusKitten/sgrab@_grab [WHAT] count of project id [WHO]": "hent [WHAT] antall prosjekt-id [WHO]", + "NexusKitten/sgrab@_grab [WHAT] count of user [WHO]": "hent [WHAT] antall bruker [WHO]", + "NexusKitten/sgrab@_location": "sted", + "NexusKitten/sgrab@_love": "liker", + "NexusKitten/sgrab@_name of project id [WHO]": "navn på prosjekt-id [WHO]", + "NexusKitten/sgrab@_view": "visning", + "battery@_Battery": "Batteri", + "battery@_battery level": "batterinivå", + "battery@_charging?": "lading?", + "battery@_seconds until charged": "sekunder til oppladet", + "battery@_seconds until empty": "sekunder til tom", + "battery@_when battery level changed": "når batterinivået endret seg", + "battery@_when charging changed": "når lading endret", + "battery@_when time until charged changed": "når tid til lading endret seg", + "battery@_when time until empty changed": "når tiden til tom endret seg", + "box2d@griffpatch.applyAngForce": "spinn med kraft [force]", + "box2d@griffpatch.applyForce": "skyv med kraft [force] i retning [dir]", + "box2d@griffpatch.categoryName": "Fysikk", + "box2d@griffpatch.changeScroll": "endre rulling med x: [ox] y: [oy]", + "box2d@griffpatch.changeVelocity": "endre hastighet med sx: [sx] sy: [sy]", + "box2d@griffpatch.disablePhysics": "deaktiver fysikk for denne figuren", + "box2d@griffpatch.doTick": "trinn simulering", + "box2d@griffpatch.getAngVelocity": "vinkelhastighet", + "box2d@griffpatch.getDensity": "tetthet", + "box2d@griffpatch.getFriction": "friksjon", + "box2d@griffpatch.getGravityX": "tyngdekraft x", + "box2d@griffpatch.getGravityY": "tyngdekraft y", + "box2d@griffpatch.getRestitution": "sprette", + "box2d@griffpatch.getScrollX": "x rulle", + "box2d@griffpatch.getScrollY": "y rulle", + "box2d@griffpatch.getStatic": "fikset?", + "box2d@griffpatch.getTickRate": "simuleringshastighet", + "box2d@griffpatch.getVelocityX": "x hastighet", + "box2d@griffpatch.getVelocityY": "y hastighet", + "box2d@griffpatch.setAngVelocity": "sett angulær hastighet til [force]", + "box2d@griffpatch.setDensity": "sett tettheten til [density]", + "box2d@griffpatch.setDensityValue": "sett tettheten til [density]", + "box2d@griffpatch.setFriction": "sett friksjon til [friction]", + "box2d@griffpatch.setFrictionValue": "sett friksjon til [friction]", + "box2d@griffpatch.setGravity": "sett tyngdekraften til x: [gx] y: [gy]", + "box2d@griffpatch.setPhysics": "aktiver for [shape] modus [mode]", + "box2d@griffpatch.setPosition": "gå til x: [x] y: [y] [space]", + "box2d@griffpatch.setProperties": "sett tetthet [density] ruhet [friction] sprett [restitution]", + "box2d@griffpatch.setRestitution": "sett sprett til [restitution]", + "box2d@griffpatch.setRestitutionValue": "sett sprett til [restitution]", + "box2d@griffpatch.setScroll": "sett rulle til x: [ox] y: [oy]", + "box2d@griffpatch.setStage": "sett scenegrensene til [stageType]", + "box2d@griffpatch.setStatic": "set fikset til [static]", + "box2d@griffpatch.setTickRate": "sett simuleringstakten til [rate]/s", + "box2d@griffpatch.setVelocity": "sett hastighet til sx: [sx] sy: [sy]", + "clipboard@_Clipboard": "Utklippstavle", + "clipboard@_clipboard": "Utklippstavle", + "clipboard@_copy to clipboard: [TEXT]": "kopier til utklippstavle: [TEXT]", + "clipboard@_last pasted text": "siste kopierte tekst", + "clipboard@_reset clipboard": "nullstill utklippstavlen", + "clipboard@_when something is copied": "når noe blir kopiert", + "clipboard@_when something is pasted": "når noe blir limt inn", + "clouddata-ping@_is cloud data server [SERVER] up?": "er skydata-serveren [SERVER] oppe?", + "cs2627883/numericalencoding@_Decode [ENCODED] back to text": "Dekode [ENCODED] tilbake til tekst", + "cs2627883/numericalencoding@_Encode [DATA] to numbers": "Kode [DATA] til tall", + "cs2627883/numericalencoding@_Hello!": "Hei!", + "cs2627883/numericalencoding@_Numerical Encoding": "Numerisk koding", + "cs2627883/numericalencoding@_decoded": "dekodet", + "cs2627883/numericalencoding@_encoded": "kodet", + "cursor@_Mouse Cursor": "Mus Pekkeren", + "cursor@_bottom left": "nederst til venstre", + "cursor@_bottom right": "nederst til høyre", + "cursor@_center": "senter", + "cursor@_cursor": "pekeren", + "cursor@_hide cursor": "skjul pekeren", + "cursor@_set cursor to [cur]": "sett markøren til [cur]", + "cursor@_set cursor to current costume center: [position] max size: [size]": "sett markøren til midten av gjeldende drakt: [position] maks størrelse: [size]", + "cursor@_top left": "øverst til venstre", + "cursor@_top right": "øverst til høyre", + "cursor@_{size} (unreliable)": "{size} (upålitelig)", + "encoding@_Convert the character [string] to [CodeList]": "Konverter tegnet [string] til [CodeList]", + "encoding@_Decode [string] with [code]": "Dekode [string] med [code]", + "encoding@_Encode [string] in [code]": "Kod [string] i [code]", + "encoding@_Encoding": "Koding", + "encoding@_Hash [string] with [hash]": "Hash [string] med [hash]", + "encoding@_Randomly generated [position] character string": "Tilfeldig generert [position] tegnstreng", + "encoding@_Use [wordbank] to generate a random [position] character string": "Bruk [wordbank] for å generere en tilfeldig [position] tegnstreng.", + "encoding@_[string] corresponding to the [CodeList] character": "[string] som tilsvarer [CodeList] tegnet", + "fetch@_Fetch": "Hent", + "files@_Accepted formats: {formats}": "Aksepterte formater: {formats}", + "files@_Files": "Filer", + "files@_Hello, world!": "Hei, verden!", + "files@_Select or drop file": "Velg eller slipp fil", + "files@_any": "noe", + "files@_download URL [url] as [file]": "last ned URL [url] som [file]", + "files@_download [text] as [file]": "last ned [text] som [file]", + "files@_only show selector (unreliable)": "bare vis velger (upålitelig)", + "files@_open a [extension] file": "åpne en [extension] fil", + "files@_open a [extension] file as [as]": "åpne en [extension] fil som [as]", + "files@_open a file": "åpne en fil", + "files@_open a file as [as]": "åpne en fil som [as]", + "files@_open selector immediately": "åpne velger umiddelbart", + "files@_set open file selector mode to [mode]": "sett åpen filvelgermodus til [mode]", + "files@_show modal": "vis modal", + "files@_text": "tekst", + "gamejolt@GameJoltAPI_gamejoltBool": "På Game Jolt?", + "gamejolt@_1 point": "1 poeng", + "gamejolt@_Achieve trophy of ID [ID]": "Oppnå troféet med ID [ID]", + "gamejolt@_Add [namespace] request with [parameters] to batch": "Legg til [namespace] forespørsel med [parameters] til batch", + "gamejolt@_Add [username] score [value] in table of ID [ID] with text [text] and comment [extraData]": "Legg til [username] poeng [value] i tabellen med ID [ID] med tekst [text] og kommentar [extraData]", + "gamejolt@_Add score [value] in table of ID [ID] with text [text] and comment [extraData]": "Legg til poeng [value] i tabellen med ID [ID] med tekst [text] og kommentar [extraData]", + "gamejolt@_Autologin available?": "Autologin tilgjengelig?", + "gamejolt@_Batch in JSON": "Batch i JSON", + "gamejolt@_Clear batch": "Rydd batch", + "gamejolt@_Close": "Lukk", + "gamejolt@_Data Storage Blocks": "Data lagringsblokker", + "gamejolt@_Debug Blocks": "Feilsøkingsblokker", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s [betterOrWorse] than [value] in table of ID [ID]": "Hent [amount] [globalOrPerUser] poeng [betterOrWorse] enn [value] i tabellen med ID [ID]", + "gamejolt@_Fetch [amount] [globalOrPerUser] score/s in table of ID [ID]": "Hent [amount] [globalOrPerUser] poeng i tabellen med ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s [betterOrWorse] than [value] in table of ID [ID]": "Hent [amount] [username] poeng [betterOrWorse] enn [value] i tabellen med ID [ID]", + "gamejolt@_Fetch [amount] [username] score/s in table of ID [ID]": "Hent [amount] [username] poeng i tabellen med ID [ID]", + "gamejolt@_Fetch [globalOrPerUser] keys matching with [pattern]": "Hent [globalOrPerUser] nøkler som matcher med [pattern]", + "gamejolt@_Fetch [trophyFetchGroup] trophies": "Hent [trophyFetchGroup] trofeer", + "gamejolt@_Fetch all [globalOrPerUser] keys": "Hent alle [globalOrPerUser] nøkler", + "gamejolt@_Fetch batch [parameter]": "Hent batch [parameter]", + "gamejolt@_Fetch logged in user": "Hent påloggede bruker", + "gamejolt@_Fetch score tables": "Hent poengtabeller", + "gamejolt@_Fetch server's time": "Hent serverens tid", + "gamejolt@_Fetch trophy of ID[ID]": "Hent trofé med ID[ID]", + "gamejolt@_Fetch user's [usernameOrID] by [fetchType]": "Hent brukerens [usernameOrID] ved [fetchType]", + "gamejolt@_Fetch user's friend IDs": "Hent brukerens venn-ID-er", + "gamejolt@_Fetched [globalOrPerUser] data at [key]": "Hentet [globalOrPerUser] data på [key]", + "gamejolt@_Fetched batch data in JSON": "Hentet batch-data i JSON", + "gamejolt@_Fetched key at index [index]": "Hentet nøkkel på indeks [index]", + "gamejolt@_Fetched keys in JSON": "Hentede nøkler i JSON", + "gamejolt@_Fetched rank of [value] in table of ID [ID]": "Hentet rangering av [value] i tabell med ID [ID]", + "gamejolt@_Fetched score [scoreDataType] at index [index]": "Hentet poeng [scoreDataType] på indeks [index]", + "gamejolt@_Fetched score data in JSON": "Hentet poengdata i JSON", + "gamejolt@_Fetched server's [timeType]": "Hentet serverens [timeType]", + "gamejolt@_Fetched server's time in JSON": "Hentet serverens tid i JSON", + "gamejolt@_Fetched table [tableDataType] at index [index]": "Hentet tabell [tableDataType] på indeks [index]", + "gamejolt@_Fetched table [tableDataType] at index[index] (Deprecated)": "Hentet tabell [tableDataType] på indeks [index] (Utgått)", + "gamejolt@_Fetched tables in JSON": "Hentede tabeller i JSON", + "gamejolt@_Fetched trophies in JSON": "Hentede trofeer i JSON", + "gamejolt@_Fetched trophy [trophyDataType] at index [index]": "Hentet trofé [trophyDataType] på indeks [index]", + "gamejolt@_Fetched user's [userDataType]": "Hentet brukerens [userDataType]", + "gamejolt@_Fetched user's data in JSON": "Hentet brukerdata i JSON", + "gamejolt@_Fetched user's friend ID at index[index]": "Hentet brukerens venn-ID på indeks [index]", + "gamejolt@_Fetched user's friend IDs in JSON": "Hentet brukerens venn-ID-er i JSON", + "gamejolt@_In debug mode?": "I feilsøkingsmodus?", + "gamejolt@_Last API error": "Siste API-feil", + "gamejolt@_Logged in user's username": "Logget inn brukernavn for brukeren", + "gamejolt@_Logged in?": "Logget inn?", + "gamejolt@_Login automatically": "Logg inn automatisk", + "gamejolt@_Login with [username] and [token]": "Logg inn med [username] og [token]", + "gamejolt@_Logout": "Logg ut", + "gamejolt@_Open": "Åpne", + "gamejolt@_Ping session": "Ping økt", + "gamejolt@_Remove [globalOrPerUser] data at [key]": "Fjern [globalOrPerUser] data på [key]", + "gamejolt@_Remove trophy of ID [ID]": "Fjern troféet med ID [ID]", + "gamejolt@_Score Blocks": "Poengblokker", + "gamejolt@_Session Blocks": "Øktblokker", + "gamejolt@_Session open?": "Åpen økt?", + "gamejolt@_Set [globalOrPerUser] data at [key] to [data]": "Sett [globalOrPerUser] data på [key] til [data]", + "gamejolt@_Set game ID to [ID] and private key to [key]": "Sett spill-ID til [ID] og privat nøkkel til [key]", + "gamejolt@_Set session status to [status]": "Sett øktstatus til [status]", + "gamejolt@_Time Blocks": "Tidsblokker", + "gamejolt@_Trophy Blocks": "Trophy Blokker", + "gamejolt@_Turn debug mode [toggle]": "Slå på feilsøkingsmodus [toggle]", + "gamejolt@_Update [globalOrPerUser] data at [key] by [operationType] [value]": "Oppdater [globalOrPerUser] data på [key] med [operationType] [value]", + "gamejolt@_User Blocks": "Brukerblokkeringer", + "gamejolt@_[openOrClose] session": "[openOrClose] økt", + "gamejolt@_achievement date": "dato for prestasjon", + "gamejolt@_active": "aktiv", + "gamejolt@_adding": "legger til", + "gamejolt@_all": "alle", + "gamejolt@_all achieved": "alle oppnådd", + "gamejolt@_all unachieved": "alle uoppnådd", + "gamejolt@_appending": "tilføyer", + "gamejolt@_better": "bedre", + "gamejolt@_comment": "kommentar", + "gamejolt@_day": "dag", + "gamejolt@_description": "beskrivelse", + "gamejolt@_developer username": "utvikler brukernavn", + "gamejolt@_difficulty": "vanskelighetsgrad", + "gamejolt@_dividing by": "deler med", + "gamejolt@_guest": "gjest", + "gamejolt@_hour": "time", + "gamejolt@_idle": "ledig", + "gamejolt@_image URL": "bilde-URL", + "gamejolt@_in parallel": "i parallel", + "gamejolt@_index": "indeks", + "gamejolt@_key": "nøkkel", + "gamejolt@_last login": "siste pålogging", + "gamejolt@_last login timestamp": "siste påloggstidspunkt", + "gamejolt@_minute": "minutt", + "gamejolt@_month": "måned", + "gamejolt@_multiplying by": "multiplisere med", + "gamejolt@_name": "navn", + "gamejolt@_off": "av", + "gamejolt@_on": "på", + "gamejolt@_optional": "valgfritt", + "gamejolt@_prepending": "foranstilt", + "gamejolt@_primary": "primær", + "gamejolt@_private key": "privat nøkkel", + "gamejolt@_private token": "privat token", + "gamejolt@_score date": "poengdato", + "gamejolt@_score timestamp": "score tidspunkt", + "gamejolt@_second": "sekund", + "gamejolt@_sequentially": "sekvensielt", + "gamejolt@_sequentially, break on error": "sekvensielt, avbryt ved feil", + "gamejolt@_sign up date": "registreringsdato", + "gamejolt@_sign up timestamp": "registrerings tidsstempel", + "gamejolt@_subtracting": "trekke", + "gamejolt@_text": "tekst", + "gamejolt@_timestamp": "tidspunkt", + "gamejolt@_timezone": "tidssone", + "gamejolt@_title": "tittel", + "gamejolt@_user": "bruker", + "gamejolt@_user ID": "bruker-ID", + "gamejolt@_username": "brukernavn", + "gamejolt@_website": "nettsted", + "gamejolt@_worse": "verre", + "gamejolt@_year": "år", + "gamepad@_D-pad down (14)": "D-pad ned (14)", + "gamepad@_D-pad left (15)": "D-pad venstre (15)", + "gamepad@_D-pad right (16)": "D-pad høyre (16)", + "gamepad@_D-pad up (13)": "D-pad opp (13)", + "gamepad@_Gamepad": "Spillkontroller", + "gamepad@_Left bumper (5)": "Venstre støtfanger (5)", + "gamepad@_Left stick (1 & 2)": "Venstre spak (1 & 2)", + "gamepad@_Left stick (11)": "Venstre spak (11)", + "gamepad@_Left stick horizontal (1)": "Venstre spak horisontal (1)", + "gamepad@_Left stick vertical (2)": "Venstre spak vertikal (2)", + "gamepad@_Left trigger (7)": "Venstre avtrekker (7)", + "gamepad@_Right bumper (6)": "Høyre støtfanger (6)", + "gamepad@_Right stick (12)": "Høyre spak (12)", + "gamepad@_Right stick (3 & 4)": "Høyre spak (3 & 4)", + "gamepad@_Right stick horizontal (3)": "Høyre spak horisontal (3)", + "gamepad@_Right stick vertical (4)": "Høyre spak vertikal (4)", + "gamepad@_Right trigger (8)": "Høyre avtrekker (8)", + "gamepad@_Select/View (9)": "Velg/Vis (9)", + "gamepad@_Start/Menu (10)": "Start/Meny (10)", + "gamepad@_any": "noe", + "gamepad@_button [b] on pad [i] pressed?": "knapp [b] på pad [i] trykket?", + "gamepad@_direction of axes [axis] on pad [pad]": "retning av akser [axis] på pad [pad]", + "gamepad@_gamepad [pad] connected?": "spillkontroll [pad] tilkoblet?", + "gamepad@_magnitude of axes [axis] on pad [pad]": "størrelsen på aksene [axis] på pad [pad]", + "gamepad@_rumble strong [s] and weak [w] for [t] sec. on pad [i]": "rumle sterkt [s] og svakt [w] i [t] sek. på pad [i]", + "gamepad@_value of axis [b] on pad [i]": "verdi av akse [b] på pad [i]", + "gamepad@_value of button [b] on pad [i]": "verdi av knappen [b] på pad [i]", + "iframe@_Iframe": "IFrame", + "iframe@_It works!": "Det fungerer!", + "iframe@_close iframe": "lukk iframe", + "iframe@_height": "høyde", + "iframe@_hide iframe": "skjul iframe", + "iframe@_interactive": "interaktiv", + "iframe@_resize behavior": "endre størrelsesoppførsel", + "iframe@_scale": "skala", + "iframe@_set iframe height to [HEIGHT]": "set iframe høyde til [HEIGHT]", + "iframe@_set iframe interactive to [INTERACTIVE]": "sett iframe interaktiv til [INTERACTIVE]", + "iframe@_set iframe resize behavior to [RESIZE]": "sett iframe resize oppførsel til [RESIZE]", + "iframe@_set iframe width to [WIDTH]": "set iframe bredde til [WIDTH]", + "iframe@_set iframe x position to [X]": "sett iframe x-posisjon til [X]", + "iframe@_set iframe y position to [Y]": "sett iframe y-posisjon til [Y]", + "iframe@_show HTML [HTML]": "vis HTML [HTML]", + "iframe@_show iframe": "vis iframe", + "iframe@_show website [URL]": "vis nettside [URL]", + "iframe@_viewport": "visningsområde", + "iframe@_visible": "synlig", + "iframe@_width": "bredde", + "itchio@_Error: Data not found.": "Feil: Data ikke funnet.", + "itchio@_Fetch game data [user][game][secret]": "Hent spilldata [user][game][secret]", + "itchio@_Game data?": "Spilldata?", + "itchio@_Open [prefix] itch.io [page] window with [width]width and [height]height": "Åpne [prefix] itch.io [page] vindu med [width]bredde og [height]høyde", + "itchio@_Return game [data]": "Returner spill [data]", + "itchio@_Return game data [user][game][secret] in .json": "Returner spilldata [user][game][secret] i .json", + "itchio@_Return game data in .json": "Returner spilldata i .json", + "itchio@_Return game rewards [rewards] by index:[index]": "Returner spillbelønninger [rewards] etter indeks:[index]", + "itchio@_Return game sale [sale]": "Returner spill salg [sale]", + "itchio@_Return game sub products [subProducts] by index:[index]": "Returner spill underprodukter [subProducts] etter indeks:[index]", + "itchio@_Return rewards list length": "Returner lengden på belønningslisten", + "itchio@_Return sub products list length": "Returner lengden på underproduktlisten", + "itchio@_Rewards": "Belønninger", + "itchio@_Rewards?": "Belønninger?", + "itchio@_Sale": "Salg", + "itchio@_Sale?": "Salg?", + "itchio@_Sub products": "Underprodukter", + "itchio@_Sub products?": "Underprodukter?", + "itchio@_Window": "Vindu", + "itchio@_amount": "beløp", + "itchio@_amount remaining": "gjenstående beløp", + "itchio@_available": "tilgjengelig", + "itchio@_cover image URL": "URL-adresse til forsidebilde", + "itchio@_end date": "sluttdato", + "itchio@_game": "spill", + "itchio@_game argument not found": "spillargument ikke funnet", + "itchio@_name": "navn", + "itchio@_original price": "original pris", + "itchio@_price": "pris", + "itchio@_title": "tittel", + "itchio@_user": "bruker", + "itchio@_user argument not found": "brukerargument ikke funnet", + "itchio@itchio_error": "Feil:", + "itchio@itchio_errors": "Feiler:", + "lab/text@_# of lines": "# av linjer", + "lab/text@_Animated Text": "Animert Tekst", + "lab/text@_Enable Non-Scratch Lab Features": "Aktiver funksjoner som er ikke tilgjengelig i scratch lab", + "lab/text@_Hello!": "Hei!", + "lab/text@_Here we go!": "Her går vi!", + "lab/text@_Incompatible with Scratch Lab:": "Inkompatibel med Scratch Lab.", + "lab/text@_Welcome to my project!": "Velkommen til mitt prosjekt!", + "lab/text@_[ANIMATE] duration": "[ANIMATE] varighet", + "lab/text@_[ANIMATE] text [TEXT]": "[ANIMATE] tekst [TEXT]", + "lab/text@_add line [TEXT]": "legg til linje [TEXT]", + "lab/text@_align text to [ALIGN]": "juster teksten til [ALIGN]", + "lab/text@_animate [ANIMATE] until done": "animere [ANIMATE] til ferdig", + "lab/text@_center": "senter", + "lab/text@_displayed text": "vist tekst", + "lab/text@_is animating?": "er animerer?", + "lab/text@_is showing text?": "viser teksten?", + "lab/text@_left": "venstre", + "lab/text@_rainbow": "regnbue", + "lab/text@_random font": "tilfeldig skrifttype", + "lab/text@_reset [ANIMATE] duration": "tilbakestill [ANIMATE] varighet", + "lab/text@_reset text width": "tilbakestill tekstbredde", + "lab/text@_reset typing delay": "nullstill skriveforsinkelse", + "lab/text@_right": "riktig", + "lab/text@_set [ANIMATE] duration to [NUM] seconds": "sett [ANIMATE] varighet til [NUM] sekunder", + "lab/text@_set font to [FONT]": "sett skrifttype til [FONT]", + "lab/text@_set text color to [COLOR]": "sett tekstfarge til [COLOR]", + "lab/text@_set typing delay to [NUM] seconds": "sett skriveforsinkelse til [NUM] sekunder", + "lab/text@_set width to [WIDTH]": "sett bredde til [WIDTH]", + "lab/text@_set width to [WIDTH] aligned [ALIGN]": "sett bredde til [WIDTH] justert [ALIGN]", + "lab/text@_show sprite": "vis sprite", + "lab/text@_show text [TEXT]": "vis tekst [TEXT]", + "lab/text@_start [ANIMATE] animation": "start [ANIMATE] animasjon", + "lab/text@_text [ATTRIBUTE]": "tekst [ATTRIBUTE]", + "lab/text@_typing delay": "skriveforsinkelse", + "lab/text@disableCompatibilityMode": "Dette vil aktivere nye blokker og funksjoner som IKKE VIL FUNGERE i den offisielle Scratch Laben.\n\nØnsker du å fortsette?", + "local-storage@_Local Storage": "Lokal lagring", + "local-storage@_Local Storage extension: project must run the \"set storage namespace ID\" block before it can use other blocks": "Lokal lagring utvidelse: prosjektet må kjøre blokken \"sett lagringsnavnerom-ID\" før det kan bruke andre blokker", + "local-storage@_delete all keys": "slett alle nøkler", + "local-storage@_delete key [KEY]": "slett nøkkel [KEY]", + "local-storage@_get key [KEY]": "få nøkkel [KEY]", + "local-storage@_project title": "prosjekttittel", + "local-storage@_score": "poengsum", + "local-storage@_set key [KEY] to [VALUE]": "sett nøkkel [KEY] til [VALUE]", + "local-storage@_set storage namespace ID to [ID]": "sett lagringsnavnerom-ID til [ID]", + "local-storage@_when another window changes storage": "når et annet vindu endrer lagring", + "navigator@_browser": "nettleser", + "navigator@_dark": "mørk", + "navigator@_device memory in GB": "enhetens minne i GB", + "navigator@_light": "lys", + "navigator@_operating system": "operativsystem", + "navigator@_user prefers [THEME] color scheme?": "bruker foretrekker [THEME] fargeskjema?", + "navigator@_user prefers more contrast?": "brukeren foretrekker mer kontrast?", + "navigator@_user prefers reduced motion?": "bruker foretrekker redusert bevegelse?", + "pointerlock@_Pointerlock": "Pointerlås", + "pointerlock@_disabled": "deaktivert", + "pointerlock@_enabled": "aktivert", + "pointerlock@_pointer locked?": "peker låst?", + "pointerlock@_set pointer lock [enabled]": "sette pekerlås [enabled]", + "qxsck/data-analysis@average": "gjennomsnittet av [NUMBERS]", + "qxsck/data-analysis@maximum": "maksimum av [NUMBERS]", + "qxsck/data-analysis@median": "median av [NUMBERS]", + "qxsck/data-analysis@minimum": "minimum av [NUMBERS]", + "qxsck/data-analysis@mode": "modus av [NUMBERS]", + "qxsck/data-analysis@name": "Dataanalyse", + "qxsck/data-analysis@variance": "variansen til [NUMBERS]", + "qxsck/var-and-list@addValueInList": "legg til [VALUE] i [LIST]", + "qxsck/var-and-list@clearList": "slett alle [LIST]", + "qxsck/var-and-list@copyList": "kopier [LIST1] til [LIST2]", + "qxsck/var-and-list@deleteOfList": "slett [INDEX] av [LIST]", + "qxsck/var-and-list@getIndexOfList": "første indeks av [VALUE] i [LIST]", + "qxsck/var-and-list@getIndexesOfList": "indekser av [VALUE] i [LIST]", + "qxsck/var-and-list@getList": "verdi av [LIST]", + "qxsck/var-and-list@getValueOfList": "element [INDEX] av [LIST]", + "qxsck/var-and-list@getVar": "verdi av [VAR]", + "qxsck/var-and-list@length": "lengden av [LIST]", + "qxsck/var-and-list@listContains": "[LIST] inneholder [VALUE] ?", + "qxsck/var-and-list@name": "Variabel og liste", + "qxsck/var-and-list@replaceOfList": "erstatt element [INDEX] av [LIST] med [VALUE]", + "qxsck/var-and-list@seriListsToJson": "konverter alle lister som starter med [START] til json", + "qxsck/var-and-list@seriVarsToJson": "konverter alle variabler som starter med [START] til json", + "qxsck/var-and-list@setVar": "sett verdien av [VAR] til [VALUE]", + "runtime-options@_Infinity": "Uendelighet", + "runtime-options@_Runtime Options": "Kjøretidsalternativer", + "runtime-options@_[thing] enabled?": "[thing] aktivert?", + "runtime-options@_clone limit": "klon grense", + "runtime-options@_default ({n})": "standard ({n})", + "runtime-options@_disabled": "deaktivert", + "runtime-options@_enabled": "aktivert", + "runtime-options@_framerate": "Bildetakt", + "runtime-options@_framerate limit": "grense for bildefrekvens", + "runtime-options@_height": "høyde", + "runtime-options@_high quality pen": "Høy kvalitet penn", + "runtime-options@_interpolation": "interpolasjon", + "runtime-options@_remove fencing": "Fjern gjerde", + "runtime-options@_remove misc limits": "fjern diverse begrensninger", + "runtime-options@_run green flag [flag]": "kjør grønt flagg [flag]", + "runtime-options@_set [thing] to [enabled]": "sett [thing] til [enabled]", + "runtime-options@_set clone limit to [limit]": "sett klon-grensen til [limit]", + "runtime-options@_set framerate limit to [fps]": "begrens bildefrekvensen til [fps]", + "runtime-options@_set stage size width: [width] height: [height]": "sett scenestørrelse bredde: [width] høyde: [height]", + "runtime-options@_set username to [username]": "sett brukernavn til [username]", + "runtime-options@_stage [dimension]": "scene [dimension]", + "runtime-options@_stage size": "scenestørrelse", + "runtime-options@_turbo mode": "turbo modus", + "runtime-options@_username": "brukernavn", + "runtime-options@_width": "bredde", + "sound@_Sound": "Lyd", + "sound@_play sound from url: [path] until done": "spill lyd fra nettadresse: [path] til ferdig", + "sound@_start sound from url: [path]": "start lyd fra url: [path]", + "stretch@_Stretch": "Strekke", + "stretch@_change stretch by x: [DX] y: [DY]": "endre strekk med x: [DX] y: [DY]", + "stretch@_change stretch x by [DX]": "endre strekk x med [DX]", + "stretch@_change stretch y by [DY]": "endre strekk y med [DY]", + "stretch@_set stretch to x: [X] y: [Y]": "sett strekk til x: [X] y: [Y]", + "stretch@_set stretch x to [X]": "sett strekk x til [X]", + "stretch@_set stretch y to [Y]": "sett strekk y til [Y]", + "stretch@_x stretch": "x strekk", + "stretch@_y stretch": "y strekk" + }, "nl": { "-SIPC-/consoles@_Clear Console": "console wissen", "-SIPC-/consoles@_Create a collapsed group named [string]": "creëer samengevouwen groep genaamd [string]", @@ -813,18 +1529,42 @@ "-SIPC-/consoles@_Warning": "waarschuwing", "-SIPC-/consoles@_Warning [string]": "waarschuwing [string]", "-SIPC-/consoles@_group": "groep", + "-SIPC-/time@_April": "april", + "-SIPC-/time@_August": "augustus", + "-SIPC-/time@_December": "december", + "-SIPC-/time@_February": "februari", + "-SIPC-/time@_January": "januari", + "-SIPC-/time@_July": "juli", + "-SIPC-/time@_June": "juni", + "-SIPC-/time@_March": "maart", + "-SIPC-/time@_May": "mei", + "-SIPC-/time@_November": "november", + "-SIPC-/time@_October": "oktober", + "-SIPC-/time@_September": "september", "-SIPC-/time@_Time": "Tijd", "-SIPC-/time@_convert [time] to timestamp": "[time] in tijdstempel", - "-SIPC-/time@_convert [timestamp] to datetime": "[timestamp] in datum en tijd", + "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "zet [timestamp] om naar JJJJ-MM-DD UU:MM:SS", "-SIPC-/time@_current time zone": "huidige tijdzone", "-SIPC-/time@_current timestamp": "huidige tijdstempel", "-SIPC-/time@_day": "dag", + "-SIPC-/time@_days": "dagen", + "-SIPC-/time@_difference between [DATE] and now in [TIME_MENU]": "verschil tussen [DATE] en nu in [TIME_MENU]", + "-SIPC-/time@_difference between [START] and [END] in [TIME_MENU]": "verschil tussen [START] en [END] in [TIME_MENU]", + "-SIPC-/time@_exact": "exacte", + "-SIPC-/time@_format [VALUE] seconds as [ROUND] time": "formatteer [VALUE] seconden als [ROUND] tijd", "-SIPC-/time@_get [Timedata] from [timestamp]": "[Timedata] van [timestamp]", "-SIPC-/time@_hour": "uur", + "-SIPC-/time@_hours": "uren", "-SIPC-/time@_minute": "minuut", + "-SIPC-/time@_minutes": "minuten", "-SIPC-/time@_month": "maand", + "-SIPC-/time@_months": "maanden", + "-SIPC-/time@_number of days in [MONTH] [YEAR]": "aantal dagen in [MONTH] [YEAR]", + "-SIPC-/time@_rounded": "afgeronde", "-SIPC-/time@_second": "seconde", + "-SIPC-/time@_seconds": "seconden", "-SIPC-/time@_year": "jaar", + "-SIPC-/time@_years": "jaren", "0832/rxFS2@clean": "wis het bestandssysteem", "0832/rxFS2@del": "verwijder [STR]", "0832/rxFS2@folder": "maak [STR] [STR2]", @@ -1013,6 +1753,7 @@ "box2d@griffpatch.categoryName": "Fysica-Simulatie", "box2d@griffpatch.changeScroll": "verander scroll met x: [ox] y: [oy]", "box2d@griffpatch.changeVelocity": "verander snelheid met sx: [sx] sy: [sy]", + "box2d@griffpatch.disablePhysics": "zet simulatie uit voor deze sprite", "box2d@griffpatch.doTick": "voer simulatie één keer uit", "box2d@griffpatch.getAngVelocity": "hoeksnelheid", "box2d@griffpatch.getDensity": "dichtheid", @@ -1023,6 +1764,7 @@ "box2d@griffpatch.getScrollX": "x-scroll", "box2d@griffpatch.getScrollY": "y-scroll", "box2d@griffpatch.getStatic": "vastgezet?", + "box2d@griffpatch.getTickRate": "simulatiefrequentie", "box2d@griffpatch.getTouching": "alle sprites die [where] aanraken", "box2d@griffpatch.getVelocityX": "snelheid x", "box2d@griffpatch.getVelocityY": "snelheid y", @@ -1040,6 +1782,7 @@ "box2d@griffpatch.setScroll": "stel scroll in op x: [ox] y: [oy]", "box2d@griffpatch.setStage": "maak grenstype [stageType]", "box2d@griffpatch.setStatic": "maak vastzettype [static]", + "box2d@griffpatch.setTickRate": "maak simulatiefrequentie [rate] / sec", "box2d@griffpatch.setVelocity": "stel snelheid in op sx: [sx] sy: [sy]", "clipboard@_Clipboard": "Klembord", "clipboard@_clipboard": "klembord", @@ -1074,7 +1817,6 @@ "encoding@_Randomly generated [position] character string": "willekeurige string met [position] tekens", "encoding@_Use [wordbank] to generate a random [position] character string": "gebruik [wordbank] in een willekeurige string met [position] tekens", "encoding@_[string] corresponding to the [CodeList] character": "teken nr. [string] in [CodeList]", - "encoding@_apple": "appel", "files@_Accepted formats: {formats}": "Geaccepteerde formaten: {formats}", "files@_Files": "Bestanden", "files@_Hello, world!": "Hallo, wereld!", @@ -1395,7 +2137,9 @@ "runtime-options@_set stage size width: [width] height: [height]": "maak speelveldbreedte: [width] en -hoogte: [height]", "runtime-options@_set username to [username]": "maak gebruikersnaam [username]", "runtime-options@_stage [dimension]": "[dimension] van speelveld", + "runtime-options@_stage size": "speelveldgrootte", "runtime-options@_turbo mode": "turbomodus", + "runtime-options@_username": "gebruikersnaam", "runtime-options@_width": "breedte", "sound@_Sound": "Geluid", "sound@_play sound from url: [path] until done": "start geluid van URL: [path] en wacht", @@ -1550,7 +2294,6 @@ "encoding@_Randomly generated [position] character string": "Случайно сгенерированная строка c длиной [position]", "encoding@_Use [wordbank] to generate a random [position] character string": "Использовать [wordbank] чтобы случайно сгенерировать строку с длиной [position] ", "encoding@_[string] corresponding to the [CodeList] character": "символ соответствующий [string] в [CodeList]", - "encoding@_apple": "яблоко", "files@_Files": "Файлы", "files@_Hello, world!": "Привет, мир!", "files@_Select or drop file": "Выберите или \"закиньте\" файл", @@ -1797,6 +2540,7 @@ "runtime-options@_set username to [username]": "задать имя пользователя как [username]", "runtime-options@_stage [dimension]": "[dimension] сцены", "runtime-options@_turbo mode": "турбо режим", + "runtime-options@_username": "имя пользователя", "runtime-options@_width": "ширина", "sound@_Sound": "Звук", "sound@_play sound from url: [path] until done": "играть звук из url: [path] до конца", @@ -1857,7 +2601,6 @@ "-SIPC-/consoles@_group": "群组", "-SIPC-/time@_Time": "时间", "-SIPC-/time@_convert [time] to timestamp": "把时间[time]转换为时间戳", - "-SIPC-/time@_convert [timestamp] to datetime": "把时间戳[timestamp]转换为时间", "-SIPC-/time@_current time zone": "时区", "-SIPC-/time@_current timestamp": "时间戳", "-SIPC-/time@_day": "日", @@ -2071,7 +2814,6 @@ "encoding@_Randomly generated [position] character string": "生成长度为[position]的随机字符串", "encoding@_Use [wordbank] to generate a random [position] character string": "用源字符串[wordbank]生成长度为[position]的随机字符串", "encoding@_[string] corresponding to the [CodeList] character": "ID[string]在[CodeList]对应的字符", - "encoding@_apple": "苹果", "fetch@_Fetch": "请求API", "files@_Files": "文件", "files@_Hello, world!": "你好,世界!", @@ -2170,7 +2912,7 @@ "qxsck/data-analysis@mode": "[NUMBERS]里所有数字的众数", "qxsck/data-analysis@name": "数据分析", "qxsck/data-analysis@variance": "[NUMBERS]里所有数字的方差", - "qxsck/var-and-list@addValueInList": "把[VALUE]加入列表[LIST]", + "qxsck/var-and-list@addValueInList": "在列表[LIST]的末尾添加[VALUE]", "qxsck/var-and-list@clearList": "删除列表[LIST]的所有值", "qxsck/var-and-list@copyList": "复制列表 [LIST1] 的数据到列表 [LIST2]", "qxsck/var-and-list@deleteOfList": "删除列表[LIST]的第[INDEX]项", @@ -2184,8 +2926,8 @@ "qxsck/var-and-list@name": "变量与列表", "qxsck/var-and-list@replaceOfList": "把列表[LIST]第[INDEX]项的值替换为[VALUE]", "qxsck/var-and-list@seriListsToJson": "把所有以[START]开头的列表转换为JSON", - "qxsck/var-and-list@seriVarsToJson": "把所有以[START]开头的变量转换为JSON", - "qxsck/var-and-list@setVar": "把变量[VAR]的值修改为[VALUE]", + "qxsck/var-and-list@seriVarsToJson": "将所有以[START]开头的变量转换为JSON", + "qxsck/var-and-list@setVar": "将变量[VAR]的值修改为[VALUE]", "runtime-options@_Infinity": "无限", "runtime-options@_Runtime Options": "运行选项", "runtime-options@_[thing] enabled?": "启用了[thing]?", From 4fd28708dcc4601bd5350b84affbe0713a6e1740 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:07:29 +0000 Subject: [PATCH 097/196] battery: The "please merge more control" update (#1286) pretty please --- extensions/battery.js | 1 + images/battery.svg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/battery.js b/extensions/battery.js index bda7f9ede6..649662b78d 100644 --- a/extensions/battery.js +++ b/extensions/battery.js @@ -63,6 +63,7 @@ return { name: Scratch.translate("Battery"), id: "battery", + color1: "#cf8436", blocks: [ { opcode: "charging", diff --git a/images/battery.svg b/images/battery.svg index c4c3c255ad..591140d43d 100644 --- a/images/battery.svg +++ b/images/battery.svg @@ -1 +1 @@ - \ No newline at end of file + From 8b86a4f6fd253fe06cb7d3290f6b2a3d522e1107 Mon Sep 17 00:00:00 2001 From: veggiecan0419 <137440114+veggiecan0419@users.noreply.github.com> Date: Thu, 8 Feb 2024 01:03:15 +0300 Subject: [PATCH 098/196] A few fixes (#1287) ## Changes made ### images/README.md in the Longman dictionary section - Corrected it to say veggiecan/longmanDictonary **.svg** => **.png** - Removed the link to my github profile - Changed the "created by" from **veggiecan** => **Veggiecan0419** (like it is for browser fullscreen) ### development/homepage-template.ejs - Updated the "developer documentation" link --- development/homepage-template.ejs | 2 +- images/README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/development/homepage-template.ejs b/development/homepage-template.ejs index 95b3c5fbf3..1491e491c5 100644 --- a/development/homepage-template.ejs +++ b/development/homepage-template.ejs @@ -392,7 +392,7 @@ -
    GitHub - - Developer Documentation + Developer Documentation diff --git a/images/README.md b/images/README.md index 6e7e485d79..495fbb26d6 100644 --- a/images/README.md +++ b/images/README.md @@ -248,8 +248,8 @@ All images in this folder are licensed under the [GNU General Public License ver ## Alestore/nfcwarp.svg - Created by [@HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1636726352 -## veggiecan/LongmanDictionary.svg -- Created by [veggiecan](https://github.com/veggiecan0419) +## veggiecan/LongmanDictionary.png +- Created by Veggiecan0419 - The ship is based on [this](https://www.ldoceonline.com/external/images/logo_home_smartphone.svg?version=1.2.61) logo from the [ldoceonline](https://www.ldoceonline.com/) website ## Lily/Skins.svg @@ -293,4 +293,4 @@ All images in this folder are licensed under the [GNU General Public License ver - Created by [HamsterCreativity](https://github.com/HamsterCreativity) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1694716263 ## Lily/Video.svg - - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 \ No newline at end of file + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 From edafcfeeeaa5284373eeeca716e47dbb17017173 Mon Sep 17 00:00:00 2001 From: Obvious Alex C <76855369+David-Orangemoon@users.noreply.github.com> Date: Wed, 7 Feb 2024 22:53:31 -0500 Subject: [PATCH 099/196] obviousAlexC/penPlus: optimize (#1288) got the demo on my testing chromebook running from 5fps to 15fps also got the mast on my main computer running 15 fps from 10 fps on firefox (Firefox has the weakest webgl version.) --- extensions/obviousAlexC/penPlus.js | 240 ++++++++++++++--------------- 1 file changed, 117 insertions(+), 123 deletions(-) diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js index e58df86768..4031f72c20 100644 --- a/extensions/obviousAlexC/penPlus.js +++ b/extensions/obviousAlexC/penPlus.js @@ -38,6 +38,30 @@ let lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + //?Link some stuff to the draw region + //?And some fun statistics + let trianglesDrawn = 0; + let inDrawRegion = false; + let currentDrawShader = undefined; + let penPlusDrawRegion = { + enter: () => { + trianglesDrawn = 0; + inDrawRegion = true; + //lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); + gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); + gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + }, + exit: () => { + inDrawRegion = false; + gl.bindFramebuffer( + gl.FRAMEBUFFER, + renderer._allSkins[renderer._penSkinId]._framebuffer.framebuffer + ); + triFunctions.drawOnScreen(); + gl.useProgram(penPlusShaders.pen.program); + }, + }; + //?Buffer handling and pen loading { gl.bindTexture(gl.TEXTURE_2D, depthBufferTexture); @@ -152,7 +176,7 @@ //?Call it to have it consistant updateCanvasSize(); - //?Call every frame because I don't know of a way to detect when the stage is resized + //?Call every frame because I don't know of a way to detect when the stage is resized through window resizing (2/7/24) thought I should clarify window.addEventListener("resize", updateCanvasSize); vm.runtime.on("STAGE_SIZE_CHANGED", () => { @@ -235,86 +259,86 @@ untextured: { Shaders: { vert: ` - attribute highp vec4 a_position; - attribute highp vec4 a_color; - varying highp vec4 v_color; - - void main() - { - v_color = a_color; - gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); - } - `, + attribute highp vec4 a_position; + attribute highp vec4 a_color; + varying highp vec4 v_color; + + void main() + { + v_color = a_color; + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); + } + `, frag: ` - varying highp vec4 v_color; - - void main() - { - gl_FragColor = v_color; - gl_FragColor.rgb *= gl_FragColor.a; - } - `, + varying highp vec4 v_color; + + void main() + { + gl_FragColor = v_color; + gl_FragColor.rgb *= gl_FragColor.a; + } + `, }, ProgramInf: null, }, textured: { Shaders: { vert: ` - attribute highp vec4 a_position; - attribute highp vec4 a_color; - attribute highp vec2 a_texCoord; - - varying highp vec4 v_color; - varying highp vec2 v_texCoord; - - void main() - { - v_color = a_color; - v_texCoord = a_texCoord; - gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); - } - `, + attribute highp vec4 a_position; + attribute highp vec4 a_color; + attribute highp vec2 a_texCoord; + + varying highp vec4 v_color; + varying highp vec2 v_texCoord; + + void main() + { + v_color = a_color; + v_texCoord = a_texCoord; + gl_Position = a_position * vec4(a_position.w,a_position.w,-1.0/a_position.w,1); + } + `, frag: ` - uniform sampler2D u_texture; - - varying highp vec2 v_texCoord; - varying highp vec4 v_color; - - void main() - { - gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; - gl_FragColor.rgb *= gl_FragColor.a; - - } - `, + uniform sampler2D u_texture; + + varying highp vec2 v_texCoord; + varying highp vec4 v_color; + + void main() + { + gl_FragColor = texture2D(u_texture, v_texCoord) * v_color; + gl_FragColor.rgb *= gl_FragColor.a; + + } + `, }, ProgramInf: null, }, draw: { Shaders: { vert: ` - attribute highp vec4 a_position; - - varying highp vec2 v_texCoord; - attribute highp vec2 a_texCoord; - - void main() - { - gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); - v_texCoord = (a_position.xy / 2.0) + vec2(0.5,0.5); - } - `, + attribute highp vec4 a_position; + + varying highp vec2 v_texCoord; + attribute highp vec2 a_texCoord; + + void main() + { + gl_Position = a_position * vec4(a_position.w,a_position.w,0,1); + v_texCoord = (a_position.xy / 2.0) + vec2(0.5,0.5); + } + `, frag: ` - varying highp vec2 v_texCoord; - - uniform sampler2D u_drawTex; - - void main() - { - gl_FragColor = texture2D(u_drawTex, v_texCoord); - gl_FragColor.rgb *= gl_FragColor.a; - } - `, + varying highp vec2 v_texCoord; + + uniform sampler2D u_drawTex; + + void main() + { + gl_FragColor = texture2D(u_drawTex, v_texCoord); + gl_FragColor.rgb *= gl_FragColor.a; + } + `, }, ProgramInf: null, }, @@ -454,16 +478,12 @@ gl.bindBuffer(gl.ARRAY_BUFFER, depthVertexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, null); } - - //?Link some stuff to the draw region - //?Might be a better way but I've tried many different things and they didn't work. - let drawnFirst = false; - renderer.oldEnterDrawRegion = renderer.enterDrawRegion; - renderer.enterDrawRegion = (region) => { - triFunctions.drawOnScreen(); - renderer.oldEnterDrawRegion(region); - drawnFirst = false; - }; + //renderer.oldEnterDrawRegion = renderer.enterDrawRegion; + //renderer.enterDrawRegion = (region) => { + // console.log(region) + // renderer.oldEnterDrawRegion(region); + // drawnFirst = false; + //}; //?Override pen Clear with pen+ renderer.penClear = (penSkinID) => { @@ -471,7 +491,7 @@ lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); //Pen+ Overrides default pen Clearing gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); - gl.clearColor(1, 1, 1, 1); + gl.clearColor(0, 0, 0, 0); gl.clear(gl.DEPTH_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT); @@ -498,9 +518,8 @@ //?Have this here for ez pz tri drawing on the canvas const triFunctions = { drawTri: (x1, y1, x2, y2, x3, y3, penColor, targetID) => { - lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); - gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + if (!inDrawRegion) renderer.enterDrawRegion(penPlusDrawRegion); + trianglesDrawn += 1; //? get triangle attributes for current sprite. const triAttribs = triangleAttributesOfAllSprites[targetID]; @@ -590,18 +609,11 @@ gl.useProgram(penPlusShaders.untextured.ProgramInf.program); gl.drawArrays(gl.TRIANGLES, 0, 3); - //? Hacky fix but it works. - - gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); - - gl.useProgram(penPlusShaders.pen.program); - if (!drawnFirst) triFunctions.drawOnScreen(); }, drawTextTri: (x1, y1, x2, y2, x3, y3, targetID, texture) => { - lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); - gl.viewport(0, 0, nativeSize[0], nativeSize[1]); + if (!inDrawRegion) renderer.enterDrawRegion(penPlusDrawRegion); + trianglesDrawn += 1; //? get triangle attributes for current sprite. const triAttribs = triangleAttributesOfAllSprites[targetID]; if (triAttribs) { @@ -712,18 +724,12 @@ gl.uniform1i(u_texture_Location_text, 0); gl.drawArrays(gl.TRIANGLES, 0, 3); - - gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); - - gl.useProgram(penPlusShaders.pen.program); - if (!drawnFirst) triFunctions.drawOnScreen(); }, //? this is so I don't have to go through the hassle of replacing default scratch shaders //? many of curse words where exchanged between me and a pillow while writing this extension //? but I have previaled! drawOnScreen: () => { - drawnFirst = true; gl.viewport(0, 0, nativeSize[0], nativeSize[1]); vertexBufferData = new Float32Array([ -1, -1, 0, 1, 0, 1, @@ -731,6 +737,12 @@ 1, -1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, + + -1, -1, 0, 1, 0, 1, + + -1, 1, 0, 1, 0, 0, + + 1, 1, 0, 1, 1, 0, ]); //? Bind Positional Data @@ -758,28 +770,8 @@ gl.uniform1i(u_depthTexture_Location_draw, 1); - gl.drawArrays(gl.TRIANGLES, 0, 3); - - vertexBufferData = new Float32Array([ - -1, -1, 0, 1, 0, 1, - - -1, 1, 0, 1, 0, 0, - - 1, 1, 0, 1, 1, 0, - ]); - - gl.bufferData(gl.ARRAY_BUFFER, vertexBufferData, gl.DYNAMIC_DRAW); - - gl.drawArrays(gl.TRIANGLES, 0, 3); - - lastFB = gl.getParameter(gl.FRAMEBUFFER_BINDING); - gl.bindFramebuffer(gl.FRAMEBUFFER, triFrameBuffer); - let occ = gl.getParameter(gl.COLOR_CLEAR_VALUE); - gl.clearColor(0, 0, 0, 0); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.clearColor(occ[0], occ[1], occ[2], occ[3]); + gl.drawArrays(gl.TRIANGLES, 0, 6); gl.bindFramebuffer(gl.FRAMEBUFFER, lastFB); - gl.useProgram(penPlusShaders.pen.program); }, setValueAccordingToCaseTriangle: ( @@ -1117,7 +1109,6 @@ return { blocks: [ { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, text: "Pen Properties", }, @@ -1168,7 +1159,6 @@ filter: "sprite", }, { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, text: "Square Pen Blocks", }, @@ -1241,7 +1231,6 @@ filter: "sprite", }, { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, text: "Triangle Blocks", }, @@ -1389,7 +1378,6 @@ filter: "sprite", }, { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, text: "Color", }, @@ -1416,7 +1404,6 @@ }, }, { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, text: "Images", }, @@ -1568,9 +1555,13 @@ }, }, { - opcode: "__NOUSEOPCODE", blockType: Scratch.BlockType.LABEL, - text: "Advanced options", + text: "Advanced Blocks", + }, + { + opcode: "getTrianglesDrawn", + blockType: Scratch.BlockType.REPORTER, + text: "Triangles Drawn", }, { disableMonitor: true, @@ -2548,6 +2539,9 @@ return ""; } } + getTrianglesDrawn() { + return trianglesDrawn; + } turnAdvancedSettingOff({ Setting, onOrOff }) { if (onOrOff == "on") { penPlusAdvancedSettings[Setting] = true; From 4281af4c0955deac9f22c475b6526899c1e22795 Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sat, 10 Feb 2024 01:48:28 -0600 Subject: [PATCH 100/196] Update l10n (#1291) --- translations/extension-runtime.json | 57 +++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/translations/extension-runtime.json b/translations/extension-runtime.json index 7d995835e9..b1d34fc10a 100644 --- a/translations/extension-runtime.json +++ b/translations/extension-runtime.json @@ -717,18 +717,42 @@ "-SIPC-/consoles@_Warning": "警告", "-SIPC-/consoles@_Warning [string]": "警告 [string]", "-SIPC-/consoles@_group": "グループ", + "-SIPC-/time@_April": "4月", + "-SIPC-/time@_August": "8月", + "-SIPC-/time@_December": "12月", + "-SIPC-/time@_February": "2月", + "-SIPC-/time@_January": "1月", + "-SIPC-/time@_July": "7月", + "-SIPC-/time@_June": "6月", + "-SIPC-/time@_March": "3月", + "-SIPC-/time@_May": "5月", + "-SIPC-/time@_November": "11月", + "-SIPC-/time@_October": "10月", + "-SIPC-/time@_September": "9月", "-SIPC-/time@_Time": "時間", "-SIPC-/time@_convert [time] to timestamp": "[time]をタイムスタンプに変換する", "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "[timestamp]をYYYY-MM-DD HH:MM:SSに変換する", "-SIPC-/time@_current time zone": "現在のタイムゾーン", "-SIPC-/time@_current timestamp": "現在のタイムスタンプ", "-SIPC-/time@_day": "日", + "-SIPC-/time@_days": "日", + "-SIPC-/time@_difference between [DATE] and now in [TIME_MENU]": "[DATE]と現在時刻の[TIME_MENU]との差", + "-SIPC-/time@_difference between [START] and [END] in [TIME_MENU]": "[START]と[END]の[TIME_MENU]での差", + "-SIPC-/time@_exact": "正確", + "-SIPC-/time@_format [VALUE] seconds as [ROUND] time": "[VALUE]秒を[ROUND]な時間としてフォーマット", "-SIPC-/time@_get [Timedata] from [timestamp]": "[timestamp]から[Timedata]を取得する", "-SIPC-/time@_hour": "時", + "-SIPC-/time@_hours": "時", "-SIPC-/time@_minute": "分", + "-SIPC-/time@_minutes": "分", "-SIPC-/time@_month": "月", + "-SIPC-/time@_months": "月", + "-SIPC-/time@_number of days in [MONTH] [YEAR]": "[YEAR]年の[MONTH]月の日数", + "-SIPC-/time@_rounded": "切り上げた", "-SIPC-/time@_second": "秒", + "-SIPC-/time@_seconds": "秒", "-SIPC-/time@_year": "年", + "-SIPC-/time@_years": "年", "0832/rxFS2@clean": "ファイルシステムを削除する", "0832/rxFS2@del": "[STR]を削除", "0832/rxFS2@folder": "[STR]を[STR2]にセットする", @@ -741,8 +765,8 @@ "0832/rxFS2@start": "[STR]を作成", "0832/rxFS2@sync": "[STR]のロケーションを[STR2]に変更する", "0832/rxFS2@webin": "[STR]をウェブから読み込む", - "Alestore/nfcwarp@_NFC supported?": "NFCはサポートされていますか?", - "Alestore/nfcwarp@_Only works in Chrome on Android": "Android上のChromeのみで動作", + "Alestore/nfcwarp@_NFC supported?": "NFCはサポートされているか", + "Alestore/nfcwarp@_Only works in Chrome on Android": "Android上のChromeでのみで動作", "Alestore/nfcwarp@_read NFC tag": "NFCタグを読み取る", "CST1229/zip@_1 (fast, large)": "1(高速、大きい)", "CST1229/zip@_9 (slowest, smallest)": "9(低速、小さい)", @@ -763,7 +787,9 @@ "CST1229/zip@_file [FILE] as [TYPE]": "ファイル[FILE]を[TYPE]として", "CST1229/zip@_folder": "フォルダー", "CST1229/zip@_go to directory [DIR]": "[DIR]ディレクトリへ行く", - "CST1229/zip@_modification date": "データ修正", + "CST1229/zip@_long modification date": "詳細な最終更新日時", + "CST1229/zip@_modification date": "最終更新日時", + "CST1229/zip@_modified days since 2000": "2000年以降の変更された日数", "CST1229/zip@_name": "名前", "CST1229/zip@_new file": "新しいファイル", "CST1229/zip@_new folder": "新しいフォルダ", @@ -2599,17 +2625,42 @@ "-SIPC-/consoles@_Warning": "警告", "-SIPC-/consoles@_Warning [string]": "打印警告[string]", "-SIPC-/consoles@_group": "群组", + "-SIPC-/time@_April": "四月", + "-SIPC-/time@_August": "八月", + "-SIPC-/time@_December": "十二月", + "-SIPC-/time@_February": "二月", + "-SIPC-/time@_January": "一月", + "-SIPC-/time@_July": "七月", + "-SIPC-/time@_June": "六月", + "-SIPC-/time@_March": "三月", + "-SIPC-/time@_May": "五月", + "-SIPC-/time@_November": "十一月", + "-SIPC-/time@_October": "十月", + "-SIPC-/time@_September": "九月", "-SIPC-/time@_Time": "时间", "-SIPC-/time@_convert [time] to timestamp": "把时间[time]转换为时间戳", + "-SIPC-/time@_convert [timestamp] to YYYY-MM-DD HH:MM:SS": "解析[timestamp]为年-月-日 时-分-秒", "-SIPC-/time@_current time zone": "时区", "-SIPC-/time@_current timestamp": "时间戳", "-SIPC-/time@_day": "日", + "-SIPC-/time@_days": "日", + "-SIPC-/time@_difference between [DATE] and now in [TIME_MENU]": "[DATE]与现在[TIME_MENU]的差距", + "-SIPC-/time@_difference between [START] and [END] in [TIME_MENU]": "[START]和[END]在[TIME_MENU]的差距", + "-SIPC-/time@_exact": "精确", + "-SIPC-/time@_format [VALUE] seconds as [ROUND] time": "将[VALUE]秒转换为[ROUND]时间", "-SIPC-/time@_get [Timedata] from [timestamp]": "从时间戳[timestamp]获取[Timedata]", "-SIPC-/time@_hour": "时", + "-SIPC-/time@_hours": "时", "-SIPC-/time@_minute": "分", + "-SIPC-/time@_minutes": "分", "-SIPC-/time@_month": "月", + "-SIPC-/time@_months": "月", + "-SIPC-/time@_number of days in [MONTH] [YEAR]": "在[MONTH][YEAR]之间的天数", + "-SIPC-/time@_rounded": "粗略", "-SIPC-/time@_second": "秒", + "-SIPC-/time@_seconds": "秒", "-SIPC-/time@_year": "年", + "-SIPC-/time@_years": "年", "0832/rxFS2@clean": "清空文件系统", "0832/rxFS2@del": "删除 [STR]", "0832/rxFS2@folder": "设置 [STR] 为 [STR2]", From 8ec3df48900dfcb0427eb0cf2acadc35b895af67 Mon Sep 17 00:00:00 2001 From: DNin01 <106490990+DNin01@users.noreply.github.com> Date: Sun, 11 Feb 2024 14:14:10 -0800 Subject: [PATCH 101/196] Update the AI models' names (#1293) Microsoft Bing Chat was rebranded to Copilot and Google Bard was renamed to Gemini. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b1305dbd8..68f0f25863 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Every extension is also covered under [our bug bounty](https://github.com/TurboW ## On AI language models -**Generative AI language models like ChatGPT, Bing Chat, and Bard DO NOT know how to write proper extensions for TurboWarp.** Remember that the ChatGPT knowledge cutoff is in 2021 while our extension system did not exist until late 2022, thus it *literally can't know*. Pull requests submitting extensions that are made by AI (it's really obvious) will be closed as invalid. +**Generative AI language models like ChatGPT, Copilot, and Gemini DO NOT know how to write proper extensions for TurboWarp.** Remember that the ChatGPT knowledge cutoff is in 2021 while our extension system did not exist until late 2022, thus it *literally can't know*. Pull requests submitting extensions that are made by AI (it's really obvious) will be closed as invalid. ## Writing extensions From 9067a20eba0ecc342b9e1a09567b74ecbd45a432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 08:21:34 -0600 Subject: [PATCH 102/196] build(deps): bump @turbowarp/types from `86dc10a` to `b0f4400` (#1295) --- package-lock.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8fffdb806a..46c997395b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,7 +165,8 @@ }, "node_modules/@turbowarp/types": { "version": "0.0.12", - "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#86dc10a37c1c9fa60656385303a1458548719b19", + "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#b0f4400a3fc6bed1767a7ac8b4241444ca554b72", + "integrity": "sha512-weWO3veyLNf9CMHRLExIQOqcUJV2XIYld3VdnZu9T/4YLF6jl1Ik50CJx7hz/jPjbKTg134PBoMEqc4JeCmfiQ==", "license": "Apache-2.0" }, "node_modules/@ungap/structured-clone": { From 13967870af3e881d3317436b487f65904232b12e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 08:22:33 -0600 Subject: [PATCH 103/196] build(deps): bump chokidar from 3.5.3 to 3.6.0 (#1294) --- package-lock.json | 17 +++++++---------- package.json | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46c997395b..6d8713609c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@turbowarp/scratchblocks": "^3.6.4", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.10", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "ejs": "^3.1.9", "express": "^4.18.2", "image-size": "^1.1.1", @@ -397,15 +397,9 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -418,6 +412,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } diff --git a/package.json b/package.json index 8146486a8d..e37c0f3936 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@turbowarp/scratchblocks": "^3.6.4", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.10", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "ejs": "^3.1.9", "express": "^4.18.2", "image-size": "^1.1.1", From 4ad6d648d9579dfe8c7a2d71a7d88212af47ebf2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 18 Feb 2024 00:14:26 +0000 Subject: [PATCH 104/196] Add Lily/ListTools (#623) --- extensions/Lily/ListTools.js | 645 +++++++++++++++++++++++++++++++++++ extensions/extensions.json | 1 + images/Lily/ListTools.svg | 1 + images/README.md | 4 + 4 files changed, 651 insertions(+) create mode 100644 extensions/Lily/ListTools.js create mode 100644 images/Lily/ListTools.svg diff --git a/extensions/Lily/ListTools.js b/extensions/Lily/ListTools.js new file mode 100644 index 0000000000..44e065abeb --- /dev/null +++ b/extensions/Lily/ListTools.js @@ -0,0 +1,645 @@ +// Name: List Tools +// ID: lmsListTools +// Description: An assortment of new ways to interact with lists. +// By: LilyMakesThings + +// (It's getting harder and harder to think of original descriptions now) + +(function (Scratch) { + "use strict"; + + /* -- SETUP -- */ + const vm = Scratch.vm; + const runtime = vm.runtime; + + const getVarObjectFromName = function (name, util, type) { + const stageTarget = runtime.getTargetForStage(); + const target = util.target; + let listObject = Object.create(null); + + listObject = stageTarget.lookupVariableByNameAndType(name, type); + if (listObject) return listObject; + listObject = target.lookupVariableByNameAndType(name, type); + if (listObject) return listObject; + }; + + class Data { + getInfo() { + return { + id: "lmsData", + name: "List Tools", + color1: "#ff661a", + color2: "#f2590d", + color3: "#e64d00", + blocks: [ + { + opcode: "deleteItems", + blockType: Scratch.BlockType.COMMAND, + text: "delete items [NUM1] to [NUM2] of [LIST]", + arguments: { + NUM1: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + NUM2: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "3", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "deleteAllOfItem", + blockType: Scratch.BlockType.COMMAND, + text: "delete all [ITEM] in [LIST]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "thing", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "replaceAllOfItem", + blockType: Scratch.BlockType.COMMAND, + text: "replace all [ITEM1] with [ITEM2] in [LIST]", + arguments: { + ITEM1: { + type: Scratch.ArgumentType.STRING, + defaultValue: "apple", + }, + ITEM2: { + type: Scratch.ArgumentType.STRING, + defaultValue: "banana", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "repeatList", + blockType: Scratch.BlockType.COMMAND, + text: "repeat [LIST1] [NUM] times in [LIST2]", + arguments: { + LIST1: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + LIST2: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + NUM: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "3", + }, + }, + }, + + "---", + + { + opcode: "getListJoin", + blockType: Scratch.BlockType.REPORTER, + text: "get list [LIST] joined by [STRING]", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + STRING: { + type: Scratch.ArgumentType.STRING, + defaultValue: ",", + }, + }, + }, + { + opcode: "timesItemAppears", + blockType: Scratch.BlockType.REPORTER, + text: "# of times [ITEM] appears in [LIST]", + disableMonitor: true, + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "thing", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "itemIndex", + blockType: Scratch.BlockType.REPORTER, + text: "index # [INDEX] of item [ITEM] in [LIST]", + disableMonitor: true, + arguments: { + INDEX: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "thing", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + + "---", + + { + opcode: "listIsEmpty", + blockType: Scratch.BlockType.BOOLEAN, + text: "[LIST] is empty?", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "itemNumExists", + blockType: Scratch.BlockType.BOOLEAN, + text: "item [NUM] exists in [LIST]?", + disableMonitor: true, + arguments: { + NUM: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "orderIs", + blockType: Scratch.BlockType.BOOLEAN, + text: "order of [LIST] is [ORDER]?", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + ORDER: { + type: Scratch.ArgumentType.STRING, + menu: "orderTypeSort", + }, + }, + }, + + "---", + + { + opcode: "orderList", + blockType: Scratch.BlockType.COMMAND, + text: "set order of [LIST] to [ORDER]", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + ORDER: { + type: Scratch.ArgumentType.STRING, + defaultValue: "reversed", + menu: "orderType", + }, + }, + }, + { + opcode: "setListToList", + blockType: Scratch.BlockType.COMMAND, + text: "set items of [LIST1] to [LIST2]", + arguments: { + LIST1: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + LIST2: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "joinLists", + blockType: Scratch.BlockType.COMMAND, + text: "concatenate [LIST1] onto [LIST2]", + arguments: { + LIST1: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + LIST2: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + + "---", + + { + opcode: "forEachListItem", + blockType: Scratch.BlockType.LOOP, + text: "for each item value [VAR] in [LIST]", + hideFromPalette: + !runtime.extensionManager.isExtensionLoaded("lmsTempVars2"), + arguments: { + VAR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "thread variable", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + { + opcode: "forEachListItemNum", + blockType: Scratch.BlockType.LOOP, + text: "for each item # [VAR] in [LIST]", + hideFromPalette: + !runtime.extensionManager.isExtensionLoaded("lmsTempVars2"), + arguments: { + VAR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "thread variable", + }, + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + + "---", + + { + opcode: "setListArray", + blockType: Scratch.BlockType.COMMAND, + text: "set [LIST] to array [ARRAY]", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: '["apple","banana"]', + }, + }, + }, + { + opcode: "getListArray", + blockType: Scratch.BlockType.REPORTER, + text: "[LIST] as array", + disableMonitor: true, + arguments: { + LIST: { + type: Scratch.ArgumentType.STRING, + menu: "lists", + }, + }, + }, + ], + menus: { + operator: { + acceptReporters: false, + items: [ + { + text: "=", + value: "=", + }, + { + text: ">", + value: ">", + }, + { + text: "<", + value: "<", + }, + ], + }, + orderType: { + acceptReporters: false, + items: [ + { + text: "reversed", + value: "reversed", + }, + { + text: "ascending", + value: "ascending", + }, + { + text: "descending", + value: "descending", + }, + { + text: "randomised", + value: "randomised", + }, + ], + }, + orderTypeSort: { + acceptReporters: false, + items: [ + { + text: "ascending", + value: "ascending", + }, + { + text: "descending", + value: "descending", + }, + ], + }, + indexType: { + acceptReporters: false, + items: [ + { + text: "first", + value: "first", + }, + { + text: "last", + value: "last", + }, + { + text: "random", + value: "random", + }, + ], + }, + lists: { + acceptReporters: true, + items: "_getLists", + }, + }, + }; + } + + deleteItems(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return false; + const listLength = list.value.length; + let num1 = 0; + let num2 = 0; + if (!list) return; + if (args.NUM1 > args.NUM2) { + num1 = args.NUM2 - 1; + num2 = args.NUM1 - 1; + } else { + num1 = args.NUM1 - 1; + num2 = args.NUM2 - 1; + } + const listPart1 = list.value.slice(0, num1); + const listPart2 = list.value.slice(num2 + 1, listLength); + list.value = listPart1.concat(listPart2); + } + + deleteAllOfItem(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return; + const newList = list.value.filter(function (model) { + return model !== args.ITEM; + }); + list.value = newList; + } + + replaceAllOfItem(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return; + const listLength = list.value.length; + const item1 = args.ITEM1; + const item2 = args.ITEM2; + let newList = []; + for (let i = 0; i < listLength; i++) { + if (list.value[i] === item1) { + newList.push(item2); + } else { + newList.push(list.value[i]); + } + } + list.value = newList; + } + + repeatList(args, util) { + const list1 = getVarObjectFromName(args.LIST1, util, "list"); + if (!list1) return; + const list2 = getVarObjectFromName(args.LIST2, util, "list"); + if (!list2) return; + const currentVal = list1.value; + for (let i = 0; i < args.NUM; i++) { + list1.value = list1.value.concat(currentVal); + } + } + + getListJoin(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return ""; + return list.value.join(args.STRING); + } + + timesItemAppears(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return 0; + return list.value.filter((model) => model == args.ITEM).length; + } + + itemIndex(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return 0; + let indexes = []; + for (let index = 0; index < list.value.length; index++) { + if (list.value[index] === args.ITEM) { + indexes.push(index); + } + } + + switch (args.INDEX) { + case "_first_": + return Scratch.Cast.toNumber(indexes[0] + 1); + case "_last_": + return Scratch.Cast.toNumber(indexes[indexes.length - 1] + 1); + case "_random_": + return Scratch.Cast.toNumber( + indexes[Math.floor(Math.random() * indexes.length)] + 1 + ); + default: + return Scratch.Cast.toNumber(indexes[args.INDEX - 1] + 1); + } + } + + listIsEmpty(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return true; + if (list.value.length > 0) return false; + return true; + } + + itemNumExists(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return false; + const listIndex = Scratch.Cast.toListIndex( + args.NUM, + list.value.length, + false + ); + if (listIndex === Scratch.Cast.LIST_INVALID) return false; + return true; + } + + orderIs(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return false; + + for (let i = 0; i < list.value.length - 1; i++) { + const compare = Scratch.Cast.compare(list.value[i + 1], list.value[i]); + if (compare > 0 && args.ORDER === "descending") return false; + if (compare < 0 && args.ORDER === "ascending") return false; + } + return true; + } + + orderList(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return; + if (args.ORDER === "reversed") { + list.value.reverse(); + } else if (args.ORDER === "randomised") { + const randomised = list.value + .map((value) => ({ value, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ value }) => value); + list.value = randomised; + } else if (args.ORDER === "ascending") { + list.value.sort(Scratch.Cast.compare); + } else if (args.ORDER === "descending") { + list.value.sort(Scratch.Cast.compare).reverse(); + } + list._monitorUpToDate = false; + } + + setListToList(args, util) { + const list1 = getVarObjectFromName(args.LIST1, util, "list"); + if (!list1) return; + const list2 = getVarObjectFromName(args.LIST2, util, "list"); + if (!list2) return; + list1.value = list2.value; + } + + joinLists(args, util) { + const list1 = getVarObjectFromName(args.LIST1, util, "list"); + if (!list1) return; + const list2 = getVarObjectFromName(args.LIST2, util, "list"); + if (!list2) return; + list2.value = list2.value.concat(list1.value); + } + + forEachListItem(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return false; + const listLength = list.value.length; + + const thread = util.thread; + if (!thread.variables) thread.variables = {}; + const vars = thread.variables; + + if (typeof util.stackFrame.index === "undefined") { + util.stackFrame.index = 0; + } + + if (util.stackFrame.index < listLength) { + let itemIndex = util.stackFrame.index; + vars[args.VAR] = list.value[itemIndex]; + util.stackFrame.index++; + return true; + } + } + + forEachListItemNum(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return false; + const listLength = list.value.length; + + const thread = util.thread; + if (!thread.variables) thread.variables = {}; + const vars = thread.variables; + + if (typeof util.stackFrame.index === "undefined") { + util.stackFrame.index = 0; + } + + if (util.stackFrame.index < listLength) { + util.stackFrame.index++; + vars[args.VAR] = util.stackFrame.index; + return true; + } + } + + setListArray(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return; + + let array; + try { + array = JSON.parse(args.ARRAY); + } catch (error) { + return; + } + + if (!Array.isArray(array)) return; + const newArray = array; + list.value = newArray; + list._monitorUpToDate = false; + } + + getListArray(args, util) { + const list = getVarObjectFromName(args.LIST, util, "list"); + if (!list) return ""; + return JSON.stringify(list.value); + } + + _getLists() { + // @ts-expect-error - Blockly not typed yet + // eslint-disable-next-line no-undef + const lists = + typeof Blockly === "undefined" + ? [] + : Blockly.getMainWorkspace() + .getVariableMap() + .getVariablesOfType("list") + .map((model) => model.name); + if (lists.length > 0) { + return lists; + } else { + return [""]; + } + } + } + Scratch.extensions.register(new Data()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 5b983b75b8..b8b4a1581c 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -27,6 +27,7 @@ "Lily/ClonesPlus", "Lily/LooksPlus", "Lily/MoreEvents", + "Lily/ListTools", "NexusKitten/moremotion", "CubesterYT/WindowControls", "veggiecan/browserfullscreen", diff --git a/images/Lily/ListTools.svg b/images/Lily/ListTools.svg new file mode 100644 index 0000000000..11feacec82 --- /dev/null +++ b/images/Lily/ListTools.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index 495fbb26d6..03e7082343 100644 --- a/images/README.md +++ b/images/README.md @@ -269,6 +269,10 @@ All images in this folder are licensed under the [GNU General Public License ver ## Lily/AllMenus.svg - Created by [YogaindoCR](https://github.com/YogaindoCR) in https://github.com/TurboWarp/extensions/issues/90#issuecomment-1681839774 +## Lily/ListTools.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings). + - Background "blobs" by Scratch. + ## Lily/MoreEvents.svg - Created by [@LilyMakesThings](https://github.com/LilyMakesThings). - Background "blobs" by Scratch. From 5230e8724a431529574a5816aaf270c5aa9d6a88 Mon Sep 17 00:00:00 2001 From: mybearworld <130385691+mybearworld@users.noreply.github.com> Date: Sun, 18 Feb 2024 01:38:46 +0100 Subject: [PATCH 105/196] Add mbw/xml (#1189) This extension adds capability of interacting with XML. This includes SVG and XHTML. --- extensions/extensions.json | 1 + extensions/mbw/xml.js | 556 +++++++++++++++++++++++++++++++++++++ images/mbw/xml.svg | 1 + 3 files changed, 558 insertions(+) create mode 100644 extensions/mbw/xml.js create mode 100644 images/mbw/xml.svg diff --git a/extensions/extensions.json b/extensions/extensions.json index b8b4a1581c..4ce7bf8d5c 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -60,6 +60,7 @@ "Lily/Assets", "DNin/wake-lock", "Skyhigh173/json", + "mbw/xml", "cs2627883/numericalencoding", "DT/cameracontrols", "TheShovel/CanvasEffects", diff --git a/extensions/mbw/xml.js b/extensions/mbw/xml.js new file mode 100644 index 0000000000..01e1f257db --- /dev/null +++ b/extensions/mbw/xml.js @@ -0,0 +1,556 @@ +// Name: XML +// ID: mbwxml +// Description: Create and extract values from XML. +// By: mybearworld + +(function (Scratch) { + "use strict"; + + class XML { + constructor() { + this.domParser = new DOMParser(); + } + /** + * @param {string} string + * @returns {{xml: null; error: string} | {xml: HTMLElement; error: null}} + */ + stringToXml(string) { + const doc = this.domParser.parseFromString(string, "application/xml"); + const error = doc.querySelector("parsererror"); + if (error) { + console.error(error.textContent); + return { xml: null, error: error.textContent }; + } + return { xml: doc.documentElement, error: null }; + } + /** @param {Element} element */ + xmlToString(element) { + return element.outerHTML; + } + + /** @returns {Scratch.Info} */ + getInfo() { + return { + id: "mbwxml", + name: "XML", + color1: "#6c2b5f", + blocks: [ + // For translations: + // - Block text should be translated + // - Default XML and attributes should NOT be translated because we can't expect translators + // to know how to write valid XML in their language. + { + opcode: "isValid", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is [MAYBE_XML] valid XML?"), + arguments: { + MAYBE_XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "errorMessage", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("error message of [MAYBE_XML]"), + arguments: { + MAYBE_XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + "---", + { + opcode: "tagName", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("tag name of [XML]"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "textContent", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("text of [XML]"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "world", + }, + }, + }, + "---", + { + opcode: "attributes", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("attributes of [XML]"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "hasAttribute", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("does [XML] have attribute [ATTR]?"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + ATTR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "foo", + }, + }, + }, + { + opcode: "setAttribute", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("set attribute [ATTR] of [XML] to [VALUE]"), + arguments: { + ATTR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "apple", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "foo", + }, + }, + }, + { + opcode: "getAttribute", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("attribute [ATTR] of [XML]"), + arguments: { + ATTR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "apple", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "removeAttribute", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("remove attribute [ATTR] of [XML]"), + arguments: { + ATTR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "apple", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + "---", + { + opcode: "hasChildren", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("does [XML] have children?"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "childrenAmount", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("children amount of [XML]"), + arguments: { + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "addChild", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("add child [CHILD] to [XML]"), + arguments: { + CHILD: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "replaceChild", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate( + "replace child #[NO] of [XML] with [CHILD]" + ), + arguments: { + NO: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + CHILD: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "getChild", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("child #[NO] of [XML]"), + arguments: { + NO: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + { + opcode: "removeChild", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("remove child #[NO] of [XML]"), + arguments: { + NO: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "2", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + }, + "---", + { + opcode: "querySuccessful", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("query [QUERY] on [XML] matches?"), + arguments: { + QUERY: { + type: Scratch.ArgumentType.STRING, + defaultValue: ".foo", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "querySelector", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("query [QUERY] on [XML]"), + arguments: { + QUERY: { + type: Scratch.ArgumentType.STRING, + defaultValue: ".foo", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + { + opcode: "querySelectorAll", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("query all [QUERY] on [XML]"), + arguments: { + QUERY: { + type: Scratch.ArgumentType.STRING, + defaultValue: ".foo", + }, + XML: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + }, + }, + ], + }; + } + + /** + * @param {object} args + * @param {unknown} args.MAYBE_XML + */ + isValid({ MAYBE_XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(MAYBE_XML)); + return xml !== null; + } + + /** + * @param {object} args + * @param {unknown} args.MAYBE_XML + */ + errorMessage({ MAYBE_XML }) { + const { xml, error } = this.stringToXml(Scratch.Cast.toString(MAYBE_XML)); + return xml === null ? error : ""; + } + + /** + * @param {object} args + * @param {unknown} args.XML + */ + tagName({ XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + return xml.tagName; + } + + /** + * @param {object} args + * @param {unknown} args.XML + */ + textContent({ XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + return xml.textContent; + } + + /** + * @param {object} args + * @param {unknown} args.XML + */ + attributes({ XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + return JSON.stringify([...xml.attributes].map((attr) => attr.name)); + } + + /** + * @param {object} args + * @param {unknown} args.XML + * @param {unknown} args.ATTR + */ + hasAttribute({ XML, ATTR }) { + return this.getAttribute({ XML, ATTR }) !== ""; + } + + /** + * @param {object} args + * @param {unknown} args.ATTR + * @param {unknown} args.XML + * @param {unknown} args.VALUE + */ + setAttribute({ ATTR, XML, VALUE }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + xml.setAttribute( + Scratch.Cast.toString(ATTR), + Scratch.Cast.toString(VALUE) + ); + return this.xmlToString(xml); + } + + /** + * @param {object} args + * @param {unknown} args.ATTR + * @param {unknown} args.XML + */ + getAttribute({ ATTR, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + return xml.getAttribute(Scratch.Cast.toString(ATTR)) ?? ""; + } + + /** + * @param {object} args + * @param {unknown} args.ATTR + * @param {unknown} args.XML + */ + removeAttribute({ ATTR, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + xml.removeAttribute(Scratch.Cast.toString(ATTR)); + return this.xmlToString(xml); + } + + /** + * @param {object} args + * @param {unknown} args.XML + */ + hasChildren({ XML }) { + return this.childrenAmount({ XML }) !== 0; + } + + /** + * @param {object} args + * @param {unknown} args.XML + */ + childrenAmount({ XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return 0; + } + return xml.childElementCount; + } + + /** + * @param {object} args + * @param {unknown} args.CHILD + * @param {unknown} args.XML + */ + addChild({ CHILD, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const { xml: childXML } = this.stringToXml(Scratch.Cast.toString(CHILD)); + if (childXML === null) { + return this.xmlToString(xml); + } + xml.append(childXML); + return this.xmlToString(xml); + } + + /** + * @param {object} args + * @param {unknown} args.NO + * @param {unknown} args.XML + * @param {unknown} args.CHILD + */ + replaceChild({ NO, XML, CHILD }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const { xml: childXML } = this.stringToXml(Scratch.Cast.toString(CHILD)); + if (childXML === null) { + return this.xmlToString(xml); + } + const originalChild = + xml.children[Math.floor(Scratch.Cast.toNumber(NO)) - 1]; + if (originalChild === undefined) { + return this.xmlToString(xml); + } + xml.replaceChild(childXML, originalChild); + return this.xmlToString(xml); + } + + /** + * @param {object} args + * @param {unknown} args.NO + * @param {unknown} args.XML + */ + getChild({ NO, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const child = xml.children[Math.floor(Scratch.Cast.toNumber(NO)) - 1]; + if (child === undefined) { + return ""; + } + return this.xmlToString(child); + } + + /** + * @param {object} args + * @param {unknown} args.NO + * @param {unknown} args.XML + */ + removeChild({ NO, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const child = xml.children[Math.floor(Scratch.Cast.toNumber(NO)) - 1]; + if (child === undefined) { + return this.xmlToString(xml); + } + xml.removeChild(child); + return this.xmlToString(xml); + } + + /** + * @param {object} args + * @param {unknown} args.QUERY + * @param {unknown} args.XML + */ + querySuccessful({ QUERY, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const child = xml.querySelector(Scratch.Cast.toString(QUERY)); + return child !== null; + } + + /** + * @param {object} args + * @param {unknown} args.QUERY + * @param {unknown} args.XML + */ + querySelector({ QUERY, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const child = xml.querySelector(Scratch.Cast.toString(QUERY)); + if (child === null) { + return ""; + } + return this.xmlToString(child); + } + /** + * @param {object} args + * @param {unknown} args.QUERY + * @param {unknown} args.XML + */ + querySelectorAll({ QUERY, XML }) { + const { xml } = this.stringToXml(Scratch.Cast.toString(XML)); + if (xml === null) { + return ""; + } + const child = xml.querySelectorAll(Scratch.Cast.toString(QUERY)); + if (child.length === 0) { + return ""; + } + return JSON.stringify([...child].map(this.xmlToString)); + } + } + + Scratch.extensions.register(new XML()); +})(Scratch); diff --git a/images/mbw/xml.svg b/images/mbw/xml.svg new file mode 100644 index 0000000000..2a019005fd --- /dev/null +++ b/images/mbw/xml.svg @@ -0,0 +1 @@ + \ No newline at end of file From 52126718f217f8e95d794af4ad6f0ff4abad1b3b Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sat, 17 Feb 2024 18:38:56 -0600 Subject: [PATCH 106/196] Support comments in extensions.json (#1304) --- development/builder.js | 3 ++- extensions/extensions.json | 3 ++- package-lock.json | 6 ++++++ package.json | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/development/builder.js b/development/builder.js index f2dfb46648..f327892d81 100644 --- a/development/builder.js +++ b/development/builder.js @@ -1,6 +1,7 @@ const fs = require("fs"); const AdmZip = require("adm-zip"); const pathUtil = require("path"); +const ExtendedJSON = require("@turbowarp/json"); const compatibilityAliases = require("./compatibility-aliases"); const parseMetadata = require("./parse-extension-metadata"); const { mkdirp, recursiveReadDirectory } = require("./fs-utils"); @@ -679,7 +680,7 @@ class Builder { build() { const build = new Build(this.mode); - const featuredExtensionSlugs = JSON.parse( + const featuredExtensionSlugs = ExtendedJSON.parse( fs.readFileSync( pathUtil.join(this.extensionsRoot, "extensions.json"), "utf-8" diff --git a/extensions/extensions.json b/extensions/extensions.json index 4ce7bf8d5c..58a0294d03 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -1,4 +1,5 @@ [ + // This file supports comments "lab/text", "stretch", "gamepad", @@ -86,5 +87,5 @@ "itchio", "gamejolt", "obviousAlexC/newgroundsIO", - "Lily/McUtils" + "Lily/McUtils" // McUtils should always be the last item. ] diff --git a/package-lock.json b/package-lock.json index 6d8713609c..82e96d8f91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@turbowarp/json": "^0.1.2", "@turbowarp/scratchblocks": "^3.6.4", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.10", @@ -158,6 +159,11 @@ "node": ">= 8" } }, + "node_modules/@turbowarp/json": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@turbowarp/json/-/json-0.1.2.tgz", + "integrity": "sha512-9nWywp+0SH7ROVzQPQQO9gMWBikahsqyMWp1Ku8VV0q+q6bnx6dS0aNPTjqTtF2GHAY55hcREsqKzaoUdWBSwg==" + }, "node_modules/@turbowarp/scratchblocks": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/@turbowarp/scratchblocks/-/scratchblocks-3.6.4.tgz", diff --git a/package.json b/package.json index e37c0f3936..b49d4b542b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "homepage": "https://github.com/TurboWarp/extensions#readme", "dependencies": { "@turbowarp/scratchblocks": "^3.6.4", + "@turbowarp/json": "^0.1.2", "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", "adm-zip": "^0.5.10", "chokidar": "^3.6.0", From e252617682ff47f3e4b70736655ff536371061ff Mon Sep 17 00:00:00 2001 From: GarboMuffin Date: Sat, 17 Feb 2024 18:57:23 -0600 Subject: [PATCH 107/196] Unused variables are now an ESLint error (#1305) If it's not used then why is it there? --- .eslintrc.js | 14 ++++- extensions/0832/rxFS.js | 2 +- extensions/0832/rxFS2.js | 2 +- extensions/Lily/TempVariables2.js | 2 - extensions/NOname-awa/cn-number.js | 16 +----- extensions/NOname-awa/math-and-string.js | 16 ------ extensions/TheShovel/profanity.js | 1 + extensions/ar.js | 4 -- .../docs-examples/unsandboxed/every-second.js | 1 + extensions/gamejolt.js | 2 + extensions/obviousAlexC/SensingPlus.js | 2 - extensions/obviousAlexC/newgroundsIO.js | 3 -- extensions/obviousAlexC/penPlus.js | 16 ------ extensions/true-fantom/regexp.js | 51 +------------------ 14 files changed, 22 insertions(+), 110 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index db32ceab95..3ccd0caf6f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,7 +17,19 @@ module.exports = { scaffolding: 'readonly' }, rules: { - 'no-unused-vars': 'off', + // Unused variables commonly indicate logic errors + 'no-unused-vars': [ + 'error', + { + // Unused arguments are useful, eg. it can be nice for blocks to accept `args` even if they don't use it + args: 'none', + // Allow silently eating try { } catch { } + caughtErrors: 'none', + // Variables starting with _ are intentionally unused + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + } + ], // Allow while (true) { } 'no-constant-condition': [ 'error', diff --git a/extensions/0832/rxFS.js b/extensions/0832/rxFS.js index b903ed8828..ad0969d447 100644 --- a/extensions/0832/rxFS.js +++ b/extensions/0832/rxFS.js @@ -12,7 +12,7 @@ var rxFSfi = new Array(); var rxFSsy = new Array(); - var Search, i, str, str2; + var Search, str, str2; const file = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzMC4zMTIxMiIgaGVpZ2h0PSIyNC4yNDk2NyIgdmlld0JveD0iMCwwLDMwLjMxMjEyLDI0LjI0OTY3Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzA0Ljg0Mzk0LC0xNjcuODc1MTYpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIGZpbGw9IiNmZmI5MDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMzE5Ljk5OTk5LDE3MC45MDYzN2gxMi4xMjQ4M2MxLjY3NDEyLDAgMy4wMzEyNCwxLjM1NzEyIDMuMDMxMjQsMy4wMzEydjE1LjE1NjA3YzAsMS42NzQwOCAtMS4zNTcxMiwzLjAzMTIgLTMuMDMxMjQsMy4wMzEyaC0yNC4yNDk2NmMtMS42NzQxMiwwIC0zLjAzMTIyLC0xLjM1NzEyIC0zLjAzMTIyLC0zLjAzMTJ2LTE4LjE4NzI3YzAsLTEuNjgyMzIgMS4zNDg5LC0zLjAzMTIgMy4wMzEyMiwtMy4wMzEyaDkuMDkzNjN6Ii8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTUuMTU2MDYwMDAwMDAwMDI1OjEyLjEyNDgzNTAwMDAwMDAxOS0tPg=="; diff --git a/extensions/0832/rxFS2.js b/extensions/0832/rxFS2.js index 0ba12451ef..5979e7eda9 100644 --- a/extensions/0832/rxFS2.js +++ b/extensions/0832/rxFS2.js @@ -17,7 +17,7 @@ var rxFSfi = new Array(); var rxFSsy = new Array(); - var Search, i, str, str2; + var Search, str, str2; const folder = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIyOC40NjI1IiBoZWlnaHQ9IjI3LjciIHZpZXdCb3g9IjAsMCwyOC40NjI1LDI3LjciPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMjYuMDE5NTMsLTE2NC4xMTg3NSkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0iIzk5NjZmZiIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgZm9udC1mYW1pbHk9IlNhbnMgU2VyaWYiIGZvbnQtd2VpZ2h0PSJub3JtYWwiIGZvbnQtc2l6ZT0iNDAiIHRleHQtYW5jaG9yPSJzdGFydCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjx0ZXh0IHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyNi4yNjk1MywxODUuNzY4NzUpIHNjYWxlKDAuNSwwLjUpIiBmb250LXNpemU9IjQwIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBmb250LWZhbWlseT0iU2FucyBTZXJpZiIgZm9udC13ZWlnaHQ9Im5vcm1hbCIgdGV4dC1hbmNob3I9InN0YXJ0IiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHRzcGFuIHg9IjAiIGR5PSIwIj7wn5OBPC90c3Bhbj48L3RleHQ+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6MTMuOTgwNDY4NzU6MTUuODgxMjQ5MjM3MDYwNTMtLT4="; diff --git a/extensions/Lily/TempVariables2.js b/extensions/Lily/TempVariables2.js index 57bbd95925..60e494bee9 100644 --- a/extensions/Lily/TempVariables2.js +++ b/extensions/Lily/TempVariables2.js @@ -6,8 +6,6 @@ (function (Scratch) { "use strict"; - const menuIconURI = ""; - // Object.create(null) prevents "variable [toString]" from returning a function let runtimeVariables = Object.create(null); diff --git a/extensions/NOname-awa/cn-number.js b/extensions/NOname-awa/cn-number.js index 10e72cbb02..13bc19379f 100644 --- a/extensions/NOname-awa/cn-number.js +++ b/extensions/NOname-awa/cn-number.js @@ -1,21 +1,7 @@ (function (Scratch) { "use strict"; - let Number2, - null2, - units, - Number_in, - uppercase, - i, - N_Z, - o, - j, - After_decimal_point, - unit, - k, - C_Number, - m, - n; + let i, N_Z, o, j, After_decimal_point, unit, k, C_Number, m, n; class CNNUMBER { getInfo() { diff --git a/extensions/NOname-awa/math-and-string.js b/extensions/NOname-awa/math-and-string.js index 95eebf7554..e47b3db332 100644 --- a/extensions/NOname-awa/math-and-string.js +++ b/extensions/NOname-awa/math-and-string.js @@ -1156,22 +1156,6 @@ return text.replace(new RegExp(oldStr, "g"), newStr); }; - const sortAndUniqueWords_en = (text) => { - let words = text.toLowerCase().match(/\b\w+\b/g); - words = Array.from(new Set(words)); - words.sort(); - return words.join(" "); - }; - - const sortAndUniqueWords_cn = (text) => { - let words = text.match(/[^\u4e00-\u9fa5]+|[\u4e00-\u9fa5]+/g); - words = Array.from(new Set(words)); - words.sort(function (a, b) { - return a.localeCompare(b, "zh-Hans-CN", { sensitivity: "accent" }); - }); - return words.join(" "); - }; - const countKeyword = (sentence, keyword) => { const count = (sentence.match(new RegExp(keyword, "gi")) || []).length; return count; diff --git a/extensions/TheShovel/profanity.js b/extensions/TheShovel/profanity.js index 337dc397dc..2985a4405e 100644 --- a/extensions/TheShovel/profanity.js +++ b/extensions/TheShovel/profanity.js @@ -1,6 +1,7 @@ (function (Scratch) { "use strict"; + // eslint-disable-next-line no-unused-vars const encode = (str) => btoa(str) .split("") diff --git a/extensions/ar.js b/extensions/ar.js index bce3e12091..b461f4c315 100644 --- a/extensions/ar.js +++ b/extensions/ar.js @@ -27,7 +27,6 @@ let xrSession = null; let xrState = false; let xrRefSpace; - let xrViewSpace; let xrProjectionMatrix; let xrTransform; let xrCombinedMatrix; @@ -82,7 +81,6 @@ const onSuccess = function (session) { xrSession = session; xrRefSpace = null; - xrViewSpace = null; xrHitTestSource = null; hitPosition = null; hitPositionAvailable = false; @@ -103,7 +101,6 @@ session .requestReferenceSpace("viewer") .then((viewSpace) => { - xrViewSpace = viewSpace; return session.requestHitTestSource({ space: viewSpace }); }) .then((hts) => { @@ -500,7 +497,6 @@ } stageWrapperParent = stageWrapper.parentElement; - const noop = () => {}; navigator.xr .requestSession("immersive-ar", { requiredFeatures: ["hit-test", "dom-overlay"], diff --git a/extensions/docs-examples/unsandboxed/every-second.js b/extensions/docs-examples/unsandboxed/every-second.js index 56356bcbd0..7ebd0396a3 100644 --- a/extensions/docs-examples/unsandboxed/every-second.js +++ b/extensions/docs-examples/unsandboxed/every-second.js @@ -18,6 +18,7 @@ } // highlight-start setInterval(() => { + // eslint-disable-next-line no-unused-vars const startedThreads = Scratch.vm.runtime.startHats('everysecondexample_everySecond'); }, 1000); // highlight-end diff --git a/extensions/gamejolt.js b/extensions/gamejolt.js index 3af83da2b8..b04347ec3d 100644 --- a/extensions/gamejolt.js +++ b/extensions/gamejolt.js @@ -20,9 +20,11 @@ function hex_md5(a) { return rstr2hex(rstr_md5(str2rstr_utf8(a))); } + // eslint-disable-next-line no-unused-vars function hex_hmac_md5(a, b) { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a), str2rstr_utf8(b))); } + // eslint-disable-next-line no-unused-vars function md5_vm_test() { return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; } diff --git a/extensions/obviousAlexC/SensingPlus.js b/extensions/obviousAlexC/SensingPlus.js index 4ff646a8b6..cca997e457 100644 --- a/extensions/obviousAlexC/SensingPlus.js +++ b/extensions/obviousAlexC/SensingPlus.js @@ -278,8 +278,6 @@ const touchPointsArray = makeArrayOfTouches(); //* <-- Do this for devices that really can't support that many touches. - const alreadyTapped = {}; - class SensingPlus { getInfo() { return { diff --git a/extensions/obviousAlexC/newgroundsIO.js b/extensions/obviousAlexC/newgroundsIO.js index f06999da35..c24a1ea60d 100644 --- a/extensions/obviousAlexC/newgroundsIO.js +++ b/extensions/obviousAlexC/newgroundsIO.js @@ -9031,9 +9031,6 @@ let menuIco = "data:image/png;base64,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XYQ6wi3CIsEYV6pUKVOT999/v7/kkkt8uXLl3tD71YVcnX37CiO22mqrFVdeeaVftGiRMQWaMmWKHzt2rDFn11139a1atfI//fSTMfXHH3/0d999tzFp7733NscMGwdTq1atylgMEnKKcTDtQuET4U/NwlWKb0zv63UUgT0QpgsthW2EXCMkbZQm16o777zT//333xGWrZGmxx57zN9zzz1+8eLFvkGDBr5///7mbA0ZMsTXqVPHy1v23bp187NmzfIKfewz7N7OO+/Mc98j5NRkrSR8LhizrrrqKj9mzBjfpEkTUyW8H0aZMmV8hQoVluizF/R6fyFXiEG9onTp0v/17NnT7FRAqHicrMqVK5vDhbocNGiQP/roo+05sYEwFE8TPPPMM75du3b+ySef9A888ADjQABOliWniGB6uWCMgXEYcVQH6qJSpUrGQCQOpl188cV+8uTJvkaNGmROXhFyQfJIGpwufK3A2v/888/GMJ7js88+82effbZX/BadeDDxn3/+8UjlHnvs4bt27ervuOMOf9JJJ9nzbrPNNuZ1fvDBBxYy6LxpAhM8Z4j83EAhKlEnnHCCGXEI15kZOHDgQAsJXnnlFf/777+bGjnxxBM5HieGAVufVFHoJW3xu+ya79ixozEMxjz11FNms0Jq3lCxYkVTj6jSUaNGma077LDDzBHDw0TSUJFnnHEGWoik8wVCzqnJmUL0odDzCxcuNMYlIoLTc889NzjnJQFvdF0TOce6wttiWAGDj4a4+eabzbVHYrDV+jwudtxxRzseiWQy4qTMnz/fQgUmaK1atQgVmJi3CVsKOUVHC38I0QdCdXz//fcRFq0hvC68s3fffdf369fPd+/e3R4scs5PAhn28gIPiLOTbSLd1lmStIB47JFHHjFHYvjw4Wa3mHyxzlU8cAzepUIcT9jQuHFjkgswnJDnG+ESgXgw56iZQCnDHkTemKnKIK5B5RDDPfzwwzYg6P5gQPgbUUHYuoXCZwLx3WMCFYSzBCZGFYH0GFWGkqobJsXBwktyQpYz0OQb8Rih2bNnRwPrYgInBK+5m0CaLOfitoCuFRh4u3EMeO/evc2GzZs3z997771mnBXIFnpAcnqPP/64GfeLLrrI7B0zNTDscmZWi6lk05HmucJHwnDhPqGj0EioIewuUH7A1hbFVGb++cJXslGr+W68w4Bg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"; - const vm = Scratch.vm; - const runtime = vm.runtime; - const url_Location = window.location.href.split("/")[2]; let isNG = diff --git a/extensions/obviousAlexC/penPlus.js b/extensions/obviousAlexC/penPlus.js index 4031f72c20..09a696491a 100644 --- a/extensions/obviousAlexC/penPlus.js +++ b/extensions/obviousAlexC/penPlus.js @@ -42,7 +42,6 @@ //?And some fun statistics let trianglesDrawn = 0; let inDrawRegion = false; - let currentDrawShader = undefined; let penPlusDrawRegion = { enter: () => { trianglesDrawn = 0; @@ -858,8 +857,6 @@ const lilPenDabble = (InativeSize, curTarget, util) => { checkForPen(util); - const attrib = curTarget["_customState"]["Scratch.pen"].penAttributes; - Scratch.vm.renderer.penLine( Scratch.vm.renderer._penSkinId, { @@ -1804,9 +1801,6 @@ const spritex = curTarget.x; const spritey = curTarget.y; - //correction for HQ pen - const typSize = renderer._nativeSize; - //Predifine stuff so there aren't as many calculations const wMulX = myAttributes[0]; const wMulY = myAttributes[1]; @@ -1934,9 +1928,6 @@ const spritex = curTarget.x; const spritey = curTarget.y; - //correction for HQ pen - const typSize = renderer._nativeSize; - //Predifine stuff so there aren't as many calculations const wMulX = myAttributes[0]; const wMulY = myAttributes[1]; @@ -2066,8 +2057,6 @@ squareAttributesOfAllSprites[curTarget.id] = squareDefaultAttributes; } - let valuetoSet = 0; - const attributeNum = Scratch.Cast.toNumber(target); if (attributeNum >= 7) { if (attributeNum == 11) { @@ -2078,7 +2067,6 @@ ); return; } - valuetoSet = number / penPlusAdvancedSettings._maxDepth; squareAttributesOfAllSprites[curTarget.id][attributeNum] = number / penPlusAdvancedSettings._maxDepth; return; @@ -2152,8 +2140,6 @@ ); } tintTriPoint({ point, color }, util) { - const curTarget = util.target; - const trianglePointStart = (point - 1) * 8; const targetId = util.target.id; @@ -2189,8 +2175,6 @@ ); } tintTri({ point, color }, util) { - const curTarget = util.target; - const trianglePointStart = (point - 1) * 8; const targetId = util.target.id; diff --git a/extensions/true-fantom/regexp.js b/extensions/true-fantom/regexp.js index 6d7df47cb2..92f221fef5 100644 --- a/extensions/true-fantom/regexp.js +++ b/extensions/true-fantom/regexp.js @@ -15,13 +15,6 @@ const cast = Scratch.Cast; - const toScratchData = (val) => { - return val === undefined || typeof val === "object" ? "" : val; - }; - - const toJsonData = (val) => { - return JSON.parse(val); - }; const toJsonString = (val) => { return JSON.stringify( val, @@ -32,46 +25,6 @@ ); }; - const isNotPrimitiveData = (val) => { - return val instanceof Object; - }; - const isArray = (val) => { - return val instanceof Array; - }; - const isObject = (val) => { - return val instanceof Object && !(val instanceof Array); - }; - - const toArray = (val) => { - return isArray(val) ? val : isObject(val) ? Object.values(val) : [val]; - }; - const toObject = (val) => { - return isObject(val) - ? val - : isArray(val) - ? val.reduce( - (array, currentValue, currentIndex) => ({ - ...array, - [currentIndex + 1]: currentValue, - }), - {} - ) - : { 1: val }; - }; - - const dataValues = (val) => { - return Object.values(toObject(val)); - }; - const dataKeys = (val) => { - return Object.keys(toObject(val)); - }; - const dataPairs = (val) => { - return toObject(val); - }; - const dataMap = (val) => { - return Object.entries(toObject(val)); - }; - const toRegExpData = (val) => { let arr = /\/(.*)\/(.*)/.exec(val); return new RegExp(arr[1], arr[2]); @@ -453,7 +406,7 @@ let restr = cast.toString(A); let redat = toRegExpData(restr); if (RegExpCompare(redat, restr)) { - let flagtest = new RegExp("test", cast.toString(B)); + let _flagtest = new RegExp("test", cast.toString(B)); let flags = Array.from(redat.flags); Array.from(cast.toString(B)).forEach((flag) => flags.includes(flag) ? void 0 : flags.push(flag) @@ -470,7 +423,7 @@ let restr = cast.toString(A); let redat = toRegExpData(restr); if (RegExpCompare(redat, restr)) { - let flagtest = new RegExp("test", cast.toString(B)); + let _flagtest = new RegExp("test", cast.toString(B)); let flags = Array.from(redat.flags); Array.from(cast.toString(B)).forEach((flag) => flags.includes(flag) ? flags.splice(flags.indexOf(flag), 1) : void 0 From 8cffc5af52918a666c9d59b2c52ba2d8e57e095a Mon Sep 17 00:00:00 2001 From: qxs_ck Date: Sun, 18 Feb 2024 09:09:13 +0800 Subject: [PATCH 108/196] runtime-options: fix missing Scratch.translate (#1303) --- extensions/runtime-options.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/runtime-options.js b/extensions/runtime-options.js index 6abd3d4fd1..313d70012b 100644 --- a/extensions/runtime-options.js +++ b/extensions/runtime-options.js @@ -209,7 +209,7 @@ { opcode: "whenChange", blockType: Scratch.BlockType.EVENT, - text: "when [WHAT] changed", + text: Scratch.translate("when [WHAT] changed"), isEdgeActivated: false, arguments: { WHAT: { type: Scratch.ArgumentType.STRING, menu: "changeable" }, @@ -379,7 +379,6 @@ getCloneLimit() { return Scratch.vm.runtime.runtimeOptions.maxClones; } - setCloneLimit({ limit }) { limit = Scratch.Cast.toNumber(limit); Scratch.vm.setRuntimeOptions({ From 9f6f90844eec5b40b029f9de1bd175c3a597bb8a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:21:29 +0000 Subject: [PATCH 109/196] TheShovel/CustomStyles: fix question text color (#1310) resolves https://github.com/TurboWarp/extensions/issues/1290 --- extensions/TheShovel/CustomStyles.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/TheShovel/CustomStyles.js b/extensions/TheShovel/CustomStyles.js index 4def809c7d..94dbfde34b 100644 --- a/extensions/TheShovel/CustomStyles.js +++ b/extensions/TheShovel/CustomStyles.js @@ -65,8 +65,7 @@ askBoxBG = ".sc-question-inner"; askBoxButton = ".sc-question-submit-button"; askBoxInner = ".sc-question-input"; - askBoxText = - '[class^="question_question-container_"] input[class^="question_question-label_"]'; + askBoxText = ".sc-question-text"; askBoxBorderMain = ".sc-question-input:hover"; askBoxBorderOuter = ".sc-question-input:focus"; } else { From 89d70532d03bebd46d28c966a132b7b8822095f9 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 20 Feb 2024 02:04:38 +0000 Subject: [PATCH 110/196] NexusKitten/S-Grab: Fix some responses not returning anything. (#1311) Also adds a warning regarding how unreliable the depended API is. --- extensions/NexusKitten/sgrab.js | 40 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/extensions/NexusKitten/sgrab.js b/extensions/NexusKitten/sgrab.js index c948cb1e40..93908c2477 100644 --- a/extensions/NexusKitten/sgrab.js +++ b/extensions/NexusKitten/sgrab.js @@ -22,6 +22,10 @@ color1: "#ECA90B", color2: "#EBAF00", blocks: [ + { + blockType: Scratch.BlockType.XML, + xml: "