diff --git a/docs/8to16/mesh.md b/docs/8to16/mesh.md new file mode 100644 index 0000000000..4ee1164a10 --- /dev/null +++ b/docs/8to16/mesh.md @@ -0,0 +1,51 @@ +# Cross-Project + +The Cross-Project extension lets you communicate with other projects running in the same browser or in TurboWarp Desktop, using special versions of variables and broadcasts. This allows you to for example, create a game that talks to another project running at the same time. + +> [!NOTE] +> Only projects running in the same browser as another will receive messages. However, projects running in a different window will work. + +## Setup + +To setup a connection between a project, create a broadcast or variable in one project, and then create another of the same type and name in another project. + +## Blocks + +The blocks below are equivalent to other blocks in Scratch, but are received by other projects too. + +### Broadcasts + +```scratch +when I receive cross-project [message v] :: #4cdab2 hat +``` +This block will activate when another project sends *this* project a message with the same name. It is triggered using the block below in any other project. + +```scratch +broadcast cross-project (message v) :: #4cdab2 +``` +This block sends a message to all other projects that have the extension running. It triggers the hat block above in projects that listen for the message. + +```scratch +broadcast cross-project (message v) and wait :: #4cdab2 +``` +This is similar to the block above, but waits for the broadcast to end before continuing to run the script. + +### Variables + +```scratch +(get cross-project (variable v) :: #4cdab2) +``` + +This block gets the value of a cross-project variable. + +```scratch +set cross-project (variable v) to [value] :: #4cdab2 +``` + +This block sets the value of a cross-project variable in all currently connected projects. + +```scratch +change cross-project (variable v) by (1) :: #4cdab2 +``` + +This block increments the value of a cross-project variable in all currently connected projects by the specified number. diff --git a/extensions/8to16/mesh.js b/extensions/8to16/mesh.js new file mode 100644 index 0000000000..1c42f6bd7c --- /dev/null +++ b/extensions/8to16/mesh.js @@ -0,0 +1,352 @@ +// Name: Cross-Project +// ID: eightxtwoMesh +// Description: Send and receive messages between other projects. +// By: AmpElectrecuted +// License: MPL-2.0 + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Cross-Project Broadcast extension must run unsandboxed"); + } + const vm = Scratch.vm; + + // Init the broadcastchannel + const bc = new BroadcastChannel("extensions.turbowarp.org/8to16/mesh"); + let connectedUsers = 0; + let finishedWaits = {}; + + if (!vm.runtime.extensionStorage["8to16mesh"]) + vm.runtime.extensionStorage["8to16mesh"] = { messages: [], variables: {} }; + vm.runtime.on("RUNTIME_DISPOSED", () => { + vm.runtime.extensionStorage["8to16mesh"] = { messages: [], variables: {} }; + }); + vm.runtime.on("PROJECT_LOADED", () => { + vm.runtime.extensionManager.refreshBlocks(); + }); + + // Message utilities + const getMeshages = () => + vm.runtime.extensionStorage["8to16mesh"].messages ?? []; + const addMeshage = (name) => { + if (getMeshages().includes(name)) return; + if (name === "") return; + vm.runtime.extensionStorage["8to16mesh"].messages = [ + ...getMeshages(), + name, + ]; + vm.extensionManager.refreshBlocks(); + }; + const delMeshage = (name) => { + if (!getMeshages().includes(name)) return; + vm.runtime.extensionStorage["8to16mesh"].messages = getMeshages().filter( + (n) => n !== name + ); + vm.extensionManager.refreshBlocks(); + }; + + // Var utilities + const getMeshVars = () => + vm.runtime.extensionStorage["8to16mesh"].variables ?? {}; + const addMeshVar = (name) => { + if (Object.keys(getMeshVars()).includes(name)) return; + if (name === "") return; + vm.runtime.extensionStorage["8to16mesh"].variables = { + ...getMeshVars(), + [name]: "", + }; + vm.extensionManager.refreshBlocks(); + }; + const delMeshVar = (name) => { + if (Object.keys(getMeshVars()).includes(name)) return; + vm.runtime.extensionStorage["8to16mesh"].variables = Object.fromEntries( + Object.entries(getMeshVars()).filter((v) => v[0] !== name) + ); + vm.extensionManager.refreshBlocks(); + }; + + // Handle messages on the broadcastchannel + bc.onmessage = async ({ data }) => { + switch (data.type) { + case "broadcast": { + let hats = vm.runtime.startHats("eightxtwoMesh_when", { + BROADCAST: data.name, + }); + // broadcast and wait handling + if (data.willWait) { + await new Promise((resolve) => { + const poll = () => { + if ( + hats.filter((thread) => vm.runtime.threads.includes(thread)) + .length === 0 + ) + resolve(); + else setTimeout(poll, 5); + }; + poll(); + }); + bc.postMessage({ type: "done", name: data.name }); + } + break; + } + case "var": { + vm.runtime.extensionStorage["8to16mesh"].variables[data.key] = + data.value; + break; + } + case "done": { + ++finishedWaits[data.name]; + break; + } + case "ping": { + connectedUsers = 0; + bc.postMessage({ type: "pong" }); + break; + } + case "pong": { + connectedUsers += 1; + break; + } + } + }; + setTimeout(() => bc.postMessage({ type: "ping" }), 1000); + + class Mesh { + getMeshagesForMenu() { + const meshages = getMeshages().sort(); + return meshages.length === 0 ? [""] : meshages; + } + getMeshVarsForMenu() { + const meshvars = Object.keys(getMeshVars()).sort(); + return meshvars.length === 0 ? [""] : meshvars; + } + newMsg() { + // taken from SharkPool/Camera + // in a Button Context, ScratchBlocks always exists + ScratchBlocks.prompt( + Scratch.translate("New cross-project message name:"), + "", + addMeshage, + Scratch.translate("New Message"), + "broadcast_msg" + ); + } + removeMsg() { + // taken from SharkPool/Camera + // in a Button Context, ScratchBlocks always exists + ScratchBlocks.prompt( + Scratch.translate("Remove cross-project message named:"), + "", + delMeshage, + Scratch.translate("Remove Message"), + "broadcast_msg" + ); + } + newVar() { + // taken from SharkPool/Camera + // in a Button Context, ScratchBlocks always exists + ScratchBlocks.prompt( + Scratch.translate( + "New cross-project variable name (this variable will be available to all sprites):" + ), + "", + addMeshVar, + Scratch.translate("New Variable"), + "broadcast_msg" + ); + } + removeVar() { + // taken from SharkPool/Camera + // in a Button Context, ScratchBlocks always exists + ScratchBlocks.prompt( + Scratch.translate("Remove cross-project variable named:"), + "", + delMeshVar, + Scratch.translate("Remove Variable"), + "broadcast_msg" + ); + } + getInfo() { + return { + id: "eightxtwoMesh", + name: Scratch.translate("Cross-Project"), + docsURI: "https://extensions.turbowarp.org/8to16/mesh", + color1: "#4cdab2", + color2: "#44cda5", + color3: "#3dc099", + blocks: [ + { + func: "newMsg", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Make a Message"), + }, + { + func: "removeMsg", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Remove a Message"), + hideFromPalette: getMeshages().length === 0, + }, + { + opcode: "when", + blockType: Scratch.BlockType.EVENT, + text: Scratch.translate("when I receive cross-project [BROADCAST]"), + isEdgeActivated: false, + hideFromPalette: getMeshages().length === 0, + arguments: { + BROADCAST: { + type: Scratch.ArgumentType.STRING, + menu: "MESHES_NOACCEPT", + }, + }, + }, + { + opcode: "broadcast", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("broadcast cross-project [BROADCAST]"), + hideFromPalette: getMeshages().length === 0, + arguments: { + BROADCAST: { + type: Scratch.ArgumentType.STRING, + menu: "MESHES_ACCEPT", + }, + }, + }, + { + opcode: "broadcastWait", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "broadcast cross-project [BROADCAST] and wait" + ), + hideFromPalette: getMeshages().length === 0, + arguments: { + BROADCAST: { + type: Scratch.ArgumentType.STRING, + menu: "MESHES_ACCEPT", + }, + }, + }, + ...(getMeshages().length === 0 ? [] : ["---"]), + { + func: "newVar", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Make a Variable"), + }, + { + func: "removeVar", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate("Remove a Variable"), + hideFromPalette: Object.keys(getMeshVars()).length === 0, + }, + { + opcode: "getVar", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("get cross-project [VAR]"), + arguments: { + VAR: { + type: Scratch.ArgumentType.STRING, + menu: "VARS", + }, + }, + hideFromPalette: Object.keys(getMeshVars()).length === 0, + }, + { + opcode: "setVar", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set cross-project [VAR] to [VALUE]"), + arguments: { + VAR: { + type: Scratch.ArgumentType.STRING, + menu: "VARS", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + }, + hideFromPalette: Object.keys(getMeshVars()).length === 0, + }, + { + opcode: "changeVar", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("change cross-project [VAR] by [VALUE]"), + arguments: { + VAR: { + type: Scratch.ArgumentType.STRING, + menu: "VARS", + }, + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + hideFromPalette: Object.keys(getMeshVars()).length === 0, + }, + ], + menus: { + MESHES_ACCEPT: { + acceptReporters: true, + items: "getMeshagesForMenu", + }, + MESHES_NOACCEPT: { + acceptReporters: false, + items: "getMeshagesForMenu", + }, + VARS: { + acceptReporters: true, + items: "getMeshVarsForMenu", + }, + }, + }; + } + + broadcast({ BROADCAST }) { + vm.runtime.startHats("eightxtwoMesh_when", { BROADCAST }); + bc.postMessage({ + type: "broadcast", + name: BROADCAST, + willWait: false, + }); + } + async broadcastWait({ BROADCAST }) { + finishedWaits[BROADCAST] = 0; + vm.runtime.startHats("eightxtwoMesh_when", { BROADCAST }); // TODO: add wait here + bc.postMessage({ + type: "broadcast", + name: BROADCAST, + willWait: true, + }); + await new Promise((resolve) => { + const poll = () => { + if (finishedWaits[BROADCAST] >= connectedUsers) resolve(); + else setTimeout(poll, 25); + }; + poll(); + }); + delete finishedWaits[BROADCAST]; + } + + getVar({ VAR }) { + return vm.runtime.extensionStorage["8to16mesh"].variables[VAR]; + } + setVar({ VAR, VALUE }) { + vm.runtime.extensionStorage["8to16mesh"].variables[VAR] = VALUE; + bc.postMessage({ + type: "var", + key: VAR, + value: VALUE, + }); + } + changeVar({ VAR, VALUE }) { + vm.runtime.extensionStorage["8to16mesh"].variables[VAR] += + Scratch.Cast.toNumber(VALUE); + bc.postMessage({ + type: "var", + key: VAR, + value: vm.runtime.extensionStorage["8to16mesh"].variables[VAR], + }); + } + } + + Scratch.extensions.register(new Mesh()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index 5f766f06d1..641abe64fc 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -96,6 +96,7 @@ "CubesterYT/Webhooks", "Lily/CommentBlocks", "veggiecan/LongmanDictionary", + "8to16/mesh", "Alestore/nfcwarp", "NishiOwO/dectalk", "steamworks", diff --git a/images/8to16/mesh.svg b/images/8to16/mesh.svg new file mode 100644 index 0000000000..e1390d2246 --- /dev/null +++ b/images/8to16/mesh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index b2dde23dc9..958780713a 100644 --- a/images/README.md +++ b/images/README.md @@ -325,6 +325,9 @@ All images in this folder are licensed under the [GNU General Public License ver ## DogeisCut/FormatNumbers.png - Created by [@Dillon](https://github.com/DillonRGaming) + ## 8to16/mesh.svg + - Created by [@kx1bx1](https://github.com/kx1bx1) in https://github.com/TurboWarp/extensions/pull/2382#issuecomment-3731596025 + ## NishiOwO/dectalk.png - Created by [@Dogo6647](https://github.com/Dogo6647) - Used Arimo from Google Fonts.