diff --git a/docs/8to16/mesh.md b/docs/8to16/mesh.md
new file mode 100644
index 0000000000..09d45e26f1
--- /dev/null
+++ b/docs/8to16/mesh.md
@@ -0,0 +1,51 @@
+# Mesh
+
+Mesh 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 [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 (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 (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 (variable v) :: #4cdab2)
+```
+
+This block gets the value of a mesh variable.
+
+```scratch
+set (variable v) to [value] :: #4cdab2
+```
+
+This block sets the value of a mesh variable in all currently connected projects.
+
+```scratch
+change (variable v) by (1) :: #4cdab2
+```
+
+This block increments the value of a mesh 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..41e0152a35
--- /dev/null
+++ b/extensions/8to16/mesh.js
@@ -0,0 +1,350 @@
+// Name: Mesh
+// ID: eightxtwoMesh
+// Description: Send and receive messages between other projects.
+// By: 8to16
+// License: MPL-2.0
+
+(function (Scratch) {
+ "use strict";
+
+ if (!Scratch.extensions.unsandboxed) {
+ throw new Error("Mesh 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" }), 50);
+
+ 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 message name:"),
+ "",
+ addMeshage,
+ Scratch.translate("Mesh Manager"),
+ "broadcast_msg"
+ );
+ }
+ removeMsg() {
+ // taken from SharkPool/Camera
+ // in a Button Context, ScratchBlocks always exists
+ ScratchBlocks.prompt(
+ Scratch.translate("Remove message named:"),
+ "",
+ delMeshage,
+ Scratch.translate("Mesh Manager"),
+ "broadcast_msg"
+ );
+ }
+ newVar() {
+ // taken from SharkPool/Camera
+ // in a Button Context, ScratchBlocks always exists
+ ScratchBlocks.prompt(
+ Scratch.translate(
+ "New variable name (this variable will be available to all sprites):"
+ ),
+ "",
+ addMeshVar,
+ Scratch.translate("Mesh Manager"),
+ "broadcast_msg"
+ );
+ }
+ removeVar() {
+ // taken from SharkPool/Camera
+ // in a Button Context, ScratchBlocks always exists
+ ScratchBlocks.prompt(
+ Scratch.translate("Remove variable named:"),
+ "",
+ delMeshVar,
+ Scratch.translate("Mesh Manager"),
+ "broadcast_msg"
+ );
+ }
+ getInfo() {
+ return {
+ id: "eightxtwoMesh",
+ name: Scratch.translate("Mesh"),
+ 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 [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 [BROADCAST]"),
+ hideFromPalette: getMeshages().length === 0,
+ arguments: {
+ BROADCAST: {
+ type: Scratch.ArgumentType.STRING,
+ menu: "MESHES_ACCEPT",
+ },
+ },
+ },
+ {
+ opcode: "broadcastWait",
+ blockType: Scratch.BlockType.COMMAND,
+ text: Scratch.translate("broadcast [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 [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 [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 [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 442f300d59..f9c324c9f2 100644
--- a/extensions/extensions.json
+++ b/extensions/extensions.json
@@ -94,6 +94,7 @@
"Lily/CommentBlocks",
"veggiecan/LongmanDictionary",
"CubesterYT/TurboHook",
+ "8to16/mesh",
"Alestore/nfcwarp",
"steamworks",
"itchio",
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 fef54d00ef..15054fff9c 100644
--- a/images/README.md
+++ b/images/README.md
@@ -323,4 +323,7 @@ All images in this folder are licensed under the [GNU General Public License ver
- Created by [@SharkPool-SP](https://github.com/SharkPool-SP/)
## DogeisCut/FormatNumbers.png
- - Created by [@Dillon](https://github.com/DillonRGaming)
\ No newline at end of file
+ - 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