├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── download-translations.yml │ ├── format-pr.yml │ ├── label.yml │ ├── upload-translations.yml │ └── validate.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── extension.code-snippets ├── CNAME ├── CONTRIBUTING.md ├── README.md ├── development ├── build-production.js ├── builder.js ├── colors.js ├── compatibility-aliases.js ├── docs-template.ejs ├── download-translations.js ├── fs-utils.js ├── get-credits-for-gui.js ├── homepage-template.ejs ├── parse-extension-metadata.js ├── parse-extension-translations.js ├── render-docs.js ├── render-template.js ├── server.js ├── transifex-common.js ├── upload-translations.js └── validate.js ├── docs ├── CST1229 │ └── zip.md ├── CubesterYT │ ├── KeySimulation.md │ ├── TurboHook.md │ └── WindowControls.md ├── DNin │ └── wake-lock.md ├── Lily │ └── Skins.md ├── TheShovel │ └── ShovelUtils.md ├── Xeltalliv │ └── simple3D.md ├── ar.md ├── bitwise.md ├── box2d.md ├── gamejolt.md ├── godslayerakp │ └── ws.md ├── local-storage.md └── steamworks.md ├── eslint.config.js ├── extensions ├── -SIPC- │ ├── consoles.js │ ├── recording.js │ └── time.js ├── 0832 │ ├── rxFS.js │ └── rxFS2.js ├── Alestore │ └── nfcwarp.js ├── CST1229 │ ├── images.js │ └── zip.js ├── Clay │ └── htmlEncode.js ├── CubesterYT │ ├── KeySimulation.js │ ├── TurboHook.js │ └── WindowControls.js ├── DNin │ └── wake-lock.js ├── DT │ └── cameracontrols.js ├── JeremyGamer13 │ └── tween.js ├── Lily │ ├── AllMenus.js │ ├── Assets.js │ ├── Cast.js │ ├── ClonesPlus.js │ ├── CommentBlocks.js │ ├── HackedBlocks.js │ ├── ListTools.js │ ├── LooksPlus.js │ ├── McUtils.js │ ├── MoreEvents.js │ ├── MoreTimers.js │ ├── Skins.js │ ├── SoundExpanded.js │ ├── TempVariables.js │ ├── TempVariables2.js │ ├── Video.js │ └── lmsutils.js ├── Longboost │ └── color_channels.js ├── Medericoder │ └── textcase.js ├── NOname-awa │ ├── cn-number.js │ ├── global-coordinate.js │ ├── graphics2d.js │ ├── math-and-string.js │ ├── more-comparisons.js │ ├── regular-expression.js │ └── sort-unique-words.js ├── NexusKitten │ ├── controlcontrols.js │ ├── moremotion.js │ └── sgrab.js ├── PwLDev │ └── vibration.js ├── SharkPool │ ├── Camera.js │ └── Font-Manager.js ├── Skyhigh173 │ ├── bigint.js │ └── json.js ├── TheShovel │ ├── CanvasEffects.js │ ├── ColorPicker.js │ ├── CustomStyles.js │ ├── LZ-String.js │ ├── ShovelUtils.js │ ├── profanity.js │ └── profanityAPI.js ├── WP-Studio01 │ └── text2speech.js ├── Xeltalliv │ ├── clippingblending.js │ └── simple3D.js ├── XeroName │ └── Deltatime.js ├── XmerOriginals │ └── closecontrol.js ├── ZXMushroom63 │ └── searchApi.js ├── ar.js ├── battery.js ├── bitwise.js ├── box2d.js ├── clipboard.js ├── clouddata-ping.js ├── cloudlink.js ├── codeGIO │ └── ExtraUtilities.js ├── cs2627883 │ └── numericalencoding.js ├── cursor.js ├── docs-examples │ └── unsandboxed │ │ ├── block-utility-examples.js │ │ ├── broadcast-1.js │ │ ├── broadcast-2.js │ │ ├── broadcast-3.js │ │ ├── broadcast-4.js │ │ ├── broadcast-5.js │ │ ├── every-second.js │ │ ├── hello-world-unsandboxed.js │ │ ├── turbo-mode.js │ │ ├── when-key-pressed-restart.js │ │ ├── when-key-pressed-stage.js │ │ ├── when-key-pressed.js │ │ ├── when-space-key-pressed.js │ │ └── when.js ├── encoding.js ├── extensions.json ├── fetch.js ├── files.js ├── gamejolt.js ├── gamepad.js ├── godslayerakp │ ├── http.js │ └── ws.js ├── iframe.js ├── itchio.js ├── lab │ └── text.js ├── local-storage.js ├── mbw │ └── xml.js ├── mdwalters │ └── notifications.js ├── navigator.js ├── numerical-encoding-2.js ├── obviousAlexC │ ├── SensingPlus.js │ ├── newgroundsIO.js │ └── penPlus.js ├── penplus.js ├── pointerlock.js ├── qxsck │ ├── data-analysis.js │ └── var-and-list.js ├── rixxyx.js ├── runtime-options.js ├── shreder95ua │ └── resolution.js ├── sound.js ├── steamworks.js ├── stretch.js ├── text.js ├── true-fantom │ ├── base.js │ ├── couplers.js │ ├── math.js │ ├── network-m.js │ ├── network.js │ └── regexp.js ├── turboloader │ └── audiostream.js ├── utilities.js ├── veggiecan │ ├── LongmanDictionary.js │ ├── browserfullscreen.js │ └── mobilekeyboard.js └── vercte │ └── dictionaries.js ├── images ├── -SIPC- │ ├── consoles.svg │ └── time.svg ├── 0832 │ └── rxFS2.svg ├── Alestore │ └── nfcwarp.svg ├── CST1229 │ ├── images.svg │ └── zip.svg ├── Clay │ └── htmlEncode.svg ├── CubesterYT │ ├── KeySimulation.svg │ ├── TurboHook.svg │ └── WindowControls.svg ├── DNin │ └── wake-lock.svg ├── DT │ └── cameracontrols.svg ├── JeremyGamer13 │ └── tween.svg ├── Lily │ ├── AllMenus.svg │ ├── Assets.svg │ ├── Cast.svg │ ├── ClonesPlus.svg │ ├── CommentBlocks.svg │ ├── HackedBlocks.png │ ├── ListTools.svg │ ├── LooksPlus.svg │ ├── McUtils.png │ ├── MoreEvents.svg │ ├── MoreTimers.svg │ ├── Skins.svg │ ├── SoundExpanded.svg │ ├── TempVariables2.svg │ ├── Video.svg │ └── lmsutils.svg ├── Longboost │ └── color_channels.svg ├── NOname-awa │ ├── global-coordinate.svg │ ├── graphics2d.svg │ ├── math-and-string.svg │ └── more-comparisons.svg ├── NexusKitten │ ├── controlcontrols.svg │ ├── moremotion.svg │ └── sgrab.svg ├── PwLDev │ └── vibration.svg ├── README.md ├── SharkPool │ ├── Camera.svg │ └── Font-Manager.svg ├── Skyhigh173 │ ├── bigint.svg │ └── json.svg ├── TheShovel │ ├── CanvasEffects.svg │ ├── ColorPicker.svg │ ├── CustomStyles.svg │ ├── LZ-String.svg │ └── ShovelUtils.png ├── Xeltalliv │ ├── clippingblending.svg │ └── simple3D.png ├── XeroName │ └── Deltatime.svg ├── XmerOriginals │ └── closecontrol.svg ├── ZXMushroom63 │ └── searchApi.svg ├── ar.svg ├── battery.svg ├── bitwise.svg ├── box2d.svg ├── clipboard.svg ├── clouddata-ping.svg ├── cloudlink.svg ├── cursor.png ├── encoding.svg ├── fetch.svg ├── files.svg ├── gamejolt.png ├── gamepad.svg ├── godslayerakp │ ├── http.svg │ └── ws.png ├── iframe.svg ├── itchio.svg ├── lab │ └── text.svg ├── local-storage.svg ├── mbw │ └── xml.svg ├── mdwalters │ └── notifications.svg ├── navigator.svg ├── numerical-encoding-2.svg ├── obviousAlexC │ ├── SensingPlus.svg │ ├── newgroundsIO.svg │ └── penPlus.svg ├── penplus.svg ├── pointerlock.png ├── qxsck │ ├── data-analysis.svg │ └── var-and-list.svg ├── rixxyx.svg ├── runtime-options.svg ├── shreder95ua │ └── resolution.svg ├── sound.svg ├── stretch.svg ├── text.svg ├── true-fantom │ ├── base.svg │ ├── couplers.svg │ ├── math.svg │ ├── network.svg │ └── regexp.svg ├── unknown.svg ├── utilities.svg ├── veggiecan │ ├── LongmanDictionary.svg │ ├── browserfullscreen.svg │ └── mobilekeyboard.svg └── vercte │ └── dictionaries.svg ├── licenses ├── Apache-2.0.txt ├── CC-0.txt ├── CC-BY-2.5.txt ├── CC-BY-4.0.txt ├── CC-BY-SA-4.0.txt ├── GPL-3.0.txt ├── LGPL-3.0.txt ├── MIT.txt ├── MPL-2.0.txt └── OFL-Lobster.txt ├── package-lock.json ├── package.json ├── samples ├── AR cube.sb3 ├── Additive Fire.sb3 ├── Ask for the manager.sb3 ├── Base 64.sb3 ├── Box2D.sb3 ├── Clipping Sprites and Pen.sb3 ├── Cloud Variables.sb3 ├── Moving Around with Pointer Lock.sb3 ├── Party Time.sb3 ├── Pen Plus.sb3 ├── Recolor the Dango.sb3 ├── Simple3D template.sb3 ├── Stretch.sb3 ├── Temporary Variables - Thread vs. Runtime.sb3 ├── Tickle Me.sb3 └── Tweening.sb3 ├── translations ├── extension-metadata.json └── extension-runtime.json ├── tsconfig.json └── website ├── Lobster.woff2 ├── dango.mp4 ├── dango.png ├── favicon.ico ├── hello.html ├── hello.txt ├── hello.zip ├── meow.mp3 ├── robot.png ├── test ├── README.md ├── bad-xml.js ├── broken.js ├── button.js ├── cjk.js ├── l10n.js ├── label.js ├── loops.js └── scratchx.js └── turbowarp.svg /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | 8 | concurrency: 9 | group: "deploy" 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | # If you are forking and want to set up your own website, adjust the repository and branch 17 | # below to match your repository or remove the condition entirely. 18 | if: ${{ github.repository == 'TurboWarp/extensions' && github.ref == 'refs/heads/master' }} 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - name: Install Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 22 29 | - name: Install dependencies 30 | run: npm ci 31 | - name: Build for production 32 | run: npm run build 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: 36 | path: ./build/ 37 | 38 | deploy: 39 | environment: 40 | name: github-pages 41 | url: ${{ steps.deployment.outputs.page_url }} 42 | permissions: 43 | pages: write 44 | id-token: write 45 | runs-on: ubuntu-latest 46 | needs: build 47 | steps: 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v4 51 | -------------------------------------------------------------------------------- /.github/workflows/download-translations.yml: -------------------------------------------------------------------------------- 1 | name: Download translations 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # Time 10:44 was chosen at random to avoid periods of high load. 7 | - cron: "44 10 * * WED,SAT" 8 | 9 | concurrency: 10 | group: "download-translations" 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | download-translations: 15 | runs-on: ubuntu-latest 16 | 17 | # This workflow is not useful to forks without setting up Transifex and modifying the 18 | # workflow to use your organization, project, resources, API token, ... 19 | if: ${{ github.repository == 'TurboWarp/extensions' && github.ref == 'refs/heads/master' }} 20 | 21 | steps: 22 | - name: Checkout fork 23 | uses: actions/checkout@v4 24 | with: 25 | # Commits will be written to this fork, then pull requested to the main repository. 26 | repository: "DangoCat/extensions" 27 | token: "${{ secrets.UPDATE_TRANSLATIONS_FORK_GH_TOKEN }}" 28 | # We will push later so the token has to be stored. 29 | persist-credentials: true 30 | - name: Checkout upstream 31 | run: | 32 | git remote add upstream "https://github.com/$UPSTREAM_REPO.git" 33 | git fetch upstream 34 | git checkout upstream/master 35 | env: 36 | UPSTREAM_REPO: "TurboWarp/extensions" 37 | - name: Install Node.js 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 22 41 | - name: Install dependencies 42 | run: npm ci 43 | - name: Download translations 44 | run: npm run download-translations 45 | env: 46 | TRANSIFEX_TOKEN: "${{ secrets.TRANSIFEX_TOKEN }}" 47 | TRANSIFEX_ORGANIZATION: "turbowarp" 48 | TRANSIFEX_PROJECT: "turbowarp" 49 | TRANSIFEX_RUNTIME_RESOURCE: "extensions" 50 | TRANSIFEX_METADATA_RESOURCE: "extension-metadata" 51 | - name: Delete old branches, commit, push, pull request 52 | run: | 53 | if [[ ! $(git status --porcelain) ]]; then 54 | echo "No changes" 55 | exit 0 56 | fi 57 | 58 | # Remove old branches, which also closes the pull requests 59 | all_branches=$(GH_TOKEN="$FORK_GH_TOKEN" gh api "repos/$FORK_REPO/branches" --paginate | jq -r '.[].name') 60 | for branch in $all_branches; do 61 | if [[ $branch == update-translations-* ]]; then 62 | echo "Deleting branch: $branch" 63 | git push -d origin "$branch" 64 | else 65 | echo "Keeping branch: $branch" 66 | fi 67 | done 68 | 69 | # Create new branch 70 | new_branch="update-translations-$(date -u +%Y%m%d%H%M%S)" 71 | git checkout -b "$new_branch" 72 | 73 | # Commit 74 | date="$(date -u "+%Y-%m-%d")" 75 | title="[Automated] Update translations $date" 76 | git add . 77 | git config --global user.name "DangoCat[bot]" 78 | git config --global user.email "dangocat@users.noreply.github.com" 79 | git commit -m "$title" 80 | 81 | # Push 82 | git push origin "$new_branch" 83 | 84 | # Create pull request 85 | GH_TOKEN="$UPSTREAM_GH_TOKEN" gh pr create \ 86 | --head "dangocat:$new_branch" \ 87 | --repo "$UPSTREAM_REPO" \ 88 | --title "$title" \ 89 | --body "This pull request was made by a robot." 90 | env: 91 | FORK_REPO: "DangoCat/extensions" 92 | # This token has contents write permissions on fork repository 93 | FORK_GH_TOKEN: "${{ secrets.UPDATE_TRANSLATIONS_FORK_GH_TOKEN }}" 94 | UPSTREAM_REPO: "${{ github.repository }}" 95 | # This token has pull request write permissions on upstream repository 96 | UPSTREAM_GH_TOKEN: "${{ secrets.UPDATE_TRANSLATIONS_UPSTREAM_GH_TOKEN }}" 97 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: Autolaber 2 | 3 | on: 4 | # pull_request_target is dangerous but necessary to assign labels to the pull request. 5 | pull_request_target: 6 | # Only label on initial open so as to not trigger a whole lot unnecessarily 7 | # and also allow humans to override without their changes getting overwritten 8 | # on the next commit. 9 | types: [opened] 10 | 11 | jobs: 12 | label-pull-request: 13 | runs-on: ubuntu-latest 14 | 15 | # Disabled by default for forks since this is probably not useful. 16 | if: ${{ github.repository == 'TurboWarp/extensions' }} 17 | 18 | permissions: 19 | pull-requests: write 20 | 21 | steps: 22 | # This is a sensitive workflow because we have write permissions for pull-requests but we are 23 | # processing remote code that we can't trust. Be careful not to place any trust in the contents 24 | # of the pull request. 25 | - name: Assign labels 26 | run: | 27 | LABEL_NEW_EXTENSION="pr: new extension" 28 | LABEL_CHANGE_EXTENSION="pr: change existing extension" 29 | LABEL_OTHER="pr: other" 30 | 31 | # grep doesn't have good multiline support and installing pcregrep through apt is slow, so 32 | # make our own tiny regex checker tool. 33 | cat > matches <", 12 | "// License: ${5|Choose,MPL-2.0,MIT|}", 13 | "", 14 | "(function (Scratch) {", 15 | " \"use strict\";", 16 | "", 17 | " if (!Scratch.extensions.unsandboxed) {", 18 | " throw new Error(\"$1 extension must run unsandboxed\");", 19 | " }", 20 | "", 21 | " class ${6:MyExtension} {", 22 | " getInfo() {", 23 | " return {", 24 | " id: \"$2\",", 25 | " name: Scratch.translate(\"$1\"),", 26 | " blocks: [", 27 | " {", 28 | " opcode: \"${7:block}\",$0", 29 | " },", 30 | " ],", 31 | " };", 32 | " }", 33 | "", 34 | " $7(args, util) {}", 35 | " }", 36 | "", 37 | " Scratch.extensions.register(new $6());", 38 | "})(Scratch);", 39 | ], 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | extensions.turbowarp.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TurboWarp Extension Gallery 2 | 3 | User-contributed unsandboxed extension gallery for TurboWarp. 4 | 5 | https://extensions.turbowarp.org/ 6 | 7 | ## Contributing 8 | 9 | See [CONTRIBUTING.md](CONTRIBUTING.md). 10 | 11 | ## License 12 | 13 | Extensions (in the `extensions` folder) will have a comment at the top of the file describing the license for the code. In the past [MIT](./licenses/MIT.txt) was the default, however now [MPL-2.0](./licenses/MPL-2.0.txt) is recommended. Some extensions may contain a mix of several. 14 | 15 | Sample projects (in the `samples` folder) are licensed under [CC-BY 4.0](./licenses/CC-BY-4.0.txt). 16 | 17 | Everything else, such as the extension images, development server, and website are licensed under the [GNU General Public License version 3](licenses/GPL-3.0.txt). 18 | 19 | See [images/README.md](images/README.md) for attribution information for each image. 20 | -------------------------------------------------------------------------------- /development/build-production.js: -------------------------------------------------------------------------------- 1 | const pathUtil = require("path"); 2 | const Builder = require("./builder"); 3 | 4 | const outputDirectory = pathUtil.join(__dirname, "../build"); 5 | const l10nOutput = pathUtil.join(__dirname, "../build-l10n"); 6 | 7 | const builder = new Builder("production"); 8 | const build = builder.build(); 9 | 10 | build.export(outputDirectory); 11 | console.log(`Built to ${outputDirectory}`); 12 | 13 | build.exportL10N(l10nOutput); 14 | console.log(`Exported L10N to ${l10nOutput}`); 15 | -------------------------------------------------------------------------------- /development/colors.js: -------------------------------------------------------------------------------- 1 | const enableColor = !process.env.NO_COLOR; 2 | const color = (i) => (enableColor ? i : ""); 3 | 4 | module.exports = { 5 | RESET: color("\x1b[0m"), 6 | BOLD: color("\x1b[1m"), 7 | RED: color("\x1b[31m"), 8 | GREEN: color("\x1b[32m"), 9 | }; 10 | -------------------------------------------------------------------------------- /development/compatibility-aliases.js: -------------------------------------------------------------------------------- 1 | const extensions = { 2 | // maps old path to new path 3 | "/LukeManiaStudios/ClonesPlus.js": "/Lily/ClonesPlus.js", 4 | "/LukeManiaStudios/CommentBlocks.js": "/Lily/CommentBlocks.js", 5 | "/LukeManiaStudios/lmsutils.js": "/Lily/lmsutils.js", 6 | "/LukeManiaStudios/LooksPlus.js": "/Lily/LooksPlus.js", 7 | "/LukeManiaStudios/McUtils.js": "/Lily/McUtils.js", 8 | "/LukeManiaStudios/MoreTimers.js": "/Lily/MoreTimers.js", 9 | "/LukeManiaStudios/TempVariables.js": "/Lily/TempVariables.js", 10 | "/LukeManiaStudios/TempVariables2.js": "/Lily/TempVariables2.js", 11 | }; 12 | 13 | module.exports = extensions; 14 | -------------------------------------------------------------------------------- /development/fs-utils.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const pathUtil = require("path"); 3 | 4 | /** 5 | * Recursively read a directory. 6 | * @param {string} directory 7 | * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. 8 | * The return result includes files in subdirectories, but not the subdirectories themselves. 9 | */ 10 | const recursiveReadDirectory = (directory) => { 11 | const result = []; 12 | for (const name of fs.readdirSync(directory)) { 13 | if (name.startsWith(".")) { 14 | // Ignore .eslintrc.js, .DS_Store, etc. 15 | continue; 16 | } 17 | const absolutePath = pathUtil.join(directory, name); 18 | const stat = fs.statSync(absolutePath); 19 | if (stat.isDirectory()) { 20 | for (const [ 21 | relativeToChildName, 22 | childAbsolutePath, 23 | ] of recursiveReadDirectory(absolutePath)) { 24 | // This always needs to use / on all systems 25 | result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); 26 | } 27 | } else { 28 | result.push([name, absolutePath]); 29 | } 30 | } 31 | return result; 32 | }; 33 | 34 | /** 35 | * Synchronous create a directory and any parents. Does nothing if the folder already exists. 36 | * @param {string} directory 37 | */ 38 | const mkdirp = (directory) => { 39 | try { 40 | fs.mkdirSync(directory, { 41 | recursive: true, 42 | }); 43 | } catch (e) { 44 | if (e.code !== "ENOENT") { 45 | throw e; 46 | } 47 | } 48 | }; 49 | 50 | module.exports = { 51 | recursiveReadDirectory, 52 | mkdirp, 53 | }; 54 | -------------------------------------------------------------------------------- /development/get-credits-for-gui.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const https = require("https"); 4 | const fsUtils = require("./fs-utils"); 5 | const parseMetadata = require("./parse-extension-metadata"); 6 | 7 | class AggregatePersonInfo { 8 | /** @param {Person} person */ 9 | constructor(person) { 10 | this.name = person.name; 11 | 12 | /** @type {Set} */ 13 | this.links = new Set(); 14 | } 15 | 16 | /** @param {string} link */ 17 | addLink(link) { 18 | this.links.add(link); 19 | } 20 | } 21 | 22 | /** 23 | * @param {string} username 24 | * @returns {Promise} 25 | */ 26 | const getUserID = (username) => 27 | new Promise((resolve, reject) => { 28 | process.stdout.write(`Getting user ID for ${username}... `); 29 | const request = https.get(`https://api.scratch.mit.edu/users/${username}`); 30 | 31 | request.on("response", (response) => { 32 | const data = []; 33 | response.on("data", (newData) => { 34 | data.push(newData); 35 | }); 36 | 37 | response.on("end", () => { 38 | const allData = Buffer.concat(data); 39 | const json = JSON.parse(allData.toString("utf-8")); 40 | const userID = String(json.id); 41 | process.stdout.write(`${userID}\n`); 42 | resolve(userID); 43 | }); 44 | 45 | response.on("error", (error) => { 46 | process.stdout.write("error\n"); 47 | reject(error); 48 | }); 49 | }); 50 | 51 | request.on("error", (error) => { 52 | process.stdout.write("error\n"); 53 | reject(error); 54 | }); 55 | 56 | request.end(); 57 | }); 58 | 59 | const run = async () => { 60 | /** 61 | * @type {Map} 62 | */ 63 | const aggregate = new Map(); 64 | 65 | const extensionRoot = path.resolve(__dirname, "../extensions/"); 66 | for (const [name, absolutePath] of fsUtils.recursiveReadDirectory( 67 | extensionRoot 68 | )) { 69 | if (!name.endsWith(".js")) { 70 | continue; 71 | } 72 | 73 | const code = fs.readFileSync(absolutePath, "utf-8"); 74 | const metadata = parseMetadata(code); 75 | 76 | for (const person of [...metadata.by, ...metadata.original]) { 77 | const personID = person.name.toLowerCase(); 78 | if (!aggregate.has(personID)) { 79 | aggregate.set(personID, new AggregatePersonInfo(person)); 80 | } 81 | 82 | if (person.link) { 83 | aggregate.get(personID).addLink(person.link); 84 | } 85 | } 86 | } 87 | 88 | const result = []; 89 | 90 | for (const id of [...aggregate.keys()].sort()) { 91 | const info = aggregate.get(id); 92 | 93 | if (info.links.size > 0) { 94 | const link = [...info.links.values()].sort()[0]; 95 | const username = link.match(/users\/(.+?)\/?$/)[1]; 96 | const userID = await getUserID(username); 97 | result.push({ 98 | userID, 99 | username, 100 | }); 101 | } else { 102 | result.push({ 103 | username: info.name, 104 | }); 105 | } 106 | } 107 | 108 | return result; 109 | }; 110 | 111 | run() 112 | .then((result) => { 113 | console.log(JSON.stringify(result, null, 4)); 114 | }) 115 | .catch((error) => { 116 | console.error(error); 117 | process.exit(1); 118 | }); 119 | -------------------------------------------------------------------------------- /development/parse-extension-metadata.js: -------------------------------------------------------------------------------- 1 | class Person { 2 | constructor(name, link) { 3 | /** @type {string} */ 4 | this.name = name; 5 | /** @type {string|null} */ 6 | this.link = link; 7 | } 8 | 9 | toHTML() { 10 | // Don't need to bother escaping here. There's no vulnerability. 11 | if (this.link) { 12 | return `${this.name}`; 13 | } 14 | return this.name; 15 | } 16 | } 17 | 18 | class Extension { 19 | constructor() { 20 | this.id = ""; 21 | this.name = ""; 22 | this.description = ""; 23 | this.license = ""; 24 | /** @type {Person[]} */ 25 | this.by = []; 26 | /** @type {Person[]} */ 27 | this.original = []; 28 | this.context = ""; 29 | this.scratchCompatible = false; 30 | } 31 | } 32 | 33 | /** 34 | * @param {string} string 35 | * @param {string} split 36 | * @returns {string[]} 37 | */ 38 | const splitFirst = (string, split) => { 39 | const idx = string.indexOf(split); 40 | if (idx === -1) { 41 | return [string]; 42 | } 43 | return [string.substring(0, idx), string.substring(idx + split.length)]; 44 | }; 45 | 46 | /** 47 | * @param {string} person 48 | * @returns {Person} 49 | */ 50 | const parsePerson = (person) => { 51 | const parts = splitFirst(person, "<"); 52 | if (parts.length === 1) { 53 | return new Person(person, null); 54 | } 55 | 56 | const name = parts[0].trim(); 57 | const link = parts[1].replace(">", ""); 58 | return new Person(name, link); 59 | }; 60 | 61 | /** 62 | * @param {string} extensionCode 63 | * @return {Extension} 64 | */ 65 | const parseMetadata = (extensionCode) => { 66 | const metadata = new Extension(); 67 | 68 | for (const line of extensionCode.split("\n")) { 69 | if (!line.startsWith("//")) { 70 | // End of header. 71 | break; 72 | } 73 | 74 | const withoutComment = line.substring(2).trim(); 75 | const parts = splitFirst(withoutComment, ":"); 76 | if (parts.length === 1) { 77 | // Invalid. 78 | continue; 79 | } 80 | 81 | const key = parts[0].toLowerCase().trim(); 82 | const value = parts[1].trim(); 83 | 84 | switch (key) { 85 | case "id": 86 | metadata.id = value; 87 | break; 88 | case "name": 89 | metadata.name = value; 90 | break; 91 | case "description": 92 | metadata.description = value; 93 | break; 94 | case "license": 95 | metadata.license = value; 96 | break; 97 | case "by": 98 | metadata.by.push(parsePerson(value)); 99 | break; 100 | case "original": 101 | metadata.original.push(parsePerson(value)); 102 | break; 103 | case "context": 104 | metadata.context = value; 105 | break; 106 | case "scratch-compatible": 107 | metadata.scratchCompatible = value === "true"; 108 | break; 109 | default: 110 | // TODO 111 | break; 112 | } 113 | } 114 | 115 | return metadata; 116 | }; 117 | 118 | module.exports = parseMetadata; 119 | -------------------------------------------------------------------------------- /development/parse-extension-translations.js: -------------------------------------------------------------------------------- 1 | const espree = require("espree"); 2 | const esquery = require("esquery"); 3 | const parseMetadata = require("./parse-extension-metadata"); 4 | 5 | /** 6 | * @fileoverview Parses extension code to find calls to Scratch.translate() and statically 7 | * evaluate its arguments. 8 | */ 9 | 10 | const evaluateAST = (node) => { 11 | if (node.type == "Literal") { 12 | return node.value; 13 | } 14 | 15 | if (node.type === "ObjectExpression") { 16 | const object = {}; 17 | for (const { key, value } of node.properties) { 18 | // Normally Identifier refers to a variable, but inside of key we treat it as a string. 19 | let evaluatedKey; 20 | if (key.type === "Identifier") { 21 | evaluatedKey = key.name; 22 | } else { 23 | evaluatedKey = evaluateAST(key); 24 | } 25 | 26 | object[evaluatedKey] = evaluateAST(value); 27 | } 28 | return object; 29 | } 30 | 31 | console.error(`Can't evaluate node:`, node); 32 | throw new Error(`Can't evaluate ${node.type} node at build-time`); 33 | }; 34 | 35 | /** 36 | * Generate default ID for a translation that has no explicit ID. 37 | * @param {string} string 38 | * @returns {string} 39 | */ 40 | const defaultIdForString = (string) => { 41 | // hardcoded in VM 42 | return `_${string}`; 43 | }; 44 | 45 | /** 46 | * @param {string} js 47 | * @returns {Record} 48 | */ 49 | const parseTranslations = (js) => { 50 | const metadata = parseMetadata(js); 51 | if (!metadata.name) { 52 | throw new Error(`Extension needs a // Name: to generate translations`); 53 | } 54 | 55 | let defaultDescription = `Part of the '${metadata.name}' extension.`; 56 | if (metadata.context) { 57 | defaultDescription += ` ${metadata.context}`; 58 | } 59 | 60 | const ast = espree.parse(js, { 61 | ecmaVersion: 2022, 62 | }); 63 | const selector = esquery.parse( 64 | 'CallExpression[callee.object.name="Scratch"][callee.property.name="translate"]' 65 | ); 66 | const matches = esquery.match(ast, selector); 67 | 68 | const result = {}; 69 | for (const match of matches) { 70 | const args = match.arguments; 71 | if (args.length === 0) { 72 | throw new Error(`Scratch.translate() needs at least 1 argument`); 73 | } 74 | 75 | const evaluated = evaluateAST(args[0]); 76 | 77 | let id; 78 | let english; 79 | let description; 80 | 81 | if (typeof evaluated === "string") { 82 | id = defaultIdForString(evaluated); 83 | english = evaluated; 84 | description = defaultDescription; 85 | } else if (typeof evaluated === "object" && evaluated !== null) { 86 | english = evaluated.default; 87 | id = evaluated.id || defaultIdForString(english); 88 | 89 | description = [defaultDescription, evaluated.description] 90 | .filter((i) => i) 91 | .join(" "); 92 | } else { 93 | throw new Error( 94 | `Not a valid argument for Scratch.translate(): ${evaluated}` 95 | ); 96 | } 97 | 98 | if (typeof id !== "string") { 99 | throw new Error( 100 | `Scratch.translate() passed a value for id that is not a string: ${id}` 101 | ); 102 | } 103 | if (typeof english !== "string") { 104 | throw new Error( 105 | `Scratch.translate() passed a value for default that is not a string: ${english}` 106 | ); 107 | } 108 | if (typeof description !== "string") { 109 | throw new Error( 110 | `Scratch.translate() passed a value for description that is not a string: ${description}` 111 | ); 112 | } 113 | 114 | result[id] = { 115 | string: english, 116 | developer_comment: description, 117 | }; 118 | } 119 | 120 | return result; 121 | }; 122 | 123 | module.exports = parseTranslations; 124 | -------------------------------------------------------------------------------- /development/render-template.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const ejs = require("ejs"); 3 | 4 | // TODO: Investigate the value of removing dependency on `ejs` and possibly writing our own DSL. 5 | 6 | const renderTemplate = (path, data) => { 7 | const inputEJS = fs.readFileSync(path, "utf-8"); 8 | const outputHTML = ejs.render(inputEJS, data); 9 | return outputHTML; 10 | }; 11 | 12 | module.exports = renderTemplate; 13 | -------------------------------------------------------------------------------- /development/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const Builder = require("./builder"); 3 | 4 | let mostRecentBuild = null; 5 | const builder = new Builder("development"); 6 | builder.startWatcher((newBuild) => { 7 | mostRecentBuild = newBuild; 8 | }); 9 | 10 | const app = express(); 11 | app.set("strict routing", true); 12 | app.set("x-powered-by", false); 13 | 14 | app.use((req, res, next) => { 15 | // If we don't tell the browser not to cache files, it does by default, and people will get confused when 16 | // script changes aren't being applied if they don't do a reload without cache. 17 | res.setHeader("Cache-Control", "no-store"); 18 | res.setHeader("Pragma", "no-cache"); 19 | 20 | // Prevent browser from trying to guess file types. 21 | res.setHeader("X-Content-Type-Options", "nosniff"); 22 | 23 | // No need to leak Referer headers. 24 | res.setHeader("Referrer-Policy", "no-referrer"); 25 | 26 | // We want all resources used by the website to be local. 27 | // This CSP does *not* apply to the extensions, just the website. 28 | res.setHeader( 29 | "Content-Security-Policy", 30 | "default-src 'self' 'unsafe-inline' data: blob:" 31 | ); 32 | 33 | // Allows loading cross-origin example images and matches GitHub pages. 34 | res.setHeader("Access-Control-Allow-Origin", "*"); 35 | 36 | next(); 37 | }); 38 | 39 | app.get("/*", (req, res, next) => { 40 | if (!mostRecentBuild) { 41 | res.contentType("text/plain"); 42 | res.status(500); 43 | res.send("Build Failed; See Console"); 44 | return; 45 | } 46 | 47 | const fileInBuild = mostRecentBuild.getFile(decodeURIComponent(req.path)); 48 | if (!fileInBuild) { 49 | return next(); 50 | } 51 | 52 | res.contentType(fileInBuild.getType()); 53 | res.send(fileInBuild.read()); 54 | }); 55 | 56 | app.use((req, res) => { 57 | res.contentType("text/plain"); 58 | res.status(404); 59 | res.send("404 Not Found"); 60 | }); 61 | 62 | // The port the server runs on matters. The editor only treats port 8000 as unsandboxed. 63 | const PORT = 8000; 64 | app.listen(8000, () => { 65 | console.log(`Development server is ready on http://localhost:${PORT}/`); 66 | }); 67 | -------------------------------------------------------------------------------- /development/transifex-common.js: -------------------------------------------------------------------------------- 1 | const { transifexApi } = require("@transifex/api"); 2 | 3 | const TOKEN = process.env.TRANSIFEX_TOKEN; 4 | if (!TOKEN) { 5 | console.error("Missing TRANSIFEX_TOKEN."); 6 | process.exit(1); 7 | } 8 | 9 | const ORGANIZATION_NAME = process.env.TRANSIFEX_ORGANIZATION; 10 | if (!ORGANIZATION_NAME) { 11 | console.error("Missing TRANSIFEX_ORGANIZATION."); 12 | process.exit(1); 13 | } 14 | 15 | const PROJECT_NAME = process.env.TRANSIFEX_PROJECT; 16 | if (!PROJECT_NAME) { 17 | console.error("Missing TRANSIFEX_PROJECT."); 18 | process.exit(1); 19 | } 20 | 21 | const RUNTIME_RESOURCE = process.env.TRANSIFEX_RUNTIME_RESOURCE; 22 | if (!RUNTIME_RESOURCE) { 23 | console.error("Missing TRANSIFEX_RUNTIME_RESOURCE."); 24 | process.exit(1); 25 | } 26 | 27 | const METADATA_RESOURCE = process.env.TRANSIFEX_METADATA_RESOURCE; 28 | if (!METADATA_RESOURCE) { 29 | console.error("Missing TRANSIFEX_METADATA_RESOURCE."); 30 | process.exit(1); 31 | } 32 | 33 | const SOURCE_LOCALE = "en"; 34 | 35 | transifexApi.setup({ 36 | auth: TOKEN, 37 | }); 38 | 39 | module.exports = { 40 | transifexApi, 41 | ORGANIZATION_NAME, 42 | PROJECT_NAME, 43 | RUNTIME_RESOURCE, 44 | METADATA_RESOURCE, 45 | SOURCE_LOCALE, 46 | }; 47 | -------------------------------------------------------------------------------- /development/upload-translations.js: -------------------------------------------------------------------------------- 1 | const { 2 | transifexApi, 3 | ORGANIZATION_NAME, 4 | PROJECT_NAME, 5 | RUNTIME_RESOURCE, 6 | METADATA_RESOURCE, 7 | } = require("./transifex-common"); 8 | const Builder = require("./builder"); 9 | 10 | const uploadRuntimeStrings = async (strings) => { 11 | if ( 12 | strings["lab/text@_Animated Text"].string !== "Animated Text" || 13 | strings["lab/text@_Animated Text"].developer_comment !== 14 | "Part of the 'Animated Text' extension." || 15 | Object.keys(strings).length < 1500 16 | ) { 17 | throw new Error("Sanity check failed."); 18 | } 19 | 20 | await transifexApi.ResourceStringsAsyncUpload.upload({ 21 | resource: { 22 | data: { 23 | id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${RUNTIME_RESOURCE}`, 24 | type: "resources", 25 | }, 26 | }, 27 | content: JSON.stringify(strings), 28 | }); 29 | }; 30 | 31 | const uploadMetadataStrings = async (strings) => { 32 | if ( 33 | strings["lab/text@name"].string !== "Animated Text" || 34 | strings["lab/text@name"].developer_comment !== 35 | "Name of the 'Animated Text' extension in the extension gallery." || 36 | Object.keys(strings).length < 150 37 | ) { 38 | throw new Error("Sanity check failed."); 39 | } 40 | 41 | await transifexApi.ResourceStringsAsyncUpload.upload({ 42 | resource: { 43 | data: { 44 | id: `o:${ORGANIZATION_NAME}:p:${PROJECT_NAME}:r:${METADATA_RESOURCE}`, 45 | type: "resources", 46 | }, 47 | }, 48 | content: JSON.stringify(strings), 49 | }); 50 | }; 51 | 52 | const run = async () => { 53 | console.log("Building..."); 54 | const builder = new Builder(); 55 | const build = builder.build(); 56 | 57 | console.log("Generating strings..."); 58 | const l10n = build.generateL10N(); 59 | 60 | console.log("Uploading runtime strings..."); 61 | await uploadRuntimeStrings(l10n["extension-runtime"]); 62 | 63 | console.log("Uploading metadata strings..."); 64 | await uploadMetadataStrings(l10n["extension-metadata"]); 65 | }; 66 | 67 | run().catch((err) => { 68 | console.error(err); 69 | process.exit(1); 70 | }); 71 | -------------------------------------------------------------------------------- /development/validate.js: -------------------------------------------------------------------------------- 1 | const Builder = require("./builder"); 2 | const Colors = require("./colors"); 3 | 4 | const builder = new Builder("production"); 5 | const errors = builder.validate(); 6 | 7 | if (errors.length === 0) { 8 | console.log( 9 | `${Colors.GREEN}${Colors.BOLD}Validation checks passed.${Colors.RESET}` 10 | ); 11 | process.exit(0); 12 | } else { 13 | console.error( 14 | `${Colors.RED}${Colors.BOLD}${errors.length} ${ 15 | errors.length === 1 ? "file" : "files" 16 | } failed validation.${Colors.RESET}` 17 | ); 18 | console.error(""); 19 | for (const { fileName, error } of errors) { 20 | console.error(`${Colors.BOLD}${fileName}${Colors.RESET}: ${error}`); 21 | } 22 | console.error(``); 23 | process.exit(1); 24 | } 25 | -------------------------------------------------------------------------------- /docs/CubesterYT/KeySimulation.md: -------------------------------------------------------------------------------- 1 | # Key Simulation 2 | 3 | This extension allows you to simulate key presses and mouse clicks on the project. 4 | 5 | ## Press key 6 | 7 | ```scratch 8 | press (space v) for (0.1) seconds and (continue v) :: #BF0000 9 | ``` 10 | 11 | This will trigger "key down?" and "when key pressed" blocks. It won't type text into text fields in the editor, for example. 12 | 13 | When `0` is used as a duration, the key will be pressed for exactly one frame. 14 | 15 | ## Click mouse 16 | 17 | ```scratch 18 | click (left v) mouse button at x: (0) y: (0) for (0.1) seconds and (continue v) :: #BF0000 19 | ``` 20 | 21 | This will trigger "mouse down?" and "when this sprite clicked" blocks as well as update the "mouse x" and "mouse y" blocks. It can't be used to click on buttons in the editor, for example. 22 | 23 | When `0` is used as a duration, the button will be pressed for exactly one frame. 24 | 25 | ## Move mouse 26 | 27 | ```scratch 28 | move mouse to x: (0) y: (0) :: #BF0000 29 | ``` 30 | 31 | This will update the values of the "mouse x" and "mouse y" block without clicking. 32 | -------------------------------------------------------------------------------- /docs/CubesterYT/TurboHook.md: -------------------------------------------------------------------------------- 1 | # TurboHook 2 | 3 | This extension allows you to post to webhooks from some common third-party websites or programs. 4 | 5 | ## WebHook block 6 | 7 | Posts a message to a webhook. 8 | 9 | ```scratch 10 | webhook data: [] webhook url: [] :: #3c48c2 11 | ``` 12 | The empty area is where you insert the data reporter and/or connector reporter. The string area is where you put your webhook's URL. 13 | 14 | ## Data Reporter 15 | 16 | The Menu area of the Data Reporter currently has three options, ("content","name","icon"). 17 | 18 | ### Content 19 | 20 | ```scratch 21 | ((content v) [] :: #3c48c2) 22 | ``` 23 | When choosing "content", you can type in the text you want to send as the body of the webhook. 24 | 25 | ### Name 26 | 27 | ```scratch 28 | ((name v) [] :: #3c48c2) 29 | ``` 30 | When choosing "name", you can type in the name of the name you want the webhook to display as on the third-party website. 31 | 32 | ### Icon 33 | 34 | ```scratch 35 | ((icon v) [] :: #3c48c2) 36 | ``` 37 | When choosing "icon", you can type in a URL to the image you want to set as the profile picture of the webhook. 38 | 39 | ## Connector Reporter 40 | 41 | ```scratch 42 | ([], [] :: #3c48c2) 43 | ``` 44 | This reporter connects two different data reporters together and/or another connector reporter so you can send multiple pieces of data to the webhook. 45 | -------------------------------------------------------------------------------- /docs/DNin/wake-lock.md: -------------------------------------------------------------------------------- 1 | # Wake Lock 2 | 3 | The Wake Lock feature allows you to keep your computer's screen on while a project is running. This can be helpful when playing media or performing an important but time-consuming task. 4 | 5 | ## Activating and Releasing Wake Lock 6 | 7 | ```scratch 8 | set wake lock to (on v) :: #0FBD8C 9 | ``` 10 | This block will activate wake lock. 11 | 12 | If you ever need to check that wake lock has properly been activated, use the `is wake lock active?` boolean reporter. 13 | 14 | To release wake lock, simply change "on" to "off". 15 | 16 | You can also insert boolean reporters into the menu input. 17 | 18 | Wake lock will also be released automatically when the project stops or is restarted to ensure it isn't accidentally left on forever. 19 | 20 | ## Browser support 21 | 22 | > [!WARNING] 23 | > Not all browsers support wake lock **(notably, Firefox does not)**. In these browsers requesting wake lock will not do anything. 24 | 25 | ## Note 26 | 27 | The wake lock block takes a moment to finish running as it activates wake lock, so if you put it in a script with other blocks, it will yield briefly, so try keeping it separate from your other scripts. The `is wake lock active?` boolean reporter, however, does not have a delay. 28 | -------------------------------------------------------------------------------- /docs/Lily/Skins.md: -------------------------------------------------------------------------------- 1 | # Skins 2 | 3 | This extension allows you to load and display images onto sprites, as Skins. 4 | 5 | In this extension, a "Skin" is an image that can replace what a sprite looks like. 6 | 7 | Unlike costumes, Skins are not loaded when the project is opened. Instead, Skins are loaded with blocks as the project is running. 8 | 9 | ## Loading Skins 10 | 11 | Skins can be created in 3 different ways. Each way requires you to give the skin a name, which will be used by other blocks to reference the skin later. 12 | 13 | Loading a skin with the same name as another skin will overwrite the data of that skin. Any sprite that was using this skin will now show the new skin. 14 | 15 | --- 16 | 17 | ```scratch 18 | create SVG skin [] as [my skin] :: #6b56ff 19 | ``` 20 | The first way is by creating a new skin with SVG markup data. The advantage to this is that it loads much quicker than the other loading blocks. The obvious disadvantage is that, unlike the other 2 blocks, it can only work with SVGs. 21 | 22 | --- 23 | 24 | ```scratch 25 | load skin from (costume 1 v) as [my skin] :: #6b56ff 26 | ``` 27 | The second way is by loading a skin from a costume. 28 | 29 | > [!IMPORTANT] 30 | > It's important to note that this block will require the Advanced Option "Remove raw asset data after loading to save RAM" to be disabled in the packager in order for this block to work correctly in a packaged environment. **You do not need to do this within the editor.** 31 | > 32 | > If you intend to package your project, we don't encourage using this block for that reason. **None of the other blocks in this extension require this option to be disabled.** 33 | 34 | --- 35 | 36 | ```scratch 37 | load skin from URL [https://...] as [my skin] :: #6b56ff 38 | ``` 39 | The final way is loading a skin through a URL. This block allows you to load any bitmap image as well as SVGs. 40 | 41 | ```scratch 42 | load skin from URL (snapshot stage :: #9966ff) as [my skin] :: #6b56ff 43 | ``` 44 | While this block can work with a website URL, it's primarily designed to work with data URIs. Try using this with the "snapshot stage" block from the "Looks Plus" extension. 45 | 46 | For the final 2 blocks, the block will pause the script for a moment in order to load the skin. Treat them like "wait" blocks in your scripts, don't expect them to finish instantaneously. 47 | 48 | ## Using Skins 49 | 50 | ```scratch 51 | set skin of (myself v) to [my skin] :: #6b56ff 52 | ``` 53 | Skins can be applied to a sprite with this block, so long as you loaded the skin beforehand. Skins can be applied to multiple sprites/clones. 54 | 55 | Using the "myself" option will apply the skin to the sprite the block is running in: if the block is running in a clone, it will apply the skin to the clone. **Do not confuse "myself" with the sprite's name.** 56 | 57 | Skins will automatically be removed from every sprite when the project has stopped. 58 | 59 | --- 60 | 61 | ```scratch 62 | restore skin of (myself v) :: #6b56ff 63 | ``` 64 | You can remove the skin of a sprite with the "restore skin" block. This will remove the skin from that specific sprite. 65 | 66 | --- 67 | 68 | ```scratch 69 | restore targets with skin [my skin] :: #6b56ff 70 | ``` 71 | You can remove a skin from every sprite that has it applied with the "restore targets with skin" block. "Target" refers to "sprite" in this context. 72 | 73 | ## Deleting Skins 74 | 75 | Skins that have been loaded will still exist after the project has stopped. In order to truly delete a skin, you have 2 methods. 76 | 77 | --- 78 | 79 | ```scratch 80 | delete skin [my skin] :: #6b56ff 81 | ``` 82 | Delete a specified skin, and reset any sprite that had it applied. 83 | 84 | --- 85 | 86 | ```scratch 87 | delete all skins :: #6b56ff 88 | ``` 89 | Delete every skin that has been loaded and reset all sprites that had any skin applied. 90 | 91 | ## Other Blocks 92 | 93 | ```scratch 94 | 95 | ``` 96 | Check whether a skin is actually loaded. This becomes true **after** the block has finished loading the skin. 97 | 98 | --- 99 | 100 | ```scratch 101 | ((width v) of skin [my skin] :: #6b56ff) 102 | ``` 103 | Get the width/height of a skin. The values are rounded. 104 | 105 | --- 106 | 107 | ```scratch 108 | (current skin of (myself v) :: #6b56ff) 109 | ``` 110 | The name of the skin that is applied to the specified sprite. -------------------------------------------------------------------------------- /docs/TheShovel/ShovelUtils.md: -------------------------------------------------------------------------------- 1 | # ShovelUtils 2 | 3 | Shovel Utils is an extension focused mostly on injecting and modifying sprites and assets inside the project, as well as several other functions. 4 | 5 | > [!CAUTION] 6 | > **Modifying and importing assets can be dangerous, and has the potential to corrupt your project. Be careful!** 7 | 8 | ## Importing Assets 9 | 10 | Shovel Utils offers an easy way to import several types of assets, including sprites, costumes, sounds, extensions, and even full projects. 11 | 12 | --- 13 | 14 | > [!TIP] 15 | > **This goes for all blocks that fetch from a link: If you're experiences errors and are not able to import an asset from a link, check your console! You may be running into a CORS error. To resolve this, use a proxy like [corsproxy.io](https://corsproxy.io).** 16 | 17 | ```scratch 18 | import sprite from [Link or data uri here] 19 | ``` 20 | 21 | Imports a sprite into the project using a DataURI or link. Be mindful of sprite names; having two or more sprites with the same name can cause issues. 22 | 23 | ```scratch 24 | import image from [https://extensions.turbowarp.org/dango.png] name [Dango] 25 | ``` 26 | 27 | Imports a costume from a PNG, Bitmap, or JPEG. **Does not work with SVGS**. The costume imports into the current sprite/backdrop the user has selected. 28 | 29 | ```scratch 30 | import sound from [https://extensions.turbowarp.org/meow.mp3] name [Meow] 31 | ``` 32 | 33 | Imports a sound from any Scratch-compatible sound file. The sound imports into the current sprite/backdrop the user has selected. 34 | 35 | ```scratch 36 | import project from [https://extensions.turbowarp.org/samples/Box2D.sb3] 37 | ``` 38 | 39 | Imports a full project from a link. This project will completely replace the contents of the current one. If the project is unsandboxed, it will ask permission before swapping contents. 40 | 41 | ```scratch 42 | load extension from [https://extensions.turbowarp.org/utilities.js] 43 | ``` 44 | 45 | Imports any extension from a link. Extensions from the [Extension Gallery](https://extensions.turbowarp.org) can run unsandboxed, and don't require permission to import. 46 | 47 | ## Other Ways to Modify The Project 48 | 49 | Aside from importing assets, Shovel Utils provides multiple miscellaneous features to modify and straight up delete parts of your projects. 50 | 51 | ```scratch 52 | set editing target to [Sprite1] 53 | ``` 54 | 55 | Sets the selected sprite in the editor. You can also set your input to "Stage" to set the selected target to the backdrop. This does work packaged, however will not have a visual effect. 56 | 57 | ```scratch 58 | (all sprites ::) 59 | ``` 60 | 61 | Gets the names of all the sprites (and the stage) as a JSON array. This can then be parsed using the JSON Extension. 62 | 63 | ```scratch 64 | restart project 65 | ``` 66 | 67 | Emulates a green flag click on a project, even if the green flag isn't present. 68 | 69 | ```scratch 70 | delete costume [costume1] in [Sprite1] 71 | ``` 72 | 73 | Deletes a costume from the specified sprite. If the costume doesn't exist, the block simply doesn't do anything. 74 | 75 | ```scratch 76 | delete sprite [Sprite1] 77 | ``` 78 | 79 | Deletes a the specified sprite. If the user has the "Sprite Deletion Confirmation" addon enabled and the project is unpackaged, it will ask permission before deleting sprites. 80 | 81 | ## Miscellaneous Features 82 | 83 | Aside from project modification, there's several utility blocks present in Shovel Utils. 84 | 85 | ```scratch 86 | (fps::) 87 | ``` 88 | 89 | Get the accurate FPS, or frames per second, of the current project. This is *not* the same as the "framerate limit" block from Runtime Options, as the block in Shovel Utils accounts for lag. 90 | 91 | ```scratch 92 | (list [MyList] as array) 93 | ``` 94 | 95 | Get the values of a list, exported as a JSON array. If the specified list has not been created yet, or is empty, the block will return empty. 96 | 97 | ```scratch 98 | set list [MyList] to [⟦1,2⟧] 99 | ``` 100 | 101 | Sets the values of lists. Accepts JSON arrays as inputs. If the specified list has not been created yet, the block simply doesn't do anything. 102 | 103 | ```scratch 104 | (brightness of [ #ffffff] ::) 105 | ``` 106 | 107 | Gets the brightness of a hex value. Reports a whole number between 0 and 255. To transfer this to a value between 0 and 100 (what TurboWarp uses), divide the output of the block by 2.55 and round. 108 | -------------------------------------------------------------------------------- /docs/bitwise.md: -------------------------------------------------------------------------------- 1 | # Bitwise 2 | 3 | This extension allows you to perform bit shifts and logic operations on integers as if they were encoded in binary. These operations can be used to create programmer calculators and more. 4 | 5 | The extension uses signed 32-bit integers, which can range from -2,147,483,648 to 2,147,483,647. 6 | 7 | ## Blocks 8 | 9 | ```scratch 10 | 11 | ``` 12 | Returns true if the input is a binary number. Binary numbers can only contain 0s and 1s, so this essentially tells you if the input only contains 0s and 1s. 13 | 14 | --- 15 | 16 | ```scratch 17 | ([32] to binary :: #17cde6) 18 | ``` 19 | 20 | Converts the input from decimal to binary and returns the result. 21 | 22 | Note: All other blocks except for `is () binary?` and `() to number` will treat this result as a decimal number, so make sure to convert it back to decimal before you perform operations on it. 23 | 24 | --- 25 | 26 | ```scratch 27 | ([0000000000100000] to number :: #17cde6) 28 | ``` 29 | 30 | Converts the input from binary to decimal and returns the result. 31 | 32 | --- 33 | 34 | ```scratch 35 | ([x] >> [y] :: #17cde6) 36 | ``` 37 | Arithmatically shifts each bit of the binary representation of _x_ to the right _y_ times and returns the result. 38 | 39 | Example: `32` → `100000` → `010000` → `16` 40 | 41 | The sign will be preserved, so negative numbers will stay negative. 42 | 43 | This is essentially the same thing as dividing by 2^*y* and rounding down to the nearest integer. 44 | 45 | --- 46 | 47 | ```scratch 48 | ([x] \<\< [y] :: #17cde6) 49 | ``` 50 | Arithmatically shifts each bit of the binary representation of _x_ to the left _y_ times and returns the result. 51 | 52 | Example: `32` → `0100000` → `1000000` → `64` 53 | 54 | The sign will be preserved, so negative numbers will stay negative. 55 | 56 | This is essentially the same thing as multiplying by 2^*y*. 57 | 58 | --- 59 | 60 | ```scratch 61 | ([x] >>> [y] :: #17cde6) 62 | ``` 63 | Logically shifts each bit of the binary representation of _x_ to the right _y_ times and returns the result. 64 | 65 | This also moves the sign bit, so it can result in negative numbers becoming positive. 66 | 67 | --- 68 | 69 | ```scratch 70 | ([x] ↻ [y] :: #17cde6) 71 | ``` 72 | Shifts each bit of the binary representation of _x_ to the right _y_ times. Numbers shifted past one end will reappear on the other (circle around). 73 | 74 | Example: `3` → `00000000000000000000000000000011` → `10000000000000000000000000000001` → `-2147483647` 75 | 76 | --- 77 | 78 | ```scratch 79 | ([x] ↺ [y] :: #17cde6) 80 | ``` 81 | Shifts each bit of the binary representation of _x_ to the left _y_ times. Numbers shifted past one end will reappear on the other (circle around). 82 | 83 | Example: `-2147483647` → `10000000000000000000000000000001` → `00000000000000000000000000000011` → `3` 84 | 85 | --- 86 | 87 | ```scratch 88 | ([x] and [y] :: #17cde6) 89 | ``` 90 | Logically ANDs the binary representation of the inputs together and returns the result. 91 | 92 | Example: 93 | 94 | ``` 95 | 14 1110 96 | AND 7 AND 0111 97 | —————————————————— 98 | 6 0110 99 | ``` 100 | 101 | --- 102 | 103 | ```scratch 104 | ([x] or [y] :: #17cde6) 105 | ``` 106 | 107 | Logically ORs the binary representation of the inputs together and returns the result. 108 | 109 | Example: 110 | 111 | ``` 112 | 14 1110 113 | OR 7 OR 0111 114 | —————————————————— 115 | 15 1111 116 | ``` 117 | 118 | --- 119 | 120 | ```scratch 121 | ([x] xor [y] :: #17cde6) 122 | ``` 123 | 124 | Logically XORs the binary representation of the inputs together and returns the result. 125 | 126 | Example: 127 | 128 | ``` 129 | 14 1110 130 | XOR 3 XOR 0011 131 | —————————————————— 132 | 13 1101 133 | ``` 134 | 135 | --- 136 | 137 | ```scratch 138 | (not [x] :: #17cde6) 139 | ``` 140 | 141 | Flips all bits of the binary representation of _x_ and returns the result. 142 | 143 | Example: 144 | 145 | ``` 146 | NOT 42 NOT ...00101010 147 | ————————————————————————— 148 | -43 ...11010101 149 | ``` 150 | -------------------------------------------------------------------------------- /docs/steamworks.md: -------------------------------------------------------------------------------- 1 | # Steamworks 2 | 3 | The Steamworks extension lets you use these Steam APIs: 4 | 5 | - Basic user information (name, id, level, country) 6 | - Achievements 7 | - DLC 8 | - Opening URLs in the Steam Overlay 9 | 10 | This extension was not created by and is not supported by Valve. 11 | 12 | ## Enabling Steamworks 13 | 14 | The Steamworks SDK will be automatically downloaded and enabled when a project using the Steamworks extension is put into the [TurboWarp Packager](https://packager.turbowarp.org/). You'll be asked to provide your game's App ID, which you can find on the Steamworks website. If you don't have an App ID yet, [use the demo game](#demo-game). You will also need to select one of these environments: 15 | 16 | - Electron Windows application (64-bit) 17 | - Electron macOS application 18 | (Warning: macOS games published on Steam need to be notarized by Apple, which the packager doesn't support. You can still test your game on a Mac, but you won't be able to publish it for macOS yet.) 19 | - Electron Linux application (64-bit) 20 | 21 | You may have to look under "Other environments" to find some of these. The blocks will not work in the editor, 32-bit environments, ARM environments, plain HTML files, WKWebView, or NW.js. You can still run the blocks, they just won't interact with Steam at all. 22 | 23 | You can run the packaged executable directly as usual; you don't need to start the game from Steam for the Steamworks extension to function. However there are a couple caveats when doing this: 24 | 25 | - On macOS and Linux, when not started through Steam, the Steam Overlay may not work 26 | - On Linux, when not started through Steam, Steamworks may not work if Steam is installed from Flatpak/Snap instead of as a native package 27 | 28 | ## Security considerations 29 | 30 | > [!CAUTION] 31 | > **Using the Steamworks extension will not prevent people from pirating your game.** 32 | > 33 | > The Steamworks extension is also inherently client-side, so a cheater could manipulate all of the Steamworks blocks to return whatever they want. You shouldn't use them for things that are security critical. 34 | 35 | ## Demo game 36 | 37 | To test the Steamworks extension without paying for a Steamworks Partner Program membership, you can use the Steamworks demo game. It's called Spacewar and its App ID is `480`. You don't need to install or run Spacewar; rather, you can use its App ID to test various Steamworks APIs as if you were the Spacewar developers. 38 | 39 | Spacewar has achievements with the following API Names, which can used for testing the achievement blocks: 40 | 41 | - `ACH_WIN_ONE_GAME` 42 | - `ACH_WIN_100_GAMES` 43 | - `ACH_TRAVEL_FAR_ACCUM` 44 | - `ACH_TRAVEL_FAR_SINGLE` 45 | 46 | ## Basic information 47 | 48 | Remember that Steamworks is only properly enabled when your project is packaged in a few specific environments. You can detect if this is the case using: 49 | 50 | ```scratch 51 | 52 | ``` 53 | 54 | Then you can get basic information about the user using: 55 | 56 | ```scratch 57 | (get user (name v) :: #136C9F) 58 | ``` 59 | 60 | ## Achievements 61 | 62 | Achievements are created on the Steamworks website. The **API Name** of each achievement is what you need to provide in your project's code to the Steamworks extension. 63 | 64 | This would unlock the `ACH_WIN_ONE_GAME` achievement from Spacewar: 65 | 66 | ```scratch 67 | when this sprite clicked 68 | set achievement [ACH_WIN_ONE_GAME] unlocked to (true v) :: #136C9F 69 | ``` 70 | 71 | You can also detect if an achievement has already been unlocked: 72 | 73 | ```scratch 74 | when flag clicked 75 | forever 76 | if then 77 | say [Unlocked!] 78 | else 79 | say [Not unlocked :(] 80 | end 81 | end 82 | ``` 83 | 84 | ## DLC 85 | 86 | Each DLC has its own App ID which you can find on the Steamworks website. You can detect if it is installed using: 87 | 88 | ```scratch 89 | if <(DLC v) [1234] installed? :: #136C9F> then 90 | 91 | end 92 | ``` 93 | 94 | ## Overlay 95 | 96 | The Steamworks extension has a block to open URLs in the Steam Overlay's web browser. If the overlay is not working, it might open in the Steam app instead. If that also doesn't work, it will open in the default web browser. Regardless, packaged projects never display security prompts like "The project wants to open a new window or tab". 97 | 98 | ```scratch 99 | open (URL v) [https://example.com/] in overlay :: #136C9F 100 | ``` 101 | -------------------------------------------------------------------------------- /extensions/Clay/htmlEncode.js: -------------------------------------------------------------------------------- 1 | // Name: HTML Encode 2 | // ID: clayhtmlencode 3 | // Description: Escape untrusted text to safely include in HTML. 4 | // By: ClaytonTDM 5 | // License: MIT 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | class HtmlEncode { 11 | getInfo() { 12 | return { 13 | id: "claytonhtmlencode", 14 | name: Scratch.translate("HTML Encode"), 15 | blocks: [ 16 | { 17 | opcode: "encode", 18 | blockType: Scratch.BlockType.REPORTER, 19 | text: Scratch.translate("encode [text] as HTML-safe"), 20 | arguments: { 21 | text: { 22 | type: Scratch.ArgumentType.STRING, 23 | // don't use a script tag as the example here as the closing script 24 | // tag might break things when this extension gets inlined in packed 25 | // projects 26 | defaultValue: `

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

`, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }; 32 | } 33 | 34 | encode({ text }) { 35 | return Scratch.Cast.toString(text).replace(/["'&<>]/g, (a) => { 36 | switch (a) { 37 | case "&": 38 | return "&"; 39 | case '"': 40 | return "'"; 41 | case "'": 42 | return """; 43 | case ">": 44 | return ">"; 45 | case "<": 46 | return "<"; 47 | } 48 | // this should never happen... 49 | return ""; 50 | }); 51 | } 52 | } 53 | 54 | Scratch.extensions.register(new HtmlEncode()); 55 | })(Scratch); 56 | -------------------------------------------------------------------------------- /extensions/DNin/wake-lock.js: -------------------------------------------------------------------------------- 1 | // Name: Wake Lock 2 | // ID: dninwakelock 3 | // Description: Prevent the computer from falling asleep. 4 | // By: D-ScratchNinja 5 | // License: MIT 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | if (!Scratch.extensions.unsandboxed) { 11 | throw new Error("Wake Lock extension must run unsandboxed"); 12 | } 13 | 14 | /** @type {WakeLockSentinel} */ 15 | let wakeLock = null; 16 | let latestEnabled = false; 17 | let promise = Promise.resolve(); 18 | 19 | class WakeLock { 20 | constructor(runtime) { 21 | this.runtime = runtime; 22 | this.runtime.on("PROJECT_STOP_ALL", this.stopAll.bind(this)); 23 | 24 | document.addEventListener("visibilitychange", () => { 25 | // If enabled, reacquire wake lock when document becomes visible again 26 | if (wakeLock !== null && document.visibilityState === "visible") { 27 | latestEnabled = false; 28 | this.setWakeLock({ 29 | enabled: true, 30 | }); 31 | } 32 | }); 33 | } 34 | 35 | getInfo() { 36 | return { 37 | id: "dninwakelock", 38 | name: Scratch.translate("Wake Lock"), 39 | docsURI: "https://extensions.turbowarp.org/DNin/wake-lock", 40 | blocks: [ 41 | { 42 | opcode: "setWakeLock", 43 | blockType: Scratch.BlockType.COMMAND, 44 | text: Scratch.translate({ 45 | default: "turn wake lock [enabled]", 46 | description: "[enabled] is a drop down with items 'on' and 'off'", 47 | }), 48 | arguments: { 49 | enabled: { 50 | type: Scratch.ArgumentType.STRING, 51 | menu: "state", 52 | defaultValue: "true", 53 | }, 54 | }, 55 | }, 56 | { 57 | opcode: "isLocked", 58 | blockType: Scratch.BlockType.BOOLEAN, 59 | text: Scratch.translate("is wake lock active?"), 60 | }, 61 | ], 62 | menus: { 63 | state: { 64 | acceptReporters: true, 65 | items: [ 66 | { 67 | text: Scratch.translate("on"), 68 | value: "true", 69 | }, 70 | { 71 | text: Scratch.translate("off"), 72 | value: "false", 73 | }, 74 | ], 75 | }, 76 | }, 77 | }; 78 | } 79 | 80 | stopAll() { 81 | this.setWakeLock({ 82 | enabled: false, 83 | }); 84 | } 85 | 86 | setWakeLock(args) { 87 | if (!navigator.wakeLock) { 88 | // Not supported in this browser. 89 | return; 90 | } 91 | const enable = Scratch.Cast.toBoolean(args.enabled); 92 | if (enable && document.visibilityState === "hidden") { 93 | // Can't request wake lock while document is hidden. 94 | return; 95 | } 96 | 97 | const previousEnabled = latestEnabled; 98 | latestEnabled = enable; 99 | if (latestEnabled && !previousEnabled) { 100 | promise = promise 101 | .then(() => navigator.wakeLock.request("screen")) 102 | .then((sentinel) => { 103 | wakeLock = sentinel; 104 | wakeLock.addEventListener("release", () => { 105 | if (document.visibilityState === "visible") { 106 | // If the document is hidden, wake lock should be reacquired when it's visible again. 107 | wakeLock = null; 108 | latestEnabled = false; 109 | } 110 | }); 111 | }) 112 | .catch((error) => { 113 | console.error(error); 114 | // Allow to retry 115 | latestEnabled = false; 116 | }); 117 | return promise; 118 | } else if (!latestEnabled && previousEnabled) { 119 | promise = promise 120 | .then(() => { 121 | if (wakeLock) { 122 | return wakeLock.release(); 123 | } else { 124 | // Attempt to enable in the first place didn't work 125 | } 126 | }) 127 | .then(() => { 128 | wakeLock = null; 129 | }) 130 | .catch((error) => { 131 | console.error(error); 132 | wakeLock = null; 133 | }); 134 | return promise; 135 | } 136 | } 137 | 138 | isLocked() { 139 | return !!wakeLock; 140 | } 141 | } 142 | 143 | Scratch.extensions.register(new WakeLock(Scratch.vm.runtime)); 144 | })(Scratch); 145 | -------------------------------------------------------------------------------- /extensions/Lily/AllMenus.js: -------------------------------------------------------------------------------- 1 | // Name: All Menus 2 | // ID: lmsAllMenus 3 | // Description: Special category with every menu from every Scratch category and extensions. 4 | // By: LilyMakesThings 5 | // License: MIT AND LGPL-3.0 6 | // Scratch-compatible: true 7 | 8 | (function (Scratch) { 9 | "use strict"; 10 | 11 | let blockXML; 12 | 13 | const blocklist = [ 14 | "looks_costumenumbername", 15 | "extension_wedo_tilt_menu", 16 | 17 | // Unused menu in More Events that won't be translated 18 | "lmsMoreEvents_menu_state", 19 | ]; 20 | 21 | const escapeXML = (text) => 22 | text.replace(/["'&<>]/g, (i) => { 23 | switch (i) { 24 | case "&": 25 | return "&"; 26 | case '"': 27 | return "'"; 28 | case "'": 29 | return """; 30 | case ">": 31 | return ">"; 32 | case "<": 33 | return "<"; 34 | } 35 | return ""; 36 | }); 37 | 38 | const refreshMenus = () => { 39 | if (!window.ScratchBlocks) return; 40 | Scratch.vm.removeListener("BLOCKSINFO_UPDATE", refreshMenus); 41 | 42 | let allBlocks = Object.keys(ScratchBlocks.Blocks); 43 | 44 | allBlocks = allBlocks.filter( 45 | (item) => item.includes("menu") && !blocklist.includes(item) 46 | ); 47 | 48 | const menuBlocks = allBlocks.map( 49 | (item) => 50 | '' 51 | ); 52 | 53 | blockXML = menuBlocks.join(""); 54 | Scratch.vm.runtime.extensionManager.refreshBlocks(); 55 | }; 56 | 57 | Scratch.vm.addListener("BLOCKSINFO_UPDATE", refreshMenus); 58 | 59 | class AllMenus { 60 | constructor() { 61 | Scratch.vm.runtime.on("EXTENSION_ADDED", () => { 62 | refreshMenus(); 63 | }); 64 | } 65 | 66 | getInfo() { 67 | return { 68 | id: "lmsAllMenus", 69 | name: Scratch.translate("All Menus"), 70 | blocks: [ 71 | { 72 | blockType: Scratch.BlockType.XML, 73 | xml: blockXML, 74 | }, 75 | ], 76 | }; 77 | } 78 | } 79 | 80 | refreshMenus(); 81 | 82 | Scratch.extensions.register(new AllMenus()); 83 | })(Scratch); 84 | -------------------------------------------------------------------------------- /extensions/Lily/Cast.js: -------------------------------------------------------------------------------- 1 | // Name: Cast 2 | // ID: lmsCast 3 | // Description: Convert values between types. 4 | // By: LilyMakesThings 5 | // License: MIT AND LGPL-3.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | const Cast = Scratch.Cast; 11 | 12 | class CastUtil { 13 | getInfo() { 14 | return { 15 | id: "lmsCast", 16 | name: Scratch.translate("Cast"), 17 | blocks: [ 18 | { 19 | opcode: "toType", 20 | blockType: Scratch.BlockType.REPORTER, 21 | text: Scratch.translate("cast [INPUT] to [TYPE]"), 22 | allowDropAnywhere: true, 23 | disableMonitor: true, 24 | arguments: { 25 | INPUT: { 26 | type: Scratch.ArgumentType.STRING, 27 | defaultValue: "apple", 28 | }, 29 | TYPE: { 30 | type: Scratch.ArgumentType.STRING, 31 | menu: "type", 32 | }, 33 | }, 34 | }, 35 | { 36 | opcode: "typeOf", 37 | blockType: Scratch.BlockType.REPORTER, 38 | text: Scratch.translate("type of [INPUT]"), 39 | disableMonitor: true, 40 | arguments: { 41 | INPUT: { 42 | type: Scratch.ArgumentType.STRING, 43 | defaultValue: "apple", 44 | }, 45 | }, 46 | }, 47 | ], 48 | menus: { 49 | type: { 50 | acceptReporters: true, 51 | items: [ 52 | { 53 | text: Scratch.translate("number"), 54 | value: "number", 55 | }, 56 | { 57 | text: Scratch.translate("string"), 58 | value: "string", 59 | }, 60 | { 61 | text: Scratch.translate("boolean"), 62 | value: "boolean", 63 | }, 64 | { 65 | text: Scratch.translate("default"), 66 | value: "default", 67 | }, 68 | ], 69 | }, 70 | }, 71 | }; 72 | } 73 | 74 | toType(args) { 75 | const input = args.INPUT; 76 | switch (args.TYPE) { 77 | case "number": 78 | return Cast.toNumber(input); 79 | case "string": 80 | return Cast.toString(input); 81 | case "boolean": 82 | return Cast.toBoolean(input); 83 | default: 84 | return input; 85 | } 86 | } 87 | 88 | typeOf(args) { 89 | return typeof args.INPUT; 90 | } 91 | } 92 | 93 | Scratch.extensions.register(new CastUtil()); 94 | })(Scratch); 95 | -------------------------------------------------------------------------------- /extensions/Lily/CommentBlocks.js: -------------------------------------------------------------------------------- 1 | // Name: Comment Blocks 2 | // ID: lmscomments 3 | // Description: Annotate your scripts. 4 | // By: LilyMakesThings 5 | // License: MIT AND LGPL-3.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | class CommentBlocks { 11 | getInfo() { 12 | const defaultValue = Scratch.translate({ 13 | default: "comment", 14 | description: "Default comment value", 15 | }); 16 | return { 17 | id: "lmscomments", 18 | name: Scratch.translate("Comment Blocks"), 19 | color1: "#e4db8c", 20 | color2: "#c6be79", 21 | color3: "#a8a167", 22 | blocks: [ 23 | /* eslint-disable extension/should-translate */ 24 | { 25 | opcode: "commentHat", 26 | blockType: Scratch.BlockType.HAT, 27 | text: "// [COMMENT]", 28 | isEdgeActivated: false, 29 | arguments: { 30 | COMMENT: { 31 | type: Scratch.ArgumentType.STRING, 32 | defaultValue: defaultValue, 33 | }, 34 | }, 35 | }, 36 | { 37 | opcode: "commentCommand", 38 | blockType: Scratch.BlockType.COMMAND, 39 | text: "// [COMMENT]", 40 | arguments: { 41 | COMMENT: { 42 | type: Scratch.ArgumentType.STRING, 43 | defaultValue: defaultValue, 44 | }, 45 | }, 46 | }, 47 | { 48 | opcode: "commentC", 49 | blockType: Scratch.BlockType.CONDITIONAL, 50 | text: "// [COMMENT]", 51 | arguments: { 52 | COMMENT: { 53 | type: Scratch.ArgumentType.STRING, 54 | defaultValue: defaultValue, 55 | }, 56 | }, 57 | }, 58 | { 59 | opcode: "commentReporter", 60 | blockType: Scratch.BlockType.REPORTER, 61 | text: "[INPUT] // [COMMENT]", 62 | allowDropAnywhere: true, 63 | arguments: { 64 | COMMENT: { 65 | type: Scratch.ArgumentType.STRING, 66 | defaultValue: defaultValue, 67 | }, 68 | INPUT: { 69 | type: Scratch.ArgumentType.STRING, 70 | defaultValue: "", 71 | }, 72 | }, 73 | }, 74 | { 75 | opcode: "commentBoolean", 76 | blockType: Scratch.BlockType.BOOLEAN, 77 | text: "[INPUT] // [COMMENT]", 78 | arguments: { 79 | COMMENT: { 80 | type: Scratch.ArgumentType.STRING, 81 | defaultValue: defaultValue, 82 | }, 83 | INPUT: { 84 | type: Scratch.ArgumentType.BOOLEAN, 85 | }, 86 | }, 87 | }, 88 | /* eslint-enable extension/should-translate */ 89 | ], 90 | }; 91 | } 92 | 93 | commentHat() { 94 | // no-op 95 | } 96 | 97 | commentCommand() { 98 | // no-op 99 | } 100 | 101 | commentC(args, util) { 102 | return true; 103 | } 104 | 105 | commentReporter(args) { 106 | return args.INPUT; 107 | } 108 | 109 | commentBoolean(args) { 110 | return args.INPUT || false; 111 | } 112 | } 113 | Scratch.extensions.register(new CommentBlocks()); 114 | })(Scratch); 115 | -------------------------------------------------------------------------------- /extensions/Lily/HackedBlocks.js: -------------------------------------------------------------------------------- 1 | // Name: Hidden Block Collection 2 | // ID: lmsHackedBlocks 3 | // Description: Various "hacked blocks" that work in Scratch but are not visible in the palette. 4 | // By: LilyMakesThings 5 | // By: pumpkinhasapatch 6 | // License: MIT AND LGPL-3.0 7 | // Scratch-compatible: true 8 | 9 | (function (Scratch) { 10 | "use strict"; 11 | 12 | class HackedBlocks { 13 | getInfo() { 14 | return { 15 | id: "lmsHackedBlocks", 16 | name: Scratch.translate("Hidden Blocks"), 17 | docsURI: "https://en.scratch-wiki.info/wiki/Hidden_Blocks#Events", 18 | blocks: [ 19 | // Use the sensing_touchingobjectmenu instead of event_ to also list sprites, since the block supports it 20 | { 21 | blockType: Scratch.BlockType.XML, 22 | xml: '', 23 | }, 24 | "---", 25 | { 26 | blockType: Scratch.BlockType.XML, 27 | xml: '10', 28 | }, 29 | { 30 | blockType: Scratch.BlockType.XML, 31 | xml: '', 32 | }, 33 | "---", 34 | // Counting blocks that function similarly to variables 35 | { 36 | blockType: Scratch.BlockType.XML, 37 | xml: '', 38 | }, 39 | { 40 | blockType: Scratch.BlockType.XML, 41 | xml: '', 42 | }, 43 | { 44 | blockType: Scratch.BlockType.XML, 45 | xml: '', 46 | }, 47 | "---", 48 | { 49 | blockType: Scratch.BlockType.XML, 50 | xml: '60', 51 | }, 52 | "---", 53 | { 54 | blockType: Scratch.BlockType.XML, 55 | xml: '', 56 | }, 57 | // Dot matrix input from the micro:bit extension 58 | // Returns a 5x5 binary grid of pixels depending on what was drawn. White pixels are 1 and green pixels are 0 59 | { 60 | blockType: Scratch.BlockType.XML, 61 | xml: '1111110101001000010001110', 62 | }, 63 | ], 64 | }; 65 | } 66 | } 67 | 68 | Scratch.extensions.register(new HackedBlocks()); 69 | })(Scratch); 70 | -------------------------------------------------------------------------------- /extensions/Lily/TempVariables.js: -------------------------------------------------------------------------------- 1 | (function (Scratch) { 2 | "use strict"; 3 | 4 | const menuIconURI = ""; 5 | 6 | // Object.create(null) prevents "variable [toString]" from returning a function 7 | let variables = Object.create(null); 8 | 9 | class TempVars { 10 | getInfo() { 11 | return { 12 | id: "lmstempvars", 13 | name: Scratch.translate("Temporary Variables"), 14 | color1: "#FF791A", 15 | color2: "#E15D00", 16 | menuIconURI: menuIconURI, 17 | blocks: [ 18 | { 19 | opcode: "setVariableTo", 20 | blockType: Scratch.BlockType.COMMAND, 21 | text: Scratch.translate("set variable [INPUTA] to [INPUTB]"), 22 | arguments: { 23 | INPUTA: { 24 | type: Scratch.ArgumentType.STRING, 25 | defaultValue: "my variable", 26 | }, 27 | INPUTB: { 28 | type: Scratch.ArgumentType.STRING, 29 | defaultValue: "0", 30 | }, 31 | }, 32 | }, 33 | { 34 | opcode: "changeVariableBy", 35 | blockType: Scratch.BlockType.COMMAND, 36 | text: Scratch.translate("change variable [INPUTA] by [INPUTB]"), 37 | arguments: { 38 | INPUTA: { 39 | type: Scratch.ArgumentType.STRING, 40 | defaultValue: "my variable", 41 | }, 42 | INPUTB: { 43 | type: Scratch.ArgumentType.STRING, 44 | defaultValue: "1", 45 | }, 46 | }, 47 | }, 48 | { 49 | opcode: "getVariable", 50 | blockType: Scratch.BlockType.REPORTER, 51 | text: Scratch.translate("variable [INPUT]"), 52 | disableMonitor: true, 53 | arguments: { 54 | INPUT: { 55 | type: Scratch.ArgumentType.STRING, 56 | defaultValue: "my variable", 57 | }, 58 | }, 59 | }, 60 | 61 | "---", 62 | 63 | { 64 | opcode: "deleteVariable", 65 | blockType: Scratch.BlockType.COMMAND, 66 | text: Scratch.translate("delete variable [INPUT]"), 67 | arguments: { 68 | INPUT: { 69 | type: Scratch.ArgumentType.STRING, 70 | defaultValue: "my variable", 71 | }, 72 | }, 73 | }, 74 | { 75 | opcode: "deleteAllVariables", 76 | blockType: Scratch.BlockType.COMMAND, 77 | text: Scratch.translate("delete all variables"), 78 | }, 79 | { 80 | opcode: "listVariables", 81 | blockType: Scratch.BlockType.REPORTER, 82 | text: Scratch.translate("list active variables"), 83 | disableMonitor: true, 84 | }, 85 | ], 86 | }; 87 | } 88 | 89 | getVariable(args) { 90 | if (args.INPUT in variables) { 91 | return variables[args.INPUT]; 92 | } else { 93 | return ""; 94 | } 95 | } 96 | 97 | setVariableTo(args) { 98 | variables[args.INPUTA] = args.INPUTB; 99 | } 100 | 101 | changeVariableBy(args) { 102 | if (args.INPUTA in variables) { 103 | const prev = Scratch.Cast.toNumber(variables[args.INPUTA]); 104 | const next = Scratch.Cast.toNumber(args.INPUTB); 105 | variables[args.INPUTA] = prev + next; 106 | } else { 107 | variables[args.INPUTA] = args.INPUTB; 108 | } 109 | } 110 | 111 | listVariables(args, util) { 112 | return Object.keys(variables).join(","); 113 | } 114 | 115 | deleteVariable(args) { 116 | Reflect.deleteProperty(variables, args.INPUT); 117 | } 118 | 119 | deleteAllVariables() { 120 | variables = Object.create(null); 121 | } 122 | } 123 | Scratch.extensions.register(new TempVars()); 124 | })(Scratch); 125 | -------------------------------------------------------------------------------- /extensions/NOname-awa/sort-unique-words.js: -------------------------------------------------------------------------------- 1 | (function (Scratch) { 2 | "use strict"; 3 | 4 | const parseEnglish = (text) => { 5 | const words = text.toLowerCase().match(/\b\w+\b/g); 6 | const uniques = Array.from(new Set(words)); 7 | uniques.sort(); 8 | return uniques; 9 | }; 10 | 11 | const parseChinese = (text) => { 12 | const words = text.match(/[^\u4e00-\u9fa5]+|[\u4e00-\u9fa5]+/g); 13 | const uniques = Array.from(new Set(words)); 14 | uniques.sort(function (a, b) { 15 | return a.localeCompare(b, "zh-Hans-CN", { sensitivity: "accent" }); 16 | }); 17 | return uniques; 18 | }; 19 | 20 | const parse = (text, language) => { 21 | if (language === "zh") { 22 | return parseChinese(text); 23 | } 24 | return parseEnglish(text); 25 | }; 26 | 27 | class SortUniqueWords { 28 | getInfo() { 29 | return { 30 | id: "nonameawasortuniquewords", 31 | name: Scratch.translate("Sort Unique Words"), 32 | color1: "#5a8b9e", 33 | color2: "#427081", 34 | color3: "#427081", 35 | blocks: [ 36 | { 37 | opcode: "words", 38 | blockType: Scratch.BlockType.REPORTER, 39 | disableMonitor: true, 40 | text: Scratch.translate( 41 | "sort unique words in [text] as [language]" 42 | ), 43 | arguments: { 44 | text: { 45 | type: Scratch.ArgumentType.STRING, 46 | defaultValue: Scratch.translate( 47 | "movie dog restaurant book school" 48 | ), 49 | }, 50 | language: { 51 | type: Scratch.ArgumentType.STRING, 52 | menu: "language", 53 | }, 54 | }, 55 | }, 56 | ], 57 | menus: { 58 | language: { 59 | acceptReporters: true, 60 | items: [ 61 | { 62 | text: Scratch.translate("English (en)"), 63 | value: "en", 64 | }, 65 | { 66 | text: Scratch.translate("Chinese (zh)"), 67 | value: "zh", 68 | }, 69 | ], 70 | }, 71 | }, 72 | }; 73 | } 74 | words(args) { 75 | const text = Scratch.Cast.toString(args.text); 76 | const words = parse(text, args.language); 77 | return words.join(" "); 78 | } 79 | } 80 | 81 | Scratch.extensions.register(new SortUniqueWords()); 82 | })(Scratch); 83 | -------------------------------------------------------------------------------- /extensions/PwLDev/vibration.js: -------------------------------------------------------------------------------- 1 | // Name: Vibration 2 | // ID: pwldevvibration 3 | // Description: Control the device's vibration. Only works on Chrome for Android. 4 | // By: PwLDev 5 | // License: MPL-2.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | if (!Scratch.extensions.unsandboxed) { 11 | throw new Error("This extension must run unsandboxed in order to work."); 12 | } 13 | 14 | class Vibration { 15 | getInfo() { 16 | return { 17 | id: "pwldevvibration", 18 | name: Scratch.translate("Vibration"), 19 | color1: "#45a15c", 20 | color2: "#317041", 21 | color3: "#35523c", 22 | blocks: [ 23 | { 24 | blockType: Scratch.BlockType.LABEL, 25 | text: Scratch.translate("Only works on Chrome for Android."), 26 | }, 27 | { 28 | opcode: "start", 29 | blockType: Scratch.BlockType.COMMAND, 30 | text: Scratch.translate("start vibrating for [SECONDS] seconds"), 31 | arguments: { 32 | SECONDS: { 33 | type: Scratch.ArgumentType.NUMBER, 34 | defaultValue: "2", 35 | }, 36 | }, 37 | }, 38 | { 39 | opcode: "startPattern", 40 | blockType: Scratch.BlockType.COMMAND, 41 | text: Scratch.translate("play vibration pattern [PATTERN]"), 42 | arguments: { 43 | PATTERN: { 44 | type: Scratch.ArgumentType.STRING, 45 | defaultValue: "1, 0.5, 1, 0.5, 1", 46 | }, 47 | }, 48 | }, 49 | { 50 | opcode: "stop", 51 | blockType: Scratch.BlockType.COMMAND, 52 | text: Scratch.translate("stop vibrating"), 53 | }, 54 | ], 55 | }; 56 | } 57 | 58 | start(args) { 59 | if (navigator.vibrate) { 60 | navigator.vibrate(Scratch.Cast.toNumber(args.SECONDS) * 1000); 61 | } 62 | } 63 | 64 | startPattern(args) { 65 | if (navigator.vibrate) { 66 | const pattern = Scratch.Cast.toString(args.PATTERN) 67 | .match(/[\w\-.]+/g) // Make into array 68 | ?.map((val) => Scratch.Cast.toNumber(val) * 1000); // Convert to numbers in milliseconds 69 | if (pattern) { 70 | navigator.vibrate(pattern); 71 | } 72 | } 73 | } 74 | 75 | stop() { 76 | if (navigator.vibrate) { 77 | navigator.vibrate(0); 78 | } 79 | } 80 | } 81 | 82 | Scratch.extensions.register(new Vibration()); 83 | })(Scratch); 84 | -------------------------------------------------------------------------------- /extensions/TheShovel/profanityAPI.js: -------------------------------------------------------------------------------- 1 | (function (Scratch) { 2 | "use strict"; 3 | class profanityAPI { 4 | getInfo() { 5 | return { 6 | id: "profanityAPI", 7 | name: Scratch.translate("Profanity API"), 8 | color1: "#cf6a3c", 9 | color2: "#cf6a3c", 10 | color3: "#cf6a3c", 11 | blocks: [ 12 | { 13 | opcode: "checkProfanity", 14 | blockType: Scratch.BlockType.REPORTER, 15 | text: Scratch.translate("remove profanity from [TEXT]"), 16 | arguments: { 17 | TEXT: { 18 | type: Scratch.ArgumentType.STRING, 19 | defaultValue: Scratch.translate("Hello, I love pizza!"), 20 | }, 21 | }, 22 | }, 23 | ], 24 | }; 25 | } 26 | 27 | checkProfanity({ TEXT }) { 28 | return Scratch.fetch( 29 | "https://www.purgomalum.com/service/plain?text=" + TEXT 30 | ) 31 | .then((r) => r.text()) 32 | .catch(() => ""); 33 | } 34 | } 35 | 36 | Scratch.extensions.register(new profanityAPI()); 37 | })(Scratch); 38 | -------------------------------------------------------------------------------- /extensions/WP-Studio01/text2speech.js: -------------------------------------------------------------------------------- 1 | (function (Scratch) { 2 | "use strict"; 3 | 4 | class Tools { 5 | getInfo() { 6 | return { 7 | id: "wpstudio01tts", 8 | name: Scratch.translate("System Text To Speech"), 9 | blocks: [ 10 | { 11 | opcode: "speak", 12 | blockType: Scratch.BlockType.COMMAND, 13 | text: Scratch.translate("speak [TEXT]"), 14 | arguments: { 15 | TEXT: { 16 | type: Scratch.ArgumentType.STRING, 17 | defaultValue: Scratch.translate("Hello"), 18 | }, 19 | }, 20 | }, 21 | ], 22 | }; 23 | } 24 | 25 | speak(args) { 26 | return new Promise((resolve, reject) => { 27 | const utterance = new SpeechSynthesisUtterance(args.TEXT); 28 | utterance.onend = () => { 29 | resolve(); 30 | }; 31 | utterance.onerror = () => { 32 | reject(new Error("Utterance error")); 33 | }; 34 | speechSynthesis.speak(utterance); 35 | }); 36 | } 37 | } 38 | 39 | Scratch.extensions.register(new Tools()); 40 | })(Scratch); 41 | -------------------------------------------------------------------------------- /extensions/XeroName/Deltatime.js: -------------------------------------------------------------------------------- 1 | // Name: Delta Time 2 | // ID: dtbyxeroname 3 | // Description: Precise delta timing blocks. 4 | // By: XeroName 5 | // License: MIT 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | const icon = 11 | "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgdmlld0JveD0iMCAwIDYwMCA2MDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjMwMCIgY3k9IjMwMCIgcj0iMzAwIiBmaWxsPSIjMjAyMDIwIi8+CjxwYXRoIGQ9Ik04Ny44NjggNTEyLjEzMkM2MC4wMTA0IDQ4NC4yNzQgMzcuOTEyNSA0NTEuMjAzIDIyLjgzNjEgNDE0LjgwNUM3Ljc1OTcyIDM3OC40MDcgLTMuNDQ0MTZlLTA2IDMzOS4zOTcgMCAzMDBDMy40NDQxNmUtMDYgMjYwLjYwMyA3Ljc1OTc0IDIyMS41OTMgMjIuODM2MiAxODUuMTk1QzM3LjkxMjYgMTQ4Ljc5NyA2MC4wMTA0IDExNS43MjYgODcuODY4IDg3Ljg2NzlDMTE1LjcyNiA2MC4wMTA0IDE0OC43OTcgMzcuOTEyNSAxODUuMTk1IDIyLjgzNjFDMjIxLjU5MyA3Ljc1OTcxIDI2MC42MDQgLTkuODYyNjZlLTA2IDMwMCAwQzMzOS4zOTcgOS44NjI2OGUtMDYgMzc4LjQwNyA3Ljc1OTc1IDQxNC44MDUgMjIuODM2MkM0NTEuMjAzIDM3LjkxMjYgNDg0LjI3NSA2MC4wMTA0IDUxMi4xMzIgODcuODY4TDMwMCAzMDBMODcuODY4IDUxMi4xMzJaIiBmaWxsPSIjMzAzMDMwIi8+CjxwYXRoIGQ9Ik0zMzAgNDM1TDIzMCAxODUiIHN0cm9rZT0iIzYxMjM2MSIgc3Ryb2tlLXdpZHRoPSIzMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0zMjAgMTg1SDQyME01MjAgMTg1SDQyME00MjAgMTg1VjQzNU0yOTkuNDUxIDQzMy42MjlMMjAwLjkyOCAxODcuMzIxQzIwMC41OTMgMTg2LjQ4MyAxOTkuNDA3IDE4Ni40ODMgMTk5LjA3MiAxODcuMzIxTDEwMC41NDkgNDMzLjYyOUMxMDAuMjg2IDQzNC4yODUgMTAwLjc3IDQzNSAxMDEuNDc3IDQzNUgyOTguNTIzQzI5OS4yMyA0MzUgMjk5LjcxNCA0MzQuMjg1IDI5OS40NTEgNDMzLjYyOVoiIHN0cm9rZT0iIzYwNjA2MCIgc3Ryb2tlLXdpZHRoPSIzMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0zMTAgNDE1TDIxMCAxNjUiIHN0cm9rZT0iI0ZGNUNGRiIgc3Ryb2tlLXdpZHRoPSIzMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxwYXRoIGQ9Ik0zMDAgMTY1SDQwME01MDAgMTY1SDQwME00MDAgMTY1VjQxNU0yNzkuNDUxIDQxMy42MjlMMTgwLjkyOCAxNjcuMzIxQzE4MC41OTMgMTY2LjQ4MyAxNzkuNDA3IDE2Ni40ODMgMTc5LjA3MiAxNjcuMzIxTDgwLjU0ODYgNDEzLjYyOUM4MC4yODU4IDQxNC4yODUgODAuNzY5NiA0MTUgODEuNDc3IDQxNUgyNzguNTIzQzI3OS4yMyA0MTUgMjc5LjcxNCA0MTQuMjg1IDI3OS40NTEgNDEzLjYyOVoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMzIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K"; 12 | 13 | if (!Scratch.extensions.unsandboxed) { 14 | throw new Error("DeltaTime must be run unsandboxed"); 15 | } 16 | 17 | const vm = Scratch.vm; 18 | 19 | let deltaTime = 0; 20 | let previousTime = 0; 21 | 22 | vm.runtime.on("BEFORE_EXECUTE", () => { 23 | const now = performance.now(); 24 | 25 | if (previousTime === 0) { 26 | // First frame. We used to always return 0 here, but that can break projects that 27 | // expect delta time to always be non-zero. Instead we'll make our best guess. 28 | deltaTime = 1 / vm.runtime.frameLoop.framerate; 29 | } else { 30 | deltaTime = (now - previousTime) / 1000; 31 | } 32 | 33 | previousTime = now; 34 | }); 35 | 36 | class Dt { 37 | getInfo() { 38 | return { 39 | id: "dtbyxeroname", 40 | name: Scratch.translate("Delta Time"), 41 | color1: "#333333", 42 | color2: "#444444", 43 | color3: "#ffffff", 44 | menuIconURI: icon, 45 | blocks: [ 46 | { 47 | opcode: "dt", 48 | blockType: Scratch.BlockType.REPORTER, 49 | // eslint-disable-next-line extension/should-translate 50 | text: "ΔT", 51 | }, 52 | { 53 | opcode: "fps", 54 | blockType: Scratch.BlockType.REPORTER, 55 | // eslint-disable-next-line extension/should-translate 56 | text: "fps", 57 | }, 58 | ], 59 | }; 60 | } 61 | 62 | dt() { 63 | return deltaTime; 64 | } 65 | 66 | fps() { 67 | return +(1 / deltaTime).toFixed(2); 68 | } 69 | } 70 | 71 | Scratch.extensions.register(new Dt()); 72 | })(Scratch); 73 | -------------------------------------------------------------------------------- /extensions/XmerOriginals/closecontrol.js: -------------------------------------------------------------------------------- 1 | // Name: Ask Before Closing Tab 2 | // ID: xmerclosecontrol 3 | // Description: Show a prompt when someone tries to close the tab. 4 | // By: XmerOriginals 5 | // License: MPL-2.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | let enabled = false; 11 | 12 | window.addEventListener("beforeunload", (e) => { 13 | if (enabled) { 14 | e.preventDefault(); 15 | } 16 | }); 17 | 18 | class CloseControl { 19 | getInfo() { 20 | return { 21 | id: "xmerclosecontrol", 22 | name: Scratch.translate("Ask Before Closing Tab"), 23 | blocks: [ 24 | { 25 | opcode: "setControl", 26 | blockType: Scratch.BlockType.COMMAND, 27 | text: Scratch.translate("set ask before closing tab to [OPTION]"), 28 | arguments: { 29 | OPTION: { 30 | type: Scratch.ArgumentType.STRING, 31 | menu: "option", 32 | }, 33 | }, 34 | }, 35 | { 36 | opcode: "getControl", 37 | blockType: Scratch.BlockType.BOOLEAN, 38 | text: Scratch.translate("ask before closing tab enabled?"), 39 | }, 40 | ], 41 | menus: { 42 | option: { 43 | acceptReporters: true, 44 | items: [ 45 | { 46 | text: Scratch.translate("enabled"), 47 | value: "true", 48 | }, 49 | { 50 | text: Scratch.translate("disabled"), 51 | value: "false", 52 | }, 53 | ], 54 | }, 55 | }, 56 | }; 57 | } 58 | 59 | setControl({ OPTION }) { 60 | enabled = Scratch.Cast.toBoolean(OPTION); 61 | } 62 | 63 | getControl() { 64 | return enabled; 65 | } 66 | } 67 | 68 | Scratch.extensions.register(new CloseControl()); 69 | })(Scratch); 70 | -------------------------------------------------------------------------------- /extensions/battery.js: -------------------------------------------------------------------------------- 1 | // Name: Battery 2 | // ID: battery 3 | // Description: Access information about the battery of phones or laptops. May not work on all devices and browsers. 4 | // License: MIT AND MPL-2.0 5 | 6 | (function (Scratch) { 7 | "use strict"; 8 | 9 | /** @type {Promise|null} */ 10 | let getBatteryPromise = null; 11 | /** @type {BatteryManager|null} */ 12 | let cachedBattery = null; 13 | /** @type {boolean} */ 14 | let batteryError = false; 15 | const withBattery = (callback) => { 16 | // Getting the BatteryManager is async the first time. Usually it's very fast, but we shouldn't assume that it is. 17 | // All the logic here lets us return values immediately when we have already got the battery instead of forcing 18 | // a delay by returning a promise. 19 | if (!navigator.getBattery || batteryError) { 20 | return callback(null); 21 | } 22 | if (cachedBattery) { 23 | return callback(cachedBattery); 24 | } 25 | if (!getBatteryPromise) { 26 | getBatteryPromise = navigator 27 | .getBattery() 28 | .then((battery) => { 29 | getBatteryPromise = null; 30 | cachedBattery = battery; 31 | 32 | cachedBattery.addEventListener("chargingchange", () => { 33 | Scratch.vm.runtime.startHats("battery_chargingChanged"); 34 | }); 35 | cachedBattery.addEventListener("levelchange", () => { 36 | Scratch.vm.runtime.startHats("battery_levelChanged"); 37 | }); 38 | cachedBattery.addEventListener("chargingtimechange", () => { 39 | Scratch.vm.runtime.startHats("battery_chargeTimeChanged"); 40 | }); 41 | cachedBattery.addEventListener("dischargingtimechange", () => { 42 | Scratch.vm.runtime.startHats("battery_dischargeTimeChanged"); 43 | }); 44 | 45 | return cachedBattery; 46 | }) 47 | .catch((error) => { 48 | getBatteryPromise = null; 49 | console.error("Could not get battery", error); 50 | batteryError = true; 51 | return null; 52 | }); 53 | } 54 | return getBatteryPromise.then((battery) => { 55 | return callback(battery); 56 | }); 57 | }; 58 | 59 | // Try to get the battery immediately so that event blocks work. 60 | withBattery(() => {}); 61 | 62 | class BatteryExtension { 63 | getInfo() { 64 | return { 65 | name: Scratch.translate("Battery"), 66 | id: "battery", 67 | color1: "#cf8436", 68 | blocks: [ 69 | { 70 | opcode: "charging", 71 | blockType: Scratch.BlockType.BOOLEAN, 72 | text: Scratch.translate("charging?"), 73 | }, 74 | { 75 | opcode: "level", 76 | blockType: Scratch.BlockType.REPORTER, 77 | text: Scratch.translate("battery level"), 78 | }, 79 | { 80 | opcode: "chargeTime", 81 | blockType: Scratch.BlockType.REPORTER, 82 | text: Scratch.translate("seconds until charged"), 83 | }, 84 | { 85 | opcode: "dischargeTime", 86 | blockType: Scratch.BlockType.REPORTER, 87 | text: Scratch.translate("seconds until empty"), 88 | }, 89 | { 90 | opcode: "chargingChanged", 91 | blockType: Scratch.BlockType.EVENT, 92 | text: Scratch.translate("when charging changed"), 93 | isEdgeActivated: false, 94 | }, 95 | { 96 | opcode: "levelChanged", 97 | blockType: Scratch.BlockType.EVENT, 98 | text: Scratch.translate("when battery level changed"), 99 | isEdgeActivated: false, 100 | }, 101 | { 102 | opcode: "chargeTimeChanged", 103 | blockType: Scratch.BlockType.EVENT, 104 | text: Scratch.translate("when time until charged changed"), 105 | isEdgeActivated: false, 106 | }, 107 | { 108 | opcode: "dischargeTimeChanged", 109 | blockType: Scratch.BlockType.EVENT, 110 | text: Scratch.translate("when time until empty changed"), 111 | isEdgeActivated: false, 112 | }, 113 | ], 114 | }; 115 | } 116 | charging() { 117 | return withBattery((battery) => { 118 | if (!battery) return true; 119 | return battery.charging; 120 | }); 121 | } 122 | level() { 123 | return withBattery((battery) => { 124 | if (!battery) return 100; 125 | return battery.level * 100; 126 | }); 127 | } 128 | chargeTime() { 129 | return withBattery((battery) => { 130 | if (!battery) return 0; 131 | return battery.chargingTime; 132 | }); 133 | } 134 | dischargeTime() { 135 | return withBattery((battery) => { 136 | if (!battery) return Infinity; 137 | return battery.dischargingTime; 138 | }); 139 | } 140 | } 141 | 142 | Scratch.extensions.register(new BatteryExtension()); 143 | })(Scratch); 144 | -------------------------------------------------------------------------------- /extensions/clouddata-ping.js: -------------------------------------------------------------------------------- 1 | // Name: Ping Cloud Data 2 | // ID: clouddataping 3 | // Description: Determine whether a cloud variable server is probably up. 4 | // Original: TheShovel 5 | // License: MIT AND MPL-2.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | /** 11 | * @typedef CacheEntry 12 | * @property {number} expires 13 | * @property {boolean} value 14 | */ 15 | 16 | /** @type {Map>} */ 17 | const computing = new Map(); 18 | /** @type {Map} */ 19 | const computed = new Map(); 20 | 21 | /** 22 | * @param {string} uri 23 | * @returns {Promise} 24 | */ 25 | const pingWebSocket = async (uri) => { 26 | if (!(await Scratch.canFetch(uri))) { 27 | return { 28 | expires: 0, 29 | value: false, 30 | }; 31 | } 32 | 33 | /** @type {WebSocket} */ 34 | let ws; 35 | try { 36 | // Permission is checked earlier. 37 | // eslint-disable-next-line extension/check-can-fetch 38 | ws = new WebSocket(uri); 39 | } catch (e) { 40 | return { 41 | expires: 0, 42 | value: false, 43 | }; 44 | } 45 | 46 | let timeoutId; 47 | const isUp = await new Promise((resolve) => { 48 | ws.onopen = () => { 49 | setTimeout(() => { 50 | resolve(true); 51 | }, 2000); 52 | }; 53 | ws.onclose = () => { 54 | resolve(false); 55 | }; 56 | ws.onerror = () => { 57 | resolve(false); 58 | }; 59 | timeoutId = setTimeout(() => { 60 | ws.close(); 61 | }, 5000); 62 | }); 63 | 64 | ws.close(); 65 | clearTimeout(timeoutId); 66 | 67 | return { 68 | expires: Date.now() + 60000, 69 | value: isUp, 70 | }; 71 | }; 72 | 73 | /** 74 | * @param {string} uri 75 | * @returns {boolean|Promise} 76 | */ 77 | const cachedPingWebSocket = (uri) => { 78 | const computingEntry = computing.get(uri); 79 | if (computingEntry) { 80 | return computingEntry.then((entry) => entry.value); 81 | } 82 | 83 | const computedEntry = computed.get(uri); 84 | if (computedEntry && Date.now() < computedEntry.expires) { 85 | return computedEntry.value; 86 | } 87 | 88 | const promise = pingWebSocket(uri); 89 | computing.set(uri, promise); 90 | return promise.then((entry) => { 91 | computing.delete(uri); 92 | computed.set(uri, entry); 93 | return entry.value; 94 | }); 95 | }; 96 | 97 | class PingUtil { 98 | getInfo() { 99 | return { 100 | id: "clouddataping", 101 | name: Scratch.translate("Ping Cloud Data"), 102 | blocks: [ 103 | { 104 | opcode: "ping", 105 | blockType: Scratch.BlockType.BOOLEAN, 106 | text: Scratch.translate("is cloud data server [SERVER] up?"), 107 | arguments: { 108 | SERVER: { 109 | type: Scratch.ArgumentType.STRING, 110 | defaultValue: "wss://clouddata.turbowarp.org", 111 | }, 112 | }, 113 | }, 114 | ], 115 | }; 116 | } 117 | 118 | ping({ SERVER }) { 119 | return cachedPingWebSocket(SERVER); 120 | } 121 | } 122 | 123 | Scratch.extensions.register(new PingUtil()); 124 | })(Scratch); 125 | -------------------------------------------------------------------------------- /extensions/cs2627883/numericalencoding.js: -------------------------------------------------------------------------------- 1 | // Name: Numerical Encoding V1 2 | // ID: cs2627883NumericalEncoding 3 | // Description: Use V2 instead as it is more efficient. V1 only exists for compatibility reasons. 4 | // By: cs2627883 5 | // License: MIT 6 | 7 | // https://github.com/CS2627883/Turbowarp-Encoding-Extension/blob/main/Encoding.js 8 | 9 | (function (Scratch) { 10 | "use strict"; 11 | 12 | // There are 149,186 unicode characters, so the maximum character code length is 6 13 | const MAX_CHAR_LEN = 6; 14 | 15 | /** 16 | * @param {string} str 17 | * @returns {string} 18 | */ 19 | const encode = (str) => { 20 | let encoded = ""; 21 | for (let i = 0; i < str.length; ++i) { 22 | // Get character 23 | const char = String(str.charCodeAt(i)); 24 | // Pad encodedChar with 0s to ensure all encodedchars are the same length 25 | const encodedChar = "0".repeat(MAX_CHAR_LEN - char.length) + char; 26 | encoded += encodedChar; 27 | } 28 | return encoded; 29 | }; 30 | 31 | /** 32 | * @param {string} str 33 | * @returns {string} 34 | */ 35 | const decode = (str) => { 36 | if (str === "") { 37 | return ""; 38 | } 39 | let decoded = ""; 40 | // Create regex to split by char length 41 | const regex = new RegExp(".{1," + MAX_CHAR_LEN + "}", "g"); 42 | // Split into array of characters 43 | const split = str.match(regex); 44 | for (let i = 0; i < split.length; i++) { 45 | // Get character from char code 46 | const decodedChar = String.fromCharCode(+split[i]); 47 | decoded += decodedChar; 48 | } 49 | return decoded; 50 | }; 51 | 52 | class NumericalEncodingExtension { 53 | /** @type {string|number} */ 54 | encoded = 0; 55 | 56 | /** @type {string|number} */ 57 | decoded = 0; 58 | 59 | getInfo() { 60 | return { 61 | id: "cs2627883NumericalEncoding", 62 | name: Scratch.translate("Numerical Encoding V1"), 63 | blocks: [ 64 | { 65 | opcode: "NumericalEncode", 66 | blockType: Scratch.BlockType.COMMAND, 67 | text: Scratch.translate("encode [DATA] to numbers"), 68 | arguments: { 69 | DATA: { 70 | type: Scratch.ArgumentType.STRING, 71 | defaultValue: Scratch.translate("Hello!"), 72 | }, 73 | }, 74 | }, 75 | { 76 | opcode: "NumericalDecode", 77 | blockType: Scratch.BlockType.COMMAND, 78 | text: Scratch.translate("decode [ENCODED] back to text"), 79 | arguments: { 80 | ENCODED: { 81 | type: Scratch.ArgumentType.STRING, 82 | defaultValue: encode(Scratch.translate("Hello!")), 83 | }, 84 | }, 85 | }, 86 | { 87 | opcode: "GetNumericalEncoded", 88 | blockType: Scratch.BlockType.REPORTER, 89 | text: Scratch.translate("encoded"), 90 | }, 91 | { 92 | opcode: "GetNumericalDecoded", 93 | blockType: Scratch.BlockType.REPORTER, 94 | text: Scratch.translate("decoded"), 95 | }, 96 | ], 97 | }; 98 | } 99 | NumericalEncode(args) { 100 | this.encoded = encode(Scratch.Cast.toString(args.DATA)); 101 | } 102 | NumericalDecode(args) { 103 | this.decoded = decode(Scratch.Cast.toString(args.ENCODED)); 104 | } 105 | GetNumericalEncoded(args) { 106 | return this.encoded; 107 | } 108 | GetNumericalDecoded(args) { 109 | return this.decoded; 110 | } 111 | } 112 | 113 | Scratch.extensions.register(new NumericalEncodingExtension()); 114 | })(Scratch); 115 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/block-utility-examples.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This Block Utility example must run unsandboxed'); 8 | } 9 | 10 | class BlockUtilityExamples { 11 | getInfo() { 12 | return { 13 | id: 'blockutilityexamples', 14 | name: 'BlockUtility Examples', 15 | blocks: [ 16 | { 17 | opcode: 'getSpriteName', 18 | text: 'sprite name', 19 | blockType: Scratch.BlockType.REPORTER, 20 | }, 21 | { 22 | opcode: 'doesVariableExist', 23 | text: 'is there a [TYPE] named [NAME]?', 24 | blockType: Scratch.BlockType.BOOLEAN, 25 | arguments: { 26 | NAME: { 27 | type: Scratch.ArgumentType.STRING, 28 | defaultValue: 'my variable' 29 | }, 30 | TYPE: { 31 | type: Scratch.ArgumentType.STRING, 32 | menu: 'TYPE_MENU', 33 | defaultValue: 'list' 34 | } 35 | } 36 | } 37 | ], 38 | menus: { 39 | TYPE_MENU: { 40 | acceptReporters: true, 41 | items: [ 42 | // Value here corresponds to the internal types of the variables 43 | // in scratch-vm. And yes, broadcasts are actually variables. 44 | // https://github.com/TurboWarp/scratch-vm/blob/20c60193c1c567a65cca87b16d22c51963565a43/src/engine/variable.js#L43-L67 45 | { 46 | text: 'variable', 47 | value: '' 48 | }, 49 | 'list', 50 | { 51 | text: 'broadcast', 52 | value: 'broadcast_msg' 53 | } 54 | ] 55 | } 56 | } 57 | }; 58 | } 59 | // highlight-start 60 | getSpriteName(args, util) { 61 | return util.target.getName(); 62 | } 63 | doesVariableExist(args, util) { 64 | const variable = util.target.lookupVariableByNameAndType(args.NAME.toString(), args.TYPE); 65 | // Remember: Boolean blocks need to explicitly return a boolean on their own 66 | return !!variable; 67 | } 68 | // highlight-end 69 | } 70 | Scratch.extensions.register(new BlockUtilityExamples()); 71 | })(Scratch); 72 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/broadcast-1.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class Broadcast1 { 6 | getInfo() { 7 | return { 8 | id: 'broadcast1example', 9 | name: 'Broadcast Example 1', 10 | blocks: [ 11 | { 12 | opcode: 'whenReceived', 13 | // highlight-start 14 | blockType: Scratch.BlockType.HAT, 15 | text: 'when I receive the event', 16 | isEdgeActivated: false 17 | // highlight-end 18 | }, 19 | { 20 | opcode: 'broadcast', 21 | blockType: Scratch.BlockType.COMMAND, 22 | text: 'broadcast the event' 23 | } 24 | ] 25 | }; 26 | } 27 | // highlight-start 28 | broadcast(args, util) { 29 | util.startHats('broadcast1example_whenReceived'); 30 | } 31 | // highlight-end 32 | } 33 | Scratch.extensions.register(new Broadcast1()); 34 | }(Scratch)); 35 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/broadcast-2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class Broadcast2 { 6 | getInfo() { 7 | return { 8 | id: 'broadcast2example', 9 | name: 'Broadcast Example 2', 10 | blocks: [ 11 | { 12 | opcode: 'whenReceived', 13 | blockType: Scratch.BlockType.HAT, 14 | text: 'when I receive [EVENT_OPTION]', 15 | isEdgeActivated: false, 16 | // highlight-start 17 | arguments: { 18 | EVENT_OPTION: { 19 | type: Scratch.ArgumentType.STRING, 20 | menu: 'EVENT_FIELD' 21 | } 22 | } 23 | // highlight-end 24 | }, 25 | { 26 | opcode: 'broadcast', 27 | blockType: Scratch.BlockType.COMMAND, 28 | text: 'broadcast [EVENT]', 29 | // highlight-start 30 | arguments: { 31 | EVENT: { 32 | type: Scratch.ArgumentType.STRING, 33 | menu: 'EVENT_FIELD' 34 | } 35 | } 36 | // highlight-end 37 | }, 38 | { 39 | opcode: 'broadcastAll', 40 | blockType: Scratch.BlockType.COMMAND, 41 | text: 'broadcast all', 42 | } 43 | ], 44 | menus: { 45 | EVENT_FIELD: { 46 | // highlight-next-line 47 | acceptReporters: false, 48 | items: [ 49 | 'Event 1', 50 | 'Event 2', 51 | 'Event 3' 52 | ] 53 | } 54 | } 55 | }; 56 | } 57 | // highlight-start 58 | broadcast({EVENT}, util) { 59 | util.startHats('broadcast2example_whenReceived', { 60 | EVENT_OPTION: EVENT 61 | }); 62 | } 63 | broadcastAll(args, util) { 64 | util.startHats('broadcast2example_whenReceived'); 65 | } 66 | // highlight-end 67 | } 68 | Scratch.extensions.register(new Broadcast2()); 69 | }(Scratch)); 70 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/broadcast-3.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class Broadcast3 { 6 | getInfo() { 7 | return { 8 | id: 'broadcast3example', 9 | name: 'Broadcast Example 3', 10 | blocks: [ 11 | { 12 | opcode: 'whenReceived', 13 | blockType: Scratch.BlockType.HAT, 14 | text: 'when I receive [EVENT_OPTION]', 15 | isEdgeActivated: false, 16 | // highlight-next-line 17 | shouldRestartExistingThreads: true, 18 | arguments: { 19 | EVENT_OPTION: { 20 | type: Scratch.ArgumentType.STRING, 21 | menu: 'EVENT_FIELD' 22 | } 23 | } 24 | }, 25 | { 26 | opcode: 'broadcast', 27 | blockType: Scratch.BlockType.COMMAND, 28 | text: 'broadcast [EVENT]', 29 | arguments: { 30 | EVENT: { 31 | type: Scratch.ArgumentType.STRING, 32 | menu: 'EVENT_FIELD' 33 | } 34 | } 35 | } 36 | ], 37 | menus: { 38 | EVENT_FIELD: { 39 | acceptReporters: false, 40 | items: [ 41 | 'Event 1', 42 | 'Event 2', 43 | 'Event 3' 44 | ] 45 | } 46 | } 47 | }; 48 | } 49 | broadcast({EVENT}, util) { 50 | util.startHats('broadcast3example_whenReceived', { 51 | EVENT_OPTION: EVENT 52 | }); 53 | } 54 | } 55 | Scratch.extensions.register(new Broadcast3()); 56 | }(Scratch)); 57 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/broadcast-4.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class Broadcast4 { 6 | getInfo() { 7 | return { 8 | id: 'broadcast4example', 9 | name: 'Broadcast Example 4', 10 | blocks: [ 11 | { 12 | opcode: 'whenReceived', 13 | blockType: Scratch.BlockType.HAT, 14 | text: 'when I receive [EVENT_OPTION]', 15 | isEdgeActivated: false, 16 | arguments: { 17 | EVENT_OPTION: { 18 | type: Scratch.ArgumentType.STRING, 19 | menu: 'EVENT_FIELD' 20 | } 21 | } 22 | }, 23 | { 24 | opcode: 'broadcast', 25 | blockType: Scratch.BlockType.COMMAND, 26 | text: 'broadcast [EVENT] in [TARGET]', 27 | arguments: { 28 | EVENT: { 29 | type: Scratch.ArgumentType.STRING, 30 | menu: 'EVENT_FIELD' 31 | }, 32 | TARGET: { 33 | type: Scratch.ArgumentType.STRING, 34 | menu: 'TARGET_MENU' 35 | } 36 | } 37 | } 38 | ], 39 | menus: { 40 | EVENT_FIELD: { 41 | acceptReporters: false, 42 | items: [ 43 | 'Event 1', 44 | 'Event 2', 45 | 'Event 3' 46 | ] 47 | }, 48 | TARGET_MENU: { 49 | acceptReporters: true, 50 | items: [ 51 | 'all sprites', 52 | 'this sprite', 53 | 'the stage' 54 | ] 55 | } 56 | } 57 | }; 58 | } 59 | // highlight-start 60 | broadcast({EVENT, TARGET}, util) { 61 | const argumentFilter = { 62 | EVENT_OPTION: EVENT 63 | }; 64 | 65 | let targetFilter = null; 66 | if (TARGET === 'this sprite') targetFilter = util.target; 67 | if (TARGET === 'the stage') targetFilter = util.runtime.getTargetForStage(); 68 | 69 | util.startHats('broadcast4example_whenReceived', argumentFilter, targetFilter); 70 | } 71 | // highlight-end 72 | } 73 | Scratch.extensions.register(new Broadcast4()); 74 | }(Scratch)); 75 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/broadcast-5.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class Broadcast5 { 6 | getInfo() { 7 | return { 8 | id: 'broadcast5example', 9 | name: 'Broadcast Example 5', 10 | blocks: [ 11 | { 12 | opcode: 'whenReceived', 13 | blockType: Scratch.BlockType.EVENT, 14 | text: 'when I receive [EVENT_OPTION]', 15 | isEdgeActivated: false, 16 | arguments: { 17 | EVENT_OPTION: { 18 | type: Scratch.ArgumentType.STRING, 19 | menu: 'EVENT_FIELD' 20 | } 21 | } 22 | }, 23 | { 24 | opcode: 'broadcast', 25 | blockType: Scratch.BlockType.REPORTER, 26 | text: 'broadcast [EVENT]', 27 | arguments: { 28 | EVENT: { 29 | type: Scratch.ArgumentType.STRING, 30 | menu: 'EVENT_FIELD' 31 | } 32 | } 33 | } 34 | ], 35 | menus: { 36 | EVENT_FIELD: { 37 | acceptReporters: false, 38 | items: [ 39 | 'Event 1', 40 | 'Event 2', 41 | 'Event 3' 42 | ] 43 | } 44 | } 45 | }; 46 | } 47 | broadcast({EVENT, TARGET}, util) { 48 | // highlight-start 49 | const threads = util.startHats('broadcast5example_whenReceived', { 50 | EVENT_OPTION: EVENT 51 | }); 52 | return `Started ${threads.length} new threads!`; 53 | // highlight-end 54 | } 55 | } 56 | Scratch.extensions.register(new Broadcast5()); 57 | }(Scratch)); 58 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/every-second.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | class EverySecond { 6 | getInfo() { 7 | return { 8 | id: 'everysecondexample', 9 | name: 'Every Second', 10 | blocks: [ 11 | { 12 | opcode: 'everySecond', 13 | blockType: Scratch.BlockType.HAT, 14 | text: 'every second', 15 | isEdgeActivated: false 16 | } 17 | ] 18 | }; 19 | } 20 | } 21 | // highlight-start 22 | setInterval(() => { 23 | const startedThreads = Scratch.vm.runtime.startHats('everysecondexample_everySecond'); 24 | }, 1000); 25 | // highlight-end 26 | Scratch.extensions.register(new EverySecond()); 27 | }(Scratch)); 28 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/hello-world-unsandboxed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This Hello World example must run unsandboxed'); 8 | } 9 | 10 | class HelloWorld { 11 | getInfo() { 12 | return { 13 | id: 'helloworldunsandboxed', 14 | name: 'Unsandboxed Hello World', 15 | blocks: [ 16 | { 17 | opcode: 'hello', 18 | blockType: Scratch.BlockType.REPORTER, 19 | text: 'Hello!' 20 | } 21 | ] 22 | }; 23 | } 24 | hello() { 25 | return 'World!'; 26 | } 27 | } 28 | Scratch.extensions.register(new HelloWorld()); 29 | })(Scratch); 30 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/turbo-mode.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | // highlight-start 7 | if (!Scratch.extensions.unsandboxed) { 8 | throw new Error('This Turbo Mode example must run unsandboxed'); 9 | } 10 | const vm = Scratch.vm; 11 | // highlight-end 12 | 13 | class TurboMode { 14 | getInfo() { 15 | return { 16 | id: 'turbomodeunsandboxed', 17 | name: 'Turbo Mode', 18 | blocks: [ 19 | { 20 | opcode: 'set', 21 | blockType: Scratch.BlockType.COMMAND, 22 | text: 'set turbo mode to [ENABLED]', 23 | arguments: { 24 | ENABLED: { 25 | type: Scratch.ArgumentType.STRING, 26 | menu: 'ENABLED_MENU' 27 | } 28 | } 29 | } 30 | ], 31 | menus: { 32 | ENABLED_MENU: { 33 | acceptReporters: true, 34 | items: ['on', 'off'] 35 | } 36 | } 37 | }; 38 | } 39 | // highlight-start 40 | set(args) { 41 | vm.setTurboMode(args.ENABLED === 'on'); 42 | } 43 | // highlight-end 44 | } 45 | Scratch.extensions.register(new TurboMode()); 46 | })(Scratch); 47 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/when-key-pressed-restart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This example must run unsandboxed'); 8 | } 9 | 10 | class WhenKeyPressed { 11 | getInfo() { 12 | return { 13 | id: 'restartexampleunsandboxed', 14 | name: 'Restart Threads Example', 15 | blocks: [ 16 | { 17 | blockType: Scratch.BlockType.EVENT, 18 | opcode: 'whenPressed', 19 | text: 'when [KEY] key pressed', 20 | isEdgeActivated: false, 21 | // highlight-next-line 22 | shouldRestartExistingThreads: true, 23 | arguments: { 24 | KEY: { 25 | type: Scratch.ArgumentType.STRING, 26 | menu: 'key' 27 | } 28 | } 29 | } 30 | ], 31 | menus: { 32 | key: { 33 | acceptReporters: false, 34 | items: [ 35 | { 36 | text: 'space', 37 | value: ' ' 38 | }, 39 | 'a', 40 | 'b', 41 | 'c', 42 | // ... 43 | ] 44 | } 45 | } 46 | }; 47 | } 48 | } 49 | 50 | document.addEventListener('keydown', (e) => { 51 | Scratch.vm.runtime.startHats('restartexampleunsandboxed_whenPressed', { 52 | KEY: e.key 53 | }); 54 | }); 55 | 56 | Scratch.extensions.register(new WhenKeyPressed()); 57 | })(Scratch); 58 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/when-key-pressed-stage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This example must run unsandboxed'); 8 | } 9 | 10 | class WhenKeyPressedInStage { 11 | getInfo() { 12 | return { 13 | id: 'eventexample3unsandboxed', 14 | name: 'Event Block Example 3', 15 | blocks: [ 16 | { 17 | blockType: Scratch.BlockType.EVENT, 18 | opcode: 'whenPressed', 19 | text: 'when [KEY] key pressed', 20 | isEdgeActivated: false, 21 | arguments: { 22 | KEY: { 23 | type: Scratch.ArgumentType.STRING, 24 | menu: 'key' 25 | } 26 | } 27 | } 28 | ], 29 | menus: { 30 | key: { 31 | acceptReporters: false, 32 | items: [ 33 | { 34 | text: 'space', 35 | value: ' ' 36 | }, 37 | 'a', 38 | 'b', 39 | 'c', 40 | // ... 41 | ] 42 | } 43 | } 44 | }; 45 | } 46 | } 47 | 48 | document.addEventListener('keydown', (e) => { 49 | Scratch.vm.runtime.startHats('eventexample3unsandboxed_whenPressed', { 50 | KEY: e.key 51 | // highlight-next-line 52 | }, Scratch.vm.runtime.getTargetForStage()); 53 | }); 54 | 55 | Scratch.extensions.register(new WhenKeyPressedInStage()); 56 | })(Scratch); 57 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/when-key-pressed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This example must run unsandboxed'); 8 | } 9 | 10 | class WhenKeyPressed { 11 | getInfo() { 12 | return { 13 | id: 'eventexample2unsandboxed', 14 | name: 'Event Block Example 2', 15 | blocks: [ 16 | { 17 | blockType: Scratch.BlockType.EVENT, 18 | opcode: 'whenPressed', 19 | text: 'when [KEY] key pressed', 20 | isEdgeActivated: false, // required boilerplate 21 | // highlight-start 22 | arguments: { 23 | KEY: { 24 | type: Scratch.ArgumentType.STRING, 25 | menu: 'key' 26 | } 27 | } 28 | // highlight-end 29 | } 30 | ], 31 | menus: { 32 | key: { 33 | acceptReporters: false, 34 | items: [ 35 | { 36 | // startHats filters by *value*, not by text 37 | text: 'space', 38 | value: ' ' 39 | }, 40 | 'a', 41 | 'b', 42 | 'c', 43 | // ... 44 | ] 45 | } 46 | } 47 | }; 48 | } 49 | } 50 | 51 | document.addEventListener('keydown', (e) => { 52 | // highlight-start 53 | Scratch.vm.runtime.startHats('eventexample2unsandboxed_whenPressed', { 54 | KEY: e.key 55 | }); 56 | // highlight-end 57 | }); 58 | 59 | Scratch.extensions.register(new WhenKeyPressed()); 60 | })(Scratch); 61 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/when-space-key-pressed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This example must run unsandboxed'); 8 | } 9 | 10 | class WhenSpaceKeyPressed { 11 | getInfo() { 12 | return { 13 | id: 'eventexampleunsandboxed', 14 | name: 'Event Block Example', 15 | blocks: [ 16 | // highlight-start 17 | { 18 | blockType: Scratch.BlockType.EVENT, 19 | opcode: 'whenSpacePressed', 20 | text: 'when space key pressed', 21 | isEdgeActivated: false // required boilerplate 22 | } 23 | // highlight-end 24 | ] 25 | }; 26 | } 27 | // Notice: whenSpacePressed does not have a function defined! 28 | } 29 | 30 | // highlight-start 31 | document.addEventListener('keydown', (e) => { 32 | if (e.key === ' ') { 33 | Scratch.vm.runtime.startHats('eventexampleunsandboxed_whenSpacePressed'); 34 | } 35 | }); 36 | // highlight-end 37 | 38 | Scratch.extensions.register(new WhenSpaceKeyPressed()); 39 | })(Scratch); 40 | -------------------------------------------------------------------------------- /extensions/docs-examples/unsandboxed/when.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable -- passing the linting step requires content not covered when this is introduced */ 2 | 3 | (function(Scratch) { 4 | 'use strict'; 5 | 6 | if (!Scratch.extensions.unsandboxed) { 7 | throw new Error('This example must run unsandboxed'); 8 | } 9 | 10 | class When { 11 | getInfo() { 12 | return { 13 | id: 'whenunsandboxed', 14 | name: 'When', 15 | blocks: [ 16 | { 17 | // highlight-start 18 | blockType: Scratch.BlockType.HAT, 19 | opcode: 'when', 20 | text: 'when [CONDITION]', 21 | isEdgeActivated: false, // required boilerplate 22 | arguments: { 23 | CONDITION: { 24 | type: Scratch.BlockType.BOOLEAN 25 | } 26 | } 27 | // highlight-end 28 | } 29 | ] 30 | }; 31 | } 32 | // highlight-start 33 | when(args) { 34 | return Scratch.Cast.toBoolean(args.CONDITION); 35 | } 36 | // highlight-end 37 | } 38 | 39 | // highlight-start 40 | Scratch.vm.runtime.on('BEFORE_EXECUTE', () => { 41 | // startHats is the same as before! 42 | Scratch.vm.runtime.startHats('whenunsandboxed_when'); 43 | }); 44 | // highlight-end 45 | 46 | Scratch.extensions.register(new When()); 47 | })(Scratch); 48 | -------------------------------------------------------------------------------- /extensions/extensions.json: -------------------------------------------------------------------------------- 1 | [ 2 | // This file supports comments 3 | "lab/text", 4 | "stretch", 5 | "gamepad", 6 | "box2d", 7 | "files", 8 | "pointerlock", 9 | "cursor", 10 | "runtime-options", 11 | "fetch", 12 | "text", 13 | "local-storage", 14 | "true-fantom/base", 15 | "bitwise", 16 | "Skyhigh173/bigint", 17 | "utilities", 18 | "sound", 19 | "Lily/Video", 20 | "iframe", 21 | "Clay/htmlEncode", 22 | "Xeltalliv/clippingblending", 23 | "clipboard", 24 | "obviousAlexC/penPlus", 25 | "penplus", 26 | "Xeltalliv/simple3D", 27 | "Lily/Skins", 28 | "obviousAlexC/SensingPlus", 29 | "CubesterYT/KeySimulation", 30 | "Lily/ClonesPlus", 31 | "Lily/LooksPlus", 32 | "Lily/MoreEvents", 33 | "Lily/ListTools", 34 | "veggiecan/mobilekeyboard", 35 | "NexusKitten/moremotion", 36 | "CubesterYT/WindowControls", 37 | "veggiecan/browserfullscreen", 38 | "shreder95ua/resolution", 39 | "XmerOriginals/closecontrol", 40 | "navigator", 41 | "battery", 42 | "PwLDev/vibration", 43 | "TheShovel/CustomStyles", 44 | "TheShovel/ColorPicker", 45 | "NexusKitten/controlcontrols", 46 | "mdwalters/notifications", 47 | "XeroName/Deltatime", 48 | "ar", 49 | "encoding", 50 | "Lily/SoundExpanded", 51 | "Lily/TempVariables2", 52 | "Lily/MoreTimers", 53 | "clouddata-ping", 54 | "cloudlink", 55 | "true-fantom/network", 56 | "true-fantom/math", 57 | "true-fantom/regexp", 58 | "true-fantom/couplers", 59 | "Lily/AllMenus", 60 | "Lily/HackedBlocks", 61 | "Lily/Cast", 62 | "-SIPC-/time", 63 | "-SIPC-/consoles", 64 | "ZXMushroom63/searchApi", 65 | "TheShovel/ShovelUtils", 66 | "Lily/Assets", 67 | "SharkPool/Font-Manager", 68 | "DNin/wake-lock", 69 | "Skyhigh173/json", 70 | "mbw/xml", 71 | "numerical-encoding-2", 72 | "cs2627883/numericalencoding", 73 | "SharkPool/Camera", 74 | "DT/cameracontrols", 75 | "TheShovel/CanvasEffects", 76 | "Longboost/color_channels", 77 | "CST1229/zip", 78 | "CST1229/images", 79 | "TheShovel/LZ-String", 80 | "0832/rxFS2", 81 | "NexusKitten/sgrab", 82 | "NOname-awa/graphics2d", 83 | "NOname-awa/more-comparisons", 84 | "JeremyGamer13/tween", 85 | "rixxyx", 86 | "Lily/lmsutils", 87 | "qxsck/data-analysis", 88 | "qxsck/var-and-list", 89 | "vercte/dictionaries", 90 | "godslayerakp/http", 91 | "godslayerakp/ws", 92 | "Lily/CommentBlocks", 93 | "veggiecan/LongmanDictionary", 94 | "CubesterYT/TurboHook", 95 | "Alestore/nfcwarp", 96 | "steamworks", 97 | "itchio", 98 | "gamejolt", 99 | "obviousAlexC/newgroundsIO", 100 | "Lily/McUtils" // McUtils should always be the last item. 101 | ] 102 | -------------------------------------------------------------------------------- /extensions/fetch.js: -------------------------------------------------------------------------------- 1 | // Name: Fetch 2 | // ID: fetch 3 | // Description: Make requests to the broader internet. 4 | // License: MIT AND MPL-2.0 5 | 6 | (function (Scratch) { 7 | "use strict"; 8 | 9 | class Fetch { 10 | getInfo() { 11 | return { 12 | id: "fetch", 13 | name: Scratch.translate("Fetch"), 14 | blocks: [ 15 | { 16 | opcode: "get", 17 | blockType: Scratch.BlockType.REPORTER, 18 | // eslint-disable-next-line extension/should-translate 19 | text: "GET [URL]", 20 | arguments: { 21 | URL: { 22 | type: Scratch.ArgumentType.STRING, 23 | defaultValue: "https://extensions.turbowarp.org/hello.txt", 24 | }, 25 | }, 26 | }, 27 | ], 28 | }; 29 | } 30 | 31 | get(args) { 32 | return Scratch.fetch(args.URL) 33 | .then((r) => r.text()) 34 | .catch(() => ""); 35 | } 36 | } 37 | 38 | Scratch.extensions.register(new Fetch()); 39 | })(Scratch); 40 | -------------------------------------------------------------------------------- /extensions/mdwalters/notifications.js: -------------------------------------------------------------------------------- 1 | // Name: Notifications 2 | // ID: mdwaltersnotifications 3 | // Description: Display notifications. 4 | // License: MIT 5 | 6 | (function (Scratch) { 7 | "use strict"; 8 | 9 | let denied = false; 10 | /** @type {Notification|null} */ 11 | let notification = null; 12 | 13 | const askForNotificationPermission = async () => { 14 | try { 15 | const allowedByVM = await Scratch.canNotify(); 16 | if (!allowedByVM) { 17 | throw new Error("Denied by VM"); 18 | } 19 | 20 | const allowedByBrowser = await Notification.requestPermission(); 21 | if (allowedByBrowser === "denied") { 22 | throw new Error("Denied by browser"); 23 | } 24 | 25 | denied = false; 26 | return true; 27 | } catch (e) { 28 | denied = true; 29 | console.warn("Could not request notification permissions", e); 30 | return false; 31 | } 32 | }; 33 | 34 | const isAndroid = () => navigator.userAgent.includes("Android"); 35 | 36 | const getServiceWorkerRegistration = () => { 37 | if (!("serviceWorker" in navigator)) return null; 38 | // This is only needed on Android 39 | if (!isAndroid()) return null; 40 | return navigator.serviceWorker.getRegistration(); 41 | }; 42 | 43 | class Notifications { 44 | constructor() { 45 | Scratch.vm.runtime.on("RUNTIME_DISPOSED", () => { 46 | this._closeNotification(); 47 | }); 48 | } 49 | getInfo() { 50 | return { 51 | id: "mdwaltersnotifications", 52 | name: Scratch.translate("Notifications"), 53 | blocks: [ 54 | { 55 | opcode: "requestPermission", 56 | blockType: Scratch.BlockType.COMMAND, 57 | text: Scratch.translate("request notification permission"), 58 | }, 59 | { 60 | opcode: "hasPermission", 61 | blockType: Scratch.BlockType.BOOLEAN, 62 | text: Scratch.translate("has notification permission?"), 63 | disableMonitor: true, 64 | }, 65 | { 66 | opcode: "showNotification", 67 | blockType: Scratch.BlockType.COMMAND, 68 | text: Scratch.translate("create notification with text [text]"), 69 | arguments: { 70 | text: { 71 | type: Scratch.ArgumentType.STRING, 72 | defaultValue: Scratch.translate({ 73 | default: "Hello, world!", 74 | description: "Default text in the create notification block", 75 | }), 76 | }, 77 | }, 78 | }, 79 | { 80 | opcode: "closeNotification", 81 | blockType: Scratch.BlockType.COMMAND, 82 | text: Scratch.translate("close notification"), 83 | }, 84 | ], 85 | }; 86 | } 87 | 88 | requestPermission() { 89 | return askForNotificationPermission(); 90 | } 91 | 92 | hasPermission() { 93 | if (denied) { 94 | return false; 95 | } 96 | return askForNotificationPermission(); 97 | } 98 | 99 | async _showNotification(text) { 100 | if (await this.hasPermission()) { 101 | const title = Scratch.translate({ 102 | default: "Notification from project", 103 | description: "Title of notifications created by the project", 104 | }); 105 | const options = { 106 | body: text, 107 | }; 108 | try { 109 | notification = new Notification(title, options); 110 | } catch (e) { 111 | // On Android we need to go through the service worker. 112 | const registration = await getServiceWorkerRegistration(); 113 | if (registration) { 114 | try { 115 | await registration.showNotification(title, options); 116 | } catch (e2) { 117 | console.error("Could not show notification", e, e2); 118 | } 119 | } else { 120 | console.error("Could not show notification", e); 121 | } 122 | } 123 | } 124 | } 125 | 126 | showNotification(args) { 127 | this._showNotification(Scratch.Cast.toString(args.text)); 128 | } 129 | 130 | async _closeNotification() { 131 | if (notification) { 132 | notification.close(); 133 | notification = null; 134 | } 135 | 136 | const registration = await getServiceWorkerRegistration(); 137 | if (registration) { 138 | const notifications = await registration.getNotifications(); 139 | for (const notification of notifications) { 140 | notification.close(); 141 | } 142 | } 143 | } 144 | 145 | closeNotification() { 146 | this._closeNotification(); 147 | } 148 | } 149 | 150 | Scratch.extensions.register(new Notifications()); 151 | })(Scratch); 152 | -------------------------------------------------------------------------------- /extensions/navigator.js: -------------------------------------------------------------------------------- 1 | // Name: Navigator 2 | // ID: navigatorinfo 3 | // Description: Details about the user's browser and operating system. 4 | // Context: "Navigator" refers to someone's browser 5 | // License: MIT AND MPL-2.0 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | class NavigatorInfo { 11 | getInfo() { 12 | return { 13 | id: "navigatorinfo", 14 | name: Scratch.translate("Navigator Info"), 15 | blocks: [ 16 | { 17 | opcode: "getOS", 18 | blockType: Scratch.BlockType.REPORTER, 19 | text: Scratch.translate("operating system"), 20 | }, 21 | { 22 | opcode: "getBrowser", 23 | blockType: Scratch.BlockType.REPORTER, 24 | text: Scratch.translate("browser"), 25 | }, 26 | { 27 | opcode: "getMemory", 28 | blockType: Scratch.BlockType.REPORTER, 29 | text: Scratch.translate("device memory in GB"), 30 | }, 31 | { 32 | opcode: "getPreferredColorScheme", 33 | blockType: Scratch.BlockType.BOOLEAN, 34 | text: Scratch.translate("user prefers [THEME] color scheme?"), 35 | arguments: { 36 | THEME: { 37 | type: Scratch.ArgumentType.STRING, 38 | menu: "THEME", 39 | defaultValue: "dark", 40 | }, 41 | }, 42 | }, 43 | { 44 | opcode: "getPreferredReducedMotion", 45 | blockType: Scratch.BlockType.BOOLEAN, 46 | text: Scratch.translate("user prefers reduced motion?"), 47 | }, 48 | { 49 | opcode: "getPreferredContrast", 50 | blockType: Scratch.BlockType.BOOLEAN, 51 | text: Scratch.translate("user prefers more contrast?"), 52 | }, 53 | ], 54 | menus: { 55 | THEME: { 56 | acceptReporters: true, 57 | items: [ 58 | { 59 | text: Scratch.translate("light"), 60 | value: "light", 61 | }, 62 | { 63 | text: Scratch.translate("dark"), 64 | value: "dark", 65 | }, 66 | ], 67 | }, 68 | }, 69 | }; 70 | } 71 | 72 | getOS() { 73 | const userAgent = navigator.userAgent; 74 | if (userAgent.includes("Windows")) { 75 | return "Windows"; 76 | } else if (userAgent.includes("Android")) { 77 | return "Android"; 78 | } else if ( 79 | userAgent.includes("iPhone") || 80 | userAgent.includes("iPod") || 81 | userAgent.includes("iPad") 82 | ) { 83 | return "iOS"; 84 | } else if (userAgent.includes("Linux")) { 85 | return "Linux"; 86 | } else if (userAgent.includes("CrOS")) { 87 | return "ChromeOS"; 88 | } else if (userAgent.includes("Mac OS")) { 89 | return "macOS"; 90 | } 91 | return "Other"; 92 | } 93 | 94 | getBrowser() { 95 | const userAgent = navigator.userAgent; 96 | if (userAgent.includes("Chrome")) { 97 | return "Chrome"; 98 | } else if (userAgent.includes("Firefox")) { 99 | return "Firefox"; 100 | } else if (userAgent.includes("Safari")) { 101 | return "Safari"; 102 | } 103 | return "Other"; 104 | } 105 | 106 | getMemory() { 107 | // @ts-expect-error 108 | if (navigator.deviceMemory == undefined) { 109 | return "Unsupported"; 110 | } else { 111 | // @ts-expect-error 112 | return navigator.deviceMemory; 113 | } 114 | } 115 | 116 | getPreferredColorScheme(args) { 117 | return ( 118 | window.matchMedia("(prefers-color-scheme: dark)").matches === 119 | (args.THEME === "dark") 120 | ); 121 | } 122 | 123 | getPreferredReducedMotion() { 124 | return !!window.matchMedia("(prefers-reduced-motion: reduce)").matches; 125 | } 126 | 127 | getPreferredContrast() { 128 | return !!window.matchMedia("(prefers-contrast: more)").matches; 129 | } 130 | } 131 | 132 | Scratch.extensions.register(new NavigatorInfo()); 133 | })(Scratch); 134 | -------------------------------------------------------------------------------- /extensions/pointerlock.js: -------------------------------------------------------------------------------- 1 | // Name: Pointerlock 2 | // ID: pointerlock 3 | // Description: Adds blocks for mouse locking. Mouse x & y blocks will report the change since the previous frame while the pointer is locked. Replaces the pointerlock experiment. 4 | // License: MIT AND MPL-2.0 5 | 6 | (function (Scratch) { 7 | "use strict"; 8 | 9 | if (!Scratch.extensions.unsandboxed) { 10 | throw new Error("pointerlock extension must be run unsandboxed"); 11 | } 12 | 13 | const vm = Scratch.vm; 14 | 15 | const canvas = vm.runtime.renderer.canvas; 16 | const mouse = vm.runtime.ioDevices.mouse; 17 | let isLocked = false; 18 | let isPointerLockEnabled = false; 19 | 20 | let rect = canvas.getBoundingClientRect(); 21 | window.addEventListener("resize", () => { 22 | rect = canvas.getBoundingClientRect(); 23 | }); 24 | 25 | const postMouseData = (e, isDown) => { 26 | const { movementX, movementY } = e; 27 | const { width, height } = rect; 28 | const x = mouse._clientX + movementX; 29 | const y = mouse._clientY - movementY; 30 | mouse._clientX = x; 31 | mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); 32 | mouse._clientY = y; 33 | mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); 34 | if (typeof isDown === "boolean") { 35 | const data = { 36 | button: e.button, 37 | isDown, 38 | }; 39 | originalPostIOData(data); 40 | } 41 | }; 42 | 43 | const mouseDevice = vm.runtime.ioDevices.mouse; 44 | const originalPostIOData = mouseDevice.postData.bind(mouseDevice); 45 | mouseDevice.postData = (data) => { 46 | if (!isPointerLockEnabled) { 47 | return originalPostIOData(data); 48 | } 49 | }; 50 | 51 | document.addEventListener( 52 | "mousedown", 53 | (e) => { 54 | // @ts-expect-error 55 | if (canvas.contains(e.target)) { 56 | if (isLocked) { 57 | postMouseData(e, true); 58 | } else if (isPointerLockEnabled) { 59 | canvas.requestPointerLock(); 60 | } 61 | } 62 | }, 63 | true 64 | ); 65 | document.addEventListener( 66 | "mouseup", 67 | (e) => { 68 | if (isLocked) { 69 | postMouseData(e, false); 70 | // @ts-expect-error 71 | } else if (isPointerLockEnabled && canvas.contains(e.target)) { 72 | canvas.requestPointerLock(); 73 | } 74 | }, 75 | true 76 | ); 77 | document.addEventListener( 78 | "mousemove", 79 | (e) => { 80 | if (isLocked) { 81 | postMouseData(e); 82 | } 83 | }, 84 | true 85 | ); 86 | 87 | document.addEventListener("pointerlockchange", () => { 88 | isLocked = document.pointerLockElement === canvas; 89 | }); 90 | document.addEventListener("pointerlockerror", (e) => { 91 | console.error("Pointer lock error", e); 92 | }); 93 | 94 | const oldStep = vm.runtime._step; 95 | vm.runtime._step = function (...args) { 96 | const ret = oldStep.call(this, ...args); 97 | if (isPointerLockEnabled) { 98 | const { width, height } = rect; 99 | mouse._clientX = width / 2; 100 | mouse._clientY = height / 2; 101 | mouse._scratchX = 0; 102 | mouse._scratchY = 0; 103 | } 104 | return ret; 105 | }; 106 | 107 | vm.runtime.on("PROJECT_LOADED", () => { 108 | isPointerLockEnabled = false; 109 | if (isLocked) { 110 | document.exitPointerLock(); 111 | } 112 | }); 113 | 114 | class Pointerlock { 115 | getInfo() { 116 | return { 117 | id: "pointerlock", 118 | name: Scratch.translate("Pointerlock"), 119 | blocks: [ 120 | { 121 | opcode: "setLocked", 122 | blockType: Scratch.BlockType.COMMAND, 123 | text: Scratch.translate("set pointer lock [enabled]"), 124 | arguments: { 125 | enabled: { 126 | type: Scratch.ArgumentType.STRING, 127 | defaultValue: "true", 128 | menu: "enabled", 129 | }, 130 | }, 131 | }, 132 | { 133 | opcode: "isLocked", 134 | blockType: Scratch.BlockType.BOOLEAN, 135 | text: Scratch.translate("pointer locked?"), 136 | }, 137 | ], 138 | menus: { 139 | enabled: { 140 | acceptReporters: true, 141 | items: [ 142 | { 143 | text: Scratch.translate("enabled"), 144 | value: "true", 145 | }, 146 | { 147 | text: Scratch.translate("disabled"), 148 | value: "false", 149 | }, 150 | ], 151 | }, 152 | }, 153 | }; 154 | } 155 | 156 | setLocked(args) { 157 | isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; 158 | if (!isPointerLockEnabled && isLocked) { 159 | document.exitPointerLock(); 160 | } 161 | } 162 | 163 | isLocked() { 164 | return isLocked; 165 | } 166 | } 167 | 168 | Scratch.extensions.register(new Pointerlock()); 169 | })(Scratch); 170 | -------------------------------------------------------------------------------- /extensions/shreder95ua/resolution.js: -------------------------------------------------------------------------------- 1 | // Name: Screen Resolution 2 | // ID: shreder95resolution 3 | // Description: Get the resolution of the primary screen. 4 | // By: shreder95ua 5 | // License: MIT 6 | 7 | (function (Scratch) { 8 | "use strict"; 9 | 10 | class Resolution { 11 | getInfo() { 12 | return { 13 | id: "shreder95resolution", 14 | name: Scratch.translate("Screen resolution"), 15 | color1: "#FFAB19", 16 | color2: "#EC9C13", 17 | color3: "#CF8B17", 18 | blocks: [ 19 | { 20 | opcode: "getWidth", 21 | text: Scratch.translate("primary screen width"), 22 | blockType: Scratch.BlockType.REPORTER, 23 | }, 24 | { 25 | opcode: "getHeight", 26 | text: Scratch.translate("primary screen height"), 27 | blockType: Scratch.BlockType.REPORTER, 28 | }, 29 | ], 30 | }; 31 | } 32 | getWidth() { 33 | return window.screen.width; 34 | } 35 | getHeight() { 36 | return window.screen.height; 37 | } 38 | } 39 | Scratch.extensions.register(new Resolution()); 40 | })(Scratch); 41 | -------------------------------------------------------------------------------- /images/CST1229/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/Clay/htmlEncode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/Lily/HackedBlocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/Lily/HackedBlocks.png -------------------------------------------------------------------------------- /images/Lily/McUtils.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/Lily/McUtils.png -------------------------------------------------------------------------------- /images/Lily/SoundExpanded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/Lily/lmsutils.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/NexusKitten/moremotion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/PwLDev/vibration.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/TheShovel/ShovelUtils.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/TheShovel/ShovelUtils.png -------------------------------------------------------------------------------- /images/Xeltalliv/simple3D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/Xeltalliv/simple3D.png -------------------------------------------------------------------------------- /images/XeroName/Deltatime.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/XmerOriginals/closecontrol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/battery.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/cursor.png -------------------------------------------------------------------------------- /images/gamejolt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/gamejolt.png -------------------------------------------------------------------------------- /images/godslayerakp/ws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/godslayerakp/ws.png -------------------------------------------------------------------------------- /images/mbw/xml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/penplus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/pointerlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/images/pointerlock.png -------------------------------------------------------------------------------- /images/rixxyx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/shreder95ua/resolution.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/true-fantom/network.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/unknown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/veggiecan/browserfullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /licenses/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 TurboWarp Extensions Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/OFL-Lobster.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Lobster Project Authors (https://github.com/impallari/The-Lobster-Font), with Reserved Font Name "Lobster". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@turbowarp/extensions", 3 | "version": "0.0.1", 4 | "description": "Unsandboxed extensions for TurboWarp", 5 | "exports": { 6 | "./builder": "./development/builder.js" 7 | }, 8 | "scripts": { 9 | "start": "node development/server.js", 10 | "dev": "node development/server.js", 11 | "build": "node development/build-production.js", 12 | "validate": "node development/validate.js", 13 | "lint": "eslint development extensions --max-warnings=0", 14 | "format": "prettier . --write", 15 | "check-format": "prettier . --check", 16 | "upload-translations": "node development/upload-translations.js", 17 | "download-translations": "node development/download-translations.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/TurboWarp/extensions.git" 22 | }, 23 | "license": "MIT AND LGPL-3.0-only AND GPL-3.0-only AND CC-BY-SA-4.0 AND CC-BY-4.0 AND CC-BY-2.5 AND CC0-1.0 AND Apache-2.0 AND MPL-2.0 AND BSD-3-Clause", 24 | "bugs": { 25 | "url": "https://github.com/TurboWarp/extensions/issues" 26 | }, 27 | "homepage": "https://github.com/TurboWarp/extensions#readme", 28 | "dependencies": { 29 | "@turbowarp/json": "^0.1.2", 30 | "@turbowarp/scratchblocks": "^3.6.7", 31 | "@turbowarp/types": "git+https://github.com/TurboWarp/types-tw.git#tw", 32 | "adm-zip": "^0.5.16", 33 | "chokidar": "^4.0.3", 34 | "ejs": "^3.1.10", 35 | "express": "^4.21.2", 36 | "image-size": "^2.0.2", 37 | "markdown-it": "^14.1.0" 38 | }, 39 | "devDependencies": { 40 | "@transifex/api": "^7.1.4", 41 | "eslint": "^9.27.0", 42 | "espree": "^9.6.1", 43 | "esquery": "^1.6.0", 44 | "prettier": "^3.5.3", 45 | "spdx-expression-parse": "^4.0.0" 46 | }, 47 | "private": true 48 | } 49 | -------------------------------------------------------------------------------- /samples/AR cube.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/AR cube.sb3 -------------------------------------------------------------------------------- /samples/Additive Fire.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Additive Fire.sb3 -------------------------------------------------------------------------------- /samples/Ask for the manager.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Ask for the manager.sb3 -------------------------------------------------------------------------------- /samples/Base 64.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Base 64.sb3 -------------------------------------------------------------------------------- /samples/Box2D.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Box2D.sb3 -------------------------------------------------------------------------------- /samples/Clipping Sprites and Pen.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Clipping Sprites and Pen.sb3 -------------------------------------------------------------------------------- /samples/Cloud Variables.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Cloud Variables.sb3 -------------------------------------------------------------------------------- /samples/Moving Around with Pointer Lock.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Moving Around with Pointer Lock.sb3 -------------------------------------------------------------------------------- /samples/Party Time.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Party Time.sb3 -------------------------------------------------------------------------------- /samples/Pen Plus.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Pen Plus.sb3 -------------------------------------------------------------------------------- /samples/Recolor the Dango.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Recolor the Dango.sb3 -------------------------------------------------------------------------------- /samples/Simple3D template.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Simple3D template.sb3 -------------------------------------------------------------------------------- /samples/Stretch.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Stretch.sb3 -------------------------------------------------------------------------------- /samples/Temporary Variables - Thread vs. Runtime.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Temporary Variables - Thread vs. Runtime.sb3 -------------------------------------------------------------------------------- /samples/Tickle Me.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Tickle Me.sb3 -------------------------------------------------------------------------------- /samples/Tweening.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/samples/Tweening.sb3 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "noEmit": true, 5 | "allowJs": true, 6 | "checkJs": true, 7 | "paths": { 8 | // See https://github.com/turboWarp/types#using-from-npm 9 | "scratch-vm": ["./node_modules/@turbowarp/types/index.d.ts"], 10 | "scratch-render": ["./node_modules/@turbowarp/types/index.d.ts"], 11 | "scratch-svg-renderer": ["./node_modules/@turbowarp/types/index.d.ts"], 12 | "scratch-render-fonts": ["./node_modules/@turbowarp/types/index.d.ts"], 13 | "scratch-storage": ["./node_modules/@turbowarp/types/index.d.ts"], 14 | "scratch-audio": ["./node_modules/@turbowarp/types/index.d.ts"], 15 | "scratch-parser": ["./node_modules/@turbowarp/types/index.d.ts"], 16 | "scratch-blocks": ["./node_modules/@turbowarp/types/index.d.ts"] 17 | } 18 | }, 19 | "include": [ 20 | "node_modules/@turbowarp/types/types/scratch-vm-extension.d.ts", 21 | "node_modules/@turbowarp/types/types/scratchx-extension.d.ts", 22 | "extensions/**/*", 23 | "website/**/*" 24 | ] 25 | } -------------------------------------------------------------------------------- /website/Lobster.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/Lobster.woff2 -------------------------------------------------------------------------------- /website/dango.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/dango.mp4 -------------------------------------------------------------------------------- /website/dango.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/dango.png -------------------------------------------------------------------------------- /website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/favicon.ico -------------------------------------------------------------------------------- /website/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | It works! 8 | 31 | 32 | 33 |
34 |

It works!

35 |

Transparency is supported, so you can display the project behind the frame as long as the page doesn't explicitly set a background color.

36 |

You can also use JavaScript and most other Web APIs, like this:

37 |

38 |

Due to browser security measures, blocks like "mouse x" and "mouse down?" will not work while the cursor is over the frame unless you use the disable interactivity block.

39 |

Many websites also block other sites from embedding them for security reasons.

40 |
41 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /website/hello.txt: -------------------------------------------------------------------------------- 1 | Hello, world! -------------------------------------------------------------------------------- /website/hello.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/hello.zip -------------------------------------------------------------------------------- /website/meow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/meow.mp3 -------------------------------------------------------------------------------- /website/robot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurboWarp/extensions/a73e6fd55ee6a502abe75e32adfd2c6def412f51/website/robot.png -------------------------------------------------------------------------------- /website/test/README.md: -------------------------------------------------------------------------------- 1 | DO NOT use these test extensions. They WILL be deleted and modified in ways that break your project, and they don't have any blocks you would actually want to use. 2 | -------------------------------------------------------------------------------- /website/test/bad-xml.js: -------------------------------------------------------------------------------- 1 | (function(Scratch) { 2 | 'use strict'; 3 | 4 | class XMLTest { 5 | getInfo () { 6 | return { 7 | id: 'xmltest', 8 | name: `<>"'&& Name`, 9 | docsURI: `https://example.com/&''""<<>>`, 10 | menuIconURI: `data:<>&"' category icon`, 11 | blocks: [ 12 | { 13 | blockType: `block type <>&"'`, 14 | opcode: `opcode <>&"'`, 15 | text: `<>&"' [string argument <>&"'] [inputMenu <"'&>] [fieldMenu <"'&>] [image <"'&>]`, 16 | blockIconURI: `'data:<>&"' block icon`, 17 | arguments: { 18 | [`string argument <>&"'`]: { 19 | type: Scratch.ArgumentType.STRING, 20 | defaultValue: `default string <>&"'` 21 | }, 22 | [`inputMenu <"'&>`]: { 23 | type: Scratch.ArgumentType.STRING, 24 | menu: `input <>&"'`, 25 | defaultValue: `default input <>&"'` 26 | }, 27 | [`fieldMenu <"'&>`]: { 28 | type: `argument type <>&"'`, 29 | menu: `field <>&"'`, 30 | defaultValue: `default field <>&"'` 31 | }, 32 | [`image <"'&>`]: { 33 | type: Scratch.ArgumentType.IMAGE, 34 | dataURI: `data:<>&"' image input` 35 | } 36 | } 37 | }, 38 | { 39 | opcode: 'button', 40 | blockType: Scratch.BlockType.BUTTON, 41 | text: `'"><& button text`, 42 | func: `'"><& func` 43 | } 44 | ], 45 | menus: { 46 | [`input <>&"'`]: { 47 | acceptReporters: true, 48 | items: [ 49 | `1 <>&"`, 50 | `2 <>&"`, 51 | `3 <>&"` 52 | ] 53 | }, 54 | [`field <>&"'`]: { 55 | acceptReporters: false, 56 | items: [ 57 | `1 <>&"`, 58 | `2 <>&"`, 59 | `3 <>&"` 60 | ] 61 | } 62 | } 63 | }; 64 | } 65 | [`opcode <>&"'`](args) { 66 | console.log(args); 67 | } 68 | } 69 | Scratch.extensions.register(new XMLTest()); 70 | })(Scratch); 71 | -------------------------------------------------------------------------------- /website/test/broken.js: -------------------------------------------------------------------------------- 1 | (function(Scratch) { 2 | 'use strict'; 3 | 4 | class Broken { 5 | getInfo () { 6 | return { 7 | id: 'broken', 8 | name: 'Broken Extension', 9 | blocks: [ 10 | { 11 | opcode: 'error', 12 | blockType: Scratch.BlockType.REPORTER, 13 | text: 'error' 14 | }, 15 | { 16 | opcode: 'returnUndefined', 17 | blockType: Scratch.BlockType.REPORTER, 18 | text: 'return undefined' 19 | } 20 | ] 21 | }; 22 | } 23 | 24 | error () { 25 | throw new Error("This is an error :("); 26 | } 27 | 28 | returnUndefined () { 29 | 30 | } 31 | } 32 | 33 | Scratch.extensions.register(new Broken()); 34 | })(Scratch); 35 | -------------------------------------------------------------------------------- /website/test/button.js: -------------------------------------------------------------------------------- 1 | (function(Scratch) { 2 | 'use strict'; 3 | class Test { 4 | getInfo () { 5 | return { 6 | id: 'testbutton', 7 | name: 'test 123', 8 | docsURI: 'https://extensions.turbowarp.org', 9 | blocks: [ 10 | { 11 | blockType: Scratch.BlockType.BUTTON, 12 | func: 'MAKE_A_VARIABLE', 13 | text: 'Make variable' 14 | }, 15 | { 16 | blockType: Scratch.BlockType.BUTTON, 17 | text: ':)', 18 | func: 'hello' 19 | } 20 | ] 21 | }; 22 | } 23 | hello () { 24 | alert('>:]'); 25 | } 26 | } 27 | Scratch.extensions.register(new Test()); 28 | })(Scratch); 29 | -------------------------------------------------------------------------------- /website/test/cjk.js: -------------------------------------------------------------------------------- 1 | (function (Scratch) { 2 | 'use strict'; 3 | 4 | Scratch.extensions.register({ 5 | getInfo: () => ({ 6 | id: 'testcjk', 7 | name: '这是一个测试', 8 | blocks: [ 9 | { 10 | opcode: 'test1', 11 | text: '這是一個測試', 12 | blockType: Scratch.BlockType.COMMAND 13 | }, 14 | { 15 | opcode: 'test2', 16 | text: 'これはテストです', 17 | blockType: Scratch.BlockType.COMMAND 18 | }, 19 | { 20 | opcode: 'test3', 21 | text: '이것은 테스트입니다', 22 | blockType: Scratch.BlockType.COMMAND 23 | } 24 | ] 25 | }) 26 | }); 27 | })(Scratch); 28 | -------------------------------------------------------------------------------- /website/test/l10n.js: -------------------------------------------------------------------------------- 1 | (function(Scratch) { 2 | 'use strict'; 3 | Scratch.translate.setup({ 4 | en: { 5 | test: 'EN message' 6 | }, 7 | es: { 8 | test: 'ES message' 9 | } 10 | }); 11 | class Test { 12 | getInfo() { 13 | return { 14 | id: 'testl10n', 15 | name: Scratch.translate('Default message'), 16 | blocks: [] 17 | }; 18 | } 19 | } 20 | Scratch.extensions.register(new Test()); 21 | })(Scratch); 22 | -------------------------------------------------------------------------------- /website/test/label.js: -------------------------------------------------------------------------------- 1 | (function(Scratch) { 2 | 'use strict'; 3 | class Test { 4 | getInfo() { 5 | return { 6 | id: 'testlabel', 7 | name: 'Label', 8 | blocks: [ 9 | { 10 | blockType: Scratch.BlockType.LABEL, 11 | text: 'test 123 :) <>&%"' 12 | } 13 | ] 14 | }; 15 | } 16 | } 17 | Scratch.extensions.register(new Test()); 18 | })(Scratch); 19 | -------------------------------------------------------------------------------- /website/test/loops.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/TurboWarp/scratch-vm/pull/141 2 | (function (Scratch) { 3 | "use strict"; 4 | 5 | class LoopsAndThings { 6 | getInfo() { 7 | return { 8 | id: "loopsAndThings", 9 | name: "Loops and things test", 10 | blocks: [ 11 | { 12 | opcode: "conditional", 13 | blockType: Scratch.BlockType.CONDITIONAL, 14 | text: "run branch [BRANCH] of", 15 | arguments: { 16 | BRANCH: { 17 | type: Scratch.ArgumentType.NUMBER, 18 | defaultValue: 1 19 | } 20 | }, 21 | branchCount: 3 22 | }, 23 | { 24 | opcode: "loop", 25 | blockType: Scratch.BlockType.LOOP, 26 | text: "my repeat [TIMES]", 27 | arguments: { 28 | TIMES: { 29 | type: Scratch.ArgumentType.NUMBER, 30 | defaultValue: 10 31 | } 32 | }, 33 | }, 34 | '---', 35 | { 36 | opcode: "testPromise", 37 | blockType: Scratch.BlockType.REPORTER, 38 | text: "return [VALUE] in a Promise", 39 | arguments: { 40 | VALUE: { 41 | type: Scratch.ArgumentType.STRING, 42 | defaultValue: '' 43 | } 44 | } 45 | } 46 | ] 47 | }; 48 | } 49 | 50 | conditional({BRANCH}, util) { 51 | return Scratch.Cast.toNumber(BRANCH); 52 | } 53 | 54 | loop({TIMES}, util) { 55 | const times = Math.round(Scratch.Cast.toNumber(TIMES)); 56 | if (typeof util.stackFrame.loopCounter === "undefined") { 57 | util.stackFrame.loopCounter = times; 58 | } 59 | util.stackFrame.loopCounter--; 60 | if (util.stackFrame.loopCounter >= 0) { 61 | return true; 62 | } 63 | } 64 | 65 | testPromise({VALUE}) { 66 | return Promise.resolve(VALUE); 67 | } 68 | } 69 | 70 | Scratch.extensions.register(new LoopsAndThings()); 71 | })(Scratch); -------------------------------------------------------------------------------- /website/test/scratchx.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const ext = {}; 5 | 6 | ext._getStatus = () => ({ 7 | status: 2, 8 | msg: 'Ready' 9 | }); 10 | 11 | ext.testReporter = () => Math.random(); 12 | 13 | ext.testReporterWait = (delay, callback) => { 14 | setTimeout(() => { 15 | callback(Math.random()); 16 | }, delay * 1000); 17 | }; 18 | 19 | var descriptor = { 20 | blocks: [ 21 | ['r', 'test reporter', 'testReporter'], 22 | ['R', 'test reporter 2 %n', 'testReporterWait', '1'], 23 | ], 24 | }; 25 | 26 | ScratchExtensions.register('ScratchX Test', descriptor, ext); 27 | }()); 28 | --------------------------------------------------------------------------------