From 2b45e025bffabe3d311d9b1a97a6ece307e63ade Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 24 Dec 2025 13:16:47 -0800 Subject: [PATCH 01/12] guard Builders class against simplayers --- packs/BP/scripts/classes/Builder/Builders.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packs/BP/scripts/classes/Builder/Builders.js b/packs/BP/scripts/classes/Builder/Builders.js index 336471c..3d07c3d 100644 --- a/packs/BP/scripts/classes/Builder/Builders.js +++ b/packs/BP/scripts/classes/Builder/Builders.js @@ -35,6 +35,8 @@ world.beforeEvents.playerLeave.subscribe((event) => { }); world.afterEvents.worldLoad.subscribe((event) => { for (const player of world.getAllPlayers()) { + if (!player) + continue; Builders.onJoin(player.id); } }); \ No newline at end of file From f561d281ee6141ada6206c3a7c44d64551ba03df Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 29 Dec 2025 22:35:20 -0800 Subject: [PATCH 02/12] move howto to top-level menu --- README.md | 2 +- packs/BP/scripts/classes/MenuForm.js | 7 +++---- packs/BP/scripts/classes/MenuFormBuilder.js | 13 +++++++------ packs/RP/texts/en_US.lang | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6c65927..11d40ef 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Construct's latest release can be downloaded from its [Releases Page](https://gi ### Construct Menu -Use `/construct:item` to get the Construct item, which opens the Construct menu. This item is also available in the Creative inventory under the "Items" tab. Using the Construct item will open a menu where you can manage your structures instances. You can create new instances of structures, edit existing ones, and delete them when you no longer need them. There are plenty options available to customize your building style! +Use `/construct:construct` to get the Construct item, which opens the Construct menu. This item is also available in the Creative inventory under the "Items" tab. Using the Construct item will open a menu where you can manage your structures instances. You can create new instances of structures, edit existing ones, and delete them when you no longer need them. There are plenty options available to customize your building style! *Pro tip: You can name the Construct item after a structure instance to automagically select it when you open the menu!* diff --git a/packs/BP/scripts/classes/MenuForm.js b/packs/BP/scripts/classes/MenuForm.js index 320ea2d..15b9198 100644 --- a/packs/BP/scripts/classes/MenuForm.js +++ b/packs/BP/scripts/classes/MenuForm.js @@ -34,6 +34,9 @@ export class MenuForm { if (selection === 0) { new BuilderForm(this.player); return void 0; + } else if (selection == structureCollection.getInstanceNames().length + 2) { + MenuFormBuilder.buildHowTo().show(this.player); + return void 0; } else { selection--; const selectedInstanceName = structureCollection.getInstanceNames()[selection]; @@ -80,10 +83,6 @@ export class MenuForm { return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => { if (response.canceled) return; - if (response.selection === structureCollection.getWorldStructureIds().length + 1) { - MenuFormBuilder.buildHowTo().show(this.player); - return; - } const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; return selectedStructureId || this.getOtherStructureId(); }); diff --git a/packs/BP/scripts/classes/MenuFormBuilder.js b/packs/BP/scripts/classes/MenuFormBuilder.js index a9a96ff..fdaa4cd 100644 --- a/packs/BP/scripts/classes/MenuFormBuilder.js +++ b/packs/BP/scripts/classes/MenuFormBuilder.js @@ -13,6 +13,7 @@ export class MenuFormBuilder { allInstanceNameForm.button(`${structureCollection.get(instanceName).isEnabled() ? '§2' : '§c'}${instanceName}`); }); allInstanceNameForm.button({ translate: 'construct.mainmenu.newinstance' }); + allInstanceNameForm.button({ translate: 'construct.mainmenu.howto' }); return allInstanceNameForm; } @@ -45,12 +46,12 @@ export class MenuFormBuilder { static buildHowTo() { const message = { rawtext: [ - { translate: 'construct.mainmenu.selectstructure.howto.add.header' }, { text: '\n' }, - { translate: 'construct.mainmenu.selectstructure.howto.add.structureblock' }, { text: '\n' }, - { translate: 'construct.mainmenu.selectstructure.howto.add.or' }, { text: '\n' }, - { translate: 'construct.mainmenu.selectstructure.howto.add.mcstructure' }, { text: '\n' }, - { text: '\n' }, { translate: 'construct.mainmenu.selectstructure.howto.remove.header' }, { text: '\n' }, - { translate: 'construct.mainmenu.selectstructure.howto.remove.body' }, { text: '\n' }, + { translate: 'construct.mainmenu.howto.add.header' }, { text: '\n' }, + { translate: 'construct.mainmenu.howto.add.structureblock' }, { text: '\n' }, + { translate: 'construct.mainmenu.howto.add.or' }, { text: '\n' }, + { translate: 'construct.mainmenu.howto.add.mcstructure' }, { text: '\n' }, + { text: '\n' }, { translate: 'construct.mainmenu.howto.remove.header' }, { text: '\n' }, + { translate: 'construct.mainmenu.howto.remove.body' }, { text: '\n' }, ] }; return new ActionFormData() .title(this.menuTitle) diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 3856b6e..eaea005 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -68,21 +68,21 @@ construct.mainmenu.instance.exists=§cInstance '%s' already exists. Try again wi construct.mainmenu.instance.notfound=§cStructure ID '%s' not found. If you're looking for a structure that you put in the structures folder, please restart your world and try again. ## Insert string: structure name construct.mainmenu.title=§l§2Construct ## This is the name of the pack. construct.mainmenu.selectinstance=Select an instance: +construct.mainmenu.howto=How to Add/Remove Structures construct.mainmenu.settings=Builder Settings construct.mainmenu.newinstance=Create New Instance construct.mainmenu.newinstance.prompt=Enter a name for the new instance: construct.mainmenu.newinstance.placeholder=example_instance ## The placeholder text in the textbox where the user enters the instance name. construct.mainmenu.selectstructure.header=Select a structure: construct.mainmenu.selectstructure.other=Other -construct.mainmenu.selectstructure.howto=How to Add/Remove Structures construct.mainmenu.selectstructure.other.prompt=Enter the Structure ID: construct.mainmenu.selectstructure.other.placeholder=example_structure ## The placeholder text in the textbox where the user enters the structure name. -construct.mainmenu.selectstructure.howto.add.header=§aHow to Add Structures: -construct.mainmenu.selectstructure.howto.add.structureblock=§7- Save a structure using a §fstructure block§7 or the §f/structure§7 command. -construct.mainmenu.selectstructure.howto.add.or=§7§lOR§r -construct.mainmenu.selectstructure.howto.add.mcstructure=§7- Add a §f.mcstructure§7 file to this pack's §fstructures folder§7. When selecting your structure, select the §fOther§7 option and then use the filename (without '.mcstructure') as the §fStructure ID§7. After its first use, it will be added to the list of structures. -construct.mainmenu.selectstructure.howto.remove.header=§cHow to Remove Structures: -construct.mainmenu.selectstructure.howto.remove.body=§7- Use the §f/structure delete§7 command to remove a structure from the world. +construct.mainmenu.howto.add.header=§aHow to Add Structures: +construct.mainmenu.howto.add.structureblock=§7- Save a structure using a §fstructure block§7 or the §f/structure§7 command. +construct.mainmenu.howto.add.or=§7§lOR§r +construct.mainmenu.howto.add.mcstructure=§7- Add a §f.mcstructure§7 file to this pack's §fstructures folder§7. When selecting your structure, select the §fOther§7 option and then use the filename (without '.mcstructure') as the §fStructure ID§7. After its first use, it will be added to the list of structures. +construct.mainmenu.howto.remove.header=§cHow to Remove Structures: +construct.mainmenu.howto.remove.body=§7- Use the §f/structure delete§7 command to remove a structure from the world. ## Commands construct.commands.construct=Opens the Construct menu. From d72156b533a79c3403a515b6cbfe7b240fc1ea0a Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 29 Dec 2025 22:35:40 -0800 Subject: [PATCH 03/12] save nbt data while materialgrabbing --- packs/BP/scripts/options/materialGrabber.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packs/BP/scripts/options/materialGrabber.js b/packs/BP/scripts/options/materialGrabber.js index 3fe72ee..d4bf889 100644 --- a/packs/BP/scripts/options/materialGrabber.js +++ b/packs/BP/scripts/options/materialGrabber.js @@ -140,7 +140,8 @@ function playSoundEffect(player, transferCount) { function tryTransferAmountToPlayer(slot, playerContainer, materials, grabAmount) { const itemStack = slot.getItem(); if (canAddItem(playerContainer, itemStack)) { - const itemStackToAdd = new ItemStack(itemStack.typeId, grabAmount); + const itemStackToAdd = itemStack.clone(); + itemStackToAdd.amount = grabAmount; addItem(playerContainer, itemStackToAdd); materials.remove(itemStack.typeId, grabAmount); removeAmount(slot, grabAmount); @@ -185,7 +186,8 @@ function partiallyFilledSlotPass(inventory, itemStack) { const remainderAmount = Math.max(0, (slot.amount + itemStack.amount) - slot.maxAmount); slot.amount += itemStack.amount - remainderAmount; if (remainderAmount > 0) { - const remainderStack = new ItemStack(itemStack.typeId, remainderAmount); + const remainderStack = itemStack.clone(); + remainderStack.amount = remainderAmount; addItem(inventory, remainderStack); } return true; From bdfe4ebd008919066fcb7dadf924b8e31da5b4c1 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 29 Dec 2025 22:36:24 -0800 Subject: [PATCH 04/12] easyplacing powder_snow keeps the bucket --- packs/BP/scripts/options/easyPlace.js | 2 +- packs/BP/scripts/{data.js => options/easyPlaceConversions.js} | 3 ++- packs/BP/scripts/options/fastEasyPlace.js | 2 +- packs/BP/scripts/utils.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename packs/BP/scripts/{data.js => options/easyPlaceConversions.js} (97%) diff --git a/packs/BP/scripts/options/easyPlace.js b/packs/BP/scripts/options/easyPlace.js index 93fcbb9..b4d8a72 100644 --- a/packs/BP/scripts/options/easyPlace.js +++ b/packs/BP/scripts/options/easyPlace.js @@ -1,7 +1,7 @@ import { BuilderOption } from '../classes/Builder/BuilderOption'; import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server'; import { structureCollection } from '../classes/Structure/StructureCollection'; -import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, blockIdToItemStackMap } from '../data'; +import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, blockIdToItemStackMap } from './easyPlaceConversions'; import { fetchMatchingItemSlot, placeBlock } from '../utils'; import { Builders } from '../classes/Builder/Builders'; diff --git a/packs/BP/scripts/data.js b/packs/BP/scripts/options/easyPlaceConversions.js similarity index 97% rename from packs/BP/scripts/data.js rename to packs/BP/scripts/options/easyPlaceConversions.js index e205aa4..bd4aee7 100644 --- a/packs/BP/scripts/data.js +++ b/packs/BP/scripts/options/easyPlaceConversions.js @@ -55,5 +55,6 @@ export const blockIdToItemStackMap = { export const specialItemPlacementConversions = { 'water_bucket': 'bucket', - 'lava_bucket': 'bucket' + 'lava_bucket': 'bucket', + 'powder_snow_bucket': 'bucket' }; \ No newline at end of file diff --git a/packs/BP/scripts/options/fastEasyPlace.js b/packs/BP/scripts/options/fastEasyPlace.js index 08b8ea4..bf8ccd3 100644 --- a/packs/BP/scripts/options/fastEasyPlace.js +++ b/packs/BP/scripts/options/fastEasyPlace.js @@ -1,7 +1,7 @@ import { BuilderOption } from '../classes/Builder/BuilderOption'; import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server'; import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, - blockIdToItemStackMap } from '../data'; + blockIdToItemStackMap } from './easyPlaceConversions'; import { placeBlock, fetchMatchingItemSlot } from '../utils'; import { Raycaster } from '../classes/Raycaster'; import { Builders } from '../classes/Builder/Builders'; diff --git a/packs/BP/scripts/utils.js b/packs/BP/scripts/utils.js index 9ad528a..1065d23 100644 --- a/packs/BP/scripts/utils.js +++ b/packs/BP/scripts/utils.js @@ -1,6 +1,6 @@ import { system, EntityComponentTypes, LiquidType, ItemStack } from '@minecraft/server'; import { FormCancelationReason } from '@minecraft/server-ui'; -import { specialItemPlacementConversions } from './data'; +import { specialItemPlacementConversions } from './options/easyPlaceConversions'; import { blocks, block_sounds } from './blocks'; export async function forceShow(player, form, timeout = Infinity) { From 2f39c32629bff74180d3b7b470ce0c9bd30a5173 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 29 Dec 2025 22:36:43 -0800 Subject: [PATCH 05/12] rename /item to /construct and remove ./construct --- README.md | 5 +---- packs/BP/scripts/commands/construct.js | 20 +++++--------------- packs/RP/texts/en_US.lang | 9 ++++----- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 11d40ef..e4523d9 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,9 @@ Construct uses Minecraft's vanilla structure system so that you can easily creat ### Commands -**Usage: `/construct:item`** +**Usage: `/construct:construct`** Gives you the Construct item. Use it to open the Construct menu. -**Usage: `./construct`** -Shows the Construct menu. Only available while **Canopy** is installed. - ## Join the Community Need help, want to discuss technical Minecraft, or follow future updates? [**Join our Discord!**](https://discord.gg/9KGche8fxm) diff --git a/packs/BP/scripts/commands/construct.js b/packs/BP/scripts/commands/construct.js index ca7a863..05b3ab8 100644 --- a/packs/BP/scripts/commands/construct.js +++ b/packs/BP/scripts/commands/construct.js @@ -1,5 +1,3 @@ -import { Command } from '../lib/canopy/CanopyExtension'; -import { extension } from '../config'; import { world, system, EntityComponentTypes, ItemStack, CommandPermissionLevel, CustomCommandStatus, Player } from '@minecraft/server'; import { MenuForm } from '../classes/MenuForm'; import { structureCollection } from '../classes/Structure/StructureCollection' @@ -7,18 +5,10 @@ import { Builders } from '../classes/Builder/Builders'; export const MENU_ITEM = 'construct:menu'; -const menuCmd = new Command({ - name: 'construct', - description: { translate: 'construct.commands.construct' }, - usage: 'construct', - callback: (sender) => openMenu(sender) -}); -extension.addCommand(menuCmd); - system.beforeEvents.startup.subscribe((event) => { const command = { - name: 'construct:item', - description: 'construct.commands.item', + name: 'construct:construct', + description: 'construct.commands.construct', permissionLevel: CommandPermissionLevel.Any, cheatsRequired: false }; @@ -28,13 +18,13 @@ system.beforeEvents.startup.subscribe((event) => { function givePlayerConstructItem(origin) { const player = origin.sourceEntity; if (player instanceof Player === false) - return { status: CustomCommandStatus.Failure, message: 'construct.commands.item.denyorigin' }; + return { status: CustomCommandStatus.Failure, message: 'construct.commands.construct.denyorigin' }; system.run(() => { const givenItemStack = player.getComponent(EntityComponentTypes.Inventory)?.container?.addItem(new ItemStack(MENU_ITEM)); if (givenItemStack) - player.sendMessage({ translate: 'construct.commands.item.fail' }); + player.sendMessage({ translate: 'construct.commands.construct.fail' }); else - player.sendMessage({ translate: 'construct.commands.item.success' }); + player.sendMessage({ translate: 'construct.commands.construct.success' }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index eaea005..4c64d02 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -85,11 +85,10 @@ construct.mainmenu.howto.remove.header=§cHow to Remove Structures: construct.mainmenu.howto.remove.body=§7- Use the §f/structure delete§7 command to remove a structure from the world. ## Commands -construct.commands.construct=Opens the Construct menu. -construct.commands.item=Gives you the Construct item. Use it to open the Construct menu. -construct.commands.item.denyorigin=This command can only be used by players. -construct.commands.item.fail=§cFailed to give you the Construct item. -construct.commands.item.success=§aYou recieved the Construct item! Use it to open the Construct menu. +construct.commands.construct=Gives you the Construct item. Use it to open the Construct menu. +construct.commands.construct.denyorigin=This command can only be used by players. +construct.commands.construct.fail=§cFailed to give you the Construct item. +construct.commands.construct.success=§aYou recieved the Construct item! Use it to open the Construct menu. ## Options construct.option.enabled= is now enabled! From 6f7d8aa3efa787b64b8339e56e3c3fe8d7dd9982 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 13:48:25 -0800 Subject: [PATCH 06/12] bump to 1.0.7 & mc 1.26.0 --- README.md | 2 +- packs/BP/manifest.json | 10 +++++----- packs/RP/manifest.json | 11 +++++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e4523d9..0dab28d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/ForestOfLight/Construct/total?label=Github%20downloads&logo=github)](https://github.com/ForestOfLight/Construct/releases) [![Curseforge Downloads](https://cf.way2muchnoise.eu/full_1283139_downloads.svg)](https://www.curseforge.com/minecraft-bedrock/addons/construct) -[![Minecraft - Version](https://img.shields.io/badge/Minecraft-v1.21.130_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) +[![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.0_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) [![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm) diff --git a/packs/BP/manifest.json b/packs/BP/manifest.json index d594a20..6b466a6 100644 --- a/packs/BP/manifest.json +++ b/packs/BP/manifest.json @@ -1,11 +1,11 @@ { "format_version": 2, "header": { - "name": "Construct [BP] v1.0.6", + "name": "Construct [BP] v1.0.7", "description": "Survival building addon by §aForestOfLight§r.", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", - "min_engine_version": [1, 21, 130], - "version": [1, 0, 6] + "min_engine_version": [1, 26, 0], + "version": [1, 0, 7] }, "modules": [ { @@ -26,7 +26,7 @@ "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.5.0-beta" + "version": "2.6.0-beta" }, { "module_name": "@minecraft/server-ui", @@ -34,7 +34,7 @@ }, { "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", // Construct RP - "version": [1, 0, 6] + "version": [1, 0, 7] } ], "metadata": { diff --git a/packs/RP/manifest.json b/packs/RP/manifest.json index f844e7a..8591865 100644 --- a/packs/RP/manifest.json +++ b/packs/RP/manifest.json @@ -1,11 +1,11 @@ { "format_version": 2, "header": { - "name": "Construct [RP] v1.0.6", + "name": "Construct [RP] v1.0.7", "description": "Survival building addon by §aForestOfLight§r.", "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", - "version": [1, 0, 6], - "min_engine_version": [1,17,1] + "version": [1, 0, 7], + "min_engine_version": [1,26,0] }, "modules": [ { @@ -17,9 +17,12 @@ "dependencies": [ { "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", // Construct BP - "version": [1, 0, 6] + "version": [1, 0, 7] } ], + "capabilities": [ + "pbr" + ], "metadata": { "authors": [ "ForestOfLight" ], "license": "MIT" From 68c9ad5ef23f0d4a47e214633c73b4fda2c0e442 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 13:56:51 -0800 Subject: [PATCH 07/12] decouple from Canopy --- README.md | 4 - packs/BP/scripts/config.js | 8 - .../BP/scripts/lib/canopy/CanopyExtension.js | 166 ----- packs/BP/scripts/lib/canopy/Command.js | 103 --- packs/BP/scripts/lib/canopy/Rule.js | 70 --- packs/BP/scripts/lib/canopy/extension.ipc.js | 70 --- packs/BP/scripts/lib/ipc/ipc.d.ts | 103 --- packs/BP/scripts/lib/ipc/ipc.js | 593 ------------------ 8 files changed, 1117 deletions(-) delete mode 100644 packs/BP/scripts/config.js delete mode 100644 packs/BP/scripts/lib/canopy/CanopyExtension.js delete mode 100644 packs/BP/scripts/lib/canopy/Command.js delete mode 100644 packs/BP/scripts/lib/canopy/Rule.js delete mode 100644 packs/BP/scripts/lib/canopy/extension.ipc.js delete mode 100644 packs/BP/scripts/lib/ipc/ipc.d.ts delete mode 100644 packs/BP/scripts/lib/ipc/ipc.js diff --git a/README.md b/README.md index 0dab28d..167698e 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,6 @@ Give yourself the tools to ease the survival building process with Construct, an -> [!IMPORTANT] -> This is a standalone addon, but it is also a **Canopy Extension**, which means **Canopy** can be installed in your world for the best experience! -> [Download **Canopy** here!](https://github.com/ForestOfLight/Canopy) - ## Usage Construct's latest release can be downloaded from its [Releases Page](https://github.com/ForestOfLight/Construct/releases) right here on GitHub. Install it just like any other addon, though make sure the Beta APIs experiment is enabled! diff --git a/packs/BP/scripts/config.js b/packs/BP/scripts/config.js deleted file mode 100644 index 70a9f73..0000000 --- a/packs/BP/scripts/config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { CanopyExtension } from './lib/canopy/CanopyExtension'; - -export const extension = new CanopyExtension({ - author: 'ForestOfLight', - name: 'Construct', - description: 'Survival building addon by §aForestOfLight§r.', - version: '1.0.6' -}); \ No newline at end of file diff --git a/packs/BP/scripts/lib/canopy/CanopyExtension.js b/packs/BP/scripts/lib/canopy/CanopyExtension.js deleted file mode 100644 index 8cf1acc..0000000 --- a/packs/BP/scripts/lib/canopy/CanopyExtension.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2024 ForestOfLight - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { world } from '@minecraft/server'; -import IPC from '../ipc/ipc'; -import Command from './Command'; -import Rule from './Rule'; -import { CommandCallbackRequest, CommandPrefixRequest, Ready, RegisterCommand, RegisterExtension, RegisterRule, RuleValueRequest, RuleValueSet, CommandPrefixResponse, RuleValueResponse } from './extension.ipc'; - -class CanopyExtension { - name; - version; - author; - description; - #commands = {}; - #rules = {}; - #isRegistrationReady = false; - - constructor({ name = 'Unnamed', version = '1.0.0', author = 'Unknown', description = { text: '' } }) { - this.id = this.#makeID(name); - this.name = name; - this.version = version; - this.author = author; - this.description = description; - - this.#registerExtension(); - this.#setupCommandPrefix(); - this.#handleCommandCallbacks(); - this.#handleRuleValueRequests(); - this.#handleRuleValueSetters(); - } - - addCommand(command) { - if (!(command instanceof Command)) - throw new Error('Command must be an instance of Command.'); - this.#commands[command.getName()] = command; - if (this.#isRegistrationReady) - this.#registerCommand(command); - } - - addRule(rule) { - if (!(rule instanceof Rule)) - throw new Error('Rule must be an instance of Rule.'); - this.#rules[rule.getID()] = rule; - if (this.#isRegistrationReady) - this.#registerRule(rule); - } - - getRuleValue(ruleID) { - return this.#rules[ruleID].getValue(); - } - - #makeID(name) { - if (typeof name !== 'string') - throw new Error(`[${name}] Could not register extension. Extension name must be a string.`); - const id = name.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/ /g, '_'); - if (id.length === 0) - throw new Error(`[${name}] Could not register extension. Extension name must contain at least one alphanumeric character.`); - return id; - } - - #registerExtension() { - IPC.once('canopyExtension:ready', Ready, () => { - IPC.send('canopyExtension:registerExtension', RegisterExtension, { - name: this.name, - version: this.version, - author: this.author, - description: this.description - }); - }); - IPC.once(`canopyExtension:${this.id}:ready`, Ready, () => { - this.#isRegistrationReady = true; - for (const rule of Object.values(this.#rules)) - this.#registerRule(rule); - for (const command of Object.values(this.#commands)) - this.#registerCommand(command); - }); - } - - #registerCommand(command) { - IPC.send(`canopyExtension:${this.id}:registerCommand`, RegisterCommand, { - name: command.getName(), - description: command.getDescription(), - usage: command.getUsage(), - callback: false, - args: command.getArgs(), - contingentRules: command.getContingentRules(), - adminOnly: command.isAdminOnly(), - helpEntries: command.getHelpEntries(), - helpHidden: command.isHelpHidden(), - extensionName: this.name - }); - } - - #handleCommandCallbacks() { - IPC.on(`canopyExtension:${this.id}:commandCallbackRequest`, CommandCallbackRequest, (cmdData) => { - if (cmdData.senderName === undefined) - return; - const sender = world.getPlayers({ name: cmdData.senderName })[0]; - if (!sender) - throw new Error(`Sender ${cmdData.senderName} of ${cmdData.commandName} not found.`); - const parsedArgs = JSON.parse(cmdData.args); - this.#commands[cmdData.commandName].runCallback(sender, parsedArgs); - }); - } - - #registerRule(rule) { - IPC.send(`canopyExtension:${this.id}:registerRule`, RegisterRule, { - identifier: rule.getID(), - description: rule.getDescription(), - contingentRules: rule.getContigentRules(), - independentRules: rule.getIndependentRules(), - extensionName: this.name - }); - if (rule.getValue() === true) - rule.onEnable(); - } - - #handleRuleValueRequests() { - IPC.handle(`canopyExtension:${this.id}:ruleValueRequest`, RuleValueRequest, RuleValueResponse, (data) => { - const rule = this.#rules[data.ruleID]; - if (!rule) - throw new Error(`Rule ${data.ruleID} not found.`); - const value = rule.getValue(); - return { value }; - }); - } - - #handleRuleValueSetters() { - IPC.on(`canopyExtension:${this.id}:ruleValueSet`, RuleValueSet, (data) => { - const rule = this.#rules[data.ruleID]; - if (!rule) - throw new Error(`Rule ${data.ruleID} not found.`); - rule.setValue(data.value); - }); - } - - #setupCommandPrefix() { - const prefix = IPC.invoke(`canopyExtension:commandPrefixRequest`, CommandPrefixRequest, void 0, CommandPrefixResponse).then(result => { - Command.setPrefix(result.prefix); - }); - } -} - -export { CanopyExtension, Command, Rule }; \ No newline at end of file diff --git a/packs/BP/scripts/lib/canopy/Command.js b/packs/BP/scripts/lib/canopy/Command.js deleted file mode 100644 index 9d0651d..0000000 --- a/packs/BP/scripts/lib/canopy/Command.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2024 ForestOfLight - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -class Command { - #name; - #description; - #usage; - #callback; - #args; - #contingentRules; - #adminOnly; - #helpEntries; - #helpHidden; - static #prefix = ''; - - constructor({ name, description = '', usage, callback, args = [], contingentRules = [], adminOnly = false, helpEntries = [], helpHidden = false }) { - this.#name = name; - this.#description = description; - this.#usage = usage; - this.#callback = callback; - this.#args = args; - this.#contingentRules = contingentRules; - this.#adminOnly = adminOnly; - this.#helpEntries = helpEntries; - this.#helpHidden = helpHidden; - } - - getName() { - return this.#name; - } - - getDescription() { - return this.#description; - } - - getUsage() { - return this.#usage; - } - - getCallback() { - return this.#callback; - } - - getArgs() { - return this.#args; - } - - getContingentRules() { - return this.#contingentRules; - } - - isAdminOnly() { - return this.#adminOnly; - } - - getHelpEntries() { - return this.#helpEntries; - } - - isHelpHidden() { - return this.#helpHidden; - } - - runCallback(sender, args) { - this.#callback(sender, args); - } - - sendUsage(sender) { - sender.sendMessage(`§cUsage: ${Command.#prefix}${this.#usage}`); - } - - static setPrefix(prefix) { - Command.#prefix = prefix; - } - - static getPrefix() { - return Command.#prefix; - } -} - -export default Command; \ No newline at end of file diff --git a/packs/BP/scripts/lib/canopy/Rule.js b/packs/BP/scripts/lib/canopy/Rule.js deleted file mode 100644 index 3ba7b4f..0000000 --- a/packs/BP/scripts/lib/canopy/Rule.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2024 ForestOfLight - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { world } from '@minecraft/server'; - -class Rule { - #identifier; - #description; - #contingentRules; - #independentRules; - - constructor({ identifier, description, contingentRules = [], independentRules = [] }) { - this.#identifier = identifier; - this.#description = description; - this.#contingentRules = contingentRules; - this.#independentRules = independentRules; - } - - getID() { - return this.#identifier; - } - - getDescription() { - return this.#description; - } - - getContigentRules() { - return this.#contingentRules; - } - - getIndependentRules() { - return this.#independentRules; - } - - getValue() { - const value = world.getDynamicProperty(this.#identifier); - if (String(value) === 'true') - return true; - if (['false', 'undefined'].includes(String(value))) - return false; - throw new Error(`Rule ${this.#identifier} has an invalid value: ${value} (${typeof value})`); - } - - setValue(value) { - world.setDynamicProperty(this.#identifier, value); - } -} - -export default Rule; \ No newline at end of file diff --git a/packs/BP/scripts/lib/canopy/extension.ipc.js b/packs/BP/scripts/lib/canopy/extension.ipc.js deleted file mode 100644 index 5e00ab3..0000000 --- a/packs/BP/scripts/lib/canopy/extension.ipc.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable new-cap */ -import { PROTO } from '../ipc/ipc' - -const description = PROTO.Object({ - text: PROTO.Optional(PROTO.String), - translate: PROTO.Optional(PROTO.String), - with: PROTO.Optional(PROTO.Array(PROTO.String)) -}); - -export const Ready = PROTO.Void; - -export const RegisterExtension = PROTO.Object({ - name: PROTO.String, - version: PROTO.String, - author: PROTO.String, - description: description, - isEndstone: PROTO.Boolean -}); - -export const RegisterCommand = PROTO.Object({ - name: PROTO.String, - description: description, - usage: PROTO.String, - callback: PROTO.Optional(PROTO.Undefined), - args: PROTO.Optional(PROTO.Array(PROTO.Object({ - type: PROTO.String, - name: PROTO.String - }))), - contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)), - adminOnly: PROTO.Optional(PROTO.Boolean), - helpEntries: PROTO.Optional(PROTO.Array(PROTO.Object({ - usage: PROTO.String, - description: description - }))), - helpHidden: PROTO.Optional(PROTO.Boolean), - extensionName: PROTO.Optional(PROTO.String) -}); - -export const RegisterRule = PROTO.Object({ - identifier: PROTO.String, - description: description, - contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)), - independentRules: PROTO.Optional(PROTO.Array(PROTO.String)), - extensionName: PROTO.Optional(PROTO.String) -}); - -export const RuleValueRequest = PROTO.Object({ - ruleID: PROTO.String -}); - -export const RuleValueResponse = PROTO.Object({ - value: PROTO.Boolean -}); - -export const RuleValueSet = PROTO.Object({ - ruleID: PROTO.String, - value: PROTO.Boolean -}); - -export const CommandCallbackRequest = PROTO.Object({ - commandName: PROTO.String, - senderName: PROTO.Optional(PROTO.String), - args: PROTO.String -}); - -export const CommandPrefixRequest = PROTO.Void; - -export const CommandPrefixResponse = PROTO.Object({ - prefix: PROTO.String -}); \ No newline at end of file diff --git a/packs/BP/scripts/lib/ipc/ipc.d.ts b/packs/BP/scripts/lib/ipc/ipc.d.ts deleted file mode 100644 index dc6ef67..0000000 --- a/packs/BP/scripts/lib/ipc/ipc.d.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2025 OmniacDev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -export declare namespace PROTO { - interface Serializable { - serialize(value: T, stream: ByteQueue): Generator; - deserialize(stream: ByteQueue): Generator; - } - class ByteQueue { - private _buffer; - private _data_view; - private _length; - private _offset; - get end(): number; - get front(): number; - get data_view(): DataView; - constructor(size?: number); - write(...values: number[]): void; - read(amount?: number): number[]; - ensure_capacity(size: number): void; - static from_uint8array(array: Uint8Array): ByteQueue; - to_uint8array(): Uint8Array; - } - namespace MIPS { - function serialize(byte_queue: PROTO.ByteQueue): Generator; - function deserialize(str: string): Generator; - } - const Void: PROTO.Serializable; - const Null: PROTO.Serializable; - const Undefined: PROTO.Serializable; - const Int8: PROTO.Serializable; - const Int16: PROTO.Serializable; - const Int32: PROTO.Serializable; - const UInt8: PROTO.Serializable; - const UInt16: PROTO.Serializable; - const UInt32: PROTO.Serializable; - const UVarInt32: PROTO.Serializable; - const Float32: PROTO.Serializable; - const Float64: PROTO.Serializable; - const String: PROTO.Serializable; - const Boolean: PROTO.Serializable; - const UInt8Array: PROTO.Serializable; - const Date: PROTO.Serializable; - function Object(obj: { - [K in keyof T]: PROTO.Serializable; - }): PROTO.Serializable; - function Array(value: PROTO.Serializable): PROTO.Serializable; - function Tuple(...values: { - [K in keyof T]: PROTO.Serializable; - }): PROTO.Serializable; - function Optional(value: PROTO.Serializable): PROTO.Serializable; - function Map(key: PROTO.Serializable, value: PROTO.Serializable): PROTO.Serializable>; - function Set(value: PROTO.Serializable): PROTO.Serializable>; - type Endpoint = string; - type Header = { - guid: string; - encoding: string; - index: number; - final: boolean; - }; - const Endpoint: PROTO.Serializable; - const Header: PROTO.Serializable
; -} -export declare namespace NET { - function serialize(byte_queue: PROTO.ByteQueue, max_size?: number): Generator; - function deserialize(strings: string[]): Generator; - function emit, T>(endpoint: string, serializer: S & PROTO.Serializable, value: T): Generator; - function listen>(endpoint: string, serializer: S & PROTO.Serializable, callback: (value: T) => Generator): () => void; -} -export declare namespace IPC { - /** Sends a message with `args` to `channel` */ - function send, T>(channel: string, serializer: S & PROTO.Serializable, value: T): void; - /** Sends an `invoke` message through IPC, and expects a result asynchronously. */ - function invoke, T, RS extends PROTO.Serializable, R>(channel: string, serializer: TS & PROTO.Serializable, value: T, deserializer: RS & PROTO.Serializable): Promise; - /** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */ - function on, T>(channel: string, deserializer: S & PROTO.Serializable, listener: (value: T) => void): () => void; - /** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */ - function once, T>(channel: string, deserializer: S & PROTO.Serializable, listener: (value: T) => void): () => void; - /** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */ - function handle, T, RS extends PROTO.Serializable, R>(channel: string, deserializer: TS & PROTO.Serializable, serializer: RS & PROTO.Serializable, listener: (value: T) => R): () => void; -} -export default IPC; diff --git a/packs/BP/scripts/lib/ipc/ipc.js b/packs/BP/scripts/lib/ipc/ipc.js deleted file mode 100644 index 8b24afe..0000000 --- a/packs/BP/scripts/lib/ipc/ipc.js +++ /dev/null @@ -1,593 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2025 OmniacDev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { ScriptEventSource, system, world } from '@minecraft/server'; -export var PROTO; -(function (PROTO) { - class ByteQueue { - get end() { - return this._length + this._offset; - } - get front() { - return this._offset; - } - get data_view() { - return this._data_view; - } - constructor(size = 256) { - this._buffer = new Uint8Array(size); - this._data_view = new DataView(this._buffer.buffer); - this._length = 0; - this._offset = 0; - } - write(...values) { - this.ensure_capacity(values.length); - this._buffer.set(values, this.end); - this._length += values.length; - } - read(amount = 1) { - if (this._length > 0) { - const max_amount = amount > this._length ? this._length : amount; - const values = this._buffer.subarray(this._offset, this._offset + max_amount); - this._length -= max_amount; - this._offset += max_amount; - return globalThis.Array.from(values); - } - return []; - } - ensure_capacity(size) { - if (this.end + size > this._buffer.length) { - const larger_buffer = new Uint8Array((this.end + size) * 2); - larger_buffer.set(this._buffer.subarray(this._offset, this.end), 0); - this._buffer = larger_buffer; - this._offset = 0; - this._data_view = new DataView(this._buffer.buffer); - } - } - static from_uint8array(array) { - const byte_queue = new ByteQueue(); - byte_queue._buffer = array; - byte_queue._length = array.length; - byte_queue._offset = 0; - byte_queue._data_view = new DataView(array.buffer); - return byte_queue; - } - to_uint8array() { - return this._buffer.subarray(this._offset, this.end); - } - } - PROTO.ByteQueue = ByteQueue; - let MIPS; - (function (MIPS) { - function* serialize(byte_queue) { - const uint8array = byte_queue.to_uint8array(); - let str = '(0x'; - for (let i = 0; i < uint8array.length; i++) { - const hex = uint8array[i].toString(16).padStart(2, '0').toUpperCase(); - str += hex; - yield; - } - str += ')'; - return str; - } - MIPS.serialize = serialize; - function* deserialize(str) { - if (str.startsWith('(0x') && str.endsWith(')')) { - const result = []; - const hex_str = str.slice(3, str.length - 1); - for (let i = 0; i < hex_str.length; i++) { - const hex = hex_str[i] + hex_str[++i]; - result.push(parseInt(hex, 16)); - yield; - } - return ByteQueue.from_uint8array(new Uint8Array(result)); - } - return new ByteQueue(); - } - MIPS.deserialize = deserialize; - })(MIPS = PROTO.MIPS || (PROTO.MIPS = {})); - PROTO.Void = { - *serialize() { }, - *deserialize() { } - }; - PROTO.Null = { - *serialize() { }, - *deserialize() { - return null; - } - }; - PROTO.Undefined = { - *serialize() { }, - *deserialize() { - return undefined; - } - }; - PROTO.Int8 = { - *serialize(value, stream) { - const length = 1; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt8(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getInt8(stream.front); - stream.read(1); - return value; - } - }; - PROTO.Int16 = { - *serialize(value, stream) { - const length = 2; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt16(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getInt16(stream.front); - stream.read(2); - return value; - } - }; - PROTO.Int32 = { - *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt32(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getInt32(stream.front); - stream.read(4); - return value; - } - }; - PROTO.UInt8 = { - *serialize(value, stream) { - const length = 1; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint8(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getUint8(stream.front); - stream.read(1); - return value; - } - }; - PROTO.UInt16 = { - *serialize(value, stream) { - const length = 2; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint16(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getUint16(stream.front); - stream.read(2); - return value; - } - }; - PROTO.UInt32 = { - *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint32(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getUint32(stream.front); - stream.read(4); - return value; - } - }; - PROTO.UVarInt32 = { - *serialize(value, stream) { - while (value >= 0x80) { - stream.write((value & 0x7f) | 0x80); - value >>= 7; - yield; - } - stream.write(value); - }, - *deserialize(stream) { - let value = 0; - let size = 0; - let byte; - do { - byte = stream.read()[0]; - value |= (byte & 0x7f) << (size * 7); - size += 1; - yield; - } while ((byte & 0x80) !== 0 && size < 10); - return value; - } - }; - PROTO.Float32 = { - *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setFloat32(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getFloat32(stream.front); - stream.read(4); - return value; - } - }; - PROTO.Float64 = { - *serialize(value, stream) { - const length = 8; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setFloat64(stream.end - length, value); - }, - *deserialize(stream) { - const value = stream.data_view.getFloat64(stream.front); - stream.read(8); - return value; - } - }; - PROTO.String = { - *serialize(value, stream) { - yield* PROTO.UVarInt32.serialize(value.length, stream); - for (let i = 0; i < value.length; i++) { - const code = value.charCodeAt(i); - yield* PROTO.UVarInt32.serialize(code, stream); - } - }, - *deserialize(stream) { - const length = yield* PROTO.UVarInt32.deserialize(stream); - let value = ''; - for (let i = 0; i < length; i++) { - const code = yield* PROTO.UVarInt32.deserialize(stream); - value += globalThis.String.fromCharCode(code); - } - return value; - } - }; - PROTO.Boolean = { - *serialize(value, stream) { - stream.write(value ? 1 : 0); - }, - *deserialize(stream) { - const value = stream.read()[0]; - return value === 1; - } - }; - PROTO.UInt8Array = { - *serialize(value, stream) { - yield* PROTO.UVarInt32.serialize(value.length, stream); - stream.write(...value); - }, - *deserialize(stream) { - const length = yield* PROTO.UVarInt32.deserialize(stream); - return new Uint8Array(stream.read(length)); - } - }; - PROTO.Date = { - *serialize(value, stream) { - yield* PROTO.Float64.serialize(value.getTime(), stream); - }, - *deserialize(stream) { - return new globalThis.Date(yield* PROTO.Float64.deserialize(stream)); - } - }; - function Object(obj) { - return { - *serialize(value, stream) { - for (const key in obj) { - yield* obj[key].serialize(value[key], stream); - } - }, - *deserialize(stream) { - const result = {}; - for (const key in obj) { - result[key] = yield* obj[key].deserialize(stream); - } - return result; - } - }; - } - PROTO.Object = Object; - function Array(value) { - return { - *serialize(array, stream) { - yield* PROTO.UVarInt32.serialize(array.length, stream); - for (const item of array) { - yield* value.serialize(item, stream); - } - }, - *deserialize(stream) { - const result = []; - const length = yield* PROTO.UVarInt32.deserialize(stream); - for (let i = 0; i < length; i++) { - result[i] = yield* value.deserialize(stream); - } - return result; - } - }; - } - PROTO.Array = Array; - function Tuple(...values) { - return { - *serialize(tuple, stream) { - for (let i = 0; i < values.length; i++) { - yield* values[i].serialize(tuple[i], stream); - } - }, - *deserialize(stream) { - const result = []; - for (let i = 0; i < values.length; i++) { - result[i] = yield* values[i].deserialize(stream); - } - return result; - } - }; - } - PROTO.Tuple = Tuple; - function Optional(value) { - return { - *serialize(optional, stream) { - yield* PROTO.Boolean.serialize(optional !== undefined, stream); - if (optional !== undefined) { - yield* value.serialize(optional, stream); - } - }, - *deserialize(stream) { - const defined = yield* PROTO.Boolean.deserialize(stream); - if (defined) { - return yield* value.deserialize(stream); - } - return undefined; - } - }; - } - PROTO.Optional = Optional; - function Map(key, value) { - return { - *serialize(map, stream) { - yield* PROTO.UVarInt32.serialize(map.size, stream); - for (const [k, v] of map.entries()) { - yield* key.serialize(k, stream); - yield* value.serialize(v, stream); - } - }, - *deserialize(stream) { - const size = yield* PROTO.UVarInt32.deserialize(stream); - const result = new globalThis.Map(); - for (let i = 0; i < size; i++) { - const k = yield* key.deserialize(stream); - const v = yield* value.deserialize(stream); - result.set(k, v); - } - return result; - } - }; - } - PROTO.Map = Map; - function Set(value) { - return { - *serialize(set, stream) { - yield* PROTO.UVarInt32.serialize(set.size, stream); - for (const [_, v] of set.entries()) { - yield* value.serialize(v, stream); - } - }, - *deserialize(stream) { - const size = yield* PROTO.UVarInt32.deserialize(stream); - const result = new globalThis.Set(); - for (let i = 0; i < size; i++) { - const v = yield* value.deserialize(stream); - result.add(v); - } - return result; - } - }; - } - PROTO.Set = Set; - PROTO.Endpoint = PROTO.String; - PROTO.Header = PROTO.Object({ - guid: PROTO.String, - encoding: PROTO.String, - index: PROTO.UVarInt32, - final: PROTO.Boolean - }); -})(PROTO || (PROTO = {})); -export var NET; -(function (NET) { - const FRAG_MAX = 2048; - const ENCODING = 'mcbe-ipc:v3'; - const ENDPOINTS = new Map(); - function* serialize(byte_queue, max_size = Infinity) { - const uint8array = byte_queue.to_uint8array(); - const result = []; - let acc_str = ''; - let acc_size = 0; - for (let i = 0; i < uint8array.length; i++) { - const char_code = uint8array[i] | (uint8array[++i] << 8); - const utf16_size = char_code <= 0x7f ? 1 : char_code <= 0x7ff ? 2 : char_code <= 0xffff ? 3 : 4; - const char_size = char_code > 0xff ? utf16_size : 2; - if (acc_size + char_size > max_size) { - result.push(acc_str); - acc_str = ''; - acc_size = 0; - } - if (char_code > 0xff) { - acc_str += String.fromCharCode(char_code); - acc_size += utf16_size; - } - else { - acc_str += char_code.toString(16).padStart(2, '0').toUpperCase(); - acc_size += 2; - } - yield; - } - result.push(acc_str); - return result; - } - NET.serialize = serialize; - function* deserialize(strings) { - const result = []; - for (let i = 0; i < strings.length; i++) { - const str = strings[i]; - for (let j = 0; j < str.length; j++) { - const char_code = str.charCodeAt(j); - if (char_code <= 0xff) { - const hex = str[j] + str[++j]; - const hex_code = parseInt(hex, 16); - result.push(hex_code & 0xff); - result.push(hex_code >> 8); - } - else { - result.push(char_code & 0xff); - result.push(char_code >> 8); - } - yield; - } - yield; - } - return PROTO.ByteQueue.from_uint8array(new Uint8Array(result)); - } - NET.deserialize = deserialize; - system.afterEvents.scriptEventReceive.subscribe(event => { - system.runJob((function* () { - const [serialized_endpoint, serialized_header] = event.id.split(':'); - const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint); - const endpoint = yield* PROTO.Endpoint.deserialize(endpoint_stream); - const listeners = ENDPOINTS.get(endpoint); - if (event.sourceType === ScriptEventSource.Server && listeners) { - const header_stream = yield* PROTO.MIPS.deserialize(serialized_header); - const header = yield* PROTO.Header.deserialize(header_stream); - for (let i = 0; i < listeners.length; i++) { - yield* listeners[i](header, event.message); - } - } - })()); - }); - function create_listener(endpoint, listener) { - let listeners = ENDPOINTS.get(endpoint); - if (!listeners) { - listeners = new Array(); - ENDPOINTS.set(endpoint, listeners); - } - listeners.push(listener); - return () => { - const idx = listeners.indexOf(listener); - if (idx !== -1) - listeners.splice(idx, 1); - if (listeners.length === 0) { - ENDPOINTS.delete(endpoint); - } - }; - } - function generate_id() { - const r = (Math.random() * 0x100000000) >>> 0; - return ((r & 0xff).toString(16).padStart(2, '0') + - ((r >> 8) & 0xff).toString(16).padStart(2, '0') + - ((r >> 16) & 0xff).toString(16).padStart(2, '0') + - ((r >> 24) & 0xff).toString(16).padStart(2, '0')).toUpperCase(); - } - function* emit(endpoint, serializer, value) { - const guid = generate_id(); - const endpoint_stream = new PROTO.ByteQueue(); - yield* PROTO.Endpoint.serialize(endpoint, endpoint_stream); - const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream); - const RUN = function* (header, serialized_packet) { - const header_stream = new PROTO.ByteQueue(); - yield* PROTO.Header.serialize(header, header_stream); - const serialized_header = yield* PROTO.MIPS.serialize(header_stream); - world - .getDimension('overworld') - .runCommand(`scriptevent ${serialized_endpoint}:${serialized_header} ${serialized_packet}`); - }; - const packet_stream = new PROTO.ByteQueue(); - yield* serializer.serialize(value, packet_stream); - const serialized_packets = yield* serialize(packet_stream, FRAG_MAX); - for (let i = 0; i < serialized_packets.length; i++) { - const serialized_packet = serialized_packets[i]; - yield* RUN({ guid, encoding: ENCODING, index: i, final: i === serialized_packets.length - 1 }, serialized_packet); - } - } - NET.emit = emit; - function listen(endpoint, serializer, callback) { - const buffer = new Map(); - const listener = function* (payload, serialized_packet) { - let fragment = buffer.get(payload.guid); - if (!fragment) { - fragment = { size: -1, serialized_packets: [], data_size: 0 }; - buffer.set(payload.guid, fragment); - } - if (payload.final) { - fragment.size = payload.index + 1; - } - fragment.serialized_packets[payload.index] = serialized_packet; - fragment.data_size += payload.index + 1; - if (fragment.size !== -1 && fragment.data_size === (fragment.size * (fragment.size + 1)) / 2) { - const stream = yield* deserialize(fragment.serialized_packets); - const value = yield* serializer.deserialize(stream); - yield* callback(value); - buffer.delete(payload.guid); - } - }; - return create_listener(endpoint, listener); - } - NET.listen = listen; -})(NET || (NET = {})); -export var IPC; -(function (IPC) { - /** Sends a message with `args` to `channel` */ - function send(channel, serializer, value) { - system.runJob(NET.emit(`ipc:${channel}:send`, serializer, value)); - } - IPC.send = send; - /** Sends an `invoke` message through IPC, and expects a result asynchronously. */ - function invoke(channel, serializer, value, deserializer) { - system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value)); - return new Promise(resolve => { - const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value) { - resolve(value); - terminate(); - }); - }); - } - IPC.invoke = invoke; - /** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */ - function on(channel, deserializer, listener) { - return NET.listen(`ipc:${channel}:send`, deserializer, function* (value) { - listener(value); - }); - } - IPC.on = on; - /** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */ - function once(channel, deserializer, listener) { - const terminate = NET.listen(`ipc:${channel}:send`, deserializer, function* (value) { - listener(value); - terminate(); - }); - return terminate; - } - IPC.once = once; - /** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */ - function handle(channel, deserializer, serializer, listener) { - return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value) { - const result = listener(value); - yield* NET.emit(`ipc:${channel}:handle`, serializer, result); - }); - } - IPC.handle = handle; -})(IPC || (IPC = {})); -export default IPC; From 2a839071622eb67ffa0d555d28d87a7c492d4bc0 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 14:02:06 -0800 Subject: [PATCH 08/12] remove unused imports & stop mixing returns with and without values --- packs/BP/scripts/classes/Builder/Builders.js | 2 +- packs/BP/scripts/classes/Instance/StructureInstance.js | 4 ++-- packs/BP/scripts/classes/MenuForm.js | 8 ++++---- packs/BP/scripts/classes/Outliner.js | 2 +- packs/BP/scripts/classes/Raycaster.js | 1 - 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packs/BP/scripts/classes/Builder/Builders.js b/packs/BP/scripts/classes/Builder/Builders.js index 3d07c3d..4681854 100644 --- a/packs/BP/scripts/classes/Builder/Builders.js +++ b/packs/BP/scripts/classes/Builder/Builders.js @@ -33,7 +33,7 @@ world.beforeEvents.playerLeave.subscribe((event) => { return; Builders.onLeave(event.player.id); }); -world.afterEvents.worldLoad.subscribe((event) => { +world.afterEvents.worldLoad.subscribe(() => { for (const player of world.getAllPlayers()) { if (!player) continue; diff --git a/packs/BP/scripts/classes/Instance/StructureInstance.js b/packs/BP/scripts/classes/Instance/StructureInstance.js index 925f73e..36bd6c0 100644 --- a/packs/BP/scripts/classes/Instance/StructureInstance.js +++ b/packs/BP/scripts/classes/Instance/StructureInstance.js @@ -252,7 +252,7 @@ export class StructureInstance { disableInstanceWhenNoPlayersOnline() { // This prevents a memory leak when no players are online. - world.beforeEvents.playerLeave.subscribe(event => { + world.beforeEvents.playerLeave.subscribe(() => { system.run(() => { if (world.getAllPlayers().length === 0 && this.options.isEnabled) { console.info(`[Construct] No players are online. Disabling instance: '${this.options.instanceName}'`); @@ -261,7 +261,7 @@ export class StructureInstance { } }); }); - world.afterEvents.playerJoin.subscribe(event => { + world.afterEvents.playerJoin.subscribe(() => { if (this.shouldEnableOnJoin) { console.info(`[Construct] A player rejoined. Re-enabling instance: '${this.options.instanceName}'`); this.enable(); diff --git a/packs/BP/scripts/classes/MenuForm.js b/packs/BP/scripts/classes/MenuForm.js index 15b9198..fde296e 100644 --- a/packs/BP/scripts/classes/MenuForm.js +++ b/packs/BP/scripts/classes/MenuForm.js @@ -55,13 +55,13 @@ export class MenuForm { async createNewInstance() { return MenuFormBuilder.buildNewInstance().show(this.player).then(async (response) => { if (response.canceled) - return; + return void 0; const instanceName = response.formValues[0]; if (instanceName === '') return void 0; const structureId = await this.getStructureId(); if (!structureId) - return; + return void 0; try { structureCollection.add(instanceName, structureId); } catch (e) { @@ -82,7 +82,7 @@ export class MenuForm { async getStructureId() { return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => { if (response.canceled) - return; + return void 0; const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; return selectedStructureId || this.getOtherStructureId(); }); @@ -91,7 +91,7 @@ export class MenuForm { getOtherStructureId() { return MenuFormBuilder.buildOtherStructure().show(this.player).then((response) => { if (response.canceled) - return; + return void 0; const structureId = response.formValues[0]; if (structureId === '') return void 0; diff --git a/packs/BP/scripts/classes/Outliner.js b/packs/BP/scripts/classes/Outliner.js index 34b40c1..240adcf 100644 --- a/packs/BP/scripts/classes/Outliner.js +++ b/packs/BP/scripts/classes/Outliner.js @@ -1,4 +1,4 @@ -import { MolangVariableMap, system, TicksPerSecond, world } from "@minecraft/server"; +import { MolangVariableMap, system, TicksPerSecond } from "@minecraft/server"; import { Vector } from "../lib/Vector"; export class Outliner { diff --git a/packs/BP/scripts/classes/Raycaster.js b/packs/BP/scripts/classes/Raycaster.js index 3cc5ead..ffcf08f 100644 --- a/packs/BP/scripts/classes/Raycaster.js +++ b/packs/BP/scripts/classes/Raycaster.js @@ -1,5 +1,4 @@ import { structureCollection } from "./Structure/StructureCollection"; -import { world } from "@minecraft/server"; export class Raycaster { static STEP_SIZE = 0.2; From 162ef584061f063d149ece139fbc5fe65cc3f274 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 14:59:08 -0800 Subject: [PATCH 09/12] add preview export target --- .vscode/tasks.json | 25 +++++++++++++++++++++++++ config.json | 10 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6121569..fea918a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,6 +39,31 @@ "echo": true, "close": true } + }, + { + "label": "regolith: watch preview", + "type": "shell", + "command": "regolith watch preview", + "isBackground": true, + "problemMatcher": { + "pattern": [ + { + "regexp": ".", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": ".", + "endsPattern": "." + } + }, + "presentation": { + "echo": true, + "close": true + } } ] } \ No newline at end of file diff --git a/config.json b/config.json index 69a4ea8..efbb358 100644 --- a/config.json +++ b/config.json @@ -20,6 +20,16 @@ "bpName": "project.name+'[BP]'" }, "filters": [] + }, + "preview": { + "export": { + "build": "preview", + "readOnly": false, + "target": "development", + "rpName": "project.name+'[RP]'", + "bpName": "project.name+'[BP]'" + }, + "filters": [] } } } From d61a121307b7dccd3c5acfaae43f5edb185fa952 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 15:03:55 -0800 Subject: [PATCH 10/12] fix getting stuck after flexible moving and logging out --- packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js b/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js index ed3820a..34cf36b 100644 --- a/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js +++ b/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js @@ -52,6 +52,8 @@ export class FlexibleInstanceMove { } onFlexibleMovementTick() { + if (!this.player?.isValid) + this.finish(); const playerMovement = new PlayerMovement(this.player); const instanceVelocity = this.calculateInstanceMovement(playerMovement); this.move(instanceVelocity); @@ -105,8 +107,9 @@ export class FlexibleInstanceMove { this.instance.enable(); this.instance.flexMovingPlayerId = void 0; this.allowPlayerMovement(true); - const builder = Builders.get(this.player.id); - builder.flexibleInstanceMovement = void 0; + const builder = Builders.get(this.player?.id); + if (builder) + builder.flexibleInstanceMovement = void 0; world.beforeEvents.itemUse.unsubscribe(this.onPlayerUseItemBound); world.beforeEvents.playerLeave.unsubscribe(this.onPlayerLeaveBound); this.sendFeedback({ translate: 'construct.instance.flexibleMove.finish', with: [ From c0e5e9c72a7c1e6182770c9faf429ea271ce7f06 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 15:07:02 -0800 Subject: [PATCH 11/12] add Codacy badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 167698e..8939ace 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/ForestOfLight/Construct/total?label=Github%20downloads&logo=github)](https://github.com/ForestOfLight/Construct/releases) [![Curseforge Downloads](https://cf.way2muchnoise.eu/full_1283139_downloads.svg)](https://www.curseforge.com/minecraft-bedrock/addons/construct) [![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.0_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/c091aa1237d546c4b448a25fbc847f9a)](https://app.codacy.com/gh/ForestOfLight/Construct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm) From 2fec8c4c8bf5fbaa28c4eb380901f75d97ebf6b5 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Feb 2026 15:16:35 -0800 Subject: [PATCH 12/12] solve Codacy identified issues --- packs/BP/scripts/classes/MenuForm.js | 4 ++-- packs/BP/scripts/options/easyPlaceConversions.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packs/BP/scripts/classes/MenuForm.js b/packs/BP/scripts/classes/MenuForm.js index fde296e..dcff275 100644 --- a/packs/BP/scripts/classes/MenuForm.js +++ b/packs/BP/scripts/classes/MenuForm.js @@ -29,7 +29,7 @@ export class MenuForm { try { return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => { if (response.canceled) - return; + return void 0; let selection = response.selection; if (selection === 0) { new BuilderForm(this.player); @@ -46,7 +46,7 @@ export class MenuForm { } catch (e) { if (e.message === 'Menu timed out.') { this.player.sendMessage({ translate: 'construct.menu.open.timeout' }); - return; + return void 0; } throw e; } diff --git a/packs/BP/scripts/options/easyPlaceConversions.js b/packs/BP/scripts/options/easyPlaceConversions.js index bd4aee7..57e919f 100644 --- a/packs/BP/scripts/options/easyPlaceConversions.js +++ b/packs/BP/scripts/options/easyPlaceConversions.js @@ -41,7 +41,6 @@ export const resetToBlockStates = { cluster_count: 0, respawn_anchor_charge: 0, turtle_egg_count: 0, - cluster_count: 0, composter_fill_level: 0, end_portal_eye_bit: false };