From aae9fd2f4b63fae44ee0700c4a9549e9b62141ad Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 17:28:58 -0700 Subject: [PATCH 01/46] feat(cli): add command helpers and NotAPlayerError --- .../BP/scripts/classes/Commands/lib/commandError.js | 9 +++++++++ .../BP/scripts/classes/Commands/lib/findInstance.js | 12 ++++++++++++ .../BP/scripts/classes/Commands/lib/requirePlayer.js | 8 ++++++++ packs/BP/scripts/classes/Errors/NotAPlayerError.js | 6 ++++++ packs/RP/texts/en_US.lang | 6 +++++- packs/RP/texts/zh_CN.lang | 4 ++++ 6 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packs/BP/scripts/classes/Commands/lib/commandError.js create mode 100644 packs/BP/scripts/classes/Commands/lib/findInstance.js create mode 100644 packs/BP/scripts/classes/Commands/lib/requirePlayer.js create mode 100644 packs/BP/scripts/classes/Errors/NotAPlayerError.js diff --git a/packs/BP/scripts/classes/Commands/lib/commandError.js b/packs/BP/scripts/classes/Commands/lib/commandError.js new file mode 100644 index 0000000..e0f25f1 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/lib/commandError.js @@ -0,0 +1,9 @@ +import { CustomCommandStatus } from '@minecraft/server'; +import { NotAPlayerError } from '../../Errors/NotAPlayerError'; + +export function commandError(source, err) { + if (err instanceof NotAPlayerError) { + return { status: CustomCommandStatus.Failure, message: 'construct.commands.error.notAPlayer' }; + } + throw err; +} diff --git a/packs/BP/scripts/classes/Commands/lib/findInstance.js b/packs/BP/scripts/classes/Commands/lib/findInstance.js new file mode 100644 index 0000000..3019d33 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/lib/findInstance.js @@ -0,0 +1,12 @@ +import { system } from '@minecraft/server'; +import { structureCollection } from '../../Structure/StructureCollection'; + +export function findInstance(source, name) { + if (!structureCollection.has(name)) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.error.instanceNotFound', with: [name] }] + })); + return null; + } + return structureCollection.get(name); +} diff --git a/packs/BP/scripts/classes/Commands/lib/requirePlayer.js b/packs/BP/scripts/classes/Commands/lib/requirePlayer.js new file mode 100644 index 0000000..1e88e61 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/lib/requirePlayer.js @@ -0,0 +1,8 @@ +import { PlayerCommandOrigin } from '../PlayerCommandOrigin'; +import { NotAPlayerError } from '../../Errors/NotAPlayerError'; + +export function requirePlayer(source) { + if (!(source instanceof PlayerCommandOrigin)) + throw new NotAPlayerError(); + return source.getSource(); +} diff --git a/packs/BP/scripts/classes/Errors/NotAPlayerError.js b/packs/BP/scripts/classes/Errors/NotAPlayerError.js new file mode 100644 index 0000000..252fd9a --- /dev/null +++ b/packs/BP/scripts/classes/Errors/NotAPlayerError.js @@ -0,0 +1,6 @@ +export class NotAPlayerError extends Error { + constructor(message = 'Command requires a player source.') { + super(message); + this.name = 'NotAPlayerError'; + } +} diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index c75cf37..dde3495 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -105,4 +105,8 @@ construct.option.materialgrabber.description=Pulls structure items from inventor construct.option.materialgrabber.howto=Interact with inventories using the Material Grabber item to pull structure items from them. construct.option.materialgrabber.grabbed.zero=§7Grabbed 0 items. construct.option.materialgrabber.grabbed.one=§aGrabbed 1 item. -construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert string: number of items transferred to the player \ No newline at end of file +construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert string: number of items transferred to the player + +## CLI shared errors +construct.commands.error.instanceNotFound=§cInstance "%1" not found. +construct.commands.error.notAPlayer=§cThis command can only be used by players. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 00ac1ef..d978e39 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -106,3 +106,7 @@ construct.option.materialgrabber.howto=使用"材料收集器"物品与箱子等 construct.option.materialgrabber.grabbed.zero=§7已收集 0 个物品 construct.option.materialgrabber.grabbed.one=§a已收集 1 个物品 construct.option.materialgrabber.grabbed.many=§a已收集 %s 个物品 ## 插入字符串: 收集给玩家的物品数 + +## CLI shared errors +construct.commands.error.instanceNotFound=§cInstance "%1" not found. +construct.commands.error.notAPlayer=§cThis command can only be used by players. From 4d07e2e1ae1757fc26af2e878841b1b99da19910 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:21:32 -0700 Subject: [PATCH 02/46] refactor(cli): migrate construct command into Command pipeline and split itemUse handler --- .../classes/Instance/FlexibleInstanceMove.js | 2 +- packs/BP/scripts/classes/MenuItemHandler.js | 27 ++++++++++ packs/BP/scripts/commands/ConstructCommand.js | 38 ++++++++++++++ packs/BP/scripts/commands/construct.js | 52 ------------------- packs/BP/scripts/consts.js | 2 + packs/BP/scripts/main.js | 5 +- 6 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 packs/BP/scripts/classes/MenuItemHandler.js create mode 100644 packs/BP/scripts/commands/ConstructCommand.js delete mode 100644 packs/BP/scripts/commands/construct.js create mode 100644 packs/BP/scripts/consts.js diff --git a/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js b/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js index 34cf36b..80086de 100644 --- a/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js +++ b/packs/BP/scripts/classes/Instance/FlexibleInstanceMove.js @@ -1,6 +1,6 @@ import { InputPermissionCategory, world, system } from "@minecraft/server"; import { Outliner } from "../Outliner"; -import { MENU_ITEM } from "../../commands/construct"; +import { MENU_ITEM } from "../../consts"; import { Vector } from "../../lib/Vector"; import { PlayerMovement } from "../PlayerMovement"; import { Builders } from "../Builder/Builders"; diff --git a/packs/BP/scripts/classes/MenuItemHandler.js b/packs/BP/scripts/classes/MenuItemHandler.js new file mode 100644 index 0000000..8ed9f9f --- /dev/null +++ b/packs/BP/scripts/classes/MenuItemHandler.js @@ -0,0 +1,27 @@ +import { world, system } from '@minecraft/server'; +import { MENU_ITEM } from '../consts'; +import { MenuForm } from './MenuForm'; +import { structureCollection } from './Structure/StructureCollection'; +import { Builders } from './Builder/Builders'; + +world.beforeEvents.itemUse.subscribe((event) => { + if (!event.source || event.itemStack?.typeId !== MENU_ITEM) return; + event.cancel = true; + const builder = Builders.get(event.source.id); + system.run(() => { + if (builder.isFlexibleInstanceMoving()) + return; + openMenu(event.source, event); + }); +}); + +function openMenu(player, event = void 0) { + const options = { jumpToInstance: true }; + if (event) { + const instanceNames = structureCollection.getInstanceNames(); + const instanceName = event.itemStack?.nameTag; + if (instanceNames.includes(instanceName)) + options.instanceName = instanceName; + } + new MenuForm(player, options); +} diff --git a/packs/BP/scripts/commands/ConstructCommand.js b/packs/BP/scripts/commands/ConstructCommand.js new file mode 100644 index 0000000..04e4ba2 --- /dev/null +++ b/packs/BP/scripts/commands/ConstructCommand.js @@ -0,0 +1,38 @@ +import { CommandPermissionLevel, CustomCommandStatus, EntityComponentTypes, ItemStack, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; +import { commandError } from '../classes/Commands/lib/commandError'; +import { MENU_ITEM } from '../consts'; + +export class ConstructCommand extends Command { + constructor() { + super({ + name: 'construct', + description: 'construct.commands.construct', + permissionLevel: CommandPermissionLevel.Any, + cheatsRequired: false, + allowedSources: [PlayerCommandOrigin], + callback: (source) => this.run(source) + }); + } + + run(source) { + try { + const player = requirePlayer(source); + system.run(() => { + const remaining = player.getComponent(EntityComponentTypes.Inventory) + ?.container?.addItem(new ItemStack(MENU_ITEM)); + if (remaining) + player.sendMessage({ translate: 'construct.commands.construct.fail' }); + else + player.sendMessage({ translate: 'construct.commands.construct.success' }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const constructCommand = new ConstructCommand(); diff --git a/packs/BP/scripts/commands/construct.js b/packs/BP/scripts/commands/construct.js deleted file mode 100644 index 05b3ab8..0000000 --- a/packs/BP/scripts/commands/construct.js +++ /dev/null @@ -1,52 +0,0 @@ -import { world, system, EntityComponentTypes, ItemStack, CommandPermissionLevel, CustomCommandStatus, Player } from '@minecraft/server'; -import { MenuForm } from '../classes/MenuForm'; -import { structureCollection } from '../classes/Structure/StructureCollection' -import { Builders } from '../classes/Builder/Builders'; - -export const MENU_ITEM = 'construct:menu'; - -system.beforeEvents.startup.subscribe((event) => { - const command = { - name: 'construct:construct', - description: 'construct.commands.construct', - permissionLevel: CommandPermissionLevel.Any, - cheatsRequired: false - }; - event.customCommandRegistry.registerCommand(command, givePlayerConstructItem); -}); - -function givePlayerConstructItem(origin) { - const player = origin.sourceEntity; - if (player instanceof Player === false) - 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.construct.fail' }); - else - player.sendMessage({ translate: 'construct.commands.construct.success' }); - }); - return { status: CustomCommandStatus.Success }; -} - -world.beforeEvents.itemUse.subscribe((event) => { - if (!event.source || event.itemStack?.typeId !== MENU_ITEM) return; - event.cancel = true; - const builder = Builders.get(event.source.id); - system.run(() => { - if (builder.isFlexibleInstanceMoving()) - return; - openMenu(event.source, event); - }); -}); - -function openMenu(player, event = void 0) { - const options = { jumpToInstance: true } - if (event) { - const instanceNames = structureCollection.getInstanceNames(); - const instanceName = event.itemStack?.nameTag; - if (instanceNames.includes(instanceName)) - options.instanceName = instanceName; - } - new MenuForm(player, options); -} \ No newline at end of file diff --git a/packs/BP/scripts/consts.js b/packs/BP/scripts/consts.js new file mode 100644 index 0000000..b6094db --- /dev/null +++ b/packs/BP/scripts/consts.js @@ -0,0 +1,2 @@ +export const PACK_IDENTIFIER = 'construct'; +export const MENU_ITEM = 'construct:menu'; diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 508d3f0..ba6b6f9 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -6,8 +6,11 @@ import './options/easyPlace'; import './options/fastEasyPlace'; import './options/materialGrabber'; +// Menu item handler +import './classes/MenuItemHandler'; + // Commands -import './commands/construct'; +import './commands/ConstructCommand'; // Other import './classes/BlockInfo'; From 50ed8c3e0231dc26749ddf7d1aa39c5581f78df5 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:26:34 -0700 Subject: [PATCH 03/46] feat(cli): add construct:new command --- packs/BP/scripts/commands/NewCommand.js | 46 +++++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 6 ++++ packs/RP/texts/zh_CN.lang | 6 ++++ 4 files changed, 59 insertions(+) create mode 100644 packs/BP/scripts/commands/NewCommand.js diff --git a/packs/BP/scripts/commands/NewCommand.js b/packs/BP/scripts/commands/NewCommand.js new file mode 100644 index 0000000..977f122 --- /dev/null +++ b/packs/BP/scripts/commands/NewCommand.js @@ -0,0 +1,46 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class NewCommand extends Command { + constructor() { + super({ + name: 'new', + description: 'construct.commands.new', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'structureId', type: CustomCommandParamType.String } + ], + callback: (source, instanceName, structureId) => this.run(source, instanceName, structureId) + }); + } + + run(source, instanceName, structureId) { + try { + if (structureCollection.has(instanceName)) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.new.duplicateName', with: [instanceName] }] + })); + return { status: CustomCommandStatus.Failure }; + } + if (!structureCollection.getWorldStructureIds().includes(structureId)) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.new.unknownStructure', with: [structureId] }] + })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + structureCollection.add(instanceName, structureId); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.new.success', with: [instanceName, structureId] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const newCommand = new NewCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index ba6b6f9..d7c612e 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -11,6 +11,7 @@ import './classes/MenuItemHandler'; // Commands import './commands/ConstructCommand'; +import './commands/NewCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index dde3495..02d57ab 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -110,3 +110,9 @@ construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert str ## CLI shared errors construct.commands.error.instanceNotFound=§cInstance "%1" not found. construct.commands.error.notAPlayer=§cThis command can only be used by players. + +## construct:new +construct.commands.new=Create a new instance bound to a structure. +construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". +construct.commands.new.duplicateName=§cAn instance named "%1" already exists. +construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index d978e39..181b8d8 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -110,3 +110,9 @@ construct.option.materialgrabber.grabbed.many=§a已收集 %s 个物品 ## 插 ## CLI shared errors construct.commands.error.instanceNotFound=§cInstance "%1" not found. construct.commands.error.notAPlayer=§cThis command can only be used by players. + +## construct:new +construct.commands.new=Create a new instance bound to a structure. +construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". +construct.commands.new.duplicateName=§cAn instance named "%1" already exists. +construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. From c682d7a199ba56bc7f3a3568c3c7d71faba3b0e4 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:30:49 -0700 Subject: [PATCH 04/46] feat(cli): add construct:delete command --- packs/BP/scripts/commands/DeleteCommand.js | 36 ++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 +++ packs/RP/texts/zh_CN.lang | 4 +++ 4 files changed, 45 insertions(+) create mode 100644 packs/BP/scripts/commands/DeleteCommand.js diff --git a/packs/BP/scripts/commands/DeleteCommand.js b/packs/BP/scripts/commands/DeleteCommand.js new file mode 100644 index 0000000..7799fda --- /dev/null +++ b/packs/BP/scripts/commands/DeleteCommand.js @@ -0,0 +1,36 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class DeleteCommand extends Command { + constructor() { + super({ + name: 'delete', + description: 'construct.commands.delete', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + system.run(() => { + structureCollection.delete(instanceName); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.delete.success', with: [instanceName] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const deleteCommand = new DeleteCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index d7c612e..18e60b0 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -12,6 +12,7 @@ import './classes/MenuItemHandler'; // Commands import './commands/ConstructCommand'; import './commands/NewCommand'; +import './commands/DeleteCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 02d57ab..aee85a1 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -116,3 +116,7 @@ construct.commands.new=Create a new instance bound to a structure. construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". construct.commands.new.duplicateName=§cAn instance named "%1" already exists. construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. + +## construct:delete +construct.commands.delete=Permanently delete an instance. +construct.commands.delete.success=§aDeleted instance "%1". diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 181b8d8..246b4ed 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -116,3 +116,7 @@ construct.commands.new=Create a new instance bound to a structure. construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". construct.commands.new.duplicateName=§cAn instance named "%1" already exists. construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. + +## construct:delete +construct.commands.delete=Permanently delete an instance. +construct.commands.delete.success=§aDeleted instance "%1". From cb19a3a62f36492632cf8fd51cda41e564818cc9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:34:35 -0700 Subject: [PATCH 05/46] feat(cli): add construct:rename command --- packs/BP/scripts/commands/RenameCommand.js | 43 ++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 +++ packs/RP/texts/zh_CN.lang | 5 +++ 4 files changed, 54 insertions(+) create mode 100644 packs/BP/scripts/commands/RenameCommand.js diff --git a/packs/BP/scripts/commands/RenameCommand.js b/packs/BP/scripts/commands/RenameCommand.js new file mode 100644 index 0000000..8758760 --- /dev/null +++ b/packs/BP/scripts/commands/RenameCommand.js @@ -0,0 +1,43 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class RenameCommand extends Command { + constructor() { + super({ + name: 'rename', + description: 'construct.commands.rename', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'newName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName, newName) => this.run(source, instanceName, newName) + }); + } + + run(source, instanceName, newName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + if (structureCollection.has(newName)) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.rename.duplicateName', with: [newName] }] + })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + structureCollection.rename(instanceName, newName); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.rename.success', with: [instanceName, newName] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const renameCommand = new RenameCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 18e60b0..93e8159 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -13,6 +13,7 @@ import './classes/MenuItemHandler'; import './commands/ConstructCommand'; import './commands/NewCommand'; import './commands/DeleteCommand'; +import './commands/RenameCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index aee85a1..3647230 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -120,3 +120,8 @@ construct.commands.new.unknownStructure=§cNo structure with id "%1" found in th ## construct:delete construct.commands.delete=Permanently delete an instance. construct.commands.delete.success=§aDeleted instance "%1". + +## construct:rename +construct.commands.rename=Rename an existing instance. +construct.commands.rename.success=§aRenamed instance "%1" to "%2". +construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 246b4ed..f327f62 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -120,3 +120,8 @@ construct.commands.new.unknownStructure=§cNo structure with id "%1" found in th ## construct:delete construct.commands.delete=Permanently delete an instance. construct.commands.delete.success=§aDeleted instance "%1". + +## construct:rename +construct.commands.rename=Rename an existing instance. +construct.commands.rename.success=§aRenamed instance "%1" to "%2". +construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. From 4caa9c7473bc3c727b564e03158de70e4f6aecfd Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:38:22 -0700 Subject: [PATCH 06/46] feat(cli): add construct:list command --- packs/BP/scripts/commands/ListCommand.js | 54 ++++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 6 +++ packs/RP/texts/zh_CN.lang | 6 +++ 4 files changed, 67 insertions(+) create mode 100644 packs/BP/scripts/commands/ListCommand.js diff --git a/packs/BP/scripts/commands/ListCommand.js b/packs/BP/scripts/commands/ListCommand.js new file mode 100644 index 0000000..c6b2ab8 --- /dev/null +++ b/packs/BP/scripts/commands/ListCommand.js @@ -0,0 +1,54 @@ +import { CustomCommandStatus, system, world } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class ListCommand extends Command { + constructor() { + super({ + name: 'list', + description: 'construct.commands.list', + callback: (source) => this.run(source) + }); + } + + run(source) { + try { + const names = structureCollection.getInstanceNames(); + if (names.length === 0) { + return { status: CustomCommandStatus.Success, message: 'construct.commands.list.empty' }; + } + system.run(() => { + const rawtext = [ + { translate: 'construct.commands.list.header', with: [String(names.length)] }, + { text: '\n' } + ]; + for (const name of names) { + const instance = structureCollection.get(name); + const status = this.formatStatus(instance); + rawtext.push({ + translate: 'construct.commands.list.row', + with: [name, instance.getStructureId(), status] + }); + rawtext.push({ text: '\n' }); + } + source.sendMessage({ rawtext }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } + + formatStatus(instance) { + if (!instance.hasLocation()) + return 'no location'; + if (!instance.isEnabled()) + return 'disabled'; + const { dimensionId, location } = instance.getLocation(); + const dim = dimensionId.replace('minecraft:', ''); + return `enabled @ ${location.x} ${location.y} ${location.z} (${dim})`; + } +} + +export const listCommand = new ListCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 93e8159..6b9ff4b 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -14,6 +14,7 @@ import './commands/ConstructCommand'; import './commands/NewCommand'; import './commands/DeleteCommand'; import './commands/RenameCommand'; +import './commands/ListCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 3647230..7d36c7b 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -125,3 +125,9 @@ construct.commands.delete.success=§aDeleted instance "%1". construct.commands.rename=Rename an existing instance. construct.commands.rename.success=§aRenamed instance "%1" to "%2". construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. + +## construct:list +construct.commands.list=List all registered instances. +construct.commands.list.empty=§7No instances registered. +construct.commands.list.header=§eRegistered instances (%1):§r +construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index f327f62..9299cfc 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -125,3 +125,9 @@ construct.commands.delete.success=§aDeleted instance "%1". construct.commands.rename=Rename an existing instance. construct.commands.rename.success=§aRenamed instance "%1" to "%2". construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. + +## construct:list +construct.commands.list=List all registered instances. +construct.commands.list.empty=§7No instances registered. +construct.commands.list.header=§eRegistered instances (%1):§r +construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r From 4fe45b5a7be43f7cd81f2d9faedfd80925038d9f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:41:35 -0700 Subject: [PATCH 07/46] feat(cli): add construct:place command --- packs/BP/scripts/commands/PlaceCommand.js | 50 +++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 ++ packs/RP/texts/zh_CN.lang | 4 ++ 4 files changed, 59 insertions(+) create mode 100644 packs/BP/scripts/commands/PlaceCommand.js diff --git a/packs/BP/scripts/commands/PlaceCommand.js b/packs/BP/scripts/commands/PlaceCommand.js new file mode 100644 index 0000000..a64a61f --- /dev/null +++ b/packs/BP/scripts/commands/PlaceCommand.js @@ -0,0 +1,50 @@ +import { CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { BlockCommandOrigin } from '../classes/Commands/BlockCommandOrigin'; +import { EntityCommandOrigin } from '../classes/Commands/EntityCommandOrigin'; + +export class PlaceCommand extends Command { + constructor() { + super({ + name: 'place', + description: 'construct.commands.place', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'pos', type: CustomCommandParamType.Location } + ], + callback: (source, instanceName, pos) => this.run(source, instanceName, pos) + }); + } + + run(source, instanceName, pos) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + const dimensionId = this.resolveDimensionId(source); + system.run(() => { + instance.place(dimensionId, pos); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.place.success', + with: [instanceName, String(pos.x), String(pos.y), String(pos.z), + dimensionId.replace('minecraft:', '')] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } + + resolveDimensionId(source) { + if (source instanceof PlayerCommandOrigin || source instanceof EntityCommandOrigin) + return source.getSource().dimension.id; + if (source instanceof BlockCommandOrigin) + return source.getSource().dimension.id; + return 'minecraft:overworld'; + } +} + +export const placeCommand = new PlaceCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 6b9ff4b..7e744ca 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -15,6 +15,7 @@ import './commands/NewCommand'; import './commands/DeleteCommand'; import './commands/RenameCommand'; import './commands/ListCommand'; +import './commands/PlaceCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 7d36c7b..88b27fc 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -131,3 +131,7 @@ construct.commands.list=List all registered instances. construct.commands.list.empty=§7No instances registered. construct.commands.list.header=§eRegistered instances (%1):§r construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r + +## construct:place +construct.commands.place=Place an instance at world coordinates (enables it). +construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 9299cfc..c135825 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -131,3 +131,7 @@ construct.commands.list=List all registered instances. construct.commands.list.empty=§7No instances registered. construct.commands.list.header=§eRegistered instances (%1):§r construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r + +## construct:place +construct.commands.place=Place an instance at world coordinates (enables it). +construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. From 38a1c3ccbc4f0e38c98a77b1d92b76727fa90482 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:47:39 -0700 Subject: [PATCH 08/46] feat(cli): add construct:move command --- packs/BP/scripts/commands/MoveCommand.js | 62 ++++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 ++ packs/RP/texts/zh_CN.lang | 5 ++ 4 files changed, 73 insertions(+) create mode 100644 packs/BP/scripts/commands/MoveCommand.js diff --git a/packs/BP/scripts/commands/MoveCommand.js b/packs/BP/scripts/commands/MoveCommand.js new file mode 100644 index 0000000..455dcc9 --- /dev/null +++ b/packs/BP/scripts/commands/MoveCommand.js @@ -0,0 +1,62 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { BlockCommandOrigin } from '../classes/Commands/BlockCommandOrigin'; +import { EntityCommandOrigin } from '../classes/Commands/EntityCommandOrigin'; + +export class MoveCommand extends Command { + constructor() { + super({ + name: 'move', + description: 'construct.commands.move', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + optionalParameters: [ + { name: 'pos', type: CustomCommandParamType.Location } + ], + callback: (source, instanceName, pos) => this.run(source, instanceName, pos) + }); + } + + run(source, instanceName, pos) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + let dimensionId; + let location = pos; + if (location === undefined) { + if (!(source instanceof PlayerCommandOrigin)) + return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.posRequired' }; + const player = source.getSource(); + location = player.location; + dimensionId = player.dimension.id; + } else { + dimensionId = this.resolveDimensionId(source); + } + system.run(() => { + instance.move(dimensionId, location); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.move.success', + with: [instanceName, String(Math.floor(location.x)), String(Math.floor(location.y)), + String(Math.floor(location.z)), dimensionId.replace('minecraft:', '')] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } + + resolveDimensionId(source) { + if (source instanceof PlayerCommandOrigin || source instanceof EntityCommandOrigin) + return source.getSource().dimension.id; + if (source instanceof BlockCommandOrigin) + return source.getSource().dimension.id; + return 'minecraft:overworld'; + } +} + +export const moveCommand = new MoveCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 7e744ca..59c4141 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -16,6 +16,7 @@ import './commands/DeleteCommand'; import './commands/RenameCommand'; import './commands/ListCommand'; import './commands/PlaceCommand'; +import './commands/MoveCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 88b27fc..a9b344c 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -135,3 +135,8 @@ construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r ## construct:place construct.commands.place=Place an instance at world coordinates (enables it). construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. + +## construct:move +construct.commands.move=Reposition an instance without toggling its enabled state. +construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. +construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index c135825..a49666d 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -135,3 +135,8 @@ construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r ## construct:place construct.commands.place=Place an instance at world coordinates (enables it). construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. + +## construct:move +construct.commands.move=Reposition an instance without toggling its enabled state. +construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. +construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. From 544b5c5d95e29b4accfa6b5032594ef03098a761 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:51:11 -0700 Subject: [PATCH 09/46] feat(cli): add construct:active command --- packs/BP/scripts/commands/ActiveCommand.js | 44 ++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 +++ packs/RP/texts/zh_CN.lang | 5 +++ 4 files changed, 55 insertions(+) create mode 100644 packs/BP/scripts/commands/ActiveCommand.js diff --git a/packs/BP/scripts/commands/ActiveCommand.js b/packs/BP/scripts/commands/ActiveCommand.js new file mode 100644 index 0000000..b8a9dbb --- /dev/null +++ b/packs/BP/scripts/commands/ActiveCommand.js @@ -0,0 +1,44 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class ActiveCommand extends Command { + constructor() { + super({ + name: 'active', + description: 'construct.commands.active', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'state', type: CustomCommandParamType.Boolean } + ], + callback: (source, instanceName, state) => this.run(source, instanceName, state) + }); + } + + run(source, instanceName, state) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + if (state && !instance.hasLocation()) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.error.noLocation', with: [instanceName] }] + })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + if (state) instance.enable(); + else instance.disable(); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.active.success', + with: [instanceName, String(state)] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const activeCommand = new ActiveCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 59c4141..1b1ee76 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -17,6 +17,7 @@ import './commands/RenameCommand'; import './commands/ListCommand'; import './commands/PlaceCommand'; import './commands/MoveCommand'; +import './commands/ActiveCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index a9b344c..baa4427 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -140,3 +140,8 @@ construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. construct.commands.move=Reposition an instance without toggling its enabled state. construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. + +## construct:active +construct.commands.active=Enable or disable an instance. +construct.commands.active.success=§aSet instance "%1" active=%2. +construct.commands.error.noLocation=§cInstance "%1" has no saved location. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index a49666d..1513d74 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -140,3 +140,8 @@ construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. construct.commands.move=Reposition an instance without toggling its enabled state. construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. + +## construct:active +construct.commands.active=Enable or disable an instance. +construct.commands.active.success=§aSet instance "%1" active=%2. +construct.commands.error.noLocation=§cInstance "%1" has no saved location. From 8850b3c63bc02dca576c8d888478df6502160453 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 21:58:56 -0700 Subject: [PATCH 10/46] feat(cli): add construct:layer command --- packs/BP/scripts/commands/LayerCommand.js | 45 +++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 +++ packs/RP/texts/zh_CN.lang | 5 +++ 4 files changed, 56 insertions(+) create mode 100644 packs/BP/scripts/commands/LayerCommand.js diff --git a/packs/BP/scripts/commands/LayerCommand.js b/packs/BP/scripts/commands/LayerCommand.js new file mode 100644 index 0000000..5f0ff01 --- /dev/null +++ b/packs/BP/scripts/commands/LayerCommand.js @@ -0,0 +1,45 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class LayerCommand extends Command { + constructor() { + super({ + name: 'layer', + description: 'construct.commands.layer', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'layer', type: CustomCommandParamType.Integer } + ], + callback: (source, instanceName, layer) => this.run(source, instanceName, layer) + }); + } + + run(source, instanceName, layer) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + const max = instance.getMaxLayer(); + if (layer < 0 || layer > max) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.layer.outOfBounds', + with: [String(layer), instanceName, String(max)] }] + })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + instance.setLayer(layer); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.layer.success', + with: [instanceName, String(layer)] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const layerCommand = new LayerCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 1b1ee76..3066101 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -18,6 +18,7 @@ import './commands/ListCommand'; import './commands/PlaceCommand'; import './commands/MoveCommand'; import './commands/ActiveCommand'; +import './commands/LayerCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index baa4427..7804ce2 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -145,3 +145,8 @@ construct.commands.move.posRequired=§cMust provide coordinates when not running construct.commands.active=Enable or disable an instance. construct.commands.active.success=§aSet instance "%1" active=%2. construct.commands.error.noLocation=§cInstance "%1" has no saved location. + +## construct:layer +construct.commands.layer=Set the active layer of an instance (0 = whole structure). +construct.commands.layer.success=§aSet instance "%1" layer to %2. +construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 1513d74..19dd07e 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -145,3 +145,8 @@ construct.commands.move.posRequired=§cMust provide coordinates when not running construct.commands.active=Enable or disable an instance. construct.commands.active.success=§aSet instance "%1" active=%2. construct.commands.error.noLocation=§cInstance "%1" has no saved location. + +## construct:layer +construct.commands.layer=Set the active layer of an instance (0 = whole structure). +construct.commands.layer.success=§aSet instance "%1" layer to %2. +construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). From 469906db912f5fc171fa1a1e8b3b250129eb8382 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 19 May 2026 22:13:03 -0700 Subject: [PATCH 11/46] ignore docs dir --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d716568..1cd8c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store /build -/.regolith \ No newline at end of file +/.regolith +docs/ +.claude \ No newline at end of file From 6520082d1659ec3bf2754cb31f7f19ac5eab2210 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 02:50:05 -0700 Subject: [PATCH 12/46] feat(cli): add construct:nextlayer command --- packs/BP/scripts/commands/NextLayerCommand.js | 36 +++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 +++ packs/RP/texts/zh_CN.lang | 4 +++ 4 files changed, 45 insertions(+) create mode 100644 packs/BP/scripts/commands/NextLayerCommand.js diff --git a/packs/BP/scripts/commands/NextLayerCommand.js b/packs/BP/scripts/commands/NextLayerCommand.js new file mode 100644 index 0000000..fa4684c --- /dev/null +++ b/packs/BP/scripts/commands/NextLayerCommand.js @@ -0,0 +1,36 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class NextLayerCommand extends Command { + constructor() { + super({ + name: 'nextlayer', + description: 'construct.commands.nextlayer', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + system.run(() => { + instance.increaseLayer(); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.nextlayer.success', + with: [instanceName, String(instance.getLayer())] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const nextLayerCommand = new NextLayerCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 3066101..a7d1a42 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -19,6 +19,7 @@ import './commands/PlaceCommand'; import './commands/MoveCommand'; import './commands/ActiveCommand'; import './commands/LayerCommand'; +import './commands/NextLayerCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 7804ce2..51b3c38 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -150,3 +150,7 @@ construct.commands.error.noLocation=§cInstance "%1" has no saved location. construct.commands.layer=Set the active layer of an instance (0 = whole structure). construct.commands.layer.success=§aSet instance "%1" layer to %2. construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). + +## construct:nextlayer +construct.commands.nextlayer=Step layer up by one (wraps max to 0). +construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 19dd07e..098ec04 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -150,3 +150,7 @@ construct.commands.error.noLocation=§cInstance "%1" has no saved location. construct.commands.layer=Set the active layer of an instance (0 = whole structure). construct.commands.layer.success=§aSet instance "%1" layer to %2. construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). + +## construct:nextlayer +construct.commands.nextlayer=Step layer up by one (wraps max to 0). +construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. From 609ed4a49eba03fcd4137d9825bbce214c57ebd5 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 02:52:45 -0700 Subject: [PATCH 13/46] feat(cli): add construct:prevlayer command --- packs/BP/scripts/commands/PrevLayerCommand.js | 36 +++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 +++ packs/RP/texts/zh_CN.lang | 4 +++ 4 files changed, 45 insertions(+) create mode 100644 packs/BP/scripts/commands/PrevLayerCommand.js diff --git a/packs/BP/scripts/commands/PrevLayerCommand.js b/packs/BP/scripts/commands/PrevLayerCommand.js new file mode 100644 index 0000000..1f9d395 --- /dev/null +++ b/packs/BP/scripts/commands/PrevLayerCommand.js @@ -0,0 +1,36 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class PrevLayerCommand extends Command { + constructor() { + super({ + name: 'prevlayer', + description: 'construct.commands.prevlayer', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + system.run(() => { + instance.decreaseLayer(); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.prevlayer.success', + with: [instanceName, String(instance.getLayer())] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const prevLayerCommand = new PrevLayerCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index a7d1a42..9c10cdb 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -20,6 +20,7 @@ import './commands/MoveCommand'; import './commands/ActiveCommand'; import './commands/LayerCommand'; import './commands/NextLayerCommand'; +import './commands/PrevLayerCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 51b3c38..5cf2600 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -154,3 +154,7 @@ construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance " ## construct:nextlayer construct.commands.nextlayer=Step layer up by one (wraps max to 0). construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. + +## construct:prevlayer +construct.commands.prevlayer=Step layer down by one (wraps 0 to max). +construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 098ec04..2947e21 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -154,3 +154,7 @@ construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance " ## construct:nextlayer construct.commands.nextlayer=Step layer up by one (wraps max to 0). construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. + +## construct:prevlayer +construct.commands.prevlayer=Step layer down by one (wraps 0 to max). +construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. From 8184e17b654602e4293bb0c5b68ba1c1bd8291ce Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 02:55:30 -0700 Subject: [PATCH 14/46] feat(cli): add construct:verifier command --- packs/BP/scripts/commands/VerifierCommand.js | 37 ++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 +++ packs/RP/texts/zh_CN.lang | 4 +++ 4 files changed, 46 insertions(+) create mode 100644 packs/BP/scripts/commands/VerifierCommand.js diff --git a/packs/BP/scripts/commands/VerifierCommand.js b/packs/BP/scripts/commands/VerifierCommand.js new file mode 100644 index 0000000..00ae70d --- /dev/null +++ b/packs/BP/scripts/commands/VerifierCommand.js @@ -0,0 +1,37 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class VerifierCommand extends Command { + constructor() { + super({ + name: 'verifier', + description: 'construct.commands.verifier', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String }, + { name: 'state', type: CustomCommandParamType.Boolean } + ], + callback: (source, instanceName, state) => this.run(source, instanceName, state) + }); + } + + run(source, instanceName, state) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + system.run(() => { + instance.setVerifierEnabled(state); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.verifier.success', + with: [instanceName, String(state)] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const verifierCommand = new VerifierCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 9c10cdb..160c7f3 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -21,6 +21,7 @@ import './commands/ActiveCommand'; import './commands/LayerCommand'; import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; +import './commands/VerifierCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 5cf2600..e831205 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -158,3 +158,7 @@ construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. ## construct:prevlayer construct.commands.prevlayer=Step layer down by one (wraps 0 to max). construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. + +## construct:verifier +construct.commands.verifier=Toggle block validation overlay for an instance. +construct.commands.verifier.success=§aSet instance "%1" verifier=%2. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 2947e21..80f27d5 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -158,3 +158,7 @@ construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. ## construct:prevlayer construct.commands.prevlayer=Step layer down by one (wraps 0 to max). construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. + +## construct:verifier +construct.commands.verifier=Toggle block validation overlay for an instance. +construct.commands.verifier.success=§aSet instance "%1" verifier=%2. From 530b6b8bcd9e24f92282884cbe1e3ac8e0ca13ab Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 02:58:06 -0700 Subject: [PATCH 15/46] feat(cli): add construct:option command --- packs/BP/scripts/commands/OptionCommand.js | 47 ++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 +++ packs/RP/texts/zh_CN.lang | 5 +++ 4 files changed, 58 insertions(+) create mode 100644 packs/BP/scripts/commands/OptionCommand.js diff --git a/packs/BP/scripts/commands/OptionCommand.js b/packs/BP/scripts/commands/OptionCommand.js new file mode 100644 index 0000000..dba4ef6 --- /dev/null +++ b/packs/BP/scripts/commands/OptionCommand.js @@ -0,0 +1,47 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { BuilderOptions } from '../classes/Builder/BuilderOptions'; +import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class OptionCommand extends Command { + constructor() { + super({ + name: 'option', + description: 'construct.commands.option', + allowedSources: [PlayerCommandOrigin], + mandatoryParameters: [ + { name: 'optionId', type: CustomCommandParamType.Enum }, + { name: 'state', type: CustomCommandParamType.Boolean } + ], + enums: [ + { name: 'optionId', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] } + ], + callback: (source, optionId, state) => this.run(source, optionId, state) + }); + } + + run(source, optionId, state) { + try { + const player = requirePlayer(source); + if (!BuilderOptions.get(optionId)) { + system.run(() => source.sendMessage({ + rawtext: [{ translate: 'construct.commands.option.unknownOption', with: [optionId] }] + })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + BuilderOptions.setValue(optionId, player.id, state); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.option.success', with: [optionId, String(state)] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const optionCommand = new OptionCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 160c7f3..d3dfad0 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -22,6 +22,7 @@ import './commands/LayerCommand'; import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; import './commands/VerifierCommand'; +import './commands/OptionCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index e831205..12f4d0a 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -162,3 +162,8 @@ construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## construct:verifier construct.commands.verifier=Toggle block validation overlay for an instance. construct.commands.verifier.success=§aSet instance "%1" verifier=%2. + +## construct:option +construct.commands.option=Toggle a per-player builder option. +construct.commands.option.success=§aSet option "%1" = %2. +construct.commands.option.unknownOption=§cUnknown option "%1". diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 80f27d5..ed3e58e 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -162,3 +162,8 @@ construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## construct:verifier construct.commands.verifier=Toggle block validation overlay for an instance. construct.commands.verifier.success=§aSet instance "%1" verifier=%2. + +## construct:option +construct.commands.option=Toggle a per-player builder option. +construct.commands.option.success=§aSet option "%1" = %2. +construct.commands.option.unknownOption=§cUnknown option "%1". From b22c3e971bbeb7e8e26fd8bb0a537795f552a83a Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:01:14 -0700 Subject: [PATCH 16/46] feat(cli): add construct:info command --- packs/BP/scripts/commands/InfoCommand.js | 57 ++++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 11 +++++ packs/RP/texts/zh_CN.lang | 11 +++++ 4 files changed, 80 insertions(+) create mode 100644 packs/BP/scripts/commands/InfoCommand.js diff --git a/packs/BP/scripts/commands/InfoCommand.js b/packs/BP/scripts/commands/InfoCommand.js new file mode 100644 index 0000000..155aa4f --- /dev/null +++ b/packs/BP/scripts/commands/InfoCommand.js @@ -0,0 +1,57 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class InfoCommand extends Command { + constructor() { + super({ + name: 'info', + description: 'construct.commands.info', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + const rawtext = [ + { translate: 'construct.commands.info.header', with: [instance.getName()] }, + { text: '\n' }, + { translate: 'construct.commands.info.structure', with: [instance.getStructureId()] }, + { text: '\n' }, + { translate: 'construct.commands.info.enabled', with: [String(instance.isEnabled())] }, + { text: '\n' } + ]; + if (instance.hasLocation()) { + const { dimensionId, location } = instance.getLocation(); + rawtext.push({ translate: 'construct.commands.info.location', + with: [String(location.x), String(location.y), String(location.z), + dimensionId.replace('minecraft:', '')] }); + } else { + rawtext.push({ translate: 'construct.commands.info.noLocation' }); + } + rawtext.push({ text: '\n' }); + rawtext.push({ translate: 'construct.commands.info.layer', + with: [String(instance.getLayer()), String(instance.getMaxLayer())] }); + rawtext.push({ text: '\n' }); + rawtext.push({ translate: 'construct.commands.info.verifier', + with: [String(instance.options.verifier.isEnabled)] }); + rawtext.push({ text: '\n' }); + const bounds = instance.getBounds(); + rawtext.push({ translate: 'construct.commands.info.bounds', + with: [String(bounds.min.x), String(bounds.min.y), String(bounds.min.z), + String(bounds.max.x), String(bounds.max.y), String(bounds.max.z)] }); + system.run(() => source.sendMessage({ rawtext })); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const infoCommand = new InfoCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index d3dfad0..3c97ef5 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -23,6 +23,7 @@ import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; import './commands/VerifierCommand'; import './commands/OptionCommand'; +import './commands/InfoCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 12f4d0a..44f8892 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -167,3 +167,14 @@ construct.commands.verifier.success=§aSet instance "%1" verifier=%2. construct.commands.option=Toggle a per-player builder option. construct.commands.option.success=§aSet option "%1" = %2. construct.commands.option.unknownOption=§cUnknown option "%1". + +## construct:info +construct.commands.info=Print instance details to chat. +construct.commands.info.header=§e=== Instance "%1" ===§r +construct.commands.info.structure=§7Structure:§r %1 +construct.commands.info.enabled=§7Enabled:§r %1 +construct.commands.info.location=§7Location:§r %1 %2 %3 in %4 +construct.commands.info.noLocation=§7Location:§r (none) +construct.commands.info.layer=§7Layer:§r %1 / %2 +construct.commands.info.verifier=§7Verifier:§r %1 +construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index ed3e58e..b752943 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -167,3 +167,14 @@ construct.commands.verifier.success=§aSet instance "%1" verifier=%2. construct.commands.option=Toggle a per-player builder option. construct.commands.option.success=§aSet option "%1" = %2. construct.commands.option.unknownOption=§cUnknown option "%1". + +## construct:info +construct.commands.info=Print instance details to chat. +construct.commands.info.header=§e=== Instance "%1" ===§r +construct.commands.info.structure=§7Structure:§r %1 +construct.commands.info.enabled=§7Enabled:§r %1 +construct.commands.info.location=§7Location:§r %1 %2 %3 in %4 +construct.commands.info.noLocation=§7Location:§r (none) +construct.commands.info.layer=§7Layer:§r %1 / %2 +construct.commands.info.verifier=§7Verifier:§r %1 +construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) From 993ffcedddce718bda3f9c57ee199cd577c1ac16 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:03:40 -0700 Subject: [PATCH 17/46] feat(cli): add construct:stats command --- packs/BP/scripts/commands/StatsCommand.js | 38 +++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 4 +++ packs/RP/texts/zh_CN.lang | 4 +++ 4 files changed, 47 insertions(+) create mode 100644 packs/BP/scripts/commands/StatsCommand.js diff --git a/packs/BP/scripts/commands/StatsCommand.js b/packs/BP/scripts/commands/StatsCommand.js new file mode 100644 index 0000000..c68de26 --- /dev/null +++ b/packs/BP/scripts/commands/StatsCommand.js @@ -0,0 +1,38 @@ +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { commandError } from '../classes/Commands/lib/commandError'; +import { InstanceFormBuilder } from '../classes/Instance/InstanceFormBuilder'; + +export class StatsCommand extends Command { + constructor() { + super({ + name: 'stats', + description: 'construct.commands.stats', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + system.run(async () => { + try { + const { stats } = await InstanceFormBuilder.buildStatistics(instance); + source.sendMessage(stats); + } catch (err) { + source.sendMessage({ translate: 'construct.commands.stats.alreadyRunning' }); + } + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const statsCommand = new StatsCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 3c97ef5..cee0a1e 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -24,6 +24,7 @@ import './commands/PrevLayerCommand'; import './commands/VerifierCommand'; import './commands/OptionCommand'; import './commands/InfoCommand'; +import './commands/StatsCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 44f8892..3db5aa0 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -178,3 +178,7 @@ construct.commands.info.noLocation=§7Location:§r (none) construct.commands.info.layer=§7Layer:§r %1 / %2 construct.commands.info.verifier=§7Verifier:§r %1 construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) + +## construct:stats +construct.commands.stats=Run the structure verifier and print statistics. +construct.commands.stats.alreadyRunning=§cA verification is already in progress. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index b752943..4af099d 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -178,3 +178,7 @@ construct.commands.info.noLocation=§7Location:§r (none) construct.commands.info.layer=§7Layer:§r %1 / %2 construct.commands.info.verifier=§7Verifier:§r %1 construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) + +## construct:stats +construct.commands.stats=Run the structure verifier and print statistics. +construct.commands.stats.alreadyRunning=§cA verification is already in progress. From 24063838495b40270a4da41608ddce623c0ae8be Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:12:20 -0700 Subject: [PATCH 18/46] feat(cli): add construct:materials command --- packs/BP/scripts/commands/MaterialsCommand.js | 59 +++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 6 ++ packs/RP/texts/zh_CN.lang | 6 ++ 4 files changed, 72 insertions(+) create mode 100644 packs/BP/scripts/commands/MaterialsCommand.js diff --git a/packs/BP/scripts/commands/MaterialsCommand.js b/packs/BP/scripts/commands/MaterialsCommand.js new file mode 100644 index 0000000..2638e56 --- /dev/null +++ b/packs/BP/scripts/commands/MaterialsCommand.js @@ -0,0 +1,59 @@ +import { CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; +import { commandError } from '../classes/Commands/lib/commandError'; + +export class MaterialsCommand extends Command { + constructor() { + super({ + name: 'materials', + description: 'construct.commands.materials', + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + optionalParameters: [ + { name: 'missing', type: CustomCommandParamType.Boolean } + ], + callback: (source, instanceName, missing) => this.run(source, instanceName, missing) + }); + } + + run(source, instanceName, missing) { + try { + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + const onlyMissing = missing === true; + let container; + let headerKey; + if (onlyMissing) { + const player = requirePlayer(source); + container = player.getComponent(EntityComponentTypes.Inventory)?.container; + headerKey = 'construct.commands.materials.headerMissing'; + } else { + headerKey = 'construct.commands.materials.headerAll'; + } + system.run(() => { + const materials = instance.getActiveMaterials(); + const materialsMap = onlyMissing + ? materials.getMaterialsDifference(container) + : undefined; + const list = materials.formatString(materialsMap); + const rawtext = [ + { translate: headerKey, with: [instanceName] }, + { text: '\n' } + ]; + if (!list.rawtext || list.rawtext.length === 0) + rawtext.push({ translate: 'construct.commands.materials.empty' }); + else + rawtext.push(list); + source.sendMessage({ rawtext }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const materialsCommand = new MaterialsCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index cee0a1e..b8a94bb 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -25,6 +25,7 @@ import './commands/VerifierCommand'; import './commands/OptionCommand'; import './commands/InfoCommand'; import './commands/StatsCommand'; +import './commands/MaterialsCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 3db5aa0..389655a 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -182,3 +182,9 @@ construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) ## construct:stats construct.commands.stats=Run the structure verifier and print statistics. construct.commands.stats.alreadyRunning=§cA verification is already in progress. + +## construct:materials +construct.commands.materials=Print the material list for an instance. +construct.commands.materials.headerAll=§eMaterials for "%1":§r +construct.commands.materials.headerMissing=§eMissing materials for "%1":§r +construct.commands.materials.empty=§7(no materials) diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 4af099d..1c009c7 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -182,3 +182,9 @@ construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) ## construct:stats construct.commands.stats=Run the structure verifier and print statistics. construct.commands.stats.alreadyRunning=§cA verification is already in progress. + +## construct:materials +construct.commands.materials=Print the material list for an instance. +construct.commands.materials.headerAll=§eMaterials for "%1":§r +construct.commands.materials.headerMissing=§eMissing materials for "%1":§r +construct.commands.materials.empty=§7(no materials) From abaf275c5d8a53bbcb0863215687b51ddafdc214 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:15:14 -0700 Subject: [PATCH 19/46] feat(cli): add construct:tag command --- packs/BP/scripts/commands/TagCommand.js | 47 +++++++++++++++++++++++++ packs/BP/scripts/main.js | 1 + packs/RP/texts/en_US.lang | 5 +++ packs/RP/texts/zh_CN.lang | 5 +++ 4 files changed, 58 insertions(+) create mode 100644 packs/BP/scripts/commands/TagCommand.js diff --git a/packs/BP/scripts/commands/TagCommand.js b/packs/BP/scripts/commands/TagCommand.js new file mode 100644 index 0000000..18b076a --- /dev/null +++ b/packs/BP/scripts/commands/TagCommand.js @@ -0,0 +1,47 @@ +import { CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server'; +import { Command } from '../classes/Commands/Command'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { findInstance } from '../classes/Commands/lib/findInstance'; +import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; +import { commandError } from '../classes/Commands/lib/commandError'; +import { MENU_ITEM } from '../consts'; + +export class TagCommand extends Command { + constructor() { + super({ + name: 'tag', + description: 'construct.commands.tag', + allowedSources: [PlayerCommandOrigin], + mandatoryParameters: [ + { name: 'instanceName', type: CustomCommandParamType.String } + ], + callback: (source, instanceName) => this.run(source, instanceName) + }); + } + + run(source, instanceName) { + try { + const player = requirePlayer(source); + const instance = findInstance(source, instanceName); + if (!instance) return { status: CustomCommandStatus.Failure }; + const equipment = player.getComponent(EntityComponentTypes.Equippable); + const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand); + if (itemStack?.typeId !== MENU_ITEM) { + system.run(() => source.sendMessage({ translate: 'construct.commands.tag.notHoldingItem' })); + return { status: CustomCommandStatus.Failure }; + } + system.run(() => { + itemStack.nameTag = instanceName; + equipment.setEquipment(EquipmentSlot.Mainhand, itemStack); + source.sendMessage({ + rawtext: [{ translate: 'construct.commands.tag.success', with: [instanceName] }] + }); + }); + return { status: CustomCommandStatus.Success }; + } catch (err) { + return commandError(source, err); + } + } +} + +export const tagCommand = new TagCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index b8a94bb..b17ac0b 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -26,6 +26,7 @@ import './commands/OptionCommand'; import './commands/InfoCommand'; import './commands/StatsCommand'; import './commands/MaterialsCommand'; +import './commands/TagCommand'; // Other import './classes/BlockInfo'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 389655a..ba04508 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -188,3 +188,8 @@ construct.commands.materials=Print the material list for an instance. construct.commands.materials.headerAll=§eMaterials for "%1":§r construct.commands.materials.headerMissing=§eMissing materials for "%1":§r construct.commands.materials.empty=§7(no materials) + +## construct:tag +construct.commands.tag=Rename the held Construct item to an instance name for quick-open. +construct.commands.tag.success=§aTagged held Construct item with instance "%1". +construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 1c009c7..0447bef 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -188,3 +188,8 @@ construct.commands.materials=Print the material list for an instance. construct.commands.materials.headerAll=§eMaterials for "%1":§r construct.commands.materials.headerMissing=§eMissing materials for "%1":§r construct.commands.materials.empty=§7(no materials) + +## construct:tag +construct.commands.tag=Rename the held Construct item to an instance name for quick-open. +construct.commands.tag.success=§aTagged held Construct item with instance "%1". +construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item. From 19cbceb1207396093cabc9e7d2bbe39d4cd914c6 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:26:25 -0700 Subject: [PATCH 20/46] feat(cli): add Command pipeline base classes The Command base class, Commands registry, FeedbackMessageType enum, and four CommandOrigin subclasses (Player/Entity/Block/Server) that every CLI command in this branch depends on. Without these files the new commands fail to resolve imports at module load. Co-Authored-By: Claude Opus 4.7 --- .../classes/Commands/BlockCommandOrigin.js | 12 ++ packs/BP/scripts/classes/Commands/Command.js | 108 ++++++++++++++++++ .../scripts/classes/Commands/CommandOrigin.js | 20 ++++ packs/BP/scripts/classes/Commands/Commands.js | 15 +++ .../classes/Commands/EntityCommandOrigin.js | 13 +++ .../classes/Commands/FeedbackMessageType.js | 7 ++ .../classes/Commands/PlayerCommandOrigin.js | 17 +++ .../classes/Commands/ServerCommandOrigin.js | 13 +++ 8 files changed, 205 insertions(+) create mode 100644 packs/BP/scripts/classes/Commands/BlockCommandOrigin.js create mode 100644 packs/BP/scripts/classes/Commands/Command.js create mode 100644 packs/BP/scripts/classes/Commands/CommandOrigin.js create mode 100644 packs/BP/scripts/classes/Commands/Commands.js create mode 100644 packs/BP/scripts/classes/Commands/EntityCommandOrigin.js create mode 100644 packs/BP/scripts/classes/Commands/FeedbackMessageType.js create mode 100644 packs/BP/scripts/classes/Commands/PlayerCommandOrigin.js create mode 100644 packs/BP/scripts/classes/Commands/ServerCommandOrigin.js diff --git a/packs/BP/scripts/classes/Commands/BlockCommandOrigin.js b/packs/BP/scripts/classes/Commands/BlockCommandOrigin.js new file mode 100644 index 0000000..33814bd --- /dev/null +++ b/packs/BP/scripts/classes/Commands/BlockCommandOrigin.js @@ -0,0 +1,12 @@ +import { CommandOrigin } from "./CommandOrigin"; +import { FeedbackMessageType } from "./FeedbackMessageType"; + +export class BlockCommandOrigin extends CommandOrigin { + getSource() { + return this.source.sourceBlock; + } + + sendMessage() { + return FeedbackMessageType.None; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/Command.js b/packs/BP/scripts/classes/Commands/Command.js new file mode 100644 index 0000000..b774650 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/Command.js @@ -0,0 +1,108 @@ +import { CustomCommandSource, CustomCommandStatus, Player, system } from "@minecraft/server"; +import { Commands } from "./Commands.js"; +import { BlockCommandOrigin } from "./BlockCommandOrigin"; +import { EntityCommandOrigin } from "./EntityCommandOrigin"; +import { ServerCommandOrigin } from "./ServerCommandOrigin"; +import { PlayerCommandOrigin } from "./PlayerCommandOrigin"; +import { PACK_IDENTIFIER } from "../../consts.js"; + +export class Command { + customCommand; + + static resolveCommandOrigin(origin) { + switch (origin.sourceType) { + case CustomCommandSource.Block: + return new BlockCommandOrigin(origin); + case CustomCommandSource.Entity: + if (origin.sourceEntity instanceof Player) + return new PlayerCommandOrigin(origin); + return new EntityCommandOrigin(origin); + case CustomCommandSource.Server: + return new ServerCommandOrigin(origin); + default: + throw new Error("Unknown command source: " + origin?.sourceType); + } + } + + constructor(customCommand) { + this.customCommand = customCommand; + this.#setDefaultArgs(); + Commands.register(this); + system.beforeEvents.startup.subscribe(this.setupForRegistry.bind(this)); + } + + getName() { + return this.customCommand.name.replace(/^[^:]+:/, ''); + } + + isCheatsRequired() { + return this.customCommand.cheatsRequired; + } + + setupForRegistry(startupEvent) { + this.#registerCommand(startupEvent.customCommandRegistry); + system.beforeEvents.startup.unsubscribe(this.setupForRegistry.bind(this)); + } + + #registerCommand(customCommandRegistry) { + this.#addPreCallback(); + this.#registerEnums(customCommandRegistry); + this.#registerSingleCommand(customCommandRegistry); + this.#registerAliasCommands(customCommandRegistry); + } + + #setDefaultArgs() { + if (this.customCommand.cheatsRequired === void 0) + this.customCommand.cheatsRequired = false; + } + + #addPreCallback() { + this.callback = (origin, ...args) => { + const source = Command.resolveCommandOrigin(origin); + if (this.#commandSourceIsNotAllowed(source)) + return { status: CustomCommandStatus.Failure, message: 'commands.generic.invalidsource' }; + return this.customCommand.callback(source, ...args); + } + } + + #registerEnums(customCommandRegistry) { + if (this.customCommand.enums) { + for (const customEnum of this.customCommand.enums) + customCommandRegistry.registerEnum(`${PACK_IDENTIFIER}:${customEnum.name}`, customEnum.values); + } + } + + #registerSingleCommand(customCommandRegistry, name = this.customCommand.name) { + customCommandRegistry.registerCommand({ + name: `${PACK_IDENTIFIER}:${name}`, + description: this.customCommand.description, + permissionLevel: this.customCommand.permissionLevel, + mandatoryParameters: this.#prepParameters(this.customCommand.mandatoryParameters), + optionalParameters: this.#prepParameters(this.customCommand.optionalParameters), + cheatsRequired: this.customCommand.cheatsRequired + }, this.callback); + } + + #prepParameters(parameters) { + if (!parameters) + return []; + for (const parameter of parameters) { + if (parameter.name) + parameter.name = `${PACK_IDENTIFIER}:${parameter.name}`; + } + return parameters; + } + + #registerAliasCommands(customCommandRegistry) { + if (this.customCommand.aliases) { + for (const alias of this.customCommand.aliases) + this.#registerSingleCommand(customCommandRegistry, alias); + } + } + + #commandSourceIsNotAllowed(source) { + if (!this.customCommand.allowedSources) + return false; + return !this.customCommand.allowedSources.includes(source.constructor); + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/CommandOrigin.js b/packs/BP/scripts/classes/Commands/CommandOrigin.js new file mode 100644 index 0000000..140bffc --- /dev/null +++ b/packs/BP/scripts/classes/Commands/CommandOrigin.js @@ -0,0 +1,20 @@ +import { FeedbackMessageType } from "./FeedbackMessageType"; + +export class CommandOrigin { + constructor(source) { + this.source = source; + } + + getType() { + return this.source.sourceType; + } + + getSource() { + throw new Error("getSource() not implemented"); + } + + sendMessage(message) { + console.error(`Unknown source type: ${this.source.sourceType}`, message); + return FeedbackMessageType.ConsoleError; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/Commands.js b/packs/BP/scripts/classes/Commands/Commands.js new file mode 100644 index 0000000..4f38e54 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/Commands.js @@ -0,0 +1,15 @@ +export class Commands { + static #commands = []; + + static register(command) { + this.#commands.push(command); + } + + static getAll() { + return [...this.#commands]; + } + + static clear() { + this.#commands = []; + } +} diff --git a/packs/BP/scripts/classes/Commands/EntityCommandOrigin.js b/packs/BP/scripts/classes/Commands/EntityCommandOrigin.js new file mode 100644 index 0000000..44f03b7 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/EntityCommandOrigin.js @@ -0,0 +1,13 @@ +import { CommandOrigin } from "./CommandOrigin"; +import { FeedbackMessageType } from "./FeedbackMessageType"; + +export class EntityCommandOrigin extends CommandOrigin { + getSource() { + return this.source.sourceEntity; + } + + sendMessage(message) { + this.getSource().sendMessage(message); + return FeedbackMessageType.ChatMessage; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/FeedbackMessageType.js b/packs/BP/scripts/classes/Commands/FeedbackMessageType.js new file mode 100644 index 0000000..df683b6 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/FeedbackMessageType.js @@ -0,0 +1,7 @@ +export const FeedbackMessageType = Object.freeze({ + None: "none", + ConsoleInfo: "info", + ConsoleWarn: "warn", + ConsoleError: "error", + ChatMessage: "message" +}); \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/PlayerCommandOrigin.js b/packs/BP/scripts/classes/Commands/PlayerCommandOrigin.js new file mode 100644 index 0000000..f533ba5 --- /dev/null +++ b/packs/BP/scripts/classes/Commands/PlayerCommandOrigin.js @@ -0,0 +1,17 @@ +import { CommandOrigin } from "./CommandOrigin"; +import { FeedbackMessageType } from "./FeedbackMessageType"; + +export class PlayerCommandOrigin extends CommandOrigin { + getType() { + return "Player"; + } + + getSource() { + return this.source.sourceEntity; + } + + sendMessage(message) { + this.getSource().sendMessage(message); + return FeedbackMessageType.ChatMessage; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Commands/ServerCommandOrigin.js b/packs/BP/scripts/classes/Commands/ServerCommandOrigin.js new file mode 100644 index 0000000..b6f322a --- /dev/null +++ b/packs/BP/scripts/classes/Commands/ServerCommandOrigin.js @@ -0,0 +1,13 @@ +import { CommandOrigin } from "./CommandOrigin"; +import { FeedbackMessageType } from "./FeedbackMessageType"; + +export class ServerCommandOrigin extends CommandOrigin { + getSource() { + return this.source.sourceType; + } + + sendMessage(message) { + console.log(message); + return FeedbackMessageType.ConsoleInfo; + } +} \ No newline at end of file From b1370b5d2839c2834d6185810f836ff6bc758930 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 03:26:58 -0700 Subject: [PATCH 21/46] chore(cli): drop unused imports and orphan lang key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused `world` import from ListCommand.js and PlaceCommand.js (plan code blocks included it but neither command uses it). - Delete orphaned `construct.commands.construct.denyorigin` lang key from en_US and zh_CN — its sole consumer was the original commands/construct.js, which was replaced by ConstructCommand using the shared `construct.commands.error.notAPlayer` key. - Carry the previously-orphaned Chinese translation forward to the notAPlayer key in zh_CN.lang. Co-Authored-By: Claude Opus 4.7 --- packs/BP/scripts/commands/ListCommand.js | 2 +- packs/BP/scripts/commands/PlaceCommand.js | 2 +- packs/RP/texts/en_US.lang | 1 - packs/RP/texts/zh_CN.lang | 3 +-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packs/BP/scripts/commands/ListCommand.js b/packs/BP/scripts/commands/ListCommand.js index c6b2ab8..bdd1dc9 100644 --- a/packs/BP/scripts/commands/ListCommand.js +++ b/packs/BP/scripts/commands/ListCommand.js @@ -1,4 +1,4 @@ -import { CustomCommandStatus, system, world } from '@minecraft/server'; +import { CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; import { structureCollection } from '../classes/Structure/StructureCollection'; import { commandError } from '../classes/Commands/lib/commandError'; diff --git a/packs/BP/scripts/commands/PlaceCommand.js b/packs/BP/scripts/commands/PlaceCommand.js index a64a61f..2f9cb34 100644 --- a/packs/BP/scripts/commands/PlaceCommand.js +++ b/packs/BP/scripts/commands/PlaceCommand.js @@ -1,4 +1,4 @@ -import { CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server'; +import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; import { findInstance } from '../classes/Commands/lib/findInstance'; import { commandError } from '../classes/Commands/lib/commandError'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index ba04508..27c0b17 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -86,7 +86,6 @@ construct.mainmenu.howto.remove.body=§7- Use the §f/structure delete§7 comman ## Commands 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. diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 0447bef..273c530 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -86,7 +86,6 @@ construct.mainmenu.howto.remove.body=§7-使用§f/structure delete§7命令从 ## Commands construct.commands.construct=获得投影构筑菜单物品,可以使用它打开投影构筑菜单 -construct.commands.construct.denyorigin=此命令只能由玩家使用 construct.commands.construct.fail=§c给予投影构筑菜单物品时发生错误 construct.commands.construct.success=§a您已获得投影构筑菜单物品, 右键(长按)使用它以打开投影构筑菜单 @@ -109,7 +108,7 @@ construct.option.materialgrabber.grabbed.many=§a已收集 %s 个物品 ## 插 ## CLI shared errors construct.commands.error.instanceNotFound=§cInstance "%1" not found. -construct.commands.error.notAPlayer=§cThis command can only be used by players. +construct.commands.error.notAPlayer=§c此命令只能由玩家使用 ## construct:new construct.commands.new=Create a new instance bound to a structure. From 7ea64d1e6aa19eb9d66ee0c11133b0502be9f33c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:08:03 -0700 Subject: [PATCH 22/46] feat: rework commands and fix bugs --- .gitignore | 3 +- package-lock.json | 36 +++++++ package.json | 5 + packs/BP/scripts/classes/Commands/Command.js | 15 ++- .../classes/Commands/lib/commandError.js | 9 -- .../classes/Commands/lib/findInstance.js | 12 --- .../classes/Commands/lib/requirePlayer.js | 8 -- .../classes/Errors/CommandResponseError.js | 14 +++ .../classes/Errors/InstanceExistsError.js | 15 +++ .../classes/Errors/InstanceNotFoundError.js | 15 +++ .../classes/Errors/InvalidInstanceError.js | 6 -- .../classes/Errors/InvalidStructureError.js | 6 -- .../scripts/classes/Errors/NotAPlayerError.js | 8 +- .../classes/Errors/StructureNotFoundError.js | 15 +++ .../classes/Instance/InstanceOptions.js | 1 + .../classes/Materials/StructureMaterials.js | 7 +- packs/BP/scripts/classes/MenuForm.js | 20 ++-- .../classes/Render/StructureOutliner.js | 7 +- .../BP/scripts/classes/Structure/Structure.js | 4 +- .../classes/Structure/StructureCollection.js | 20 ++-- packs/BP/scripts/commands/ActiveCommand.js | 42 ++++----- packs/BP/scripts/commands/ConstructCommand.js | 33 ++++--- packs/BP/scripts/commands/DeleteCommand.js | 24 ++--- packs/BP/scripts/commands/InfoCommand.js | 93 +++++++++++-------- packs/BP/scripts/commands/LayerCommand.js | 35 +++---- packs/BP/scripts/commands/ListCommand.js | 62 ++++++------- packs/BP/scripts/commands/MaterialsCommand.js | 73 ++++++++------- packs/BP/scripts/commands/MoveCommand.js | 63 +++++-------- packs/BP/scripts/commands/NewCommand.js | 54 ++++++----- packs/BP/scripts/commands/NextLayerCommand.js | 24 ++--- packs/BP/scripts/commands/OptionCommand.js | 31 +++---- packs/BP/scripts/commands/PlaceCommand.js | 53 +++++------ packs/BP/scripts/commands/PrevLayerCommand.js | 24 ++--- packs/BP/scripts/commands/RenameCommand.js | 30 ++---- packs/BP/scripts/commands/StatsCommand.js | 40 ++++---- packs/BP/scripts/commands/TagCommand.js | 40 +++----- packs/BP/scripts/commands/VerifierCommand.js | 31 +++---- packs/RP/texts/en_US.lang | 74 ++++++++------- 38 files changed, 527 insertions(+), 525 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 packs/BP/scripts/classes/Commands/lib/commandError.js delete mode 100644 packs/BP/scripts/classes/Commands/lib/findInstance.js delete mode 100644 packs/BP/scripts/classes/Commands/lib/requirePlayer.js create mode 100644 packs/BP/scripts/classes/Errors/CommandResponseError.js create mode 100644 packs/BP/scripts/classes/Errors/InstanceExistsError.js create mode 100644 packs/BP/scripts/classes/Errors/InstanceNotFoundError.js delete mode 100644 packs/BP/scripts/classes/Errors/InvalidInstanceError.js delete mode 100644 packs/BP/scripts/classes/Errors/InvalidStructureError.js create mode 100644 packs/BP/scripts/classes/Errors/StructureNotFoundError.js diff --git a/.gitignore b/.gitignore index 1cd8c8e..0a8c094 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /build /.regolith docs/ -.claude \ No newline at end of file +.claude +node_modules/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..202f6ac --- /dev/null +++ b/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "Construct", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@minecraft/server": "^2.8.0-beta.1.26.21-stable" + } + }, + "node_modules/@minecraft/common": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.3.0.tgz", + "integrity": "sha512-GLT8USFhvEyeTTFHZAgszbrnoT007hmXmK+aO4l+2A1up9/zwZ+4e8R8F0KcKrCWDjLEqkOJtPow0hQCcNJ++Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@minecraft/server": { + "version": "2.8.0-beta.1.26.21-stable", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.8.0-beta.1.26.21-stable.tgz", + "integrity": "sha512-HdR2EjmleJBBuaXH4iEobdXh7dcuWtV8AIZn2Kd+UkvlBPc4iIPSqXaEt7nqOk0tyhbKfRs/X3aoVtAMHW+kmQ==", + "license": "MIT", + "peerDependencies": { + "@minecraft/common": "^1.2.0", + "@minecraft/vanilla-data": ">=1.20.70" + } + }, + "node_modules/@minecraft/vanilla-data": { + "version": "1.26.21", + "resolved": "https://registry.npmjs.org/@minecraft/vanilla-data/-/vanilla-data-1.26.21.tgz", + "integrity": "sha512-bDmqSIjZBoaChpAdK2H3SVzhIod4/kXwY+viEA3AgftAbda1X1dkjMiBbxZcmwzRrVwT9embIB3zsEkYn1rSNA==", + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..36f01d7 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@minecraft/server": "^2.8.0-beta.1.26.21-stable" + } +} diff --git a/packs/BP/scripts/classes/Commands/Command.js b/packs/BP/scripts/classes/Commands/Command.js index b774650..84b77dd 100644 --- a/packs/BP/scripts/classes/Commands/Command.js +++ b/packs/BP/scripts/classes/Commands/Command.js @@ -1,4 +1,4 @@ -import { CustomCommandSource, CustomCommandStatus, Player, system } from "@minecraft/server"; +import { CustomCommandParamType, CustomCommandSource, CustomCommandStatus, Player, RawMessageError, system } from "@minecraft/server"; import { Commands } from "./Commands.js"; import { BlockCommandOrigin } from "./BlockCommandOrigin"; import { EntityCommandOrigin } from "./EntityCommandOrigin"; @@ -60,8 +60,15 @@ export class Command { this.callback = (origin, ...args) => { const source = Command.resolveCommandOrigin(origin); if (this.#commandSourceIsNotAllowed(source)) - return { status: CustomCommandStatus.Failure, message: 'commands.generic.invalidsource' }; - return this.customCommand.callback(source, ...args); + return { status: CustomCommandStatus.Failure, message: 'construct.error.invalidCommandSource' }; + try { + return this.customCommand.callback(source, ...args); + } catch (error) { + if (error instanceof RawMessageError) + error.sendTo(source); + else + throw error; + } } } @@ -88,6 +95,8 @@ export class Command { return []; for (const parameter of parameters) { if (parameter.name) + parameter.name = `${parameter.name}`; + if (parameter.type === CustomCommandParamType.Enum) parameter.name = `${PACK_IDENTIFIER}:${parameter.name}`; } return parameters; diff --git a/packs/BP/scripts/classes/Commands/lib/commandError.js b/packs/BP/scripts/classes/Commands/lib/commandError.js deleted file mode 100644 index e0f25f1..0000000 --- a/packs/BP/scripts/classes/Commands/lib/commandError.js +++ /dev/null @@ -1,9 +0,0 @@ -import { CustomCommandStatus } from '@minecraft/server'; -import { NotAPlayerError } from '../../Errors/NotAPlayerError'; - -export function commandError(source, err) { - if (err instanceof NotAPlayerError) { - return { status: CustomCommandStatus.Failure, message: 'construct.commands.error.notAPlayer' }; - } - throw err; -} diff --git a/packs/BP/scripts/classes/Commands/lib/findInstance.js b/packs/BP/scripts/classes/Commands/lib/findInstance.js deleted file mode 100644 index 3019d33..0000000 --- a/packs/BP/scripts/classes/Commands/lib/findInstance.js +++ /dev/null @@ -1,12 +0,0 @@ -import { system } from '@minecraft/server'; -import { structureCollection } from '../../Structure/StructureCollection'; - -export function findInstance(source, name) { - if (!structureCollection.has(name)) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.error.instanceNotFound', with: [name] }] - })); - return null; - } - return structureCollection.get(name); -} diff --git a/packs/BP/scripts/classes/Commands/lib/requirePlayer.js b/packs/BP/scripts/classes/Commands/lib/requirePlayer.js deleted file mode 100644 index 1e88e61..0000000 --- a/packs/BP/scripts/classes/Commands/lib/requirePlayer.js +++ /dev/null @@ -1,8 +0,0 @@ -import { PlayerCommandOrigin } from '../PlayerCommandOrigin'; -import { NotAPlayerError } from '../../Errors/NotAPlayerError'; - -export function requirePlayer(source) { - if (!(source instanceof PlayerCommandOrigin)) - throw new NotAPlayerError(); - return source.getSource(); -} diff --git a/packs/BP/scripts/classes/Errors/CommandResponseError.js b/packs/BP/scripts/classes/Errors/CommandResponseError.js new file mode 100644 index 0000000..523ccb9 --- /dev/null +++ b/packs/BP/scripts/classes/Errors/CommandResponseError.js @@ -0,0 +1,14 @@ +export class CommandResponseError extends Error { + constructor(message) { + super(message); + this.name = 'CommandResponseError'; + } + + getRawMessage() { + throw new Error('getRawMessage() must be implemented by subclasses of CommandResponseError'); + } + + sendTo(source) { + source.sendMessage(this.getRawMessage()); + } +} diff --git a/packs/BP/scripts/classes/Errors/InstanceExistsError.js b/packs/BP/scripts/classes/Errors/InstanceExistsError.js new file mode 100644 index 0000000..379912d --- /dev/null +++ b/packs/BP/scripts/classes/Errors/InstanceExistsError.js @@ -0,0 +1,15 @@ +import { CommandResponseError } from "./CommandResponseError"; + +export class InstanceExistsError extends CommandResponseError { + instanceName; + + constructor(instanceName) { + super(`An instance with the name "${instanceName}" already exists.`); + this.name = 'InstanceExistsError'; + this.instanceName = instanceName; + } + + getRawMessage() { + return { translate: 'construct.error.instanceExists', with: [this.instanceName] }; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Errors/InstanceNotFoundError.js b/packs/BP/scripts/classes/Errors/InstanceNotFoundError.js new file mode 100644 index 0000000..57e7950 --- /dev/null +++ b/packs/BP/scripts/classes/Errors/InstanceNotFoundError.js @@ -0,0 +1,15 @@ +import { CommandResponseError } from "./CommandResponseError"; + +export class InstanceNotFoundError extends CommandResponseError { + instanceName; + + constructor(instanceName) { + super(`§cInstance "${instanceName}" not found.`); + this.name = 'InstanceNotFoundError'; + this.instanceName = instanceName; + } + + getRawMessage() { + return { translate: 'construct.error.instanceNotFound', with: [this.instanceName] }; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Errors/InvalidInstanceError.js b/packs/BP/scripts/classes/Errors/InvalidInstanceError.js deleted file mode 100644 index 612e2dc..0000000 --- a/packs/BP/scripts/classes/Errors/InvalidInstanceError.js +++ /dev/null @@ -1,6 +0,0 @@ -export class InvalidInstanceError extends Error { - constructor(message) { - super(message); - this.name = 'InvalidInstanceError'; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Errors/InvalidStructureError.js b/packs/BP/scripts/classes/Errors/InvalidStructureError.js deleted file mode 100644 index 97e88b8..0000000 --- a/packs/BP/scripts/classes/Errors/InvalidStructureError.js +++ /dev/null @@ -1,6 +0,0 @@ -export class InvalidStructureError extends Error { - constructor(message) { - super(message); - this.name = 'InvalidStructureError'; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Errors/NotAPlayerError.js b/packs/BP/scripts/classes/Errors/NotAPlayerError.js index 252fd9a..836bb7f 100644 --- a/packs/BP/scripts/classes/Errors/NotAPlayerError.js +++ b/packs/BP/scripts/classes/Errors/NotAPlayerError.js @@ -1,6 +1,12 @@ -export class NotAPlayerError extends Error { +import { CommandResponseError } from "./CommandResponseError"; + +export class NotAPlayerError extends CommandResponseError { constructor(message = 'Command requires a player source.') { super(message); this.name = 'NotAPlayerError'; } + + getRawMessage() { + return { translate: 'construct.commands.error.notAPlayer' }; + } } diff --git a/packs/BP/scripts/classes/Errors/StructureNotFoundError.js b/packs/BP/scripts/classes/Errors/StructureNotFoundError.js new file mode 100644 index 0000000..55a0410 --- /dev/null +++ b/packs/BP/scripts/classes/Errors/StructureNotFoundError.js @@ -0,0 +1,15 @@ +import { CommandResponseError } from "./CommandResponseError"; + +export class StructureNotFoundError extends CommandResponseError { + structureId; + + constructor(structureId) { + super(`Structure with ID "${structureId}" not found.`); + this.name = 'StructureNotFoundError'; + this.structureId = structureId; + } + + getRawMessage() { + return { translate: 'construct.error.structureNotFound', with: [this.structureId] }; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Instance/InstanceOptions.js b/packs/BP/scripts/classes/Instance/InstanceOptions.js index a14550f..a3818f6 100644 --- a/packs/BP/scripts/classes/Instance/InstanceOptions.js +++ b/packs/BP/scripts/classes/Instance/InstanceOptions.js @@ -26,6 +26,7 @@ export class InstanceOptions extends Option { this.instanceName = instanceName; this.structureId = structureId; this.load(); + this.save(); } save() { diff --git a/packs/BP/scripts/classes/Materials/StructureMaterials.js b/packs/BP/scripts/classes/Materials/StructureMaterials.js index a4cc818..377e7ca 100644 --- a/packs/BP/scripts/classes/Materials/StructureMaterials.js +++ b/packs/BP/scripts/classes/Materials/StructureMaterials.js @@ -1,5 +1,6 @@ import { ItemStack, system } from "@minecraft/server"; import { Vector } from "../../lib/Vector"; +import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError"; class StructureMaterials { instance; @@ -21,11 +22,11 @@ class StructureMaterials { system.runJob(this.populateActive()); else system.runJob(this.populateAll()); - } catch (e) { - if (e.name === 'InstanceNotPlacedError') + } catch (error) { + if (error instanceof InstanceNotPlacedError) this.clear(); else - throw e; + throw error; } } diff --git a/packs/BP/scripts/classes/MenuForm.js b/packs/BP/scripts/classes/MenuForm.js index dcff275..19a51ce 100644 --- a/packs/BP/scripts/classes/MenuForm.js +++ b/packs/BP/scripts/classes/MenuForm.js @@ -3,6 +3,8 @@ import { structureCollection } from './Structure/StructureCollection'; import { MenuFormBuilder } from './MenuFormBuilder'; import { InstanceForm } from './Instance/InstanceForm'; import { BuilderForm } from './Builder/BuilderForm'; +import { InstanceExistsError } from './Errors/InstanceExistsError'; +import { StructureNotFoundError } from './Errors/StructureNotFoundError'; export class MenuForm { constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) { @@ -43,12 +45,12 @@ export class MenuForm { return selectedInstanceName || this.createNewInstance(); } }); - } catch (e) { - if (e.message === 'Menu timed out.') { + } catch (error) { + if (error.message === 'Menu timed out.') { this.player.sendMessage({ translate: 'construct.menu.open.timeout' }); return void 0; } - throw e; + throw error; } } @@ -64,16 +66,12 @@ export class MenuForm { return void 0; try { structureCollection.add(instanceName, structureId); - } catch (e) { - if (e.name === 'InvalidInstanceError') { - this.player.sendMessage({ translate: 'construct.mainmenu.instance.exists', with: [instanceName] }); + } catch (error) { + if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) { + error.sendTo(this.player); return void 0; } - if (e.name === 'InvalidStructureError') { - this.player.sendMessage({ translate: 'construct.mainmenu.instance.notfound', with: [structureId] }); - return void 0; - } - throw e; + throw error; } return instanceName; }); diff --git a/packs/BP/scripts/classes/Render/StructureOutliner.js b/packs/BP/scripts/classes/Render/StructureOutliner.js index 6d7e005..8c1b8ff 100644 --- a/packs/BP/scripts/classes/Render/StructureOutliner.js +++ b/packs/BP/scripts/classes/Render/StructureOutliner.js @@ -1,3 +1,4 @@ +import { StructureNotFoundError } from '../Errors/StructureNotFoundError'; import { Outliner } from '../Outliner'; export class StructureOutliner { @@ -13,11 +14,11 @@ export class StructureOutliner { this.bounds = this.instance.getBounds(); this.bounds.min = this.instance.toGlobalCoords(this.bounds.min); this.bounds.max = this.instance.toGlobalCoords(this.bounds.max); - } catch (e) { - if (e.name === 'InvalidStructureError') + } catch (error) { + if (error instanceof StructureNotFoundError) this.outliner.stopDraw(); else - throw e; + throw error; } } diff --git a/packs/BP/scripts/classes/Structure/Structure.js b/packs/BP/scripts/classes/Structure/Structure.js index 54c2559..64b60d2 100644 --- a/packs/BP/scripts/classes/Structure/Structure.js +++ b/packs/BP/scripts/classes/Structure/Structure.js @@ -1,6 +1,6 @@ import { world } from "@minecraft/server"; import { Vector } from "../../lib/Vector"; -import { InvalidStructureError } from "../Errors/InvalidStructureError"; +import { StructureNotFoundError } from "../Errors/StructureNotFoundError"; export class Structure { structureId; @@ -10,7 +10,7 @@ export class Structure { this.structureId = structureId; this.#structure = world.structureManager.get(structureId); if (!this.#structure) - throw new InvalidStructureError(`[Construct] Structure '${structureId}' not found on world.`); + throw new StructureNotFoundError(structureId); this.#structure.saveToWorld(); } diff --git a/packs/BP/scripts/classes/Structure/StructureCollection.js b/packs/BP/scripts/classes/Structure/StructureCollection.js index 1f4c9dc..10a2eac 100644 --- a/packs/BP/scripts/classes/Structure/StructureCollection.js +++ b/packs/BP/scripts/classes/Structure/StructureCollection.js @@ -1,7 +1,9 @@ -import { InvalidInstanceError } from '../Errors/InvalidInstanceError'; +import { InstanceExistsError } from '../Errors/InstanceExistsError'; +import { InstanceNotFoundError } from '../Errors/InstanceNotFoundError'; +import { StructureNotFoundError } from '../Errors/StructureNotFoundError'; import { InstanceOptions } from '../Instance/InstanceOptions'; import { StructureInstance } from '../Instance/StructureInstance'; -import { world } from '@minecraft/server'; +import { InvalidStructureError, world } from '@minecraft/server'; class StructureCollection { structures; @@ -27,7 +29,7 @@ class StructureCollection { add(instanceName, structureId) { if (this.structures[instanceName]) - throw new InvalidInstanceError(`Instance ${instanceName} already exists.`); + throw new InstanceExistsError(instanceName); const structure = new StructureInstance(instanceName, structureId); this.structures[instanceName] = structure; return structure; @@ -36,7 +38,7 @@ class StructureCollection { get(instanceName) { const structure = this.structures[instanceName]; if (!structure) - throw new InvalidInstanceError(`Instance ${instanceName} not found.`); + throw new InstanceNotFoundError(instanceName); return structure; } @@ -58,12 +60,12 @@ class StructureCollection { return Object.values(this.structures).filter(structure => { try { return structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options) - } catch (e) { - if (e.name === 'InvalidStructureError') { - structureCollection.delete(structure.name); + } catch (error) { + if (error instanceof StructureNotFoundError || error instanceof InvalidStructureError) { + this.delete(structure.name); return false; } else { - throw e; + throw error; } } }); @@ -101,7 +103,7 @@ class StructureCollection { rename(instanceName, newName) { const structure = this.get(instanceName); if (this.structures[newName]) - throw new Error(`Instance '${newName}' already exists.`); + throw new InstanceExistsError(newName); structure.rename(newName); this.structures[newName] = structure; delete this.structures[instanceName]; diff --git a/packs/BP/scripts/commands/ActiveCommand.js b/packs/BP/scripts/commands/ActiveCommand.js index b8a9dbb..a604e7f 100644 --- a/packs/BP/scripts/commands/ActiveCommand.js +++ b/packs/BP/scripts/commands/ActiveCommand.js @@ -1,7 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class ActiveCommand extends Command { constructor() { @@ -12,32 +11,29 @@ export class ActiveCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'state', type: CustomCommandParamType.Boolean } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, state) => this.run(source, instanceName, state) }); } run(source, instanceName, state) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - if (state && !instance.hasLocation()) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.error.noLocation', with: [instanceName] }] - })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - if (state) instance.enable(); - else instance.disable(); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.active.success', - with: [instanceName, String(state)] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + const instance = structureCollection.get(instanceName); + if (state && !instance.hasLocation()) { + source.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] }); + return void 0; } + system.run(() => { + if (state) + instance.enable(); + else + instance.disable(); + this.sendFeedback(source, instanceName, state); + }); + return { status: CustomCommandStatus.Success }; + } + + sendFeedback(source, instanceName, state) { + source.sendMessage({ translate: state ? 'construct.commands.active.true' : 'construct.commands.active.false', with: [instanceName] }); } } diff --git a/packs/BP/scripts/commands/ConstructCommand.js b/packs/BP/scripts/commands/ConstructCommand.js index 04e4ba2..4a95a53 100644 --- a/packs/BP/scripts/commands/ConstructCommand.js +++ b/packs/BP/scripts/commands/ConstructCommand.js @@ -1,8 +1,6 @@ import { CommandPermissionLevel, CustomCommandStatus, EntityComponentTypes, ItemStack, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; -import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; -import { commandError } from '../classes/Commands/lib/commandError'; import { MENU_ITEM } from '../consts'; export class ConstructCommand extends Command { @@ -10,28 +8,29 @@ export class ConstructCommand extends Command { super({ name: 'construct', description: 'construct.commands.construct', - permissionLevel: CommandPermissionLevel.Any, cheatsRequired: false, allowedSources: [PlayerCommandOrigin], + permissionLevel: CommandPermissionLevel.Any, callback: (source) => this.run(source) }); } run(source) { - try { - const player = requirePlayer(source); - system.run(() => { - const remaining = player.getComponent(EntityComponentTypes.Inventory) - ?.container?.addItem(new ItemStack(MENU_ITEM)); - if (remaining) - player.sendMessage({ translate: 'construct.commands.construct.fail' }); - else - player.sendMessage({ translate: 'construct.commands.construct.success' }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const player = source.getSource(); + system.run(() => { + this.giveMenuItem(player); + }); + return { status: CustomCommandStatus.Success }; + } + + giveMenuItem(player) { + const inventoryComponent = player.getComponent(EntityComponentTypes.Inventory); + const inventoryContainer = inventoryComponent?.container; + const remaining = inventoryContainer?.addItem(new ItemStack(MENU_ITEM)); + if (remaining) + player.sendMessage({ translate: 'construct.commands.construct.fail' }); + else + player.sendMessage({ translate: 'construct.commands.construct.success' }); } } diff --git a/packs/BP/scripts/commands/DeleteCommand.js b/packs/BP/scripts/commands/DeleteCommand.js index 7799fda..1ce4361 100644 --- a/packs/BP/scripts/commands/DeleteCommand.js +++ b/packs/BP/scripts/commands/DeleteCommand.js @@ -1,8 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; +import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server'; import { structureCollection } from '../classes/Structure/StructureCollection'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; export class DeleteCommand extends Command { constructor() { @@ -12,24 +10,18 @@ export class DeleteCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - system.run(() => { - structureCollection.delete(instanceName); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.delete.success', with: [instanceName] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + system.run(() => { + structureCollection.delete(instanceName); + source.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] }); + }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/InfoCommand.js b/packs/BP/scripts/commands/InfoCommand.js index 155aa4f..486aad1 100644 --- a/packs/BP/scripts/commands/InfoCommand.js +++ b/packs/BP/scripts/commands/InfoCommand.js @@ -1,7 +1,7 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { Vector } from '../lib/Vector'; export class InfoCommand extends Command { constructor() { @@ -11,46 +11,63 @@ export class InfoCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - const rawtext = [ - { translate: 'construct.commands.info.header', with: [instance.getName()] }, - { text: '\n' }, - { translate: 'construct.commands.info.structure', with: [instance.getStructureId()] }, - { text: '\n' }, - { translate: 'construct.commands.info.enabled', with: [String(instance.isEnabled())] }, - { text: '\n' } - ]; - if (instance.hasLocation()) { - const { dimensionId, location } = instance.getLocation(); - rawtext.push({ translate: 'construct.commands.info.location', - with: [String(location.x), String(location.y), String(location.z), - dimensionId.replace('minecraft:', '')] }); - } else { - rawtext.push({ translate: 'construct.commands.info.noLocation' }); - } - rawtext.push({ text: '\n' }); - rawtext.push({ translate: 'construct.commands.info.layer', - with: [String(instance.getLayer()), String(instance.getMaxLayer())] }); - rawtext.push({ text: '\n' }); - rawtext.push({ translate: 'construct.commands.info.verifier', - with: [String(instance.options.verifier.isEnabled)] }); - rawtext.push({ text: '\n' }); - const bounds = instance.getBounds(); - rawtext.push({ translate: 'construct.commands.info.bounds', - with: [String(bounds.min.x), String(bounds.min.y), String(bounds.min.z), - String(bounds.max.x), String(bounds.max.y), String(bounds.max.z)] }); - system.run(() => source.sendMessage({ rawtext })); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + const message = { rawtext: [ + this.getHeaderText(instance), + { text: '\n' }, + this.getStructureIdText(instance), + { text: '\n' }, + this.getLocationText(instance), + { text: '\n' }, + this.getEnabledText(instance), + { text: '\n' }, + this.getLayerText(instance), + { text: '\n' }, + this.getVerifierText(instance), + { text: '\n' }, + this.getSizeText(instance) + ]}; + source.sendMessage(message); + return { status: CustomCommandStatus.Success }; + } + + getHeaderText(instance) { + return { translate: 'construct.commands.info.header', with: [instance.getName()] }; + } + + getStructureIdText(instance) { + return { translate: 'construct.commands.info.structure', with: [instance.getStructureId()] }; + } + + getLocationText(instance) { + if (!instance.hasLocation()) + return { translate: 'construct.commands.info.noLocation' }; + const { dimensionId, location } = instance.getLocation(); + return { translate: 'construct.commands.info.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }; + } + + getEnabledText(instance) { + return { translate: 'construct.commands.info.enabled', with: [String(instance.isEnabled())] }; + } + + getLayerText(instance) { + return { translate: 'construct.commands.info.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] }; + } + + getVerifierText(instance) { + const verifier = instance.options.verifier; + return { translate: 'construct.commands.info.verifier', with: [String(verifier.isEnabled)] }; + } + + getSizeText(instance) { + const bounds = instance.getBounds(); + return { translate: 'construct.commands.info.size', with: [bounds.max.toString(), Vector.volume(bounds.min, bounds.max).toString()] }; } } diff --git a/packs/BP/scripts/commands/LayerCommand.js b/packs/BP/scripts/commands/LayerCommand.js index 5f0ff01..a926fbf 100644 --- a/packs/BP/scripts/commands/LayerCommand.js +++ b/packs/BP/scripts/commands/LayerCommand.js @@ -1,7 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class LayerCommand extends Command { constructor() { @@ -12,33 +11,21 @@ export class LayerCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'layer', type: CustomCommandParamType.Integer } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, layer) => this.run(source, instanceName, layer) }); } run(source, instanceName, layer) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - const max = instance.getMaxLayer(); - if (layer < 0 || layer > max) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.layer.outOfBounds', - with: [String(layer), instanceName, String(max)] }] - })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - instance.setLayer(layer); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.layer.success', - with: [instanceName, String(layer)] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + const instance = structureCollection.get(instanceName); + const max = instance.getMaxLayer(); + if (layer < 0 || layer > max) { + source.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] }); + return void 0; } + instance.setLayer(layer); + source.sendMessage({ translate: 'construct.commands.layer.success', with: [instanceName, String(layer)] }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/ListCommand.js b/packs/BP/scripts/commands/ListCommand.js index bdd1dc9..dccd5e4 100644 --- a/packs/BP/scripts/commands/ListCommand.js +++ b/packs/BP/scripts/commands/ListCommand.js @@ -1,53 +1,51 @@ -import { CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; import { structureCollection } from '../classes/Structure/StructureCollection'; -import { commandError } from '../classes/Commands/lib/commandError'; export class ListCommand extends Command { constructor() { super({ name: 'list', description: 'construct.commands.list', + permissionLevel: CommandPermissionLevel.Any, callback: (source) => this.run(source) }); } run(source) { - try { - const names = structureCollection.getInstanceNames(); - if (names.length === 0) { - return { status: CustomCommandStatus.Success, message: 'construct.commands.list.empty' }; - } - system.run(() => { - const rawtext = [ - { translate: 'construct.commands.list.header', with: [String(names.length)] }, - { text: '\n' } - ]; - for (const name of names) { - const instance = structureCollection.get(name); - const status = this.formatStatus(instance); - rawtext.push({ - translate: 'construct.commands.list.row', - with: [name, instance.getStructureId(), status] - }); - rawtext.push({ text: '\n' }); - } - source.sendMessage({ rawtext }); + const names = structureCollection.getInstanceNames(); + if (names.length === 0) + return { status: CustomCommandStatus.Success, message: 'construct.commands.list.empty' }; + const rawtext = [ + { translate: 'construct.commands.list.header', with: [String(names.length)] }, + { text: '\n' } + ]; + for (const name of names) { + const instance = structureCollection.get(name); + const status = this.formatStatus(instance); + rawtext.push({ + translate: 'construct.commands.list.row', + with: { rawtext: [{ text: name }, { text: instance.getStructureId() }, status] } }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + rawtext.push({ text: '\n' }); } + source.sendMessage({ rawtext }); + return { status: CustomCommandStatus.Success }; } formatStatus(instance) { - if (!instance.hasLocation()) - return 'no location'; - if (!instance.isEnabled()) - return 'disabled'; - const { dimensionId, location } = instance.getLocation(); - const dim = dimensionId.replace('minecraft:', ''); - return `enabled @ ${location.x} ${location.y} ${location.z} (${dim})`; + const statusMessage = { rawtext: [] }; + if (instance.isEnabled()) + statusMessage.rawtext.push({ translate: 'construct.commands.list.row.enabled' }); + else + statusMessage.rawtext.push({ translate: 'construct.commands.list.row.disabled' }); + if (instance.hasLocation()) { + const { dimensionId, location } = instance.getLocation(); + statusMessage.rawtext.push({ translate: 'construct.commands.list.row.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }); + } else { + statusMessage.rawtext.push({ translate: 'construct.commands.list.row.nolocation' }); + } + return statusMessage; } } diff --git a/packs/BP/scripts/commands/MaterialsCommand.js b/packs/BP/scripts/commands/MaterialsCommand.js index 2638e56..b150048 100644 --- a/packs/BP/scripts/commands/MaterialsCommand.js +++ b/packs/BP/scripts/commands/MaterialsCommand.js @@ -1,8 +1,8 @@ -import { CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; +import { NotAPlayerError } from '../classes/Errors/NotAPlayerError'; export class MaterialsCommand extends Command { constructor() { @@ -15,44 +15,45 @@ export class MaterialsCommand extends Command { optionalParameters: [ { name: 'missing', type: CustomCommandParamType.Boolean } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, missing) => this.run(source, instanceName, missing) }); } run(source, instanceName, missing) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - const onlyMissing = missing === true; - let container; - let headerKey; - if (onlyMissing) { - const player = requirePlayer(source); - container = player.getComponent(EntityComponentTypes.Inventory)?.container; - headerKey = 'construct.commands.materials.headerMissing'; - } else { - headerKey = 'construct.commands.materials.headerAll'; - } - system.run(() => { - const materials = instance.getActiveMaterials(); - const materialsMap = onlyMissing - ? materials.getMaterialsDifference(container) - : undefined; - const list = materials.formatString(materialsMap); - const rawtext = [ - { translate: headerKey, with: [instanceName] }, - { text: '\n' } - ]; - if (!list.rawtext || list.rawtext.length === 0) - rawtext.push({ translate: 'construct.commands.materials.empty' }); - else - rawtext.push(list); - source.sendMessage({ rawtext }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + const instance = structureCollection.get(instanceName); + const onlyMissing = missing === true; + if (onlyMissing) + this.assertIsPlayer(source); + const headerKey = onlyMissing ? 'construct.commands.materials.headerMissing' : 'construct.commands.materials.headerAll'; + const rawtext = [ + { translate: headerKey, with: [instanceName] }, + { text: '\n' } + ]; + const list = this.getMaterialList(source, instance, onlyMissing); + if (!list.rawtext || list.rawtext.length === 0) + rawtext.push({ translate: 'construct.commands.materials.empty' }); + else + rawtext.push(list); + source.sendMessage({ rawtext }); + return { status: CustomCommandStatus.Success }; + } + + assertIsPlayer(source) { + if (!(source instanceof PlayerCommandOrigin)) + throw new NotAPlayerError(); + } + + getMaterialList(source, instance, onlyMissing) { + const materials = instance.getActiveMaterials(); + let container; + if (onlyMissing) { + const player = source.getSource(); + const inventoryComponent = player?.getComponent(EntityComponentTypes.Inventory); + container = inventoryComponent?.container; } + const materialsMap = onlyMissing ? materials.getMaterialsDifference(container) : void 0; + return materials.formatString(materialsMap); } } diff --git a/packs/BP/scripts/commands/MoveCommand.js b/packs/BP/scripts/commands/MoveCommand.js index 455dcc9..88f7099 100644 --- a/packs/BP/scripts/commands/MoveCommand.js +++ b/packs/BP/scripts/commands/MoveCommand.js @@ -1,10 +1,8 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server'; +import { Vector } from '../lib/Vector'; +import { structureCollection } from '../classes/Structure/StructureCollection'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; -import { BlockCommandOrigin } from '../classes/Commands/BlockCommandOrigin'; -import { EntityCommandOrigin } from '../classes/Commands/EntityCommandOrigin'; export class MoveCommand extends Command { constructor() { @@ -15,47 +13,34 @@ export class MoveCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], optionalParameters: [ - { name: 'pos', type: CustomCommandParamType.Location } + { name: 'dimensionId', type: CustomCommandParamType.Enum }, // Enum defined in PlaceCommand.js + { name: 'location', type: CustomCommandParamType.Location } ], - callback: (source, instanceName, pos) => this.run(source, instanceName, pos) + permissionLevel: CommandPermissionLevel.Any, + callback: (source, instanceName, dimensionId, location) => this.run(source, instanceName, dimensionId, location) }); } - run(source, instanceName, pos) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - let dimensionId; - let location = pos; - if (location === undefined) { - if (!(source instanceof PlayerCommandOrigin)) - return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.posRequired' }; - const player = source.getSource(); - location = player.location; - dimensionId = player.dimension.id; - } else { - dimensionId = this.resolveDimensionId(source); - } - system.run(() => { - instance.move(dimensionId, location); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.move.success', - with: [instanceName, String(Math.floor(location.x)), String(Math.floor(location.y)), - String(Math.floor(location.z)), dimensionId.replace('minecraft:', '')] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + run(source, instanceName, dimensionId, location) { + const instance = structureCollection.get(instanceName); + if (dimensionId === void 0 || location === void 0) { + if (!(source instanceof PlayerCommandOrigin)) + return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.locationRequired' }; + const player = source.getSource(); + location = player.location; + dimensionId = player.dimension.id; } + this.assertDimensionExists(dimensionId); + const flooredLocation = Vector.from(location).floor(); + system.run(() => { + instance.move(dimensionId, flooredLocation); + source.sendMessage({ translate: 'construct.commands.move.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); + }); + return { status: CustomCommandStatus.Success }; } - resolveDimensionId(source) { - if (source instanceof PlayerCommandOrigin || source instanceof EntityCommandOrigin) - return source.getSource().dimension.id; - if (source instanceof BlockCommandOrigin) - return source.getSource().dimension.id; - return 'minecraft:overworld'; + assertDimensionExists(dimensionId) { + return world.getDimension(dimensionId) !== void 0; } } diff --git a/packs/BP/scripts/commands/NewCommand.js b/packs/BP/scripts/commands/NewCommand.js index 977f122..4499fec 100644 --- a/packs/BP/scripts/commands/NewCommand.js +++ b/packs/BP/scripts/commands/NewCommand.js @@ -1,7 +1,8 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { structureCollection } from '../classes/Structure/StructureCollection'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { InstanceExistsError } from '../classes/Errors/InstanceExistsError'; +import { StructureNotFoundError } from '../classes/Errors/StructureNotFoundError'; export class NewCommand extends Command { constructor() { @@ -12,35 +13,38 @@ export class NewCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'structureId', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, structureId) => this.run(source, instanceName, structureId) }); } run(source, instanceName, structureId) { - try { - if (structureCollection.has(instanceName)) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.new.duplicateName', with: [instanceName] }] - })); - return { status: CustomCommandStatus.Failure }; - } - if (!structureCollection.getWorldStructureIds().includes(structureId)) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.new.unknownStructure', with: [structureId] }] - })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - structureCollection.add(instanceName, structureId); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.new.success', with: [instanceName, structureId] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + this.tryAddStructure(source, instanceName, structureId); + return { status: CustomCommandStatus.Success }; } + + tryAddStructure(source, instanceName, structureId) { + system.run(() => { + try { + this.addStructure(source, instanceName, structureId); + } catch (error) { + this.handleStructureAdditionErrors(source, error); + } + }); + } + + addStructure(source, instanceName, structureId) { + structureCollection.add(instanceName, structureId); + source.sendMessage({ translate: 'construct.commands.new.success', with: [instanceName, structureId] }); + } + + handleStructureAdditionErrors(source, error) { + if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) + error.sendTo(source); + else + throw error; + } + } export const newCommand = new NewCommand(); diff --git a/packs/BP/scripts/commands/NextLayerCommand.js b/packs/BP/scripts/commands/NextLayerCommand.js index fa4684c..6622dc9 100644 --- a/packs/BP/scripts/commands/NextLayerCommand.js +++ b/packs/BP/scripts/commands/NextLayerCommand.js @@ -1,7 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class NextLayerCommand extends Command { constructor() { @@ -11,25 +10,16 @@ export class NextLayerCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - system.run(() => { - instance.increaseLayer(); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.nextlayer.success', - with: [instanceName, String(instance.getLayer())] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + instance.increaseLayer(); + source.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/OptionCommand.js b/packs/BP/scripts/commands/OptionCommand.js index dba4ef6..ac05107 100644 --- a/packs/BP/scripts/commands/OptionCommand.js +++ b/packs/BP/scripts/commands/OptionCommand.js @@ -1,9 +1,7 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; import { BuilderOptions } from '../classes/Builder/BuilderOptions'; -import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; -import { commandError } from '../classes/Commands/lib/commandError'; export class OptionCommand extends Command { constructor() { @@ -18,29 +16,22 @@ export class OptionCommand extends Command { enums: [ { name: 'optionId', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, optionId, state) => this.run(source, optionId, state) }); } run(source, optionId, state) { - try { - const player = requirePlayer(source); - if (!BuilderOptions.get(optionId)) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.option.unknownOption', with: [optionId] }] - })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - BuilderOptions.setValue(optionId, player.id, state); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.option.success', with: [optionId, String(state)] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + if (!BuilderOptions.get(optionId)) { + source.sendMessage({ translate: 'construct.commands.option.unknownOption', with: [optionId] }); + return void 0; } + system.run(() => { + const player = source.getSource(); + BuilderOptions.setValue(optionId, player.id, state); + source.sendMessage({ translate: 'construct.commands.option.success', with: [optionId, String(state)] }); + }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/PlaceCommand.js b/packs/BP/scripts/commands/PlaceCommand.js index 2f9cb34..99e63d4 100644 --- a/packs/BP/scripts/commands/PlaceCommand.js +++ b/packs/BP/scripts/commands/PlaceCommand.js @@ -1,49 +1,38 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; -import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; -import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; -import { BlockCommandOrigin } from '../classes/Commands/BlockCommandOrigin'; -import { EntityCommandOrigin } from '../classes/Commands/EntityCommandOrigin'; +import { Command } from '../classes/Commands/Command'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, DimensionTypes, system, world } from '@minecraft/server'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { InstanceExistsError } from '../classes/Errors/InstanceExistsError'; +import { Vector } from '../lib/Vector'; export class PlaceCommand extends Command { constructor() { super({ name: 'place', description: 'construct.commands.place', + enums: [ { name: 'dimensionId', values: Object.values(DimensionTypes.getAll().map(d => d.typeId)) } ], mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String }, - { name: 'pos', type: CustomCommandParamType.Location } + { name: 'dimensionId', type: CustomCommandParamType.Enum }, + { name: 'location', type: CustomCommandParamType.Location } ], - callback: (source, instanceName, pos) => this.run(source, instanceName, pos) + permissionLevel: CommandPermissionLevel.Any, + callback: (source, instanceName, dimensionId, location) => this.run(source, instanceName, dimensionId, location) }); } - run(source, instanceName, pos) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - const dimensionId = this.resolveDimensionId(source); - system.run(() => { - instance.place(dimensionId, pos); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.place.success', - with: [instanceName, String(pos.x), String(pos.y), String(pos.z), - dimensionId.replace('minecraft:', '')] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + run(source, instanceName, dimensionId, location) { + const instance = structureCollection.get(instanceName); + const flooredLocation = Vector.from(location).floor(); + this.assertDimensionExists(dimensionId); + system.run(() => { + instance.place(dimensionId, flooredLocation); + source.sendMessage({ translate: 'construct.commands.place.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); + }); + return { status: CustomCommandStatus.Success }; } - resolveDimensionId(source) { - if (source instanceof PlayerCommandOrigin || source instanceof EntityCommandOrigin) - return source.getSource().dimension.id; - if (source instanceof BlockCommandOrigin) - return source.getSource().dimension.id; - return 'minecraft:overworld'; + assertDimensionExists(dimensionId) { + return world.getDimension(dimensionId) !== void 0; } } diff --git a/packs/BP/scripts/commands/PrevLayerCommand.js b/packs/BP/scripts/commands/PrevLayerCommand.js index 1f9d395..7fc485e 100644 --- a/packs/BP/scripts/commands/PrevLayerCommand.js +++ b/packs/BP/scripts/commands/PrevLayerCommand.js @@ -1,7 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class PrevLayerCommand extends Command { constructor() { @@ -11,25 +10,16 @@ export class PrevLayerCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - system.run(() => { - instance.decreaseLayer(); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.prevlayer.success', - with: [instanceName, String(instance.getLayer())] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + instance.decreaseLayer(); + source.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/RenameCommand.js b/packs/BP/scripts/commands/RenameCommand.js index 8758760..e915a4e 100644 --- a/packs/BP/scripts/commands/RenameCommand.js +++ b/packs/BP/scripts/commands/RenameCommand.js @@ -1,8 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { structureCollection } from '../classes/Structure/StructureCollection'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; export class RenameCommand extends Command { constructor() { @@ -13,30 +11,20 @@ export class RenameCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'newName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, newName) => this.run(source, instanceName, newName) }); } run(source, instanceName, newName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - if (structureCollection.has(newName)) { - system.run(() => source.sendMessage({ - rawtext: [{ translate: 'construct.commands.rename.duplicateName', with: [newName] }] - })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - structureCollection.rename(instanceName, newName); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.rename.success', with: [instanceName, newName] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); + const instance = structureCollection.get(instanceName); + if (structureCollection.has(newName)) { + source.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] }); + return void 0; } + structureCollection.rename(instanceName, newName); + source.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/StatsCommand.js b/packs/BP/scripts/commands/StatsCommand.js index c68de26..a117e38 100644 --- a/packs/BP/scripts/commands/StatsCommand.js +++ b/packs/BP/scripts/commands/StatsCommand.js @@ -1,8 +1,9 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, TicksPerSecond } from '@minecraft/server'; import { InstanceFormBuilder } from '../classes/Instance/InstanceFormBuilder'; +import { structureCollection } from '../classes/Structure/StructureCollection'; +import { StructureVerifier } from '../classes/Verifier/StructureVerifier'; +import { StructureStatistics } from '../classes/Structure/StructureStatistics'; export class StatsCommand extends Command { constructor() { @@ -12,26 +13,29 @@ export class StatsCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - system.run(async () => { - try { - const { stats } = await InstanceFormBuilder.buildStatistics(instance); - source.sendMessage(stats); - } catch (err) { - source.sendMessage({ translate: 'construct.commands.stats.alreadyRunning' }); - } - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + if (this.structureVerifier) + return { status: CustomCommandStatus.Failure, error: 'construct.commands.stats.alreadyRunning' }; + system.run(async () => { + source.sendMessage(await this.getStatsMessage(instance)); + }); + return { status: CustomCommandStatus.Success }; + } + + async getStatsMessage(instance) { + const verifierOptions = { isEnabled: true, particleLifetime: 1*TicksPerSecond, isStandalone: true }; + this.structureVerifier = new StructureVerifier(instance, verifierOptions); + const verification = await this.structureVerifier.verifyStructure(true); + const statistics = new StructureStatistics(instance, verification); + const statsMessage = statistics.getMessage(); + this.structureVerifier = void 0; + return statsMessage; } } diff --git a/packs/BP/scripts/commands/TagCommand.js b/packs/BP/scripts/commands/TagCommand.js index 18b076a..663ed1a 100644 --- a/packs/BP/scripts/commands/TagCommand.js +++ b/packs/BP/scripts/commands/TagCommand.js @@ -1,10 +1,8 @@ -import { CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { requirePlayer } from '../classes/Commands/lib/requirePlayer'; -import { commandError } from '../classes/Commands/lib/commandError'; import { MENU_ITEM } from '../consts'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class TagCommand extends Command { constructor() { @@ -15,32 +13,24 @@ export class TagCommand extends Command { mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName) => this.run(source, instanceName) }); } run(source, instanceName) { - try { - const player = requirePlayer(source); - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - const equipment = player.getComponent(EntityComponentTypes.Equippable); - const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand); - if (itemStack?.typeId !== MENU_ITEM) { - system.run(() => source.sendMessage({ translate: 'construct.commands.tag.notHoldingItem' })); - return { status: CustomCommandStatus.Failure }; - } - system.run(() => { - itemStack.nameTag = instanceName; - equipment.setEquipment(EquipmentSlot.Mainhand, itemStack); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.tag.success', with: [instanceName] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + const player = source.getSource(); + const equipment = player.getComponent(EntityComponentTypes.Equippable); + const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand); + if (itemStack?.typeId !== MENU_ITEM) + return { status: CustomCommandStatus.Failure, message: 'construct.commands.tag.notHoldingItem' }; + system.run(() => { + itemStack.nameTag = instanceName; + equipment.setEquipment(EquipmentSlot.Mainhand, itemStack); + source.sendMessage({ translate: 'construct.commands.tag.success', with: [instanceName] }); + }); + return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/VerifierCommand.js b/packs/BP/scripts/commands/VerifierCommand.js index 00ae70d..2d362b2 100644 --- a/packs/BP/scripts/commands/VerifierCommand.js +++ b/packs/BP/scripts/commands/VerifierCommand.js @@ -1,7 +1,6 @@ -import { CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server'; import { Command } from '../classes/Commands/Command'; -import { findInstance } from '../classes/Commands/lib/findInstance'; -import { commandError } from '../classes/Commands/lib/commandError'; +import { structureCollection } from '../classes/Structure/StructureCollection'; export class VerifierCommand extends Command { constructor() { @@ -12,25 +11,23 @@ export class VerifierCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'state', type: CustomCommandParamType.Boolean } ], + permissionLevel: CommandPermissionLevel.Any, callback: (source, instanceName, state) => this.run(source, instanceName, state) }); } run(source, instanceName, state) { - try { - const instance = findInstance(source, instanceName); - if (!instance) return { status: CustomCommandStatus.Failure }; - system.run(() => { - instance.setVerifierEnabled(state); - source.sendMessage({ - rawtext: [{ translate: 'construct.commands.verifier.success', - with: [instanceName, String(state)] }] - }); - }); - return { status: CustomCommandStatus.Success }; - } catch (err) { - return commandError(source, err); - } + const instance = structureCollection.get(instanceName); + if (state) + instance.setVerifierEnabled(true); + else + instance.setVerifierEnabled(false); + this.sendFeedback(source, instanceName, state); + return { status: CustomCommandStatus.Success }; + } + + sendFeedback(source, instanceName, state) { + source.sendMessage({ translate: state ? 'construct.commands.verifier.enabled' : 'construct.commands.verifier.disabled', with: [instanceName] }); } } diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 27c0b17..b29a15e 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -57,15 +57,6 @@ construct.structure.statistics.stateincorrect=§7Block State Incorrect: §e%s ## construct.structure.statistics.incorrect=§7Incorrect: §c%s ## Insert string: number of incorrectly placed blocks construct.structure.statistics.missing=§7Missing: §3%s ## Insert string: number of missing blocks -## Structure Block Info Display -construct.blockinfo.header=Structure: -construct.blockinfo.none=§7None -construct.blockinfo.nosupply= §c[No Supply] ## When the targeted block is not in the player's inventory -construct.blockinfo.unknown=§7Unknown -construct.blockinfo.waterlogged=§7isWaterlogged: §3true - -construct.mainmenu.instance.exists=§cInstance '%s' already exists. Try again with a new name. ## Insert string: instance name -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 @@ -84,12 +75,14 @@ construct.mainmenu.howto.add.mcstructure=§7To transfer structures §fbetween wo 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=Gives you the Construct item. Use it to open the Construct menu. -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. +## Structure Block Info Display +construct.blockinfo.header=Structure: +construct.blockinfo.none=§7None +construct.blockinfo.nosupply= §c[No Supply] ## When the targeted block is not in the player's inventory +construct.blockinfo.unknown=§7Unknown +construct.blockinfo.waterlogged=§7isWaterlogged: §3true -## Options +## Builder Options construct.option.enabled= is now enabled! construct.option.disabled= is now disabled. construct.option.easyplace.name=Easy Place @@ -106,14 +99,14 @@ construct.option.materialgrabber.grabbed.zero=§7Grabbed 0 items. construct.option.materialgrabber.grabbed.one=§aGrabbed 1 item. construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert string: number of items transferred to the player -## CLI shared errors -construct.commands.error.instanceNotFound=§cInstance "%1" not found. -construct.commands.error.notAPlayer=§cThis command can only be used by players. +## Commands +construct.commands.construct=Gives you the Construct item. Use it to open the Construct menu. +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. ## construct:new construct.commands.new=Create a new instance bound to a structure. construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". -construct.commands.new.duplicateName=§cAn instance named "%1" already exists. construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. ## construct:delete @@ -123,30 +116,34 @@ construct.commands.delete.success=§aDeleted instance "%1". ## construct:rename construct.commands.rename=Rename an existing instance. construct.commands.rename.success=§aRenamed instance "%1" to "%2". -construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. ## construct:list construct.commands.list=List all registered instances. construct.commands.list.empty=§7No instances registered. -construct.commands.list.header=§eRegistered instances (%1):§r +construct.commands.list.header=§aRegistered instances (%1):§r construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r +construct.commands.list.row.enabled=enabled +construct.commands.list.row.disabled=disabled +construct.commands.list.row.location= at %1 in %2 +construct.commands.list.row.nolocation= (no location) ## construct:place construct.commands.place=Place an instance at world coordinates (enables it). -construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. +construct.commands.place.success=§aPlaced instance "%1" at %2 in %3. ## construct:move construct.commands.move=Reposition an instance without toggling its enabled state. -construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. -construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. +construct.commands.move.success=§aMoved instance "%1" to %2 in %3. +construct.commands.move.locationRequired=§cMust provide a dimension and coordinates when not running as a player. ## construct:active construct.commands.active=Enable or disable an instance. -construct.commands.active.success=§aSet instance "%1" active=%2. +construct.commands.active.true=§aEnabled instance "%1". +construct.commands.active.false=§aDisabled instance "%1". construct.commands.error.noLocation=§cInstance "%1" has no saved location. ## construct:layer -construct.commands.layer=Set the active layer of an instance (0 = whole structure). +construct.commands.layer=Set the active layer of an instance (Use 0 for the whole structure). construct.commands.layer.success=§aSet instance "%1" layer to %2. construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). @@ -160,7 +157,8 @@ construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## construct:verifier construct.commands.verifier=Toggle block validation overlay for an instance. -construct.commands.verifier.success=§aSet instance "%1" verifier=%2. +construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". +construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". ## construct:option construct.commands.option=Toggle a per-player builder option. @@ -169,26 +167,34 @@ construct.commands.option.unknownOption=§cUnknown option "%1". ## construct:info construct.commands.info=Print instance details to chat. -construct.commands.info.header=§e=== Instance "%1" ===§r +construct.commands.info.header=§a=== Instance Info for "%1" ===§r construct.commands.info.structure=§7Structure:§r %1 -construct.commands.info.enabled=§7Enabled:§r %1 -construct.commands.info.location=§7Location:§r %1 %2 %3 in %4 +construct.commands.info.location=§7Location:§r %1 in %2 construct.commands.info.noLocation=§7Location:§r (none) +construct.commands.info.enabled=§7Enabled:§r %1 construct.commands.info.layer=§7Layer:§r %1 / %2 -construct.commands.info.verifier=§7Verifier:§r %1 -construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) +construct.commands.info.verifier=§7Verifier running:§r %1 +construct.commands.info.size=§7Size:§r %1 (%2 blocks) ## construct:stats construct.commands.stats=Run the structure verifier and print statistics. -construct.commands.stats.alreadyRunning=§cA verification is already in progress. +construct.commands.stats.alreadyRunning=§cA verification is already in progress. Please wait until it finishes. ## construct:materials construct.commands.materials=Print the material list for an instance. -construct.commands.materials.headerAll=§eMaterials for "%1":§r -construct.commands.materials.headerMissing=§eMissing materials for "%1":§r +construct.commands.materials.headerAll=§aMaterials for "%1":§r +construct.commands.materials.headerMissing=§aMissing materials for "%1":§r construct.commands.materials.empty=§7(no materials) ## construct:tag construct.commands.tag=Rename the held Construct item to an instance name for quick-open. construct.commands.tag.success=§aTagged held Construct item with instance "%1". construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item. + +## Errors +construct.error.invalidCommandSource=§cThis command cannot be run from this source. +construct.error.instanceNotFound=§cInstance "%1" not found. ## Insert string: instance name +construct.error.instanceExists=§cInstance "%1" already exists. Try again with a new name. ## Insert string: instance name +construct.error.structureNotFound=§cStructure ID "%1" 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.error.dimensionNotFound=§cDimension "%1" not found. ## Insert string: dimension name +construct.error.notAPlayer=§cThis command can only be used by players. \ No newline at end of file From ccfc2cf6ab16df80008750f9d38a9584f90c3db2 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:09:43 -0700 Subject: [PATCH 23/46] revert: changes to chinese translation for CLI --- packs/RP/texts/zh_CN.lang | 88 +-------------------------------------- 1 file changed, 1 insertion(+), 87 deletions(-) diff --git a/packs/RP/texts/zh_CN.lang b/packs/RP/texts/zh_CN.lang index 273c530..00ac1ef 100644 --- a/packs/RP/texts/zh_CN.lang +++ b/packs/RP/texts/zh_CN.lang @@ -86,6 +86,7 @@ construct.mainmenu.howto.remove.body=§7-使用§f/structure delete§7命令从 ## Commands construct.commands.construct=获得投影构筑菜单物品,可以使用它打开投影构筑菜单 +construct.commands.construct.denyorigin=此命令只能由玩家使用 construct.commands.construct.fail=§c给予投影构筑菜单物品时发生错误 construct.commands.construct.success=§a您已获得投影构筑菜单物品, 右键(长按)使用它以打开投影构筑菜单 @@ -105,90 +106,3 @@ construct.option.materialgrabber.howto=使用"材料收集器"物品与箱子等 construct.option.materialgrabber.grabbed.zero=§7已收集 0 个物品 construct.option.materialgrabber.grabbed.one=§a已收集 1 个物品 construct.option.materialgrabber.grabbed.many=§a已收集 %s 个物品 ## 插入字符串: 收集给玩家的物品数 - -## CLI shared errors -construct.commands.error.instanceNotFound=§cInstance "%1" not found. -construct.commands.error.notAPlayer=§c此命令只能由玩家使用 - -## construct:new -construct.commands.new=Create a new instance bound to a structure. -construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". -construct.commands.new.duplicateName=§cAn instance named "%1" already exists. -construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. - -## construct:delete -construct.commands.delete=Permanently delete an instance. -construct.commands.delete.success=§aDeleted instance "%1". - -## construct:rename -construct.commands.rename=Rename an existing instance. -construct.commands.rename.success=§aRenamed instance "%1" to "%2". -construct.commands.rename.duplicateName=§cAn instance named "%1" already exists. - -## construct:list -construct.commands.list=List all registered instances. -construct.commands.list.empty=§7No instances registered. -construct.commands.list.header=§eRegistered instances (%1):§r -construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r - -## construct:place -construct.commands.place=Place an instance at world coordinates (enables it). -construct.commands.place.success=§aPlaced instance "%1" at %2 %3 %4 in %5. - -## construct:move -construct.commands.move=Reposition an instance without toggling its enabled state. -construct.commands.move.success=§aMoved instance "%1" to %2 %3 %4 in %5. -construct.commands.move.posRequired=§cMust provide coordinates when not running as a player. - -## construct:active -construct.commands.active=Enable or disable an instance. -construct.commands.active.success=§aSet instance "%1" active=%2. -construct.commands.error.noLocation=§cInstance "%1" has no saved location. - -## construct:layer -construct.commands.layer=Set the active layer of an instance (0 = whole structure). -construct.commands.layer.success=§aSet instance "%1" layer to %2. -construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). - -## construct:nextlayer -construct.commands.nextlayer=Step layer up by one (wraps max to 0). -construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. - -## construct:prevlayer -construct.commands.prevlayer=Step layer down by one (wraps 0 to max). -construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. - -## construct:verifier -construct.commands.verifier=Toggle block validation overlay for an instance. -construct.commands.verifier.success=§aSet instance "%1" verifier=%2. - -## construct:option -construct.commands.option=Toggle a per-player builder option. -construct.commands.option.success=§aSet option "%1" = %2. -construct.commands.option.unknownOption=§cUnknown option "%1". - -## construct:info -construct.commands.info=Print instance details to chat. -construct.commands.info.header=§e=== Instance "%1" ===§r -construct.commands.info.structure=§7Structure:§r %1 -construct.commands.info.enabled=§7Enabled:§r %1 -construct.commands.info.location=§7Location:§r %1 %2 %3 in %4 -construct.commands.info.noLocation=§7Location:§r (none) -construct.commands.info.layer=§7Layer:§r %1 / %2 -construct.commands.info.verifier=§7Verifier:§r %1 -construct.commands.info.bounds=§7Bounds:§r (%1, %2, %3) -> (%4, %5, %6) - -## construct:stats -construct.commands.stats=Run the structure verifier and print statistics. -construct.commands.stats.alreadyRunning=§cA verification is already in progress. - -## construct:materials -construct.commands.materials=Print the material list for an instance. -construct.commands.materials.headerAll=§eMaterials for "%1":§r -construct.commands.materials.headerMissing=§eMissing materials for "%1":§r -construct.commands.materials.empty=§7(no materials) - -## construct:tag -construct.commands.tag=Rename the held Construct item to an instance name for quick-open. -construct.commands.tag.success=§aTagged held Construct item with instance "%1". -construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item. From 002120ab6927f9a5765d6a6a9e73f6b494256ce9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:16:50 -0700 Subject: [PATCH 24/46] feat: rename /new -> /create --- .../commands/{NewCommand.js => CreateCommand.js} | 10 +++++----- packs/BP/scripts/main.js | 2 +- packs/RP/texts/en_US.lang | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename packs/BP/scripts/commands/{NewCommand.js => CreateCommand.js} (84%) diff --git a/packs/BP/scripts/commands/NewCommand.js b/packs/BP/scripts/commands/CreateCommand.js similarity index 84% rename from packs/BP/scripts/commands/NewCommand.js rename to packs/BP/scripts/commands/CreateCommand.js index 4499fec..a198c77 100644 --- a/packs/BP/scripts/commands/NewCommand.js +++ b/packs/BP/scripts/commands/CreateCommand.js @@ -4,11 +4,11 @@ import { structureCollection } from '../classes/Structure/StructureCollection'; import { InstanceExistsError } from '../classes/Errors/InstanceExistsError'; import { StructureNotFoundError } from '../classes/Errors/StructureNotFoundError'; -export class NewCommand extends Command { +export class CreateCommand extends Command { constructor() { super({ - name: 'new', - description: 'construct.commands.new', + name: 'create', + description: 'construct.commands.create', mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'structureId', type: CustomCommandParamType.String } @@ -35,7 +35,7 @@ export class NewCommand extends Command { addStructure(source, instanceName, structureId) { structureCollection.add(instanceName, structureId); - source.sendMessage({ translate: 'construct.commands.new.success', with: [instanceName, structureId] }); + source.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] }); } handleStructureAdditionErrors(source, error) { @@ -47,4 +47,4 @@ export class NewCommand extends Command { } -export const newCommand = new NewCommand(); +export const createCommand = new CreateCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index b17ac0b..6af027f 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -11,7 +11,7 @@ import './classes/MenuItemHandler'; // Commands import './commands/ConstructCommand'; -import './commands/NewCommand'; +import './commands/CreateCommand'; import './commands/DeleteCommand'; import './commands/RenameCommand'; import './commands/ListCommand'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index b29a15e..c79b8f9 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -104,10 +104,10 @@ construct.commands.construct=Gives you the Construct item. Use it to open the Co 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. -## construct:new -construct.commands.new=Create a new instance bound to a structure. -construct.commands.new.success=§aCreated instance "%1" bound to structure "%2". -construct.commands.new.unknownStructure=§cNo structure with id "%1" found in the world. +## construct:create +construct.commands.create=Create a new Construct instance bound to a structure. +construct.commands.create.success=§aCreated instance "%1" bound to structure "%2". +construct.commands.create.unknownStructure=§cNo structure with id "%1" found in the world. ## construct:delete construct.commands.delete=Permanently delete an instance. From c188f3042889c5d5513e7d2f6e286c86cfb3d80f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:17:14 -0700 Subject: [PATCH 25/46] feat: make command descriptions more clearly about Construct --- packs/RP/texts/en_US.lang | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index c79b8f9..1079a2d 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -110,15 +110,15 @@ construct.commands.create.success=§aCreated instance "%1" bound to structure "% construct.commands.create.unknownStructure=§cNo structure with id "%1" found in the world. ## construct:delete -construct.commands.delete=Permanently delete an instance. +construct.commands.delete=Permanently delete a Construct instance. construct.commands.delete.success=§aDeleted instance "%1". ## construct:rename -construct.commands.rename=Rename an existing instance. +construct.commands.rename=Rename an existing Construct instance. construct.commands.rename.success=§aRenamed instance "%1" to "%2". ## construct:list -construct.commands.list=List all registered instances. +construct.commands.list=List all registered Construct instances. construct.commands.list.empty=§7No instances registered. construct.commands.list.header=§aRegistered instances (%1):§r construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r @@ -128,45 +128,45 @@ construct.commands.list.row.location= at %1 in %2 construct.commands.list.row.nolocation= (no location) ## construct:place -construct.commands.place=Place an instance at world coordinates (enables it). +construct.commands.place=Place a Construct instance in the world. construct.commands.place.success=§aPlaced instance "%1" at %2 in %3. ## construct:move -construct.commands.move=Reposition an instance without toggling its enabled state. +construct.commands.move=Reposition a Construct instance without toggling its enabled state. construct.commands.move.success=§aMoved instance "%1" to %2 in %3. construct.commands.move.locationRequired=§cMust provide a dimension and coordinates when not running as a player. ## construct:active -construct.commands.active=Enable or disable an instance. +construct.commands.active=Enable or disable a Construct instance. construct.commands.active.true=§aEnabled instance "%1". construct.commands.active.false=§aDisabled instance "%1". construct.commands.error.noLocation=§cInstance "%1" has no saved location. ## construct:layer -construct.commands.layer=Set the active layer of an instance (Use 0 for the whole structure). +construct.commands.layer=Set the active layer of a Construct instance (Use 0 for the whole structure). construct.commands.layer.success=§aSet instance "%1" layer to %2. construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). ## construct:nextlayer -construct.commands.nextlayer=Step layer up by one (wraps max to 0). +construct.commands.nextlayer=Step a Construct instance one layer up. construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. ## construct:prevlayer -construct.commands.prevlayer=Step layer down by one (wraps 0 to max). +construct.commands.prevlayer=Step a Construct instance one layer down. construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## construct:verifier -construct.commands.verifier=Toggle block validation overlay for an instance. +construct.commands.verifier=Toggle block validation overlay for a Construct instance. construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". ## construct:option -construct.commands.option=Toggle a per-player builder option. -construct.commands.option.success=§aSet option "%1" = %2. +construct.commands.option=Toggle your Construct builder options. +construct.commands.option.success=§aSet option "%1" to %2. construct.commands.option.unknownOption=§cUnknown option "%1". ## construct:info -construct.commands.info=Print instance details to chat. +construct.commands.info=Print Construct instance details to chat. construct.commands.info.header=§a=== Instance Info for "%1" ===§r construct.commands.info.structure=§7Structure:§r %1 construct.commands.info.location=§7Location:§r %1 in %2 @@ -177,11 +177,11 @@ construct.commands.info.verifier=§7Verifier running:§r %1 construct.commands.info.size=§7Size:§r %1 (%2 blocks) ## construct:stats -construct.commands.stats=Run the structure verifier and print statistics. +construct.commands.stats=Run the Construct structure verifier and print statistics. construct.commands.stats.alreadyRunning=§cA verification is already in progress. Please wait until it finishes. ## construct:materials -construct.commands.materials=Print the material list for an instance. +construct.commands.materials=Print the material list for a Construct instance. construct.commands.materials.headerAll=§aMaterials for "%1":§r construct.commands.materials.headerMissing=§aMissing materials for "%1":§r construct.commands.materials.empty=§7(no materials) From a97c9cb796e1b08d0003a5855348e2f0efc10cea Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:21:23 -0700 Subject: [PATCH 26/46] feat: rename /list -> /instances --- .../{ListCommand.js => InstancesCommand.js} | 22 +++++++++---------- packs/BP/scripts/main.js | 2 +- packs/RP/texts/en_US.lang | 18 +++++++-------- 3 files changed, 21 insertions(+), 21 deletions(-) rename packs/BP/scripts/commands/{ListCommand.js => InstancesCommand.js} (72%) diff --git a/packs/BP/scripts/commands/ListCommand.js b/packs/BP/scripts/commands/InstancesCommand.js similarity index 72% rename from packs/BP/scripts/commands/ListCommand.js rename to packs/BP/scripts/commands/InstancesCommand.js index dccd5e4..f3d628c 100644 --- a/packs/BP/scripts/commands/ListCommand.js +++ b/packs/BP/scripts/commands/InstancesCommand.js @@ -2,11 +2,11 @@ import { CommandPermissionLevel, CustomCommandStatus, system } from '@minecraft/ import { Command } from '../classes/Commands/Command'; import { structureCollection } from '../classes/Structure/StructureCollection'; -export class ListCommand extends Command { +export class InstancesCommand extends Command { constructor() { super({ - name: 'list', - description: 'construct.commands.list', + name: 'instances', + description: 'construct.commands.instances', permissionLevel: CommandPermissionLevel.Any, callback: (source) => this.run(source) }); @@ -15,16 +15,16 @@ export class ListCommand extends Command { run(source) { const names = structureCollection.getInstanceNames(); if (names.length === 0) - return { status: CustomCommandStatus.Success, message: 'construct.commands.list.empty' }; + return { status: CustomCommandStatus.Success, message: 'construct.commands.instances.empty' }; const rawtext = [ - { translate: 'construct.commands.list.header', with: [String(names.length)] }, + { translate: 'construct.commands.instances.header', with: [String(names.length)] }, { text: '\n' } ]; for (const name of names) { const instance = structureCollection.get(name); const status = this.formatStatus(instance); rawtext.push({ - translate: 'construct.commands.list.row', + translate: 'construct.commands.instances.row', with: { rawtext: [{ text: name }, { text: instance.getStructureId() }, status] } }); rawtext.push({ text: '\n' }); @@ -36,17 +36,17 @@ export class ListCommand extends Command { formatStatus(instance) { const statusMessage = { rawtext: [] }; if (instance.isEnabled()) - statusMessage.rawtext.push({ translate: 'construct.commands.list.row.enabled' }); + statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.enabled' }); else - statusMessage.rawtext.push({ translate: 'construct.commands.list.row.disabled' }); + statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.disabled' }); if (instance.hasLocation()) { const { dimensionId, location } = instance.getLocation(); - statusMessage.rawtext.push({ translate: 'construct.commands.list.row.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }); + statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }); } else { - statusMessage.rawtext.push({ translate: 'construct.commands.list.row.nolocation' }); + statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.nolocation' }); } return statusMessage; } } -export const listCommand = new ListCommand(); +export const instancesCommand = new InstancesCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 6af027f..78a64c0 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -14,7 +14,7 @@ import './commands/ConstructCommand'; import './commands/CreateCommand'; import './commands/DeleteCommand'; import './commands/RenameCommand'; -import './commands/ListCommand'; +import './commands/InstancesCommand'; import './commands/PlaceCommand'; import './commands/MoveCommand'; import './commands/ActiveCommand'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 1079a2d..04f56e4 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -117,15 +117,15 @@ construct.commands.delete.success=§aDeleted instance "%1". construct.commands.rename=Rename an existing Construct instance. construct.commands.rename.success=§aRenamed instance "%1" to "%2". -## construct:list -construct.commands.list=List all registered Construct instances. -construct.commands.list.empty=§7No instances registered. -construct.commands.list.header=§aRegistered instances (%1):§r -construct.commands.list.row=§a%1§r §8[§r%2§8]§r §7%3§r -construct.commands.list.row.enabled=enabled -construct.commands.list.row.disabled=disabled -construct.commands.list.row.location= at %1 in %2 -construct.commands.list.row.nolocation= (no location) +## construct:instances +construct.commands.instances=List all registered Construct instances. +construct.commands.instances.empty=§7No instances registered. +construct.commands.instances.header=§aRegistered instances (%1):§r +construct.commands.instances.row=§a%1§r §8[§r%2§8]§r §7%3§r +construct.commands.instances.row.enabled=enabled +construct.commands.instances.row.disabled=disabled +construct.commands.instances.row.location= at %1 in %2 +construct.commands.instances.row.nolocation= (no location) ## construct:place construct.commands.place=Place a Construct instance in the world. From eb5268cf11409a7d26c7cc55a0ed2981128f7216 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:26:59 -0700 Subject: [PATCH 27/46] feat: rename /info -> /instanceinfo --- ...{InfoCommand.js => InstanceInfoCommand.js} | 24 +++++++++---------- packs/BP/scripts/main.js | 2 +- packs/RP/texts/en_US.lang | 20 ++++++++-------- 3 files changed, 23 insertions(+), 23 deletions(-) rename packs/BP/scripts/commands/{InfoCommand.js => InstanceInfoCommand.js} (60%) diff --git a/packs/BP/scripts/commands/InfoCommand.js b/packs/BP/scripts/commands/InstanceInfoCommand.js similarity index 60% rename from packs/BP/scripts/commands/InfoCommand.js rename to packs/BP/scripts/commands/InstanceInfoCommand.js index 486aad1..a44b9d1 100644 --- a/packs/BP/scripts/commands/InfoCommand.js +++ b/packs/BP/scripts/commands/InstanceInfoCommand.js @@ -3,11 +3,11 @@ import { Command } from '../classes/Commands/Command'; import { structureCollection } from '../classes/Structure/StructureCollection'; import { Vector } from '../lib/Vector'; -export class InfoCommand extends Command { +export class InstanceInfoCommand extends Command { constructor() { super({ - name: 'info', - description: 'construct.commands.info', + name: 'instanceinfo', + description: 'construct.commands.instanceinfo', mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String } ], @@ -38,37 +38,37 @@ export class InfoCommand extends Command { } getHeaderText(instance) { - return { translate: 'construct.commands.info.header', with: [instance.getName()] }; + return { translate: 'construct.commands.instanceinfo.header', with: [instance.getName()] }; } getStructureIdText(instance) { - return { translate: 'construct.commands.info.structure', with: [instance.getStructureId()] }; + return { translate: 'construct.commands.instanceinfo.structure', with: [instance.getStructureId()] }; } getLocationText(instance) { if (!instance.hasLocation()) - return { translate: 'construct.commands.info.noLocation' }; + return { translate: 'construct.commands.instanceinfo.noLocation' }; const { dimensionId, location } = instance.getLocation(); - return { translate: 'construct.commands.info.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }; + return { translate: 'construct.commands.instanceinfo.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] }; } getEnabledText(instance) { - return { translate: 'construct.commands.info.enabled', with: [String(instance.isEnabled())] }; + return { translate: 'construct.commands.instanceinfo.enabled', with: [String(instance.isEnabled())] }; } getLayerText(instance) { - return { translate: 'construct.commands.info.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] }; + return { translate: 'construct.commands.instanceinfo.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] }; } getVerifierText(instance) { const verifier = instance.options.verifier; - return { translate: 'construct.commands.info.verifier', with: [String(verifier.isEnabled)] }; + return { translate: 'construct.commands.instanceinfo.verifier', with: [String(verifier.isEnabled)] }; } getSizeText(instance) { const bounds = instance.getBounds(); - return { translate: 'construct.commands.info.size', with: [bounds.max.toString(), Vector.volume(bounds.min, bounds.max).toString()] }; + return { translate: 'construct.commands.instanceinfo.size', with: [bounds.max.toString(), Vector.volume(bounds.min, bounds.max).toString()] }; } } -export const infoCommand = new InfoCommand(); +export const instanceInfoCommand = new InstanceInfoCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 78a64c0..48b1923 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -23,7 +23,7 @@ import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; import './commands/VerifierCommand'; import './commands/OptionCommand'; -import './commands/InfoCommand'; +import './commands/InstanceInfoCommand'; import './commands/StatsCommand'; import './commands/MaterialsCommand'; import './commands/TagCommand'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index 04f56e4..cbc3bea 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -165,16 +165,16 @@ construct.commands.option=Toggle your Construct builder options. construct.commands.option.success=§aSet option "%1" to %2. construct.commands.option.unknownOption=§cUnknown option "%1". -## construct:info -construct.commands.info=Print Construct instance details to chat. -construct.commands.info.header=§a=== Instance Info for "%1" ===§r -construct.commands.info.structure=§7Structure:§r %1 -construct.commands.info.location=§7Location:§r %1 in %2 -construct.commands.info.noLocation=§7Location:§r (none) -construct.commands.info.enabled=§7Enabled:§r %1 -construct.commands.info.layer=§7Layer:§r %1 / %2 -construct.commands.info.verifier=§7Verifier running:§r %1 -construct.commands.info.size=§7Size:§r %1 (%2 blocks) +## construct:instanceinfo +construct.commands.instanceinfo=Print Construct instance details to chat. +construct.commands.instanceinfo.header=§a=== Instance Info for "%1" ===§r +construct.commands.instanceinfo.structure=§7Structure:§r %1 +construct.commands.instanceinfo.location=§7Location:§r %1 in %2 +construct.commands.instanceinfo.noLocation=§7Location:§r (none) +construct.commands.instanceinfo.enabled=§7Enabled:§r %1 +construct.commands.instanceinfo.layer=§7Layer:§r %1 / %2 +construct.commands.instanceinfo.verifier=§7Verifier running:§r %1 +construct.commands.instanceinfo.size=§7Size:§r %1 (%2 blocks) ## construct:stats construct.commands.stats=Run the Construct structure verifier and print statistics. From 6eb61573c548b66201288c1536e105067b978f0f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 20 May 2026 17:27:15 -0700 Subject: [PATCH 28/46] feat: rename /active -> /enable --- .../commands/{ActiveCommand.js => EnableCommand.js} | 10 +++++----- packs/BP/scripts/main.js | 2 +- packs/RP/texts/en_US.lang | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename packs/BP/scripts/commands/{ActiveCommand.js => EnableCommand.js} (84%) diff --git a/packs/BP/scripts/commands/ActiveCommand.js b/packs/BP/scripts/commands/EnableCommand.js similarity index 84% rename from packs/BP/scripts/commands/ActiveCommand.js rename to packs/BP/scripts/commands/EnableCommand.js index a604e7f..e259f45 100644 --- a/packs/BP/scripts/commands/ActiveCommand.js +++ b/packs/BP/scripts/commands/EnableCommand.js @@ -2,11 +2,11 @@ import { Command } from '../classes/Commands/Command'; import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server'; import { structureCollection } from '../classes/Structure/StructureCollection'; -export class ActiveCommand extends Command { +export class EnableCommand extends Command { constructor() { super({ - name: 'active', - description: 'construct.commands.active', + name: 'enable', + description: 'construct.commands.enable', mandatoryParameters: [ { name: 'instanceName', type: CustomCommandParamType.String }, { name: 'state', type: CustomCommandParamType.Boolean } @@ -33,8 +33,8 @@ export class ActiveCommand extends Command { } sendFeedback(source, instanceName, state) { - source.sendMessage({ translate: state ? 'construct.commands.active.true' : 'construct.commands.active.false', with: [instanceName] }); + source.sendMessage({ translate: state ? 'construct.commands.enable.true' : 'construct.commands.enable.false', with: [instanceName] }); } } -export const activeCommand = new ActiveCommand(); +export const enableCommand = new EnableCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 48b1923..359cbcf 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -17,7 +17,7 @@ import './commands/RenameCommand'; import './commands/InstancesCommand'; import './commands/PlaceCommand'; import './commands/MoveCommand'; -import './commands/ActiveCommand'; +import './commands/EnableCommand'; import './commands/LayerCommand'; import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index cbc3bea..cb5e947 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -136,10 +136,10 @@ construct.commands.move=Reposition a Construct instance without toggling its ena construct.commands.move.success=§aMoved instance "%1" to %2 in %3. construct.commands.move.locationRequired=§cMust provide a dimension and coordinates when not running as a player. -## construct:active -construct.commands.active=Enable or disable a Construct instance. -construct.commands.active.true=§aEnabled instance "%1". -construct.commands.active.false=§aDisabled instance "%1". +## construct:enable +construct.commands.enable=Enable or disable a Construct instance. +construct.commands.enable.true=§aEnabled instance "%1". +construct.commands.enable.false=§aDisabled instance "%1". construct.commands.error.noLocation=§cInstance "%1" has no saved location. ## construct:layer From 8d492910ff2cdc5eb055c9b82494aa1d42ce2a1d Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 24 May 2026 13:51:46 -0700 Subject: [PATCH 29/46] chore: add Donation badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d185826..58f0598 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![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.20_(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) +[![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://buymeacoffee.com/forestoflight) --- From 1919ba7f16dd3963523e61e26cad8cfd34f256ab Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 24 May 2026 14:03:17 -0700 Subject: [PATCH 30/46] docs: add translation descriptions to en_US.lang --- packs/RP/texts/en_US.lang | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index cb5e947..ba4cc9f 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -106,75 +106,75 @@ construct.commands.construct.success=§aYou recieved the Construct item! Use it ## construct:create construct.commands.create=Create a new Construct instance bound to a structure. -construct.commands.create.success=§aCreated instance "%1" bound to structure "%2". -construct.commands.create.unknownStructure=§cNo structure with id "%1" found in the world. +construct.commands.create.success=§aCreated instance "%1" bound to structure "%2". ## Insert strings: instance name, structure name +construct.commands.create.unknownStructure=§cNo structure with id "%1" found in the world. ## Insert string: structure name ## construct:delete construct.commands.delete=Permanently delete a Construct instance. -construct.commands.delete.success=§aDeleted instance "%1". +construct.commands.delete.success=§aDeleted instance "%1". ## Insert string: instance name ## construct:rename construct.commands.rename=Rename an existing Construct instance. -construct.commands.rename.success=§aRenamed instance "%1" to "%2". +construct.commands.rename.success=§aRenamed instance "%1" to "%2". ## Insert strings: old instance name, new instance name ## construct:instances construct.commands.instances=List all registered Construct instances. construct.commands.instances.empty=§7No instances registered. -construct.commands.instances.header=§aRegistered instances (%1):§r -construct.commands.instances.row=§a%1§r §8[§r%2§8]§r §7%3§r +construct.commands.instances.header=§aRegistered instances (%1):§r ## Insert string: number of registered instances +construct.commands.instances.row=§a%1§r §8[§r%2§8]§r §7%3§r ## Insert strings: instance name, enabled/disabled, location (if available) construct.commands.instances.row.enabled=enabled construct.commands.instances.row.disabled=disabled -construct.commands.instances.row.location= at %1 in %2 +construct.commands.instances.row.location= at %1 in %2 ## Insert strings: coordinates, dimension construct.commands.instances.row.nolocation= (no location) ## construct:place construct.commands.place=Place a Construct instance in the world. -construct.commands.place.success=§aPlaced instance "%1" at %2 in %3. +construct.commands.place.success=§aPlaced instance "%1" at %2 in %3. ## Insert strings: instance name, coordinates, dimension ## construct:move construct.commands.move=Reposition a Construct instance without toggling its enabled state. -construct.commands.move.success=§aMoved instance "%1" to %2 in %3. +construct.commands.move.success=§aMoved instance "%1" to %2 in %3. ## Insert strings: instance name, coordinates, dimension construct.commands.move.locationRequired=§cMust provide a dimension and coordinates when not running as a player. ## construct:enable construct.commands.enable=Enable or disable a Construct instance. -construct.commands.enable.true=§aEnabled instance "%1". -construct.commands.enable.false=§aDisabled instance "%1". +construct.commands.enable.true=§aEnabled instance "%1". ## Insert string: instance name +construct.commands.enable.false=§aDisabled instance "%1". ## Insert string: instance name construct.commands.error.noLocation=§cInstance "%1" has no saved location. ## construct:layer construct.commands.layer=Set the active layer of a Construct instance (Use 0 for the whole structure). -construct.commands.layer.success=§aSet instance "%1" layer to %2. -construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). +construct.commands.layer.success=§aSet instance "%1" layer to %2. ## Insert strings: instance name, layer number +construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). ## Insert strings: layer number, instance name, max layer number ## construct:nextlayer construct.commands.nextlayer=Step a Construct instance one layer up. -construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. +construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. ## Insert strings: instance name, layer number ## construct:prevlayer construct.commands.prevlayer=Step a Construct instance one layer down. -construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. +construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## Insert strings: instance name, layer number ## construct:verifier construct.commands.verifier=Toggle block validation overlay for a Construct instance. -construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". -construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". +construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". ## Insert string: instance name +construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". ## Insert string: instance name ## construct:option construct.commands.option=Toggle your Construct builder options. -construct.commands.option.success=§aSet option "%1" to %2. -construct.commands.option.unknownOption=§cUnknown option "%1". +construct.commands.option.success=§aSet option "%1" to %2. ## Insert strings: option name, option value +construct.commands.option.unknownOption=§cUnknown option "%1". ## Insert string: option name ## construct:instanceinfo construct.commands.instanceinfo=Print Construct instance details to chat. -construct.commands.instanceinfo.header=§a=== Instance Info for "%1" ===§r -construct.commands.instanceinfo.structure=§7Structure:§r %1 -construct.commands.instanceinfo.location=§7Location:§r %1 in %2 -construct.commands.instanceinfo.noLocation=§7Location:§r (none) -construct.commands.instanceinfo.enabled=§7Enabled:§r %1 -construct.commands.instanceinfo.layer=§7Layer:§r %1 / %2 -construct.commands.instanceinfo.verifier=§7Verifier running:§r %1 -construct.commands.instanceinfo.size=§7Size:§r %1 (%2 blocks) +construct.commands.instanceinfo.header=§a=== Instance Info for "%1" ===§r ## Insert string: instance name +construct.commands.instanceinfo.structure=§7Structure:§r %1 ## Insert string: structure name +construct.commands.instanceinfo.location=§7Location:§r %1 in %2 ## Insert strings: coordinates, dimension +construct.commands.instanceinfo.noLocation=§7Location:§r (none) ## Used when the instance has no saved location +construct.commands.instanceinfo.enabled=§7Enabled:§r %1 ## Insert string: true/false indicating whether the instance is enabled +construct.commands.instanceinfo.layer=§7Layer:§r %1 / %2 ## Insert strings: current layer, max layer +construct.commands.instanceinfo.verifier=§7Verifier running:§r %1 ## Insert string: true/false indicating whether the verifier is running +construct.commands.instanceinfo.size=§7Size:§r %1 (%2 blocks) ## Insert strings: size (x, y, z), total block volume count ## construct:stats construct.commands.stats=Run the Construct structure verifier and print statistics. @@ -182,13 +182,13 @@ construct.commands.stats.alreadyRunning=§cA verification is already in progress ## construct:materials construct.commands.materials=Print the material list for a Construct instance. -construct.commands.materials.headerAll=§aMaterials for "%1":§r -construct.commands.materials.headerMissing=§aMissing materials for "%1":§r +construct.commands.materials.headerAll=§aMaterials for "%1":§r ## Insert string: instance name +construct.commands.materials.headerMissing=§aMissing materials for "%1":§r ## Insert string: instance name construct.commands.materials.empty=§7(no materials) ## construct:tag construct.commands.tag=Rename the held Construct item to an instance name for quick-open. -construct.commands.tag.success=§aTagged held Construct item with instance "%1". +construct.commands.tag.success=§aTagged held Construct item with instance "%1". ## Insert string: instance name construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item. ## Errors From 374022a527b99df6a6482838c8445b74b41d574c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 24 May 2026 21:36:32 -0700 Subject: [PATCH 31/46] feat: rename /option -> /builder --- .../{OptionCommand.js => BuilderCommand.js} | 24 +++++++++---------- packs/BP/scripts/main.js | 2 +- packs/RP/texts/en_US.lang | 8 +++---- 3 files changed, 17 insertions(+), 17 deletions(-) rename packs/BP/scripts/commands/{OptionCommand.js => BuilderCommand.js} (56%) diff --git a/packs/BP/scripts/commands/OptionCommand.js b/packs/BP/scripts/commands/BuilderCommand.js similarity index 56% rename from packs/BP/scripts/commands/OptionCommand.js rename to packs/BP/scripts/commands/BuilderCommand.js index ac05107..4693238 100644 --- a/packs/BP/scripts/commands/OptionCommand.js +++ b/packs/BP/scripts/commands/BuilderCommand.js @@ -3,36 +3,36 @@ import { Command } from '../classes/Commands/Command'; import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin'; import { BuilderOptions } from '../classes/Builder/BuilderOptions'; -export class OptionCommand extends Command { +export class BuilderCommand extends Command { constructor() { super({ - name: 'option', - description: 'construct.commands.option', + name: 'builder', + description: 'construct.commands.builder', allowedSources: [PlayerCommandOrigin], mandatoryParameters: [ - { name: 'optionId', type: CustomCommandParamType.Enum }, + { name: 'builderOption', type: CustomCommandParamType.Enum }, { name: 'state', type: CustomCommandParamType.Boolean } ], enums: [ - { name: 'optionId', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] } + { name: 'builderOption', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, optionId, state) => this.run(source, optionId, state) + callback: (source, builderOption, state) => this.run(source, builderOption, state) }); } - run(source, optionId, state) { - if (!BuilderOptions.get(optionId)) { - source.sendMessage({ translate: 'construct.commands.option.unknownOption', with: [optionId] }); + run(source, builderOption, state) { + if (!BuilderOptions.get(builderOption)) { + source.sendMessage({ translate: 'construct.commands.builder.unknownOption', with: [builderOption] }); return void 0; } system.run(() => { const player = source.getSource(); - BuilderOptions.setValue(optionId, player.id, state); - source.sendMessage({ translate: 'construct.commands.option.success', with: [optionId, String(state)] }); + BuilderOptions.setValue(builderOption, player.id, state); + source.sendMessage({ translate: 'construct.commands.builder.success', with: [builderOption, String(state)] }); }); return { status: CustomCommandStatus.Success }; } } -export const optionCommand = new OptionCommand(); +export const builderCommand = new BuilderCommand(); diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 359cbcf..6a73bd3 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -22,7 +22,7 @@ import './commands/LayerCommand'; import './commands/NextLayerCommand'; import './commands/PrevLayerCommand'; import './commands/VerifierCommand'; -import './commands/OptionCommand'; +import './commands/BuilderCommand'; import './commands/InstanceInfoCommand'; import './commands/StatsCommand'; import './commands/MaterialsCommand'; diff --git a/packs/RP/texts/en_US.lang b/packs/RP/texts/en_US.lang index ba4cc9f..82e7b3b 100644 --- a/packs/RP/texts/en_US.lang +++ b/packs/RP/texts/en_US.lang @@ -160,10 +160,10 @@ construct.commands.verifier=Toggle block validation overlay for a Construct inst construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". ## Insert string: instance name construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". ## Insert string: instance name -## construct:option -construct.commands.option=Toggle your Construct builder options. -construct.commands.option.success=§aSet option "%1" to %2. ## Insert strings: option name, option value -construct.commands.option.unknownOption=§cUnknown option "%1". ## Insert string: option name +## construct:builder +construct.commands.builder=Toggle your Construct builder options. +construct.commands.builder.success=§aSet option "%1" to %2. ## Insert strings: option name, option value +construct.commands.builder.unknownOption=§cUnknown option "%1". ## Insert string: option name ## construct:instanceinfo construct.commands.instanceinfo=Print Construct instance details to chat. From cb78fb7b9af1ed74364f0d183185627559877f09 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 26 May 2026 19:20:53 -0700 Subject: [PATCH 32/46] fix: pack import on Realms accomplished by removing comments from manifests --- packs/BP/manifest.json | 2 +- packs/RP/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packs/BP/manifest.json b/packs/BP/manifest.json index b15bf3f..2ecdc13 100644 --- a/packs/BP/manifest.json +++ b/packs/BP/manifest.json @@ -33,7 +33,7 @@ "version": "2.1.0-beta" }, { - "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", // Construct RP + "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", "version": [1, 0, 9] } ], diff --git a/packs/RP/manifest.json b/packs/RP/manifest.json index 46b0b82..88bede4 100644 --- a/packs/RP/manifest.json +++ b/packs/RP/manifest.json @@ -16,7 +16,7 @@ ], "dependencies": [ { - "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", // Construct BP + "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", "version": [1, 0, 9] } ], From e0e12f5dd0e1c3bc69c294ce52fcce047f1cfadb Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 27 May 2026 05:10:44 -0700 Subject: [PATCH 33/46] feat: add ready packet --- .../scripts/classes/Extensions/Ready.ipc.js | 3 + packs/BP/scripts/classes/Extensions/Ready.js | 6 + packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts | 103 +++ packs/BP/scripts/lib/MCBE-IPC/ipc.js | 595 ++++++++++++++++++ packs/BP/scripts/main.js | 1 + 5 files changed, 708 insertions(+) create mode 100644 packs/BP/scripts/classes/Extensions/Ready.ipc.js create mode 100644 packs/BP/scripts/classes/Extensions/Ready.js create mode 100644 packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts create mode 100644 packs/BP/scripts/lib/MCBE-IPC/ipc.js diff --git a/packs/BP/scripts/classes/Extensions/Ready.ipc.js b/packs/BP/scripts/classes/Extensions/Ready.ipc.js new file mode 100644 index 0000000..5f1d47e --- /dev/null +++ b/packs/BP/scripts/classes/Extensions/Ready.ipc.js @@ -0,0 +1,3 @@ +import { PROTO } from '../../lib/MCBE-IPC/ipc' + +export const Ready = PROTO.Void; \ No newline at end of file diff --git a/packs/BP/scripts/classes/Extensions/Ready.js b/packs/BP/scripts/classes/Extensions/Ready.js new file mode 100644 index 0000000..a6b811e --- /dev/null +++ b/packs/BP/scripts/classes/Extensions/Ready.js @@ -0,0 +1,6 @@ +import { system } from "@minecraft/server"; +import { Ready } from "./Ready.ipc"; + +system.runTimeout(() => { + IPC.send(`constructExtension:ready`, Ready, void 0); +}, 1); \ No newline at end of file diff --git a/packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts b/packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts new file mode 100644 index 0000000..dc6ef67 --- /dev/null +++ b/packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts @@ -0,0 +1,103 @@ +/** + * @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/MCBE-IPC/ipc.js b/packs/BP/scripts/lib/MCBE-IPC/ipc.js new file mode 100644 index 0000000..8866a9c --- /dev/null +++ b/packs/BP/scripts/lib/MCBE-IPC/ipc.js @@ -0,0 +1,595 @@ +/** + * @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) { + const actualValue = typeof value === 'function' ? value() : value; + yield* PROTO.UVarInt32.serialize(array.length, stream); + for (const item of array) { + yield* actualValue.serialize(item, stream); + } + }, + *deserialize(stream) { + const actualValue = typeof value === 'function' ? value() : value; + const result = []; + const length = yield* PROTO.UVarInt32.deserialize(stream); + for (let i = 0; i < length; i++) { + result[i] = yield* actualValue.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; diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 6a73bd3..10a7679 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -30,3 +30,4 @@ import './commands/TagCommand'; // Other import './classes/BlockInfo'; +import './classes/Extensions/Ready' \ No newline at end of file From f7507107325ddf8bcccec97ee516839320d4e1f9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 29 May 2026 23:26:49 +0200 Subject: [PATCH 34/46] refactor: rename source -> origin in all commands --- packs/BP/scripts/commands/BuilderCommand.js | 10 +++++----- packs/BP/scripts/commands/ConstructCommand.js | 6 +++--- packs/BP/scripts/commands/CreateCommand.js | 20 +++++++++---------- packs/BP/scripts/commands/DeleteCommand.js | 6 +++--- packs/BP/scripts/commands/EnableCommand.js | 12 +++++------ .../scripts/commands/InstanceInfoCommand.js | 6 +++--- packs/BP/scripts/commands/InstancesCommand.js | 6 +++--- packs/BP/scripts/commands/LayerCommand.js | 8 ++++---- packs/BP/scripts/commands/MaterialsCommand.js | 18 ++++++++--------- packs/BP/scripts/commands/MoveCommand.js | 10 +++++----- packs/BP/scripts/commands/NextLayerCommand.js | 6 +++--- packs/BP/scripts/commands/PlaceCommand.js | 6 +++--- packs/BP/scripts/commands/PrevLayerCommand.js | 6 +++--- packs/BP/scripts/commands/RenameCommand.js | 8 ++++---- packs/BP/scripts/commands/StatsCommand.js | 6 +++--- packs/BP/scripts/commands/TagCommand.js | 8 ++++---- packs/BP/scripts/commands/VerifierCommand.js | 10 +++++----- 17 files changed, 76 insertions(+), 76 deletions(-) diff --git a/packs/BP/scripts/commands/BuilderCommand.js b/packs/BP/scripts/commands/BuilderCommand.js index 4693238..065eb1a 100644 --- a/packs/BP/scripts/commands/BuilderCommand.js +++ b/packs/BP/scripts/commands/BuilderCommand.js @@ -17,19 +17,19 @@ export class BuilderCommand extends Command { { name: 'builderOption', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, builderOption, state) => this.run(source, builderOption, state) + callback: (origin, builderOption, state) => this.run(origin, builderOption, state) }); } - run(source, builderOption, state) { + run(origin, builderOption, state) { if (!BuilderOptions.get(builderOption)) { - source.sendMessage({ translate: 'construct.commands.builder.unknownOption', with: [builderOption] }); + origin.sendMessage({ translate: 'construct.commands.builder.unknownOption', with: [builderOption] }); return void 0; } system.run(() => { - const player = source.getSource(); + const player = origin.getSource(); BuilderOptions.setValue(builderOption, player.id, state); - source.sendMessage({ translate: 'construct.commands.builder.success', with: [builderOption, String(state)] }); + origin.sendMessage({ translate: 'construct.commands.builder.success', with: [builderOption, String(state)] }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/ConstructCommand.js b/packs/BP/scripts/commands/ConstructCommand.js index 4a95a53..94d80a9 100644 --- a/packs/BP/scripts/commands/ConstructCommand.js +++ b/packs/BP/scripts/commands/ConstructCommand.js @@ -11,12 +11,12 @@ export class ConstructCommand extends Command { cheatsRequired: false, allowedSources: [PlayerCommandOrigin], permissionLevel: CommandPermissionLevel.Any, - callback: (source) => this.run(source) + callback: (origin) => this.run(origin) }); } - run(source) { - const player = source.getSource(); + run(origin) { + const player = origin.getSource(); system.run(() => { this.giveMenuItem(player); }); diff --git a/packs/BP/scripts/commands/CreateCommand.js b/packs/BP/scripts/commands/CreateCommand.js index a198c77..d169a4b 100644 --- a/packs/BP/scripts/commands/CreateCommand.js +++ b/packs/BP/scripts/commands/CreateCommand.js @@ -14,33 +14,33 @@ export class CreateCommand extends Command { { name: 'structureId', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, structureId) => this.run(source, instanceName, structureId) + callback: (origin, instanceName, structureId) => this.run(origin, instanceName, structureId) }); } - run(source, instanceName, structureId) { - this.tryAddStructure(source, instanceName, structureId); + run(origin, instanceName, structureId) { + this.tryAddStructure(origin, instanceName, structureId); return { status: CustomCommandStatus.Success }; } - tryAddStructure(source, instanceName, structureId) { + tryAddStructure(origin, instanceName, structureId) { system.run(() => { try { - this.addStructure(source, instanceName, structureId); + this.addStructure(origin, instanceName, structureId); } catch (error) { - this.handleStructureAdditionErrors(source, error); + this.handleStructureAdditionErrors(origin, error); } }); } - addStructure(source, instanceName, structureId) { + addStructure(origin, instanceName, structureId) { structureCollection.add(instanceName, structureId); - source.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] }); + origin.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] }); } - handleStructureAdditionErrors(source, error) { + handleStructureAdditionErrors(origin, error) { if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) - error.sendTo(source); + error.sendTo(origin); else throw error; } diff --git a/packs/BP/scripts/commands/DeleteCommand.js b/packs/BP/scripts/commands/DeleteCommand.js index 1ce4361..a027367 100644 --- a/packs/BP/scripts/commands/DeleteCommand.js +++ b/packs/BP/scripts/commands/DeleteCommand.js @@ -11,15 +11,15 @@ export class DeleteCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); system.run(() => { structureCollection.delete(instanceName); - source.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] }); + origin.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/EnableCommand.js b/packs/BP/scripts/commands/EnableCommand.js index e259f45..939af54 100644 --- a/packs/BP/scripts/commands/EnableCommand.js +++ b/packs/BP/scripts/commands/EnableCommand.js @@ -12,14 +12,14 @@ export class EnableCommand extends Command { { name: 'state', type: CustomCommandParamType.Boolean } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, state) => this.run(source, instanceName, state) + callback: (origin, instanceName, state) => this.run(origin, instanceName, state) }); } - run(source, instanceName, state) { + run(origin, instanceName, state) { const instance = structureCollection.get(instanceName); if (state && !instance.hasLocation()) { - source.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] }); + origin.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] }); return void 0; } system.run(() => { @@ -27,13 +27,13 @@ export class EnableCommand extends Command { instance.enable(); else instance.disable(); - this.sendFeedback(source, instanceName, state); + this.sendFeedback(origin, instanceName, state); }); return { status: CustomCommandStatus.Success }; } - sendFeedback(source, instanceName, state) { - source.sendMessage({ translate: state ? 'construct.commands.enable.true' : 'construct.commands.enable.false', with: [instanceName] }); + sendFeedback(origin, instanceName, state) { + origin.sendMessage({ translate: state ? 'construct.commands.enable.true' : 'construct.commands.enable.false', with: [instanceName] }); } } diff --git a/packs/BP/scripts/commands/InstanceInfoCommand.js b/packs/BP/scripts/commands/InstanceInfoCommand.js index a44b9d1..9b61230 100644 --- a/packs/BP/scripts/commands/InstanceInfoCommand.js +++ b/packs/BP/scripts/commands/InstanceInfoCommand.js @@ -12,11 +12,11 @@ export class InstanceInfoCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); const message = { rawtext: [ this.getHeaderText(instance), @@ -33,7 +33,7 @@ export class InstanceInfoCommand extends Command { { text: '\n' }, this.getSizeText(instance) ]}; - source.sendMessage(message); + origin.sendMessage(message); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/InstancesCommand.js b/packs/BP/scripts/commands/InstancesCommand.js index f3d628c..14dbba8 100644 --- a/packs/BP/scripts/commands/InstancesCommand.js +++ b/packs/BP/scripts/commands/InstancesCommand.js @@ -8,11 +8,11 @@ export class InstancesCommand extends Command { name: 'instances', description: 'construct.commands.instances', permissionLevel: CommandPermissionLevel.Any, - callback: (source) => this.run(source) + callback: (origin) => this.run(origin) }); } - run(source) { + run(origin) { const names = structureCollection.getInstanceNames(); if (names.length === 0) return { status: CustomCommandStatus.Success, message: 'construct.commands.instances.empty' }; @@ -29,7 +29,7 @@ export class InstancesCommand extends Command { }); rawtext.push({ text: '\n' }); } - source.sendMessage({ rawtext }); + origin.sendMessage({ rawtext }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/LayerCommand.js b/packs/BP/scripts/commands/LayerCommand.js index a926fbf..f0d0a57 100644 --- a/packs/BP/scripts/commands/LayerCommand.js +++ b/packs/BP/scripts/commands/LayerCommand.js @@ -12,19 +12,19 @@ export class LayerCommand extends Command { { name: 'layer', type: CustomCommandParamType.Integer } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, layer) => this.run(source, instanceName, layer) + callback: (origin, instanceName, layer) => this.run(origin, instanceName, layer) }); } - run(source, instanceName, layer) { + run(origin, instanceName, layer) { const instance = structureCollection.get(instanceName); const max = instance.getMaxLayer(); if (layer < 0 || layer > max) { - source.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] }); + origin.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] }); return void 0; } instance.setLayer(layer); - source.sendMessage({ translate: 'construct.commands.layer.success', with: [instanceName, String(layer)] }); + origin.sendMessage({ translate: 'construct.commands.layer.success', with: [instanceName, String(layer)] }); return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/MaterialsCommand.js b/packs/BP/scripts/commands/MaterialsCommand.js index b150048..d56f79c 100644 --- a/packs/BP/scripts/commands/MaterialsCommand.js +++ b/packs/BP/scripts/commands/MaterialsCommand.js @@ -16,39 +16,39 @@ export class MaterialsCommand extends Command { { name: 'missing', type: CustomCommandParamType.Boolean } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, missing) => this.run(source, instanceName, missing) + callback: (origin, instanceName, missing) => this.run(origin, instanceName, missing) }); } - run(source, instanceName, missing) { + run(origin, instanceName, missing) { const instance = structureCollection.get(instanceName); const onlyMissing = missing === true; if (onlyMissing) - this.assertIsPlayer(source); + this.assertIsPlayer(origin); const headerKey = onlyMissing ? 'construct.commands.materials.headerMissing' : 'construct.commands.materials.headerAll'; const rawtext = [ { translate: headerKey, with: [instanceName] }, { text: '\n' } ]; - const list = this.getMaterialList(source, instance, onlyMissing); + const list = this.getMaterialList(origin, instance, onlyMissing); if (!list.rawtext || list.rawtext.length === 0) rawtext.push({ translate: 'construct.commands.materials.empty' }); else rawtext.push(list); - source.sendMessage({ rawtext }); + origin.sendMessage({ rawtext }); return { status: CustomCommandStatus.Success }; } - assertIsPlayer(source) { - if (!(source instanceof PlayerCommandOrigin)) + assertIsPlayer(origin) { + if (!(origin instanceof PlayerCommandOrigin)) throw new NotAPlayerError(); } - getMaterialList(source, instance, onlyMissing) { + getMaterialList(origin, instance, onlyMissing) { const materials = instance.getActiveMaterials(); let container; if (onlyMissing) { - const player = source.getSource(); + const player = origin.getSource(); const inventoryComponent = player?.getComponent(EntityComponentTypes.Inventory); container = inventoryComponent?.container; } diff --git a/packs/BP/scripts/commands/MoveCommand.js b/packs/BP/scripts/commands/MoveCommand.js index 88f7099..e21add8 100644 --- a/packs/BP/scripts/commands/MoveCommand.js +++ b/packs/BP/scripts/commands/MoveCommand.js @@ -17,16 +17,16 @@ export class MoveCommand extends Command { { name: 'location', type: CustomCommandParamType.Location } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, dimensionId, location) => this.run(source, instanceName, dimensionId, location) + callback: (origin, instanceName, dimensionId, location) => this.run(origin, instanceName, dimensionId, location) }); } - run(source, instanceName, dimensionId, location) { + run(origin, instanceName, dimensionId, location) { const instance = structureCollection.get(instanceName); if (dimensionId === void 0 || location === void 0) { - if (!(source instanceof PlayerCommandOrigin)) + if (!(origin instanceof PlayerCommandOrigin)) return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.locationRequired' }; - const player = source.getSource(); + const player = origin.getSource(); location = player.location; dimensionId = player.dimension.id; } @@ -34,7 +34,7 @@ export class MoveCommand extends Command { const flooredLocation = Vector.from(location).floor(); system.run(() => { instance.move(dimensionId, flooredLocation); - source.sendMessage({ translate: 'construct.commands.move.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); + origin.sendMessage({ translate: 'construct.commands.move.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/NextLayerCommand.js b/packs/BP/scripts/commands/NextLayerCommand.js index 6622dc9..c42ca49 100644 --- a/packs/BP/scripts/commands/NextLayerCommand.js +++ b/packs/BP/scripts/commands/NextLayerCommand.js @@ -11,14 +11,14 @@ export class NextLayerCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); instance.increaseLayer(); - source.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] }); + origin.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] }); return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/PlaceCommand.js b/packs/BP/scripts/commands/PlaceCommand.js index 99e63d4..210fa5b 100644 --- a/packs/BP/scripts/commands/PlaceCommand.js +++ b/packs/BP/scripts/commands/PlaceCommand.js @@ -16,17 +16,17 @@ export class PlaceCommand extends Command { { name: 'location', type: CustomCommandParamType.Location } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, dimensionId, location) => this.run(source, instanceName, dimensionId, location) + callback: (origin, instanceName, dimensionId, location) => this.run(origin, instanceName, dimensionId, location) }); } - run(source, instanceName, dimensionId, location) { + run(origin, instanceName, dimensionId, location) { const instance = structureCollection.get(instanceName); const flooredLocation = Vector.from(location).floor(); this.assertDimensionExists(dimensionId); system.run(() => { instance.place(dimensionId, flooredLocation); - source.sendMessage({ translate: 'construct.commands.place.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); + origin.sendMessage({ translate: 'construct.commands.place.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/PrevLayerCommand.js b/packs/BP/scripts/commands/PrevLayerCommand.js index 7fc485e..d51a209 100644 --- a/packs/BP/scripts/commands/PrevLayerCommand.js +++ b/packs/BP/scripts/commands/PrevLayerCommand.js @@ -11,14 +11,14 @@ export class PrevLayerCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); instance.decreaseLayer(); - source.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] }); + origin.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] }); return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/RenameCommand.js b/packs/BP/scripts/commands/RenameCommand.js index e915a4e..5c41d3e 100644 --- a/packs/BP/scripts/commands/RenameCommand.js +++ b/packs/BP/scripts/commands/RenameCommand.js @@ -12,18 +12,18 @@ export class RenameCommand extends Command { { name: 'newName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, newName) => this.run(source, instanceName, newName) + callback: (origin, instanceName, newName) => this.run(origin, instanceName, newName) }); } - run(source, instanceName, newName) { + run(origin, instanceName, newName) { const instance = structureCollection.get(instanceName); if (structureCollection.has(newName)) { - source.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] }); + origin.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] }); return void 0; } structureCollection.rename(instanceName, newName); - source.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] }); + origin.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] }); return { status: CustomCommandStatus.Success }; } } diff --git a/packs/BP/scripts/commands/StatsCommand.js b/packs/BP/scripts/commands/StatsCommand.js index a117e38..aac2bf0 100644 --- a/packs/BP/scripts/commands/StatsCommand.js +++ b/packs/BP/scripts/commands/StatsCommand.js @@ -14,16 +14,16 @@ export class StatsCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); if (this.structureVerifier) return { status: CustomCommandStatus.Failure, error: 'construct.commands.stats.alreadyRunning' }; system.run(async () => { - source.sendMessage(await this.getStatsMessage(instance)); + origin.sendMessage(await this.getStatsMessage(instance)); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/TagCommand.js b/packs/BP/scripts/commands/TagCommand.js index 663ed1a..1073217 100644 --- a/packs/BP/scripts/commands/TagCommand.js +++ b/packs/BP/scripts/commands/TagCommand.js @@ -14,13 +14,13 @@ export class TagCommand extends Command { { name: 'instanceName', type: CustomCommandParamType.String } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName) => this.run(source, instanceName) + callback: (origin, instanceName) => this.run(origin, instanceName) }); } - run(source, instanceName) { + run(origin, instanceName) { const instance = structureCollection.get(instanceName); - const player = source.getSource(); + const player = origin.getSource(); const equipment = player.getComponent(EntityComponentTypes.Equippable); const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand); if (itemStack?.typeId !== MENU_ITEM) @@ -28,7 +28,7 @@ export class TagCommand extends Command { system.run(() => { itemStack.nameTag = instanceName; equipment.setEquipment(EquipmentSlot.Mainhand, itemStack); - source.sendMessage({ translate: 'construct.commands.tag.success', with: [instanceName] }); + origin.sendMessage({ translate: 'construct.commands.tag.success', with: [instanceName] }); }); return { status: CustomCommandStatus.Success }; } diff --git a/packs/BP/scripts/commands/VerifierCommand.js b/packs/BP/scripts/commands/VerifierCommand.js index 2d362b2..d5ce996 100644 --- a/packs/BP/scripts/commands/VerifierCommand.js +++ b/packs/BP/scripts/commands/VerifierCommand.js @@ -12,22 +12,22 @@ export class VerifierCommand extends Command { { name: 'state', type: CustomCommandParamType.Boolean } ], permissionLevel: CommandPermissionLevel.Any, - callback: (source, instanceName, state) => this.run(source, instanceName, state) + callback: (origin, instanceName, state) => this.run(origin, instanceName, state) }); } - run(source, instanceName, state) { + run(origin, instanceName, state) { const instance = structureCollection.get(instanceName); if (state) instance.setVerifierEnabled(true); else instance.setVerifierEnabled(false); - this.sendFeedback(source, instanceName, state); + this.sendFeedback(origin, instanceName, state); return { status: CustomCommandStatus.Success }; } - sendFeedback(source, instanceName, state) { - source.sendMessage({ translate: state ? 'construct.commands.verifier.enabled' : 'construct.commands.verifier.disabled', with: [instanceName] }); + sendFeedback(origin, instanceName, state) { + origin.sendMessage({ translate: state ? 'construct.commands.verifier.enabled' : 'construct.commands.verifier.disabled', with: [instanceName] }); } } From 2b90020e3769bb0ea26ecb6a8483345cf82a391a Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 29 May 2026 23:27:11 +0200 Subject: [PATCH 35/46] wip: Construct API --- packs/BP/scripts/API/ConstructAPI.js | 18 ++++ .../API/controllers/BuildersController.js | 37 ++++++++ .../API/controllers/InstancesController.js | 80 ++++++++++++++++ packs/BP/scripts/API/models/BuildersModel.js | 13 +++ packs/BP/scripts/API/models/InstancesModel.js | 44 +++++++++ packs/BP/scripts/classes/Builder/Builder.js | 17 ++++ packs/BP/scripts/classes/Builder/Builders.js | 19 ++-- .../classes/Errors/BuilderNotFoundError.js | 6 ++ .../classes/Errors/CommandResponseError.js | 4 +- .../scripts/classes/Extensions/Ready.ipc.js | 3 - packs/BP/scripts/classes/Extensions/Ready.js | 6 -- .../classes/Instance/InstanceOptions.js | 16 ++-- .../classes/Instance/StructureInstance.js | 45 ++++++++- .../classes/Materials/StructureMaterials.js | 4 + packs/BP/scripts/lib/AddonAPIKit/API.js | 92 +++++++++++++++++++ .../scripts/lib/AddonAPIKit/APIController.js | 13 +++ packs/BP/scripts/lib/AddonAPIKit/APIModels.js | 20 ++++ .../BP/scripts/lib/AddonAPIKit/AddonAPIKit.js | 7 ++ .../lib/AddonAPIKit/Errors/APICallerError.js | 11 +++ .../lib/AddonAPIKit/Errors/APIErrorEnum.js | 6 ++ .../lib/AddonAPIKit/Errors/APIServerError.js | 10 ++ .../Errors/APIVersionMismatchError.js | 6 ++ .../lib/{ => AddonAPIKit}/MCBE-IPC/ipc.d.ts | 0 .../lib/{ => AddonAPIKit}/MCBE-IPC/ipc.js | 0 packs/BP/scripts/main.js | 6 +- 25 files changed, 454 insertions(+), 29 deletions(-) create mode 100644 packs/BP/scripts/API/ConstructAPI.js create mode 100644 packs/BP/scripts/API/controllers/BuildersController.js create mode 100644 packs/BP/scripts/API/controllers/InstancesController.js create mode 100644 packs/BP/scripts/API/models/BuildersModel.js create mode 100644 packs/BP/scripts/API/models/InstancesModel.js create mode 100644 packs/BP/scripts/classes/Errors/BuilderNotFoundError.js delete mode 100644 packs/BP/scripts/classes/Extensions/Ready.ipc.js delete mode 100644 packs/BP/scripts/classes/Extensions/Ready.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/API.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/APIController.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/APIModels.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js create mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js rename packs/BP/scripts/lib/{ => AddonAPIKit}/MCBE-IPC/ipc.d.ts (100%) rename packs/BP/scripts/lib/{ => AddonAPIKit}/MCBE-IPC/ipc.js (100%) diff --git a/packs/BP/scripts/API/ConstructAPI.js b/packs/BP/scripts/API/ConstructAPI.js new file mode 100644 index 0000000..f29736d --- /dev/null +++ b/packs/BP/scripts/API/ConstructAPI.js @@ -0,0 +1,18 @@ +import { AddonAPI } from "../lib/AddonAPIKit/AddonAPIKit"; +import { PACK_IDENTIFIER } from "../consts"; +import { InstancesController } from "./controllers/InstancesController"; +import { structureCollection } from "../classes/Structure/StructureCollection"; +import { BuildersController } from "./controllers/BuildersController"; +import { Builders } from "../classes/Builder/Builders"; + +class ConstructAPI extends AddonAPI { + constructor(version) { + super(PACK_IDENTIFIER, version); + const instancesController = new InstancesController(structureCollection); + this.setupController(instancesController); + const buildersController = new BuildersController(Builders); + this.setupController(buildersController); + } +} + +export const constructAPI = new ConstructAPI("1.0.0"); \ No newline at end of file diff --git a/packs/BP/scripts/API/controllers/BuildersController.js b/packs/BP/scripts/API/controllers/BuildersController.js new file mode 100644 index 0000000..ddea020 --- /dev/null +++ b/packs/BP/scripts/API/controllers/BuildersController.js @@ -0,0 +1,37 @@ +import { BuilderNotFoundError } from "../../classes/Errors/BuilderNotFoundError"; +import { APICallerError } from "../../lib/AddonAPIKit/AddonAPIKit"; +import { BuilderIdParameterModel } from "../models/BuildersModel"; + +export class BuildersController extends APIController { + #context; + + constructor(context) { + super({ + "builders:get": { callback: this.getBuilder, parameterModel: BuilderIdParameterModel, returnModel: InstanceModel }, + "builders:edit": { callback: this.editBuilder, parameterModel: InstanceModel, returnModel: InstanceModel } + }); + this.#context = context; + } + + getBuilder(playerId) { + try { + const builder = this.#context.get(playerId); + return builder.asPacket(); + } catch(error) { + if (error instanceof BuilderNotFoundErrors) + throw new APICallerError(error); + throw error; + } + } + + editBuilder(builderOptions) { + try { + const builder = this.#context.get(builderOptions.playerId); + builder.setOptions(builderOptions); + } catch(error) { + if (error instanceof BuilderNotFoundError) + throw new APICallerError(error); + throw error; + } + } +} diff --git a/packs/BP/scripts/API/controllers/InstancesController.js b/packs/BP/scripts/API/controllers/InstancesController.js new file mode 100644 index 0000000..e84d568 --- /dev/null +++ b/packs/BP/scripts/API/controllers/InstancesController.js @@ -0,0 +1,80 @@ +import { InstanceExistsError } from "../../classes/Errors/InstanceExistsError"; +import { InstanceNotFoundError } from "../../classes/Errors/InstanceNotFoundError"; +import { StructureNotFoundError } from "../../classes/Errors/StructureNotFoundError"; +import { APICallerError, VoidModel } from "../../lib/AddonAPIKit/AddonAPIKit"; +import { AddInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../models/InstancesModel"; + +export class InstancesController extends APIController { + #context; + + constructor(context) { + super({ + "instances": { callback: this.getInstances, parameterModel: VoidModel, returnModel: InstancesModel }, + "instance:get": { callback: this.getInstance, parameterModel: InstanceNameParameterModel, returnModel: InstanceModel }, + "instance:add": { callback: this.addInstance, parameterModel: AddInstanceParameterModel, returnModel: InstanceModel }, + "instance:edit": { callback: this.editInstance, parameterModel: InstanceModel, returnModel: InstanceModel }, + "instance:delete": { callback: this.deleteInstance, parameterModel: InstanceNameParameterModel, returnModel: VoidModel }, + "instance:materials": { callback: this.getMaterials, parameterModel: InstanceNameParameterModel, returnModel: StructureMaterialsModel } + }); + this.#context = context; + } + + getInstances() { + return this.#context.getInstanceNames(); + } + + getInstance(instanceName) { + try { + const instance = this.#context.get(instanceName); + return instance.asPacket(); + } catch(error) { + if (error instanceof InstanceNotFoundError) + throw new APICallerError(error); + throw error; + } + } + + addInstance(instanceName, structureId) { + try { + const instance = this.#context.add(instanceName, structureId); + return instance.asPacket(); + } catch(error) { + if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) + throw new APICallerError(error); + throw error; + } + } + + editInstance(instanceName, instanceOptions) { + try { + const instance = this.#context.get(instanceName); + instance.setOptions(instanceOptions); + } catch(error) { + if (error instanceof InstanceNotFoundError || error instanceof InstanceExistsError || error instanceof StructureNotFoundError) + throw new APICallerError(error); + throw error; + } + } + + deleteInstance(instanceName) { + try { + this.#context.delete(instanceName); + } catch (error) { + if (error instanceof InstanceNotFoundError) + throw new APICallerError(error); + throw error; + } + } + + getMaterials(instanceName) { + try { + const instance = this.#context.get(instanceName); + const structureMaterials = instance.getActiveMaterials(); + return structureMaterials.allMaterials; + } catch(error) { + if (error instanceof InstanceNotFoundError) + throw new APICallerError(error); + throw error; + } + } +} diff --git a/packs/BP/scripts/API/models/BuildersModel.js b/packs/BP/scripts/API/models/BuildersModel.js new file mode 100644 index 0000000..36f2b3d --- /dev/null +++ b/packs/BP/scripts/API/models/BuildersModel.js @@ -0,0 +1,13 @@ +import { PROTO } from "../../lib/AddonAPIKit/AddonAPIKit"; + +export const BuilderModel = PROTO.Object({ + playerId: PROTO.String, + easyPlace: PROTO.Boolean, + fastEasyPlace: PROTO.Boolean, + materialGrabber: PROTO.Boolean, + materialInstanceName: PROTO.String +}); + +export const BuilderIdParameterModel = PROTO.Object({ + playerId: PROTO.String +}); \ No newline at end of file diff --git a/packs/BP/scripts/API/models/InstancesModel.js b/packs/BP/scripts/API/models/InstancesModel.js new file mode 100644 index 0000000..1382190 --- /dev/null +++ b/packs/BP/scripts/API/models/InstancesModel.js @@ -0,0 +1,44 @@ +import { PROTO } from '../../lib/AddonAPIKit/AddonAPIKit'; + +const LocationModel = PROTO.Object({ + x: PROTO.Float64, + y: PROTO.Float64, + z: PROTO.Float64 +}); + +export const InstanceModel = PROTO.Object({ + name: PROTO.String, + structureId: PROTO.String, + isEnabled: PROTO.Boolean, + dimensionId: PROTO.Optional(PROTO.String), + location: PROTO.Optional(LocationModel), + bounds: PROTO.Optional(PROTO.Object({ + min: LocationModel, + max: LocationModel + })), + currentLayer: PROTO.Int16, + maxLayer: PROTO.Int16, + verifier: PROTO.Object({ + isEnabled: PROTO.Boolean, + trackPlayerDistance: PROTO.Int8, + particleLifetime: PROTO.Int32 + }) +}); + +export const InstancesModel = PROTO.Array(InstanceModel); + +export const StructureMaterialsModel = PROTO.Map(PROTO.String, PROTO.Int32); + +export const InstanceNameParameterModel = PROTO.Object({ + instanceName: PROTO.String +}); + +export const AddInstanceParameterModel = PROTO.Object({ + instanceName: PROTO.String, + structureId: PROTO.String +}); + +export const EditInstanceParameterModel = PROTO.Object({ + instanceName: PROTO.String, + instance: InstanceModel +}); diff --git a/packs/BP/scripts/classes/Builder/Builder.js b/packs/BP/scripts/classes/Builder/Builder.js index a2b6607..91ba6b1 100644 --- a/packs/BP/scripts/classes/Builder/Builder.js +++ b/packs/BP/scripts/classes/Builder/Builder.js @@ -20,4 +20,21 @@ export class Builder { isFlexibleInstanceMoving() { return this.flexibleInstanceMovement !== void 0; } + + asPacket() { + return { + playerId: this.playerId, + easyPlace: this.isOptionEnabled('easyPlace'), + fastEasyPlace: this.isOptionEnabled('fastEasyPlace'), + materialGrabber: this.isOptionEnabled('materialGrabber'), + materialInstanceName: this.materialInstanceName + }; + } + + setOptions(builderOptions) { + this.setOption('easyPlace', builderOptions.easyPlace); + this.setOption('fastEasyPlace', builderOptions.fastEasyPlace); + this.setOption('materialGrabber', builderOptions.materialGrabber); + this.materialInstanceName = builderOptions.materialInstanceName; + } } \ No newline at end of file diff --git a/packs/BP/scripts/classes/Builder/Builders.js b/packs/BP/scripts/classes/Builder/Builders.js index 4681854..359fdf0 100644 --- a/packs/BP/scripts/classes/Builder/Builders.js +++ b/packs/BP/scripts/classes/Builder/Builders.js @@ -1,29 +1,36 @@ import { world } from "@minecraft/server"; import { Builder } from "./Builder"; +import { BuilderNotFoundError } from "../Errors/BuilderNotFoundError"; export class Builders { static builders = {}; static add(playerId) { - if (this.builders[playerId]) + if (Builders.builders[playerId]) return; - this.builders[playerId] = new Builder(playerId); + Builders.builders[playerId] = new Builder(playerId); } static remove(playerId) { - delete this.builders[playerId]; + delete Builders.builders[playerId]; } static get(id) { - return this.builders[id]; + const builder = Builders.builders[id]; + if (builder === void 0) + throw new BuilderNotFoundError(id); } static onJoin(playerId) { - this.add(playerId); + Builders.add(playerId); } static onLeave(playerId) { - this.remove(playerId); + Builders.remove(playerId); + } + + static getIds() { + return Object.keys(Builders.builders); } } diff --git a/packs/BP/scripts/classes/Errors/BuilderNotFoundError.js b/packs/BP/scripts/classes/Errors/BuilderNotFoundError.js new file mode 100644 index 0000000..74f57c4 --- /dev/null +++ b/packs/BP/scripts/classes/Errors/BuilderNotFoundError.js @@ -0,0 +1,6 @@ +export class BuilderNotFoundError extends Error { + constructor(builderId) { + super(`§cBuilder "${builderId}" not found.`); + this.name = 'BuilderNotFoundError'; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/classes/Errors/CommandResponseError.js b/packs/BP/scripts/classes/Errors/CommandResponseError.js index 523ccb9..480c50f 100644 --- a/packs/BP/scripts/classes/Errors/CommandResponseError.js +++ b/packs/BP/scripts/classes/Errors/CommandResponseError.js @@ -8,7 +8,7 @@ export class CommandResponseError extends Error { throw new Error('getRawMessage() must be implemented by subclasses of CommandResponseError'); } - sendTo(source) { - source.sendMessage(this.getRawMessage()); + sendTo(origin) { + origin.sendMessage(this.getRawMessage()); } } diff --git a/packs/BP/scripts/classes/Extensions/Ready.ipc.js b/packs/BP/scripts/classes/Extensions/Ready.ipc.js deleted file mode 100644 index 5f1d47e..0000000 --- a/packs/BP/scripts/classes/Extensions/Ready.ipc.js +++ /dev/null @@ -1,3 +0,0 @@ -import { PROTO } from '../../lib/MCBE-IPC/ipc' - -export const Ready = PROTO.Void; \ No newline at end of file diff --git a/packs/BP/scripts/classes/Extensions/Ready.js b/packs/BP/scripts/classes/Extensions/Ready.js deleted file mode 100644 index a6b811e..0000000 --- a/packs/BP/scripts/classes/Extensions/Ready.js +++ /dev/null @@ -1,6 +0,0 @@ -import { system } from "@minecraft/server"; -import { Ready } from "./Ready.ipc"; - -system.runTimeout(() => { - IPC.send(`constructExtension:ready`, Ready, void 0); -}, 1); \ No newline at end of file diff --git a/packs/BP/scripts/classes/Instance/InstanceOptions.js b/packs/BP/scripts/classes/Instance/InstanceOptions.js index a3818f6..de92691 100644 --- a/packs/BP/scripts/classes/Instance/InstanceOptions.js +++ b/packs/BP/scripts/classes/Instance/InstanceOptions.js @@ -46,13 +46,8 @@ export class InstanceOptions extends Option { return world.getDimension(this.dimensionId); } - enable() { - this.isEnabled = true; - this.save(); - } - - disable() { - this.isEnabled = false; + setEnabled(enable) { + this.isEnabled = enable; this.save(); } @@ -69,7 +64,7 @@ export class InstanceOptions extends Option { } setLayer(layer) { - this.currentLayer = layer; + this.currentLayer = layer.floor(); this.save(); } @@ -82,4 +77,9 @@ export class InstanceOptions extends Option { this.verifier.trackPlayerDistance = distance; this.save(); } + + setVerifierParticleLifetime(lifetime) { + this.verifier.particleLifetime = lifetime; + this.save(); + } } \ No newline at end of file diff --git a/packs/BP/scripts/classes/Instance/StructureInstance.js b/packs/BP/scripts/classes/Instance/StructureInstance.js index 36bd6c0..f474002 100644 --- a/packs/BP/scripts/classes/Instance/StructureInstance.js +++ b/packs/BP/scripts/classes/Instance/StructureInstance.js @@ -7,6 +7,8 @@ import { world, system, TicksPerSecond } from "@minecraft/server"; import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError"; import { StructureMaterials } from "../Materials/StructureMaterials"; import { VerificationRenderer } from "../Render/VerificationRenderer"; +import { structureCollection } from "../Structure/StructureCollection"; +import { InstanceExistsError } from "../Errors/InstanceExistsError"; export class StructureInstance { options; @@ -185,19 +187,26 @@ export class StructureInstance { } enable() { - this.options.enable(); + this.options.setEnabled(true); this.refreshBox(); } disable() { - this.options.disable(); + this.options.setEnabled(false); this.refreshBox(); } rename(newName) { + if (structureCollection.has(newName)) + throw new InstanceExistsError(newName); this.options.rename(newName); } + setStructure(structureId) { + this.options.structureId = structureId; + this.structure = new Structure(structureId); + } + place(dimensionId, worldLocation) { this.enable(); this.move(dimensionId, worldLocation); @@ -277,4 +286,36 @@ export class StructureInstance { toStructureCoords(worldLocation) { return Vector.from(worldLocation).subtract(this.options.worldLocation); } + + asPacket() { + const dimensionLocation = this.getLocation(); + return { + name: this.getName(), + structureId: this.getStructureId(), + isEnabled: this.isEnabled(), + dimensionId: dimensionLocation.dimensionId, + location: { x: dimensionLocation.location.x, y: dimensionLocation.location.y, z: dimensionLocation.location.z }, + bounds: this.getBounds(), + currentLayer: this.getLayer(), + maxLayer: this.getMaxLayer(), + verifier: { + isEnabled: this.options.verifier.isEnabled, + trackPlayerDistance: this.options.verifier.trackPlayerDistance, + particleLifetime: this.options.verifier.particleLifetime + } + }; + } + + setOptions(newOptions) { + const newVerifierOptions = newOptions.verifier; + this.options.rename(newOptions.name); + this.options.setStructure(newOptions.structureId); + this.options.setEnabled(newOptions.isEnabled); + this.options.move(newOptions.dimensionId, newOptions.location); + this.options.setLayer(newOptions.currentLayer); + this.options.setVerifierEnabled(newVerifierOptions.isEnabled); + this.options.setVerifierDistance(newVerifierOptions.trackPlayerDistance); + this.options.setVerifierParticleLifetime(newVerifierOptions.particleLifetime); + this.options.save(); + } } \ No newline at end of file diff --git a/packs/BP/scripts/classes/Materials/StructureMaterials.js b/packs/BP/scripts/classes/Materials/StructureMaterials.js index 377e7ca..b2c3830 100644 --- a/packs/BP/scripts/classes/Materials/StructureMaterials.js +++ b/packs/BP/scripts/classes/Materials/StructureMaterials.js @@ -30,6 +30,10 @@ class StructureMaterials { } } + get materials() { + return this.materials; + } + get(itemType) { return this.materials[itemType]; } diff --git a/packs/BP/scripts/lib/AddonAPIKit/API.js b/packs/BP/scripts/lib/AddonAPIKit/API.js new file mode 100644 index 0000000..c02c006 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/API.js @@ -0,0 +1,92 @@ +import { IPC, PROTO } from "./MCBE-IPC/ipc"; +import { APICallerError } from "./Errors/APICallerError"; +import { APIErrorEnum } from "./Errors/APIErrorEnum"; +import { APIServerError } from "./Errors/APIServerError"; +import { APIVersionMismatchError } from "./Errors/APIVersionMismatchError"; +import { ReturnModelShell } from "./APIModels"; + +export class AddonAPI { + #name; + #version; + + constructor(name, version) { + this.#name = name; + this.#version = version; + } + + get name() { + return this.#name; + } + + get version() { + return this.#version; + } + + get endpointBase() { + return this.#name + ':'; + } + + setupController(apiController) { + for (const [endpoint, features] of Object.entries(apiController.endpoints)) { + const {callback, parameterModel, returnModel} = features; + const boundCallback = callback.bind(apiController); + this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel); + } + } + + #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { + const returnPacketModel = this.#resolveReturnModel(returnDataModel); + const endpointPath = this.endpointBase + endpoint; + IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { + const apiVersion = callPacket.apiVersion; + const parameters = Object.values(callPacket.parameterMap); + return this.#handleCallback(apiVersion, callback, parameters); + }); + } + + #handleCallback(apiVersion, callback, parameters) { + try { + this.#assertVersionsMatch(apiVersion); + const returnValue = callback(...parameters); + return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue); + } catch(error) { + if (error instanceof APICallerError) + const errorPacket = this.#resolveErrorPacket(error); + return this.#bundleReturnPacket(errorPacket); + console.error(error); + const apiError = new APIServerError(error); + const errorPacket = this.#resolveErrorPacket(apiError); + return this.#bundleReturnPacket(errorPacket); + } + } + + #assertVersionsMatch(versionToCheck) { + if (versionToCheck !== this.version) { + const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck); + throw new APICallerError(apiVersionMismatchError); + } + } + + #resolveReturnModel(returnDataModel) { + let returnModel = { ...ReturnModelShell }; + returnModel.data = returnDataModel; + returnModel = PROTO.Object(returnModel); + return returnModel; + } + + #bundleReturnPacket(errorPacket, returnValue = void 0) { + return { + apiVersion: this.version, + data: returnValue, + error: errorPacket + }; + } + + #resolveErrorPacket(error) { + return { + code: error.errorCode, + name: error.thrownError.name, + message: error.thrownError.message + }; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/APIController.js b/packs/BP/scripts/lib/AddonAPIKit/APIController.js new file mode 100644 index 0000000..2ea4391 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/APIController.js @@ -0,0 +1,13 @@ +export class APIController { + #endpoints; + + constructor(endpoints) { + if (this.constructor === APIController) + throw new Error("Cannot instantiate abstract class 'APIController'"); + this.#endpoints = endpoints; + } + + get endpoints() { + return this.#endpoints; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/APIModels.js b/packs/BP/scripts/lib/AddonAPIKit/APIModels.js new file mode 100644 index 0000000..f0ca617 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/APIModels.js @@ -0,0 +1,20 @@ +import { PROTO } from "./MCBE-IPC/ipc"; + +export const VoidModel = PROTO.Void; + +export const ErrorModel = PROTO.Optional(PROTO.Object({ + code: PROTO.Int8, + name: PROTO.Optional(PROTO.String), + message: PROTO.Optional(PROTO.String) +})); + +export const ReturnModelShell = { + apiVersion: PROTO.String, + data: void 0, + error: ErrorModel +}; + +export const CallModelShell = { + apiVersion: PROTO.String, + parameterMap: void 0 +}; \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js new file mode 100644 index 0000000..d335327 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js @@ -0,0 +1,7 @@ +import { AddonAPI } from "./API"; +import { APIController } from "./APIController"; +import { VoidModel } from "./APIModels"; +import { APICallerError } from "./Errors/APICallerError"; +import { PROTO } from "./MCBE-IPC/ipc"; + +export { AddonAPI, APIController, VoidModel, APICallerError, PROTO }; \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js new file mode 100644 index 0000000..27293da --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js @@ -0,0 +1,11 @@ +import { APIErrorEnum } from "./APIErrorEnum"; + +export class APICallerError extends Error { + constructor(error) { + super(error.message); + this.errorName = error.name; + this.errorMessage = error.message; + this.errorCode = APIErrorEnum.Caller; + this.name = "APICallerError"; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js new file mode 100644 index 0000000..ba33a7d --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js @@ -0,0 +1,6 @@ +export const APIErrorEnum = Object.freeze({ + Unknown: 0, + Success: 1, + Caller: 2, + Server: 3 +}); \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js new file mode 100644 index 0000000..0b91e62 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js @@ -0,0 +1,10 @@ +import { APIErrorEnum } from "./APIErrorEnum"; + +export class APIServerError extends Error { + constructor(error) { + super(error.message); + this.thrownError = error; + this.errorCode = APIErrorEnum.Server; + this.name = "APIServerError"; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js new file mode 100644 index 0000000..6bb8fa0 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js @@ -0,0 +1,6 @@ +export class APIVersionMismatchError extends Error { + constructor(serverApiVersion, callerApiVersion) { + super(`API version numbers do not match (${callerApiVersion} != ${serverApiVersion}). Please use API version ${serverApiVersion}.`); + this.name = 'APIVersionMismatchError'; + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts similarity index 100% rename from packs/BP/scripts/lib/MCBE-IPC/ipc.d.ts rename to packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts diff --git a/packs/BP/scripts/lib/MCBE-IPC/ipc.js b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js similarity index 100% rename from packs/BP/scripts/lib/MCBE-IPC/ipc.js rename to packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js diff --git a/packs/BP/scripts/main.js b/packs/BP/scripts/main.js index 10a7679..b338eb7 100644 --- a/packs/BP/scripts/main.js +++ b/packs/BP/scripts/main.js @@ -28,6 +28,8 @@ import './commands/StatsCommand'; import './commands/MaterialsCommand'; import './commands/TagCommand'; +// API +import './API/ConstructAPI'; + // Other -import './classes/BlockInfo'; -import './classes/Extensions/Ready' \ No newline at end of file +import './classes/BlockInfo'; \ No newline at end of file From 5177f7c9867b1fa21b03ec97b60292e6fcd74034 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 00:24:43 +0200 Subject: [PATCH 36/46] feat: bump MCBE-IPC to 3.4.2 --- .../scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts | 84 ++-- .../scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js | 465 ++++++++++-------- 2 files changed, 314 insertions(+), 235 deletions(-) diff --git a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts index dc6ef67..6d6a10b 100644 --- a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts +++ b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts @@ -2,7 +2,7 @@ * @license * MIT License * - * Copyright (c) 2025 OmniacDev + * Copyright (c) 2026 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 @@ -23,11 +23,15 @@ * SOFTWARE. */ export declare namespace PROTO { - interface Serializable { - serialize(value: T, stream: ByteQueue): Generator; - deserialize(stream: ByteQueue): Generator; + interface Serializer { + serialize(value: T, stream: Buffer): Generator; } - class ByteQueue { + interface Deserializer { + deserialize(stream: Buffer): Generator; + } + interface Serializable extends Serializer, Deserializer { + } + class Buffer { private _buffer; private _data_view; private _length; @@ -36,15 +40,20 @@ export declare namespace PROTO { get front(): number; get data_view(): DataView; constructor(size?: number); - write(...values: number[]): void; - read(amount?: number): number[]; + reserve(amount: number): number; + consume(amount: number): number; + write(byte: number): void; + write(bytes: Uint8Array): void; + read(): number; + read(amount: number): Uint8Array; ensure_capacity(size: number): void; - static from_uint8array(array: Uint8Array): ByteQueue; + static from_uint8array(array: Uint8Array): Buffer; to_uint8array(): Uint8Array; } namespace MIPS { - function serialize(byte_queue: PROTO.ByteQueue): Generator; - function deserialize(str: string): Generator; + function is_valid(str: string): boolean; + function serialize(stream: PROTO.Buffer): Generator; + function deserialize(str: string): Generator; } const Void: PROTO.Serializable; const Null: PROTO.Serializable; @@ -56,48 +65,55 @@ export declare namespace PROTO { const UInt16: PROTO.Serializable; const UInt32: PROTO.Serializable; const UVarInt32: PROTO.Serializable; + const VarInt32: 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: { + function Object(s: { [K in keyof T]: PROTO.Serializable; }): PROTO.Serializable; - function Array(value: PROTO.Serializable): PROTO.Serializable; - function Tuple(...values: { + function Array(s: PROTO.Serializable): PROTO.Serializable; + function Tuple(...s: { [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
; + function Optional(s: PROTO.Serializable): PROTO.Serializable; + function Map(kS: PROTO.Serializable, vS: PROTO.Serializable): PROTO.Serializable>; + function Set(s: PROTO.Serializable): PROTO.Serializable>; + function Cached(s: PROTO.Serializable, depth?: number): 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; + type Meta = { + guid: string; + signature: string; + }; + const Meta: PROTO.Serializable; + export const SIGNATURE: string; + export let FRAG_MAX: number; + export function serialize(buffer: PROTO.Buffer, max_size?: number): Generator; + export function deserialize(strings: string[]): Generator; + export interface EmitOptions { + metaOverride?: Partial; + } + export function emit(endpoint: string, serializer: PROTO.Serializer, value: NoInfer, options?: EmitOptions): Generator; + export interface ListenOptions { + filter?: (meta: Meta) => boolean; + } + export function listen(endpoint: string, deserializer: PROTO.Deserializer, callback: (value: NoInfer, meta: Meta) => Generator, options?: ListenOptions): () => void; + export {}; } export declare namespace IPC { /** Sends a message with `args` to `channel` */ - function send, T>(channel: string, serializer: S & PROTO.Serializable, value: T): void; + function send(channel: string, serializer: PROTO.Serializer, value: NoInfer): 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; + function invoke(channel: string, serializer: PROTO.Serializer, value: NoInfer, deserializer: PROTO.Deserializer): 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; + function on(channel: string, deserializer: PROTO.Deserializer, listener: (value: NoInfer) => 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; + function once(channel: string, deserializer: PROTO.Deserializer, listener: (value: NoInfer) => 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; + function handle(channel: string, deserializer: PROTO.Deserializer, serializer: PROTO.Serializer, listener: (value: NoInfer) => NoInfer): () => void; } export default IPC; diff --git a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js index 8866a9c..8da8f92 100644 --- a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js +++ b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js @@ -2,7 +2,7 @@ * @license * MIT License * - * Copyright (c) 2025 OmniacDev + * Copyright (c) 2026 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 @@ -22,10 +22,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { ScriptEventSource, system, world } from '@minecraft/server'; +import { ScriptEventSource, system } from '@minecraft/server'; +var UTIL; +(function (UTIL) { + function generate_id() { + const r = (Math.random() * 0x100000000) >>> 0; + return r.toString(16).padStart(8, '0').toUpperCase(); + } + UTIL.generate_id = generate_id; +})(UTIL || (UTIL = {})); export var PROTO; (function (PROTO) { - class ByteQueue { + class Buffer { get end() { return this._length + this._offset; } @@ -41,20 +49,39 @@ export var PROTO; this._length = 0; this._offset = 0; } - write(...values) { - this.ensure_capacity(values.length); - this._buffer.set(values, this.end); - this._length += values.length; + reserve(amount) { + this.ensure_capacity(amount); + const end = this.end; + this._length += amount; + return end; } - 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); + consume(amount) { + if (amount > this._length) + throw new Error('not enough bytes'); + const front = this.front; + this._length -= amount; + this._offset += amount; + return front; + } + write(input) { + if (typeof input === 'number') { + const offset = this.reserve(1); + this._buffer[offset] = input; + } + else { + const offset = this.reserve(input.length); + this._buffer.set(input, offset); + } + } + read(amount) { + if (amount === undefined) { + const offset = this.consume(1); + return this._buffer[offset]; + } + else { + const offset = this.consume(amount); + return this._buffer.slice(offset, offset + amount); } - return []; } ensure_capacity(size) { if (this.end + size > this._buffer.length) { @@ -66,22 +93,26 @@ export var PROTO; } } 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; + const buffer = new Buffer(); + buffer._buffer = array; + buffer._length = array.length; + buffer._offset = 0; + buffer._data_view = new DataView(array.buffer); + return buffer; } to_uint8array() { return this._buffer.subarray(this._offset, this.end); } } - PROTO.ByteQueue = ByteQueue; + PROTO.Buffer = Buffer; let MIPS; (function (MIPS) { - function* serialize(byte_queue) { - const uint8array = byte_queue.to_uint8array(); + function is_valid(str) { + return str.startsWith('(0x') && str.endsWith(')'); + } + MIPS.is_valid = is_valid; + function* serialize(stream) { + const uint8array = stream.to_uint8array(); let str = '(0x'; for (let i = 0; i < uint8array.length; i++) { const hex = uint8array[i].toString(16).padStart(2, '0').toUpperCase(); @@ -93,17 +124,17 @@ export var PROTO; } MIPS.serialize = serialize; function* deserialize(str) { - if (str.startsWith('(0x') && str.endsWith(')')) { - const result = []; + if (is_valid(str)) { + const buffer = new Buffer(); 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)); + buffer.write(parseInt(hex, 16)); yield; } - return ByteQueue.from_uint8array(new Uint8Array(result)); + return buffer; } - return new ByteQueue(); + return new Buffer(); } MIPS.deserialize = deserialize; })(MIPS = PROTO.MIPS || (PROTO.MIPS = {})); @@ -125,120 +156,98 @@ export var PROTO; }; PROTO.Int8 = { *serialize(value, stream) { - const length = 1; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt8(stream.end - length, value); + stream.data_view.setInt8(stream.reserve(1), value); }, *deserialize(stream) { - const value = stream.data_view.getInt8(stream.front); - stream.read(1); - return value; + return stream.data_view.getInt8(stream.consume(1)); } }; PROTO.Int16 = { *serialize(value, stream) { - const length = 2; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt16(stream.end - length, value); + stream.data_view.setInt16(stream.reserve(2), value); }, *deserialize(stream) { - const value = stream.data_view.getInt16(stream.front); - stream.read(2); - return value; + return stream.data_view.getInt16(stream.consume(2)); } }; PROTO.Int32 = { *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setInt32(stream.end - length, value); + stream.data_view.setInt32(stream.reserve(4), value); }, *deserialize(stream) { - const value = stream.data_view.getInt32(stream.front); - stream.read(4); - return value; + return stream.data_view.getInt32(stream.consume(4)); } }; PROTO.UInt8 = { *serialize(value, stream) { - const length = 1; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint8(stream.end - length, value); + stream.data_view.setUint8(stream.reserve(1), value); }, *deserialize(stream) { - const value = stream.data_view.getUint8(stream.front); - stream.read(1); - return value; + return stream.data_view.getUint8(stream.consume(1)); } }; PROTO.UInt16 = { *serialize(value, stream) { - const length = 2; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint16(stream.end - length, value); + stream.data_view.setUint16(stream.reserve(2), value); }, *deserialize(stream) { - const value = stream.data_view.getUint16(stream.front); - stream.read(2); - return value; + return stream.data_view.getUint16(stream.consume(2)); } }; PROTO.UInt32 = { *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setUint32(stream.end - length, value); + stream.data_view.setUint32(stream.reserve(4), value); }, *deserialize(stream) { - const value = stream.data_view.getUint32(stream.front); - stream.read(4); - return value; + return stream.data_view.getUint32(stream.consume(4)); } }; PROTO.UVarInt32 = { *serialize(value, stream) { + value >>>= 0; while (value >= 0x80) { stream.write((value & 0x7f) | 0x80); - value >>= 7; + value >>>= 7; yield; } stream.write(value); }, *deserialize(stream) { let value = 0; - let size = 0; - let byte; - do { - byte = stream.read()[0]; + for (let size = 0; size < 5; size++) { + const byte = stream.read(); value |= (byte & 0x7f) << (size * 7); - size += 1; yield; - } while ((byte & 0x80) !== 0 && size < 10); - return value; + if ((byte & 0x80) == 0) + break; + } + return value >>> 0; + } + }; + PROTO.VarInt32 = { + *serialize(value, stream) { + const zigzag = (value << 1) ^ (value >> 31); + yield* PROTO.UVarInt32.serialize(zigzag, stream); + }, + *deserialize(stream) { + const zigzag = yield* PROTO.UVarInt32.deserialize(stream); + return (zigzag >>> 1) ^ -(zigzag & 1); } }; PROTO.Float32 = { *serialize(value, stream) { - const length = 4; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setFloat32(stream.end - length, value); + stream.data_view.setFloat32(stream.reserve(4), value); }, *deserialize(stream) { - const value = stream.data_view.getFloat32(stream.front); - stream.read(4); - return value; + return stream.data_view.getFloat32(stream.consume(4)); } }; PROTO.Float64 = { *serialize(value, stream) { - const length = 8; - stream.write(...globalThis.Array(length).fill(0)); - stream.data_view.setFloat64(stream.end - length, value); + stream.data_view.setFloat64(stream.reserve(8), value); }, *deserialize(stream) { - const value = stream.data_view.getFloat64(stream.front); - stream.read(8); - return value; + return stream.data_view.getFloat64(stream.consume(8)); } }; PROTO.String = { @@ -264,18 +273,17 @@ export var PROTO; stream.write(value ? 1 : 0); }, *deserialize(stream) { - const value = stream.read()[0]; - return value === 1; + return stream.read() !== 0; } }; PROTO.UInt8Array = { *serialize(value, stream) { yield* PROTO.UVarInt32.serialize(value.length, stream); - stream.write(...value); + stream.write(value); }, *deserialize(stream) { const length = yield* PROTO.UVarInt32.deserialize(stream); - return new Uint8Array(stream.read(length)); + return stream.read(length); } }; PROTO.Date = { @@ -286,94 +294,91 @@ export var PROTO; return new globalThis.Date(yield* PROTO.Float64.deserialize(stream)); } }; - function Object(obj) { + function Object(s) { return { *serialize(value, stream) { - for (const key in obj) { - yield* obj[key].serialize(value[key], stream); + for (const key in s) { + yield* s[key].serialize(value[key], stream); } }, *deserialize(stream) { const result = {}; - for (const key in obj) { - result[key] = yield* obj[key].deserialize(stream); + for (const key in s) { + result[key] = yield* s[key].deserialize(stream); } return result; } }; } PROTO.Object = Object; - function Array(value) { + function Array(s) { return { - *serialize(array, stream) { - const actualValue = typeof value === 'function' ? value() : value; - yield* PROTO.UVarInt32.serialize(array.length, stream); - for (const item of array) { - yield* actualValue.serialize(item, stream); + *serialize(value, stream) { + yield* PROTO.UVarInt32.serialize(value.length, stream); + for (const item of value) { + yield* s.serialize(item, stream); } }, *deserialize(stream) { - const actualValue = typeof value === 'function' ? value() : value; const result = []; const length = yield* PROTO.UVarInt32.deserialize(stream); for (let i = 0; i < length; i++) { - result[i] = yield* actualValue.deserialize(stream); + result[i] = yield* s.deserialize(stream); } return result; } }; } PROTO.Array = Array; - function Tuple(...values) { + function Tuple(...s) { return { - *serialize(tuple, stream) { - for (let i = 0; i < values.length; i++) { - yield* values[i].serialize(tuple[i], stream); + *serialize(value, stream) { + for (let i = 0; i < s.length; i++) { + yield* s[i].serialize(value[i], stream); } }, *deserialize(stream) { const result = []; - for (let i = 0; i < values.length; i++) { - result[i] = yield* values[i].deserialize(stream); + for (let i = 0; i < s.length; i++) { + result[i] = yield* s[i].deserialize(stream); } return result; } }; } PROTO.Tuple = Tuple; - function Optional(value) { + function Optional(s) { return { - *serialize(optional, stream) { - yield* PROTO.Boolean.serialize(optional !== undefined, stream); - if (optional !== undefined) { - yield* value.serialize(optional, stream); - } + *serialize(value, stream) { + const def = value !== undefined; + yield* PROTO.Boolean.serialize(def, stream); + if (def) + yield* s.serialize(value, stream); }, *deserialize(stream) { - const defined = yield* PROTO.Boolean.deserialize(stream); - if (defined) { - return yield* value.deserialize(stream); - } + const def = yield* PROTO.Boolean.deserialize(stream); + if (def) + return yield* s.deserialize(stream); return undefined; } }; } PROTO.Optional = Optional; - function Map(key, value) { + function Map(kS, vS) { 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); + *serialize(value, stream) { + yield* PROTO.UVarInt32.serialize(value.size, stream); + for (const [k, v] of value) { + yield* kS.serialize(k, stream); + yield* vS.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); + const k = yield* kS.deserialize(stream); + const v = yield* vS.deserialize(stream); result.set(k, v); } return result; @@ -381,19 +386,19 @@ export var PROTO; }; } PROTO.Map = Map; - function Set(value) { + function Set(s) { return { *serialize(set, stream) { yield* PROTO.UVarInt32.serialize(set.size, stream); - for (const [_, v] of set.entries()) { - yield* value.serialize(v, stream); + for (const v of set) { + yield* s.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); + const v = yield* s.deserialize(stream); result.add(v); } return result; @@ -401,21 +406,52 @@ export var PROTO; }; } PROTO.Set = Set; - PROTO.Endpoint = PROTO.String; - PROTO.Header = PROTO.Object({ - guid: PROTO.String, - encoding: PROTO.String, - index: PROTO.UVarInt32, - final: PROTO.Boolean - }); + function Cached(s, depth = 16) { + const cache = new globalThis.Map(); + return { + *serialize(value, stream) { + const hit = cache.get(value); + if (hit !== undefined) { + stream.write(hit); + cache.delete(value); + cache.set(value, hit); + } + else { + const buffer = new PROTO.Buffer(); + yield* s.serialize(value, buffer); + const bytes = buffer.to_uint8array(); + stream.write(bytes); + cache.set(value, bytes); + if (cache.size > depth) { + const first = cache.keys().next().value; + cache.delete(first); + } + } + }, + *deserialize(stream) { + return yield* s.deserialize(stream); + } + }; + } + PROTO.Cached = Cached; })(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 Endpoint = PROTO.String; + const Meta = PROTO.Object({ + guid: PROTO.String, + signature: PROTO.String + }); + const Header = PROTO.Object({ + meta: Meta, + index: PROTO.UVarInt32, + final: PROTO.Boolean + }); + const LISTENERS = new Map(); + NET.SIGNATURE = 'mcbe-ipc:v3'; + NET.FRAG_MAX = 2048; + function* serialize(buffer, max_size = Infinity) { + const uint8array = buffer.to_uint8array(); const result = []; let acc_str = ''; let acc_size = 0; @@ -443,7 +479,7 @@ export var NET; } NET.serialize = serialize; function* deserialize(strings) { - const result = []; + const buffer = new PROTO.Buffer(); for (let i = 0; i < strings.length; i++) { const str = strings[i]; for (let j = 0; j < str.length; j++) { @@ -451,40 +487,49 @@ export var NET; 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); + buffer.write(hex_code & 0xff); + buffer.write(hex_code >> 8); } else { - result.push(char_code & 0xff); - result.push(char_code >> 8); + buffer.write(char_code & 0xff); + buffer.write(char_code >> 8); } yield; } yield; } - return PROTO.ByteQueue.from_uint8array(new Uint8Array(result)); + return buffer; } NET.deserialize = deserialize; system.afterEvents.scriptEventReceive.subscribe(event => { system.runJob((function* () { + if (event.sourceType !== ScriptEventSource.Server) + return; const [serialized_endpoint, serialized_header] = event.id.split(':'); + if (!PROTO.MIPS.is_valid(serialized_endpoint)) + return; 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 endpoint = yield* Endpoint.deserialize(endpoint_stream); + const listeners = LISTENERS.get(endpoint); + if (listeners !== undefined && PROTO.MIPS.is_valid(serialized_header)) { 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); + const header = yield* Header.deserialize(header_stream); + for (const listener of [...listeners]) { + try { + yield* listener(header, event.message); + } + catch (e) { + console.error(`[MCBE-IPC] listener error while handling packet on "${endpoint}":`, e); + } } } })()); }); - function create_listener(endpoint, listener) { - let listeners = ENDPOINTS.get(endpoint); - if (!listeners) { + function register(endpoint, listener) { + let listeners = LISTENERS.get(endpoint); + if (listeners === undefined) { listeners = new Array(); - ENDPOINTS.set(endpoint, listeners); + LISTENERS.set(endpoint, listeners); } listeners.push(listener); return () => { @@ -492,60 +537,61 @@ export var NET; if (idx !== -1) listeners.splice(idx, 1); if (listeners.length === 0) { - ENDPOINTS.delete(endpoint); + LISTENERS.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); + function* emit(endpoint, serializer, value, options) { + const guid = options?.metaOverride?.guid ?? UTIL.generate_id(); + const signature = options?.metaOverride?.signature ?? NET.SIGNATURE; + const endpoint_stream = new PROTO.Buffer(); + yield* 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(); + const packet_stream = new PROTO.Buffer(); yield* serializer.serialize(value, packet_stream); - const serialized_packets = yield* serialize(packet_stream, FRAG_MAX); + const serialized_packets = yield* serialize(packet_stream, NET.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); + const header = { + meta: { guid, signature }, + index: i, + final: i === serialized_packets.length - 1 + }; + const header_stream = new PROTO.Buffer(); + yield* Header.serialize(header, header_stream); + const serialized_header = yield* PROTO.MIPS.serialize(header_stream); + system.sendScriptEvent(`${serialized_endpoint}:${serialized_header}`, serialized_packet); } } NET.emit = emit; - function listen(endpoint, serializer, callback) { + function listen(endpoint, deserializer, callback, options) { 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); + const listener = function* (header, fragment) { + let packet = buffer.get(header.meta.guid); + if (packet === undefined) { + if (options?.filter?.(header.meta) === false) + return; + packet = { size: -1, fragments: [], received: 0 }; + buffer.set(header.meta.guid, packet); } - if (payload.final) { - fragment.size = payload.index + 1; + if (header.final) { + packet.size = header.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); + if (packet.fragments[header.index] === undefined) { + packet.fragments[header.index] = fragment; + packet.received++; + } + else { + throw new Error(`received duplicate fragment ${header.index} for packet ${header.meta.guid}`); + } + if (packet.size !== -1 && packet.size === packet.received) { + const stream = yield* deserialize(packet.fragments); + const value = yield* deserializer.deserialize(stream); + yield* callback(value, header.meta); + buffer.delete(header.meta.guid); } }; - return create_listener(endpoint, listener); + return register(endpoint, listener); } NET.listen = listen; })(NET || (NET = {})); @@ -558,12 +604,22 @@ export var IPC; 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)); + const id = UTIL.generate_id(); return new Promise(resolve => { - const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value) { + const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value, meta) { + if (meta.signature.includes(`+correlation`) && meta.guid !== id) + return; resolve(value); terminate(); + }, { + filter: meta => !meta.signature.includes(`+correlation`) || meta.guid === id }); + system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value, { + metaOverride: { + guid: id, + signature: `${NET.SIGNATURE}+correlation` + } + })); }); } IPC.invoke = invoke; @@ -585,9 +641,16 @@ export var IPC; 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) { + return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value, meta) { const result = listener(value); - yield* NET.emit(`ipc:${channel}:handle`, serializer, result); + yield* NET.emit(`ipc:${channel}:handle`, serializer, result, { + metaOverride: meta.signature.includes(`+correlation`) + ? { + guid: meta.guid, + signature: `${NET.SIGNATURE}+correlation` + } + : undefined + }); }); } IPC.handle = handle; From d3e064894cea1f41cbefe611ca3b63ce939b99ec Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 00:25:06 +0200 Subject: [PATCH 37/46] feat: add class for making API calls --- packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js | 7 +++++++ packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js new file mode 100644 index 0000000..23fff2c --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js @@ -0,0 +1,7 @@ +import { IPC } from "./MCBE-IPC/ipc"; + +export class AddonAPICaller { + static async call(endpoint, parameterModel, parameterMap, returnDataModel) { + return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then(result => result.value); + } +} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js index d335327..566373d 100644 --- a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js +++ b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js @@ -3,5 +3,6 @@ import { APIController } from "./APIController"; import { VoidModel } from "./APIModels"; import { APICallerError } from "./Errors/APICallerError"; import { PROTO } from "./MCBE-IPC/ipc"; +import { AddonAPICaller } from "./AddonAPICaller"; -export { AddonAPI, APIController, VoidModel, APICallerError, PROTO }; \ No newline at end of file +export { AddonAPI, APIController, VoidModel, APICallerError, PROTO, AddonAPICaller }; \ No newline at end of file From 3867456e7472677aeb26092bf71168852a12d9d2 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 00:31:36 +0200 Subject: [PATCH 38/46] refactor: rename API.js -> AddonAPI.js --- packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js | 92 ++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js new file mode 100644 index 0000000..c02c006 --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js @@ -0,0 +1,92 @@ +import { IPC, PROTO } from "./MCBE-IPC/ipc"; +import { APICallerError } from "./Errors/APICallerError"; +import { APIErrorEnum } from "./Errors/APIErrorEnum"; +import { APIServerError } from "./Errors/APIServerError"; +import { APIVersionMismatchError } from "./Errors/APIVersionMismatchError"; +import { ReturnModelShell } from "./APIModels"; + +export class AddonAPI { + #name; + #version; + + constructor(name, version) { + this.#name = name; + this.#version = version; + } + + get name() { + return this.#name; + } + + get version() { + return this.#version; + } + + get endpointBase() { + return this.#name + ':'; + } + + setupController(apiController) { + for (const [endpoint, features] of Object.entries(apiController.endpoints)) { + const {callback, parameterModel, returnModel} = features; + const boundCallback = callback.bind(apiController); + this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel); + } + } + + #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { + const returnPacketModel = this.#resolveReturnModel(returnDataModel); + const endpointPath = this.endpointBase + endpoint; + IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { + const apiVersion = callPacket.apiVersion; + const parameters = Object.values(callPacket.parameterMap); + return this.#handleCallback(apiVersion, callback, parameters); + }); + } + + #handleCallback(apiVersion, callback, parameters) { + try { + this.#assertVersionsMatch(apiVersion); + const returnValue = callback(...parameters); + return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue); + } catch(error) { + if (error instanceof APICallerError) + const errorPacket = this.#resolveErrorPacket(error); + return this.#bundleReturnPacket(errorPacket); + console.error(error); + const apiError = new APIServerError(error); + const errorPacket = this.#resolveErrorPacket(apiError); + return this.#bundleReturnPacket(errorPacket); + } + } + + #assertVersionsMatch(versionToCheck) { + if (versionToCheck !== this.version) { + const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck); + throw new APICallerError(apiVersionMismatchError); + } + } + + #resolveReturnModel(returnDataModel) { + let returnModel = { ...ReturnModelShell }; + returnModel.data = returnDataModel; + returnModel = PROTO.Object(returnModel); + return returnModel; + } + + #bundleReturnPacket(errorPacket, returnValue = void 0) { + return { + apiVersion: this.version, + data: returnValue, + error: errorPacket + }; + } + + #resolveErrorPacket(error) { + return { + code: error.errorCode, + name: error.thrownError.name, + message: error.thrownError.message + }; + } +} \ No newline at end of file From 7b9375a0cb73ea8e2e7e554c79947c4514c4da1f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 00:32:05 +0200 Subject: [PATCH 39/46] refactor: continuation of last commit --- packs/BP/scripts/lib/AddonAPIKit/API.js | 92 ------------------- .../BP/scripts/lib/AddonAPIKit/AddonAPIKit.js | 2 +- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/API.js diff --git a/packs/BP/scripts/lib/AddonAPIKit/API.js b/packs/BP/scripts/lib/AddonAPIKit/API.js deleted file mode 100644 index c02c006..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/API.js +++ /dev/null @@ -1,92 +0,0 @@ -import { IPC, PROTO } from "./MCBE-IPC/ipc"; -import { APICallerError } from "./Errors/APICallerError"; -import { APIErrorEnum } from "./Errors/APIErrorEnum"; -import { APIServerError } from "./Errors/APIServerError"; -import { APIVersionMismatchError } from "./Errors/APIVersionMismatchError"; -import { ReturnModelShell } from "./APIModels"; - -export class AddonAPI { - #name; - #version; - - constructor(name, version) { - this.#name = name; - this.#version = version; - } - - get name() { - return this.#name; - } - - get version() { - return this.#version; - } - - get endpointBase() { - return this.#name + ':'; - } - - setupController(apiController) { - for (const [endpoint, features] of Object.entries(apiController.endpoints)) { - const {callback, parameterModel, returnModel} = features; - const boundCallback = callback.bind(apiController); - this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel); - } - } - - #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { - const returnPacketModel = this.#resolveReturnModel(returnDataModel); - const endpointPath = this.endpointBase + endpoint; - IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { - const apiVersion = callPacket.apiVersion; - const parameters = Object.values(callPacket.parameterMap); - return this.#handleCallback(apiVersion, callback, parameters); - }); - } - - #handleCallback(apiVersion, callback, parameters) { - try { - this.#assertVersionsMatch(apiVersion); - const returnValue = callback(...parameters); - return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue); - } catch(error) { - if (error instanceof APICallerError) - const errorPacket = this.#resolveErrorPacket(error); - return this.#bundleReturnPacket(errorPacket); - console.error(error); - const apiError = new APIServerError(error); - const errorPacket = this.#resolveErrorPacket(apiError); - return this.#bundleReturnPacket(errorPacket); - } - } - - #assertVersionsMatch(versionToCheck) { - if (versionToCheck !== this.version) { - const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck); - throw new APICallerError(apiVersionMismatchError); - } - } - - #resolveReturnModel(returnDataModel) { - let returnModel = { ...ReturnModelShell }; - returnModel.data = returnDataModel; - returnModel = PROTO.Object(returnModel); - return returnModel; - } - - #bundleReturnPacket(errorPacket, returnValue = void 0) { - return { - apiVersion: this.version, - data: returnValue, - error: errorPacket - }; - } - - #resolveErrorPacket(error) { - return { - code: error.errorCode, - name: error.thrownError.name, - message: error.thrownError.message - }; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js index 566373d..d08b378 100644 --- a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js +++ b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js @@ -1,4 +1,4 @@ -import { AddonAPI } from "./API"; +import { AddonAPI } from "./AddonAPI"; import { APIController } from "./APIController"; import { VoidModel } from "./APIModels"; import { APICallerError } from "./Errors/APICallerError"; From 4f1d5d6ab1d74ed4ba9f2b20076366eac16f2862 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 00:48:41 +0200 Subject: [PATCH 40/46] chore: change AddonAPIKit to single-file distro --- packs/BP/scripts/API/ConstructAPI.js | 2 +- .../API/controllers/BuildersController.js | 2 +- .../API/controllers/InstancesController.js | 2 +- packs/BP/scripts/API/models/BuildersModel.js | 2 +- packs/BP/scripts/API/models/InstancesModel.js | 2 +- packs/BP/scripts/lib/AddonAPIKit.js | 813 ++++++++++++++++++ .../scripts/lib/AddonAPIKit/APIController.js | 13 - packs/BP/scripts/lib/AddonAPIKit/APIModels.js | 20 - packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js | 92 -- .../scripts/lib/AddonAPIKit/AddonAPICaller.js | 7 - .../BP/scripts/lib/AddonAPIKit/AddonAPIKit.js | 8 - .../lib/AddonAPIKit/Errors/APICallerError.js | 11 - .../lib/AddonAPIKit/Errors/APIErrorEnum.js | 6 - .../lib/AddonAPIKit/Errors/APIServerError.js | 10 - .../Errors/APIVersionMismatchError.js | 6 - .../scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts | 119 --- .../scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js | 658 -------------- 17 files changed, 818 insertions(+), 955 deletions(-) create mode 100644 packs/BP/scripts/lib/AddonAPIKit.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/APIController.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/APIModels.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts delete mode 100644 packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js diff --git a/packs/BP/scripts/API/ConstructAPI.js b/packs/BP/scripts/API/ConstructAPI.js index f29736d..f27d9f9 100644 --- a/packs/BP/scripts/API/ConstructAPI.js +++ b/packs/BP/scripts/API/ConstructAPI.js @@ -1,4 +1,4 @@ -import { AddonAPI } from "../lib/AddonAPIKit/AddonAPIKit"; +import { AddonAPI } from "../lib/AddonAPIKit"; import { PACK_IDENTIFIER } from "../consts"; import { InstancesController } from "./controllers/InstancesController"; import { structureCollection } from "../classes/Structure/StructureCollection"; diff --git a/packs/BP/scripts/API/controllers/BuildersController.js b/packs/BP/scripts/API/controllers/BuildersController.js index ddea020..20d2032 100644 --- a/packs/BP/scripts/API/controllers/BuildersController.js +++ b/packs/BP/scripts/API/controllers/BuildersController.js @@ -1,5 +1,5 @@ import { BuilderNotFoundError } from "../../classes/Errors/BuilderNotFoundError"; -import { APICallerError } from "../../lib/AddonAPIKit/AddonAPIKit"; +import { APICallerError } from "../../lib/AddonAPIKit"; import { BuilderIdParameterModel } from "../models/BuildersModel"; export class BuildersController extends APIController { diff --git a/packs/BP/scripts/API/controllers/InstancesController.js b/packs/BP/scripts/API/controllers/InstancesController.js index e84d568..ba97d39 100644 --- a/packs/BP/scripts/API/controllers/InstancesController.js +++ b/packs/BP/scripts/API/controllers/InstancesController.js @@ -1,7 +1,7 @@ import { InstanceExistsError } from "../../classes/Errors/InstanceExistsError"; import { InstanceNotFoundError } from "../../classes/Errors/InstanceNotFoundError"; import { StructureNotFoundError } from "../../classes/Errors/StructureNotFoundError"; -import { APICallerError, VoidModel } from "../../lib/AddonAPIKit/AddonAPIKit"; +import { APICallerError, VoidModel } from "../../lib/AddonAPIKit"; import { AddInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../models/InstancesModel"; export class InstancesController extends APIController { diff --git a/packs/BP/scripts/API/models/BuildersModel.js b/packs/BP/scripts/API/models/BuildersModel.js index 36f2b3d..07bb00c 100644 --- a/packs/BP/scripts/API/models/BuildersModel.js +++ b/packs/BP/scripts/API/models/BuildersModel.js @@ -1,4 +1,4 @@ -import { PROTO } from "../../lib/AddonAPIKit/AddonAPIKit"; +import { PROTO } from "../../lib/AddonAPIKit"; export const BuilderModel = PROTO.Object({ playerId: PROTO.String, diff --git a/packs/BP/scripts/API/models/InstancesModel.js b/packs/BP/scripts/API/models/InstancesModel.js index 1382190..cc16bd5 100644 --- a/packs/BP/scripts/API/models/InstancesModel.js +++ b/packs/BP/scripts/API/models/InstancesModel.js @@ -1,4 +1,4 @@ -import { PROTO } from '../../lib/AddonAPIKit/AddonAPIKit'; +import { PROTO } from '../../lib/AddonAPIKit'; const LocationModel = PROTO.Object({ x: PROTO.Float64, diff --git a/packs/BP/scripts/lib/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit.js new file mode 100644 index 0000000..97670ca --- /dev/null +++ b/packs/BP/scripts/lib/AddonAPIKit.js @@ -0,0 +1,813 @@ +/** @license MIT + * AddonAPIKit - Copyright (c) 2026 ForestOfLight + * MCBE-IPC - Copyright (c) 2026 OmniacDev + * See LICENSE for details. + */ + +// MCBE-IPC/ipc.js +import { ScriptEventSource, system } from "@minecraft/server"; +var UTIL; +(function(UTIL2) { + function generate_id() { + const r = Math.random() * 4294967296 >>> 0; + return r.toString(16).padStart(8, "0").toUpperCase(); + } + UTIL2.generate_id = generate_id; +})(UTIL || (UTIL = {})); +var PROTO; +(function(PROTO2) { + class Buffer { + 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; + } + reserve(amount) { + this.ensure_capacity(amount); + const end = this.end; + this._length += amount; + return end; + } + consume(amount) { + if (amount > this._length) + throw new Error("not enough bytes"); + const front = this.front; + this._length -= amount; + this._offset += amount; + return front; + } + write(input) { + if (typeof input === "number") { + const offset = this.reserve(1); + this._buffer[offset] = input; + } else { + const offset = this.reserve(input.length); + this._buffer.set(input, offset); + } + } + read(amount) { + if (amount === void 0) { + const offset = this.consume(1); + return this._buffer[offset]; + } else { + const offset = this.consume(amount); + return this._buffer.slice(offset, offset + amount); + } + } + 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 buffer = new Buffer(); + buffer._buffer = array; + buffer._length = array.length; + buffer._offset = 0; + buffer._data_view = new DataView(array.buffer); + return buffer; + } + to_uint8array() { + return this._buffer.subarray(this._offset, this.end); + } + } + PROTO2.Buffer = Buffer; + let MIPS; + (function(MIPS2) { + function is_valid(str) { + return str.startsWith("(0x") && str.endsWith(")"); + } + MIPS2.is_valid = is_valid; + function* serialize(stream) { + const uint8array = stream.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; + } + MIPS2.serialize = serialize; + function* deserialize(str) { + if (is_valid(str)) { + const buffer = new Buffer(); + 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]; + buffer.write(parseInt(hex, 16)); + yield; + } + return buffer; + } + return new Buffer(); + } + MIPS2.deserialize = deserialize; + })(MIPS = PROTO2.MIPS || (PROTO2.MIPS = {})); + PROTO2.Void = { + *serialize() { + }, + *deserialize() { + } + }; + PROTO2.Null = { + *serialize() { + }, + *deserialize() { + return null; + } + }; + PROTO2.Undefined = { + *serialize() { + }, + *deserialize() { + return void 0; + } + }; + PROTO2.Int8 = { + *serialize(value, stream) { + stream.data_view.setInt8(stream.reserve(1), value); + }, + *deserialize(stream) { + return stream.data_view.getInt8(stream.consume(1)); + } + }; + PROTO2.Int16 = { + *serialize(value, stream) { + stream.data_view.setInt16(stream.reserve(2), value); + }, + *deserialize(stream) { + return stream.data_view.getInt16(stream.consume(2)); + } + }; + PROTO2.Int32 = { + *serialize(value, stream) { + stream.data_view.setInt32(stream.reserve(4), value); + }, + *deserialize(stream) { + return stream.data_view.getInt32(stream.consume(4)); + } + }; + PROTO2.UInt8 = { + *serialize(value, stream) { + stream.data_view.setUint8(stream.reserve(1), value); + }, + *deserialize(stream) { + return stream.data_view.getUint8(stream.consume(1)); + } + }; + PROTO2.UInt16 = { + *serialize(value, stream) { + stream.data_view.setUint16(stream.reserve(2), value); + }, + *deserialize(stream) { + return stream.data_view.getUint16(stream.consume(2)); + } + }; + PROTO2.UInt32 = { + *serialize(value, stream) { + stream.data_view.setUint32(stream.reserve(4), value); + }, + *deserialize(stream) { + return stream.data_view.getUint32(stream.consume(4)); + } + }; + PROTO2.UVarInt32 = { + *serialize(value, stream) { + value >>>= 0; + while (value >= 128) { + stream.write(value & 127 | 128); + value >>>= 7; + yield; + } + stream.write(value); + }, + *deserialize(stream) { + let value = 0; + for (let size = 0; size < 5; size++) { + const byte = stream.read(); + value |= (byte & 127) << size * 7; + yield; + if ((byte & 128) == 0) + break; + } + return value >>> 0; + } + }; + PROTO2.VarInt32 = { + *serialize(value, stream) { + const zigzag = value << 1 ^ value >> 31; + yield* PROTO2.UVarInt32.serialize(zigzag, stream); + }, + *deserialize(stream) { + const zigzag = yield* PROTO2.UVarInt32.deserialize(stream); + return zigzag >>> 1 ^ -(zigzag & 1); + } + }; + PROTO2.Float32 = { + *serialize(value, stream) { + stream.data_view.setFloat32(stream.reserve(4), value); + }, + *deserialize(stream) { + return stream.data_view.getFloat32(stream.consume(4)); + } + }; + PROTO2.Float64 = { + *serialize(value, stream) { + stream.data_view.setFloat64(stream.reserve(8), value); + }, + *deserialize(stream) { + return stream.data_view.getFloat64(stream.consume(8)); + } + }; + PROTO2.String = { + *serialize(value, stream) { + yield* PROTO2.UVarInt32.serialize(value.length, stream); + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); + yield* PROTO2.UVarInt32.serialize(code, stream); + } + }, + *deserialize(stream) { + const length = yield* PROTO2.UVarInt32.deserialize(stream); + let value = ""; + for (let i = 0; i < length; i++) { + const code = yield* PROTO2.UVarInt32.deserialize(stream); + value += globalThis.String.fromCharCode(code); + } + return value; + } + }; + PROTO2.Boolean = { + *serialize(value, stream) { + stream.write(value ? 1 : 0); + }, + *deserialize(stream) { + return stream.read() !== 0; + } + }; + PROTO2.UInt8Array = { + *serialize(value, stream) { + yield* PROTO2.UVarInt32.serialize(value.length, stream); + stream.write(value); + }, + *deserialize(stream) { + const length = yield* PROTO2.UVarInt32.deserialize(stream); + return stream.read(length); + } + }; + PROTO2.Date = { + *serialize(value, stream) { + yield* PROTO2.Float64.serialize(value.getTime(), stream); + }, + *deserialize(stream) { + return new globalThis.Date(yield* PROTO2.Float64.deserialize(stream)); + } + }; + function Object2(s) { + return { + *serialize(value, stream) { + for (const key in s) { + yield* s[key].serialize(value[key], stream); + } + }, + *deserialize(stream) { + const result = {}; + for (const key in s) { + result[key] = yield* s[key].deserialize(stream); + } + return result; + } + }; + } + PROTO2.Object = Object2; + function Array2(s) { + return { + *serialize(value, stream) { + yield* PROTO2.UVarInt32.serialize(value.length, stream); + for (const item of value) { + yield* s.serialize(item, stream); + } + }, + *deserialize(stream) { + const result = []; + const length = yield* PROTO2.UVarInt32.deserialize(stream); + for (let i = 0; i < length; i++) { + result[i] = yield* s.deserialize(stream); + } + return result; + } + }; + } + PROTO2.Array = Array2; + function Tuple(...s) { + return { + *serialize(value, stream) { + for (let i = 0; i < s.length; i++) { + yield* s[i].serialize(value[i], stream); + } + }, + *deserialize(stream) { + const result = []; + for (let i = 0; i < s.length; i++) { + result[i] = yield* s[i].deserialize(stream); + } + return result; + } + }; + } + PROTO2.Tuple = Tuple; + function Optional(s) { + return { + *serialize(value, stream) { + const def = value !== void 0; + yield* PROTO2.Boolean.serialize(def, stream); + if (def) + yield* s.serialize(value, stream); + }, + *deserialize(stream) { + const def = yield* PROTO2.Boolean.deserialize(stream); + if (def) + return yield* s.deserialize(stream); + return void 0; + } + }; + } + PROTO2.Optional = Optional; + function Map2(kS, vS) { + return { + *serialize(value, stream) { + yield* PROTO2.UVarInt32.serialize(value.size, stream); + for (const [k, v] of value) { + yield* kS.serialize(k, stream); + yield* vS.serialize(v, stream); + } + }, + *deserialize(stream) { + const size = yield* PROTO2.UVarInt32.deserialize(stream); + const result = new globalThis.Map(); + for (let i = 0; i < size; i++) { + const k = yield* kS.deserialize(stream); + const v = yield* vS.deserialize(stream); + result.set(k, v); + } + return result; + } + }; + } + PROTO2.Map = Map2; + function Set(s) { + return { + *serialize(set, stream) { + yield* PROTO2.UVarInt32.serialize(set.size, stream); + for (const v of set) { + yield* s.serialize(v, stream); + } + }, + *deserialize(stream) { + const size = yield* PROTO2.UVarInt32.deserialize(stream); + const result = new globalThis.Set(); + for (let i = 0; i < size; i++) { + const v = yield* s.deserialize(stream); + result.add(v); + } + return result; + } + }; + } + PROTO2.Set = Set; + function Cached(s, depth = 16) { + const cache = new globalThis.Map(); + return { + *serialize(value, stream) { + const hit = cache.get(value); + if (hit !== void 0) { + stream.write(hit); + cache.delete(value); + cache.set(value, hit); + } else { + const buffer = new PROTO2.Buffer(); + yield* s.serialize(value, buffer); + const bytes = buffer.to_uint8array(); + stream.write(bytes); + cache.set(value, bytes); + if (cache.size > depth) { + const first = cache.keys().next().value; + cache.delete(first); + } + } + }, + *deserialize(stream) { + return yield* s.deserialize(stream); + } + }; + } + PROTO2.Cached = Cached; +})(PROTO || (PROTO = {})); +var NET; +(function(NET2) { + const Endpoint = PROTO.String; + const Meta = PROTO.Object({ + guid: PROTO.String, + signature: PROTO.String + }); + const Header = PROTO.Object({ + meta: Meta, + index: PROTO.UVarInt32, + final: PROTO.Boolean + }); + const LISTENERS = /* @__PURE__ */ new Map(); + NET2.SIGNATURE = "mcbe-ipc:v3"; + NET2.FRAG_MAX = 2048; + function* serialize(buffer, max_size = Infinity) { + const uint8array = buffer.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 <= 127 ? 1 : char_code <= 2047 ? 2 : char_code <= 65535 ? 3 : 4; + const char_size = char_code > 255 ? utf16_size : 2; + if (acc_size + char_size > max_size) { + result.push(acc_str); + acc_str = ""; + acc_size = 0; + } + if (char_code > 255) { + 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; + } + NET2.serialize = serialize; + function* deserialize(strings) { + const buffer = new PROTO.Buffer(); + 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 <= 255) { + const hex = str[j] + str[++j]; + const hex_code = parseInt(hex, 16); + buffer.write(hex_code & 255); + buffer.write(hex_code >> 8); + } else { + buffer.write(char_code & 255); + buffer.write(char_code >> 8); + } + yield; + } + yield; + } + return buffer; + } + NET2.deserialize = deserialize; + system.afterEvents.scriptEventReceive.subscribe((event) => { + system.runJob((function* () { + if (event.sourceType !== ScriptEventSource.Server) + return; + const [serialized_endpoint, serialized_header] = event.id.split(":"); + if (!PROTO.MIPS.is_valid(serialized_endpoint)) + return; + const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint); + const endpoint = yield* Endpoint.deserialize(endpoint_stream); + const listeners = LISTENERS.get(endpoint); + if (listeners !== void 0 && PROTO.MIPS.is_valid(serialized_header)) { + const header_stream = yield* PROTO.MIPS.deserialize(serialized_header); + const header = yield* Header.deserialize(header_stream); + for (const listener of [...listeners]) { + try { + yield* listener(header, event.message); + } catch (e) { + console.error(`[MCBE-IPC] listener error while handling packet on "${endpoint}":`, e); + } + } + } + })()); + }); + function register(endpoint, listener) { + let listeners = LISTENERS.get(endpoint); + if (listeners === void 0) { + listeners = new Array(); + LISTENERS.set(endpoint, listeners); + } + listeners.push(listener); + return () => { + const idx = listeners.indexOf(listener); + if (idx !== -1) + listeners.splice(idx, 1); + if (listeners.length === 0) { + LISTENERS.delete(endpoint); + } + }; + } + function* emit(endpoint, serializer, value, options) { + const guid = options?.metaOverride?.guid ?? UTIL.generate_id(); + const signature = options?.metaOverride?.signature ?? NET2.SIGNATURE; + const endpoint_stream = new PROTO.Buffer(); + yield* Endpoint.serialize(endpoint, endpoint_stream); + const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream); + const packet_stream = new PROTO.Buffer(); + yield* serializer.serialize(value, packet_stream); + const serialized_packets = yield* serialize(packet_stream, NET2.FRAG_MAX); + for (let i = 0; i < serialized_packets.length; i++) { + const serialized_packet = serialized_packets[i]; + const header = { + meta: { guid, signature }, + index: i, + final: i === serialized_packets.length - 1 + }; + const header_stream = new PROTO.Buffer(); + yield* Header.serialize(header, header_stream); + const serialized_header = yield* PROTO.MIPS.serialize(header_stream); + system.sendScriptEvent(`${serialized_endpoint}:${serialized_header}`, serialized_packet); + } + } + NET2.emit = emit; + function listen(endpoint, deserializer, callback, options) { + const buffer = /* @__PURE__ */ new Map(); + const listener = function* (header, fragment) { + let packet = buffer.get(header.meta.guid); + if (packet === void 0) { + if (options?.filter?.(header.meta) === false) + return; + packet = { size: -1, fragments: [], received: 0 }; + buffer.set(header.meta.guid, packet); + } + if (header.final) { + packet.size = header.index + 1; + } + if (packet.fragments[header.index] === void 0) { + packet.fragments[header.index] = fragment; + packet.received++; + } else { + throw new Error(`received duplicate fragment ${header.index} for packet ${header.meta.guid}`); + } + if (packet.size !== -1 && packet.size === packet.received) { + const stream = yield* deserialize(packet.fragments); + const value = yield* deserializer.deserialize(stream); + yield* callback(value, header.meta); + buffer.delete(header.meta.guid); + } + }; + return register(endpoint, listener); + } + NET2.listen = listen; +})(NET || (NET = {})); +var IPC; +(function(IPC2) { + function send(channel, serializer, value) { + system.runJob(NET.emit(`ipc:${channel}:send`, serializer, value)); + } + IPC2.send = send; + function invoke(channel, serializer, value, deserializer) { + const id = UTIL.generate_id(); + return new Promise((resolve) => { + const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value2, meta) { + if (meta.signature.includes(`+correlation`) && meta.guid !== id) + return; + resolve(value2); + terminate(); + }, { + filter: (meta) => !meta.signature.includes(`+correlation`) || meta.guid === id + }); + system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value, { + metaOverride: { + guid: id, + signature: `${NET.SIGNATURE}+correlation` + } + })); + }); + } + IPC2.invoke = invoke; + function on(channel, deserializer, listener) { + return NET.listen(`ipc:${channel}:send`, deserializer, function* (value) { + listener(value); + }); + } + IPC2.on = on; + function once(channel, deserializer, listener) { + const terminate = NET.listen(`ipc:${channel}:send`, deserializer, function* (value) { + listener(value); + terminate(); + }); + return terminate; + } + IPC2.once = once; + function handle(channel, deserializer, serializer, listener) { + return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value, meta) { + const result = listener(value); + yield* NET.emit(`ipc:${channel}:handle`, serializer, result, { + metaOverride: meta.signature.includes(`+correlation`) ? { + guid: meta.guid, + signature: `${NET.SIGNATURE}+correlation` + } : void 0 + }); + }); + } + IPC2.handle = handle; +})(IPC || (IPC = {})); + +// Errors/APIErrorEnum.js +var APIErrorEnum = Object.freeze({ + Unknown: 0, + Success: 1, + Caller: 2, + Server: 3 +}); + +// Errors/APICallerError.js +var APICallerError = class extends Error { + constructor(error) { + super(error.message); + this.errorName = error.name; + this.errorMessage = error.message; + this.errorCode = APIErrorEnum.Caller; + this.name = "APICallerError"; + } +}; + +// Errors/APIServerError.js +var APIServerError = class extends Error { + constructor(error) { + super(error.message); + this.thrownError = error; + this.errorCode = APIErrorEnum.Server; + this.name = "APIServerError"; + } +}; + +// Errors/APIVersionMismatchError.js +var APIVersionMismatchError = class extends Error { + constructor(serverApiVersion, callerApiVersion) { + super(`API version numbers do not match (${callerApiVersion} != ${serverApiVersion}). Please use API version ${serverApiVersion}.`); + this.name = "APIVersionMismatchError"; + } +}; + +// APIModels.js +var VoidModel = PROTO.Void; +var ErrorModel = PROTO.Optional(PROTO.Object({ + code: PROTO.Int8, + name: PROTO.Optional(PROTO.String), + message: PROTO.Optional(PROTO.String) +})); +var ReturnModelShell = { + apiVersion: PROTO.String, + data: void 0, + error: ErrorModel +}; +var CallModelShell = { + apiVersion: PROTO.String, + parameterMap: void 0 +}; + +// AddonAPI.js +var AddonAPI = class { + #name; + #version; + constructor(name, version) { + this.#name = name; + this.#version = version; + } + get name() { + return this.#name; + } + get version() { + return this.#version; + } + get endpointBase() { + return this.#name + ":"; + } + setupController(apiController) { + for (const [endpoint, features] of Object.entries(apiController.endpoints)) { + const { callback, parameterModel, returnModel } = features; + const boundCallback = callback.bind(apiController); + this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel); + } + } + #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { + const returnPacketModel = this.#resolveReturnModel(returnDataModel); + const endpointPath = this.endpointBase + endpoint; + IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { + const apiVersion = callPacket.apiVersion; + const parameters = Object.values(callPacket.parameterMap); + return this.#handleCallback(apiVersion, callback, parameters); + }); + } + #handleCallback(apiVersion, callback, parameters) { + try { + this.#assertVersionsMatch(apiVersion); + const returnValue = callback(...parameters); + return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue); + } catch (error) { + if (error instanceof APICallerError) { + const errorPacket2 = this.#resolveErrorPacket(error); + return this.#bundleReturnPacket(errorPacket2); + } + console.error(error); + const apiError = new APIServerError(error); + const errorPacket = this.#resolveErrorPacket(apiError); + return this.#bundleReturnPacket(errorPacket); + } + } + #assertVersionsMatch(versionToCheck) { + if (versionToCheck !== this.version) { + const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck); + throw new APICallerError(apiVersionMismatchError); + } + } + #resolveReturnModel(returnDataModel) { + let returnModel = { ...ReturnModelShell }; + returnModel.data = returnDataModel; + returnModel = PROTO.Object(returnModel); + return returnModel; + } + #bundleReturnPacket(errorPacket, returnValue = void 0) { + return { + apiVersion: this.version, + data: returnValue, + error: errorPacket + }; + } + #resolveErrorPacket(error) { + return { + code: error.errorCode, + name: error.thrownError.name, + message: error.thrownError.message + }; + } +}; + +// APIController.js +var APIController = class _APIController { + #endpoints; + constructor(endpoints) { + if (this.constructor === _APIController) + throw new Error("Cannot instantiate abstract class 'APIController'"); + this.#endpoints = endpoints; + } + get endpoints() { + return this.#endpoints; + } +}; + +// AddonAPICaller.js +var AddonAPICaller = class { + static async call(endpoint, parameterModel, parameterMap, returnDataModel) { + return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); + } +}; +export { + APICallerError, + APIController, + AddonAPI, + AddonAPICaller, + PROTO, + VoidModel +}; +/** + * @license + * MIT License + * + * Copyright (c) 2026 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. + */ diff --git a/packs/BP/scripts/lib/AddonAPIKit/APIController.js b/packs/BP/scripts/lib/AddonAPIKit/APIController.js deleted file mode 100644 index 2ea4391..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/APIController.js +++ /dev/null @@ -1,13 +0,0 @@ -export class APIController { - #endpoints; - - constructor(endpoints) { - if (this.constructor === APIController) - throw new Error("Cannot instantiate abstract class 'APIController'"); - this.#endpoints = endpoints; - } - - get endpoints() { - return this.#endpoints; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/APIModels.js b/packs/BP/scripts/lib/AddonAPIKit/APIModels.js deleted file mode 100644 index f0ca617..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/APIModels.js +++ /dev/null @@ -1,20 +0,0 @@ -import { PROTO } from "./MCBE-IPC/ipc"; - -export const VoidModel = PROTO.Void; - -export const ErrorModel = PROTO.Optional(PROTO.Object({ - code: PROTO.Int8, - name: PROTO.Optional(PROTO.String), - message: PROTO.Optional(PROTO.String) -})); - -export const ReturnModelShell = { - apiVersion: PROTO.String, - data: void 0, - error: ErrorModel -}; - -export const CallModelShell = { - apiVersion: PROTO.String, - parameterMap: void 0 -}; \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js deleted file mode 100644 index c02c006..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/AddonAPI.js +++ /dev/null @@ -1,92 +0,0 @@ -import { IPC, PROTO } from "./MCBE-IPC/ipc"; -import { APICallerError } from "./Errors/APICallerError"; -import { APIErrorEnum } from "./Errors/APIErrorEnum"; -import { APIServerError } from "./Errors/APIServerError"; -import { APIVersionMismatchError } from "./Errors/APIVersionMismatchError"; -import { ReturnModelShell } from "./APIModels"; - -export class AddonAPI { - #name; - #version; - - constructor(name, version) { - this.#name = name; - this.#version = version; - } - - get name() { - return this.#name; - } - - get version() { - return this.#version; - } - - get endpointBase() { - return this.#name + ':'; - } - - setupController(apiController) { - for (const [endpoint, features] of Object.entries(apiController.endpoints)) { - const {callback, parameterModel, returnModel} = features; - const boundCallback = callback.bind(apiController); - this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel); - } - } - - #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { - const returnPacketModel = this.#resolveReturnModel(returnDataModel); - const endpointPath = this.endpointBase + endpoint; - IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { - const apiVersion = callPacket.apiVersion; - const parameters = Object.values(callPacket.parameterMap); - return this.#handleCallback(apiVersion, callback, parameters); - }); - } - - #handleCallback(apiVersion, callback, parameters) { - try { - this.#assertVersionsMatch(apiVersion); - const returnValue = callback(...parameters); - return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue); - } catch(error) { - if (error instanceof APICallerError) - const errorPacket = this.#resolveErrorPacket(error); - return this.#bundleReturnPacket(errorPacket); - console.error(error); - const apiError = new APIServerError(error); - const errorPacket = this.#resolveErrorPacket(apiError); - return this.#bundleReturnPacket(errorPacket); - } - } - - #assertVersionsMatch(versionToCheck) { - if (versionToCheck !== this.version) { - const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck); - throw new APICallerError(apiVersionMismatchError); - } - } - - #resolveReturnModel(returnDataModel) { - let returnModel = { ...ReturnModelShell }; - returnModel.data = returnDataModel; - returnModel = PROTO.Object(returnModel); - return returnModel; - } - - #bundleReturnPacket(errorPacket, returnValue = void 0) { - return { - apiVersion: this.version, - data: returnValue, - error: errorPacket - }; - } - - #resolveErrorPacket(error) { - return { - code: error.errorCode, - name: error.thrownError.name, - message: error.thrownError.message - }; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js deleted file mode 100644 index 23fff2c..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/AddonAPICaller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { IPC } from "./MCBE-IPC/ipc"; - -export class AddonAPICaller { - static async call(endpoint, parameterModel, parameterMap, returnDataModel) { - return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then(result => result.value); - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js deleted file mode 100644 index d08b378..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/AddonAPIKit.js +++ /dev/null @@ -1,8 +0,0 @@ -import { AddonAPI } from "./AddonAPI"; -import { APIController } from "./APIController"; -import { VoidModel } from "./APIModels"; -import { APICallerError } from "./Errors/APICallerError"; -import { PROTO } from "./MCBE-IPC/ipc"; -import { AddonAPICaller } from "./AddonAPICaller"; - -export { AddonAPI, APIController, VoidModel, APICallerError, PROTO, AddonAPICaller }; \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js deleted file mode 100644 index 27293da..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/Errors/APICallerError.js +++ /dev/null @@ -1,11 +0,0 @@ -import { APIErrorEnum } from "./APIErrorEnum"; - -export class APICallerError extends Error { - constructor(error) { - super(error.message); - this.errorName = error.name; - this.errorMessage = error.message; - this.errorCode = APIErrorEnum.Caller; - this.name = "APICallerError"; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js deleted file mode 100644 index ba33a7d..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIErrorEnum.js +++ /dev/null @@ -1,6 +0,0 @@ -export const APIErrorEnum = Object.freeze({ - Unknown: 0, - Success: 1, - Caller: 2, - Server: 3 -}); \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js deleted file mode 100644 index 0b91e62..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIServerError.js +++ /dev/null @@ -1,10 +0,0 @@ -import { APIErrorEnum } from "./APIErrorEnum"; - -export class APIServerError extends Error { - constructor(error) { - super(error.message); - this.thrownError = error; - this.errorCode = APIErrorEnum.Server; - this.name = "APIServerError"; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js b/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js deleted file mode 100644 index 6bb8fa0..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/Errors/APIVersionMismatchError.js +++ /dev/null @@ -1,6 +0,0 @@ -export class APIVersionMismatchError extends Error { - constructor(serverApiVersion, callerApiVersion) { - super(`API version numbers do not match (${callerApiVersion} != ${serverApiVersion}). Please use API version ${serverApiVersion}.`); - this.name = 'APIVersionMismatchError'; - } -} \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts deleted file mode 100644 index 6d6a10b..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2026 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 Serializer { - serialize(value: T, stream: Buffer): Generator; - } - interface Deserializer { - deserialize(stream: Buffer): Generator; - } - interface Serializable extends Serializer, Deserializer { - } - class Buffer { - private _buffer; - private _data_view; - private _length; - private _offset; - get end(): number; - get front(): number; - get data_view(): DataView; - constructor(size?: number); - reserve(amount: number): number; - consume(amount: number): number; - write(byte: number): void; - write(bytes: Uint8Array): void; - read(): number; - read(amount: number): Uint8Array; - ensure_capacity(size: number): void; - static from_uint8array(array: Uint8Array): Buffer; - to_uint8array(): Uint8Array; - } - namespace MIPS { - function is_valid(str: string): boolean; - function serialize(stream: PROTO.Buffer): 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 VarInt32: 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(s: { - [K in keyof T]: PROTO.Serializable; - }): PROTO.Serializable; - function Array(s: PROTO.Serializable): PROTO.Serializable; - function Tuple(...s: { - [K in keyof T]: PROTO.Serializable; - }): PROTO.Serializable; - function Optional(s: PROTO.Serializable): PROTO.Serializable; - function Map(kS: PROTO.Serializable, vS: PROTO.Serializable): PROTO.Serializable>; - function Set(s: PROTO.Serializable): PROTO.Serializable>; - function Cached(s: PROTO.Serializable, depth?: number): PROTO.Serializable; -} -export declare namespace NET { - type Meta = { - guid: string; - signature: string; - }; - const Meta: PROTO.Serializable; - export const SIGNATURE: string; - export let FRAG_MAX: number; - export function serialize(buffer: PROTO.Buffer, max_size?: number): Generator; - export function deserialize(strings: string[]): Generator; - export interface EmitOptions { - metaOverride?: Partial; - } - export function emit(endpoint: string, serializer: PROTO.Serializer, value: NoInfer, options?: EmitOptions): Generator; - export interface ListenOptions { - filter?: (meta: Meta) => boolean; - } - export function listen(endpoint: string, deserializer: PROTO.Deserializer, callback: (value: NoInfer, meta: Meta) => Generator, options?: ListenOptions): () => void; - export {}; -} -export declare namespace IPC { - /** Sends a message with `args` to `channel` */ - function send(channel: string, serializer: PROTO.Serializer, value: NoInfer): void; - /** Sends an `invoke` message through IPC, and expects a result asynchronously. */ - function invoke(channel: string, serializer: PROTO.Serializer, value: NoInfer, deserializer: PROTO.Deserializer): Promise>; - /** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */ - function on(channel: string, deserializer: PROTO.Deserializer, listener: (value: NoInfer) => void): () => void; - /** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */ - function once(channel: string, deserializer: PROTO.Deserializer, listener: (value: NoInfer) => void): () => void; - /** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */ - function handle(channel: string, deserializer: PROTO.Deserializer, serializer: PROTO.Serializer, listener: (value: NoInfer) => NoInfer): () => void; -} -export default IPC; diff --git a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js b/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js deleted file mode 100644 index 8da8f92..0000000 --- a/packs/BP/scripts/lib/AddonAPIKit/MCBE-IPC/ipc.js +++ /dev/null @@ -1,658 +0,0 @@ -/** - * @license - * MIT License - * - * Copyright (c) 2026 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 } from '@minecraft/server'; -var UTIL; -(function (UTIL) { - function generate_id() { - const r = (Math.random() * 0x100000000) >>> 0; - return r.toString(16).padStart(8, '0').toUpperCase(); - } - UTIL.generate_id = generate_id; -})(UTIL || (UTIL = {})); -export var PROTO; -(function (PROTO) { - class Buffer { - 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; - } - reserve(amount) { - this.ensure_capacity(amount); - const end = this.end; - this._length += amount; - return end; - } - consume(amount) { - if (amount > this._length) - throw new Error('not enough bytes'); - const front = this.front; - this._length -= amount; - this._offset += amount; - return front; - } - write(input) { - if (typeof input === 'number') { - const offset = this.reserve(1); - this._buffer[offset] = input; - } - else { - const offset = this.reserve(input.length); - this._buffer.set(input, offset); - } - } - read(amount) { - if (amount === undefined) { - const offset = this.consume(1); - return this._buffer[offset]; - } - else { - const offset = this.consume(amount); - return this._buffer.slice(offset, offset + amount); - } - } - 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 buffer = new Buffer(); - buffer._buffer = array; - buffer._length = array.length; - buffer._offset = 0; - buffer._data_view = new DataView(array.buffer); - return buffer; - } - to_uint8array() { - return this._buffer.subarray(this._offset, this.end); - } - } - PROTO.Buffer = Buffer; - let MIPS; - (function (MIPS) { - function is_valid(str) { - return str.startsWith('(0x') && str.endsWith(')'); - } - MIPS.is_valid = is_valid; - function* serialize(stream) { - const uint8array = stream.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 (is_valid(str)) { - const buffer = new Buffer(); - 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]; - buffer.write(parseInt(hex, 16)); - yield; - } - return buffer; - } - return new Buffer(); - } - 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) { - stream.data_view.setInt8(stream.reserve(1), value); - }, - *deserialize(stream) { - return stream.data_view.getInt8(stream.consume(1)); - } - }; - PROTO.Int16 = { - *serialize(value, stream) { - stream.data_view.setInt16(stream.reserve(2), value); - }, - *deserialize(stream) { - return stream.data_view.getInt16(stream.consume(2)); - } - }; - PROTO.Int32 = { - *serialize(value, stream) { - stream.data_view.setInt32(stream.reserve(4), value); - }, - *deserialize(stream) { - return stream.data_view.getInt32(stream.consume(4)); - } - }; - PROTO.UInt8 = { - *serialize(value, stream) { - stream.data_view.setUint8(stream.reserve(1), value); - }, - *deserialize(stream) { - return stream.data_view.getUint8(stream.consume(1)); - } - }; - PROTO.UInt16 = { - *serialize(value, stream) { - stream.data_view.setUint16(stream.reserve(2), value); - }, - *deserialize(stream) { - return stream.data_view.getUint16(stream.consume(2)); - } - }; - PROTO.UInt32 = { - *serialize(value, stream) { - stream.data_view.setUint32(stream.reserve(4), value); - }, - *deserialize(stream) { - return stream.data_view.getUint32(stream.consume(4)); - } - }; - PROTO.UVarInt32 = { - *serialize(value, stream) { - value >>>= 0; - while (value >= 0x80) { - stream.write((value & 0x7f) | 0x80); - value >>>= 7; - yield; - } - stream.write(value); - }, - *deserialize(stream) { - let value = 0; - for (let size = 0; size < 5; size++) { - const byte = stream.read(); - value |= (byte & 0x7f) << (size * 7); - yield; - if ((byte & 0x80) == 0) - break; - } - return value >>> 0; - } - }; - PROTO.VarInt32 = { - *serialize(value, stream) { - const zigzag = (value << 1) ^ (value >> 31); - yield* PROTO.UVarInt32.serialize(zigzag, stream); - }, - *deserialize(stream) { - const zigzag = yield* PROTO.UVarInt32.deserialize(stream); - return (zigzag >>> 1) ^ -(zigzag & 1); - } - }; - PROTO.Float32 = { - *serialize(value, stream) { - stream.data_view.setFloat32(stream.reserve(4), value); - }, - *deserialize(stream) { - return stream.data_view.getFloat32(stream.consume(4)); - } - }; - PROTO.Float64 = { - *serialize(value, stream) { - stream.data_view.setFloat64(stream.reserve(8), value); - }, - *deserialize(stream) { - return stream.data_view.getFloat64(stream.consume(8)); - } - }; - 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) { - return stream.read() !== 0; - } - }; - 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 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(s) { - return { - *serialize(value, stream) { - for (const key in s) { - yield* s[key].serialize(value[key], stream); - } - }, - *deserialize(stream) { - const result = {}; - for (const key in s) { - result[key] = yield* s[key].deserialize(stream); - } - return result; - } - }; - } - PROTO.Object = Object; - function Array(s) { - return { - *serialize(value, stream) { - yield* PROTO.UVarInt32.serialize(value.length, stream); - for (const item of value) { - yield* s.serialize(item, stream); - } - }, - *deserialize(stream) { - const result = []; - const length = yield* PROTO.UVarInt32.deserialize(stream); - for (let i = 0; i < length; i++) { - result[i] = yield* s.deserialize(stream); - } - return result; - } - }; - } - PROTO.Array = Array; - function Tuple(...s) { - return { - *serialize(value, stream) { - for (let i = 0; i < s.length; i++) { - yield* s[i].serialize(value[i], stream); - } - }, - *deserialize(stream) { - const result = []; - for (let i = 0; i < s.length; i++) { - result[i] = yield* s[i].deserialize(stream); - } - return result; - } - }; - } - PROTO.Tuple = Tuple; - function Optional(s) { - return { - *serialize(value, stream) { - const def = value !== undefined; - yield* PROTO.Boolean.serialize(def, stream); - if (def) - yield* s.serialize(value, stream); - }, - *deserialize(stream) { - const def = yield* PROTO.Boolean.deserialize(stream); - if (def) - return yield* s.deserialize(stream); - return undefined; - } - }; - } - PROTO.Optional = Optional; - function Map(kS, vS) { - return { - *serialize(value, stream) { - yield* PROTO.UVarInt32.serialize(value.size, stream); - for (const [k, v] of value) { - yield* kS.serialize(k, stream); - yield* vS.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* kS.deserialize(stream); - const v = yield* vS.deserialize(stream); - result.set(k, v); - } - return result; - } - }; - } - PROTO.Map = Map; - function Set(s) { - return { - *serialize(set, stream) { - yield* PROTO.UVarInt32.serialize(set.size, stream); - for (const v of set) { - yield* s.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* s.deserialize(stream); - result.add(v); - } - return result; - } - }; - } - PROTO.Set = Set; - function Cached(s, depth = 16) { - const cache = new globalThis.Map(); - return { - *serialize(value, stream) { - const hit = cache.get(value); - if (hit !== undefined) { - stream.write(hit); - cache.delete(value); - cache.set(value, hit); - } - else { - const buffer = new PROTO.Buffer(); - yield* s.serialize(value, buffer); - const bytes = buffer.to_uint8array(); - stream.write(bytes); - cache.set(value, bytes); - if (cache.size > depth) { - const first = cache.keys().next().value; - cache.delete(first); - } - } - }, - *deserialize(stream) { - return yield* s.deserialize(stream); - } - }; - } - PROTO.Cached = Cached; -})(PROTO || (PROTO = {})); -export var NET; -(function (NET) { - const Endpoint = PROTO.String; - const Meta = PROTO.Object({ - guid: PROTO.String, - signature: PROTO.String - }); - const Header = PROTO.Object({ - meta: Meta, - index: PROTO.UVarInt32, - final: PROTO.Boolean - }); - const LISTENERS = new Map(); - NET.SIGNATURE = 'mcbe-ipc:v3'; - NET.FRAG_MAX = 2048; - function* serialize(buffer, max_size = Infinity) { - const uint8array = buffer.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 buffer = new PROTO.Buffer(); - 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); - buffer.write(hex_code & 0xff); - buffer.write(hex_code >> 8); - } - else { - buffer.write(char_code & 0xff); - buffer.write(char_code >> 8); - } - yield; - } - yield; - } - return buffer; - } - NET.deserialize = deserialize; - system.afterEvents.scriptEventReceive.subscribe(event => { - system.runJob((function* () { - if (event.sourceType !== ScriptEventSource.Server) - return; - const [serialized_endpoint, serialized_header] = event.id.split(':'); - if (!PROTO.MIPS.is_valid(serialized_endpoint)) - return; - const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint); - const endpoint = yield* Endpoint.deserialize(endpoint_stream); - const listeners = LISTENERS.get(endpoint); - if (listeners !== undefined && PROTO.MIPS.is_valid(serialized_header)) { - const header_stream = yield* PROTO.MIPS.deserialize(serialized_header); - const header = yield* Header.deserialize(header_stream); - for (const listener of [...listeners]) { - try { - yield* listener(header, event.message); - } - catch (e) { - console.error(`[MCBE-IPC] listener error while handling packet on "${endpoint}":`, e); - } - } - } - })()); - }); - function register(endpoint, listener) { - let listeners = LISTENERS.get(endpoint); - if (listeners === undefined) { - listeners = new Array(); - LISTENERS.set(endpoint, listeners); - } - listeners.push(listener); - return () => { - const idx = listeners.indexOf(listener); - if (idx !== -1) - listeners.splice(idx, 1); - if (listeners.length === 0) { - LISTENERS.delete(endpoint); - } - }; - } - function* emit(endpoint, serializer, value, options) { - const guid = options?.metaOverride?.guid ?? UTIL.generate_id(); - const signature = options?.metaOverride?.signature ?? NET.SIGNATURE; - const endpoint_stream = new PROTO.Buffer(); - yield* Endpoint.serialize(endpoint, endpoint_stream); - const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream); - const packet_stream = new PROTO.Buffer(); - yield* serializer.serialize(value, packet_stream); - const serialized_packets = yield* serialize(packet_stream, NET.FRAG_MAX); - for (let i = 0; i < serialized_packets.length; i++) { - const serialized_packet = serialized_packets[i]; - const header = { - meta: { guid, signature }, - index: i, - final: i === serialized_packets.length - 1 - }; - const header_stream = new PROTO.Buffer(); - yield* Header.serialize(header, header_stream); - const serialized_header = yield* PROTO.MIPS.serialize(header_stream); - system.sendScriptEvent(`${serialized_endpoint}:${serialized_header}`, serialized_packet); - } - } - NET.emit = emit; - function listen(endpoint, deserializer, callback, options) { - const buffer = new Map(); - const listener = function* (header, fragment) { - let packet = buffer.get(header.meta.guid); - if (packet === undefined) { - if (options?.filter?.(header.meta) === false) - return; - packet = { size: -1, fragments: [], received: 0 }; - buffer.set(header.meta.guid, packet); - } - if (header.final) { - packet.size = header.index + 1; - } - if (packet.fragments[header.index] === undefined) { - packet.fragments[header.index] = fragment; - packet.received++; - } - else { - throw new Error(`received duplicate fragment ${header.index} for packet ${header.meta.guid}`); - } - if (packet.size !== -1 && packet.size === packet.received) { - const stream = yield* deserialize(packet.fragments); - const value = yield* deserializer.deserialize(stream); - yield* callback(value, header.meta); - buffer.delete(header.meta.guid); - } - }; - return register(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) { - const id = UTIL.generate_id(); - return new Promise(resolve => { - const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value, meta) { - if (meta.signature.includes(`+correlation`) && meta.guid !== id) - return; - resolve(value); - terminate(); - }, { - filter: meta => !meta.signature.includes(`+correlation`) || meta.guid === id - }); - system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value, { - metaOverride: { - guid: id, - signature: `${NET.SIGNATURE}+correlation` - } - })); - }); - } - 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, meta) { - const result = listener(value); - yield* NET.emit(`ipc:${channel}:handle`, serializer, result, { - metaOverride: meta.signature.includes(`+correlation`) - ? { - guid: meta.guid, - signature: `${NET.SIGNATURE}+correlation` - } - : undefined - }); - }); - } - IPC.handle = handle; -})(IPC || (IPC = {})); -export default IPC; From 5bd8452312fcaffee756044293ca413fe08e7e57 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 30 May 2026 12:51:36 +0200 Subject: [PATCH 41/46] refactor: upgrade AddonAPIKit and propogate changes --- .../API/controllers/InstancesController.js | 17 ++-- packs/BP/scripts/lib/AddonAPIKit.js | 90 +++++++++++++++---- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/packs/BP/scripts/API/controllers/InstancesController.js b/packs/BP/scripts/API/controllers/InstancesController.js index ba97d39..3cdfb4a 100644 --- a/packs/BP/scripts/API/controllers/InstancesController.js +++ b/packs/BP/scripts/API/controllers/InstancesController.js @@ -1,21 +1,20 @@ import { InstanceExistsError } from "../../classes/Errors/InstanceExistsError"; import { InstanceNotFoundError } from "../../classes/Errors/InstanceNotFoundError"; import { StructureNotFoundError } from "../../classes/Errors/StructureNotFoundError"; -import { APICallerError, VoidModel } from "../../lib/AddonAPIKit"; +import { APICallerError, VoidModel, APIController } from "../../lib/AddonAPIKit"; import { AddInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../models/InstancesModel"; export class InstancesController extends APIController { #context; constructor(context) { - super({ - "instances": { callback: this.getInstances, parameterModel: VoidModel, returnModel: InstancesModel }, - "instance:get": { callback: this.getInstance, parameterModel: InstanceNameParameterModel, returnModel: InstanceModel }, - "instance:add": { callback: this.addInstance, parameterModel: AddInstanceParameterModel, returnModel: InstanceModel }, - "instance:edit": { callback: this.editInstance, parameterModel: InstanceModel, returnModel: InstanceModel }, - "instance:delete": { callback: this.deleteInstance, parameterModel: InstanceNameParameterModel, returnModel: VoidModel }, - "instance:materials": { callback: this.getMaterials, parameterModel: InstanceNameParameterModel, returnModel: StructureMaterialsModel } - }); + super(); + this.addEndpoint("instances", this.getInstances, VoidModel, InstancesModel); + this.addEndpoint("instance:get", this.getInstance, InstanceNameParameterModel, InstanceModel); + this.addEndpoint("instance:add", this.addInstance, AddInstanceParameterModel, InstanceModel); + this.addEndpoint("instance:edit", this.editInstance, InstanceModel, InstanceModel); + this.addEndpoint("instance:delete", this.deleteInstance, InstanceNameParameterModel, VoidModel); + this.addEndpoint("instance:materials", this.getMaterials, InstanceNameParameterModel, StructureMaterialsModel); this.#context = context; } diff --git a/packs/BP/scripts/lib/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit.js index 97670ca..bb662ff 100644 --- a/packs/BP/scripts/lib/AddonAPIKit.js +++ b/packs/BP/scripts/lib/AddonAPIKit.js @@ -4,7 +4,7 @@ * See LICENSE for details. */ -// MCBE-IPC/ipc.js +// src/MCBE-IPC/ipc.js import { ScriptEventSource, system } from "@minecraft/server"; var UTIL; (function(UTIL2) { @@ -629,7 +629,7 @@ var IPC; IPC2.handle = handle; })(IPC || (IPC = {})); -// Errors/APIErrorEnum.js +// src/Errors/APIErrorEnum.js var APIErrorEnum = Object.freeze({ Unknown: 0, Success: 1, @@ -637,7 +637,7 @@ var APIErrorEnum = Object.freeze({ Server: 3 }); -// Errors/APICallerError.js +// src/Errors/APICallerError.js var APICallerError = class extends Error { constructor(error) { super(error.message); @@ -648,7 +648,7 @@ var APICallerError = class extends Error { } }; -// Errors/APIServerError.js +// src/Errors/APIServerError.js var APIServerError = class extends Error { constructor(error) { super(error.message); @@ -658,7 +658,7 @@ var APIServerError = class extends Error { } }; -// Errors/APIVersionMismatchError.js +// src/Errors/APIVersionMismatchError.js var APIVersionMismatchError = class extends Error { constructor(serverApiVersion, callerApiVersion) { super(`API version numbers do not match (${callerApiVersion} != ${serverApiVersion}). Please use API version ${serverApiVersion}.`); @@ -666,7 +666,7 @@ var APIVersionMismatchError = class extends Error { } }; -// APIModels.js +// src/APIModels.js var VoidModel = PROTO.Void; var ErrorModel = PROTO.Optional(PROTO.Object({ code: PROTO.Int8, @@ -682,14 +682,50 @@ var CallModelShell = { apiVersion: PROTO.String, parameterMap: void 0 }; +var EndpointModel = PROTO.String; +var EndpointsModel = PROTO.Array(EndpointModel); -// AddonAPI.js +// src/APIController.js +var APIController = class _APIController { + #endpoints = {}; + constructor() { + if (this.constructor === _APIController) + throw new Error("Cannot instantiate abstract class 'APIController'"); + } + get endpoints() { + return this.#endpoints; + } + addEndpoint(endpoint, callback, parameterModel, returnModel) { + this.#endpoints[endpoint] = { callback, parameterModel, returnModel }; + } +}; + +// src/EndpointsController.js +var EndpointsController = class extends APIController { + #api; + constructor(api) { + this.#api = api; + this.addEndpoint("endpoints", this.getEndpoints, VoidModel, EndpointsModel); + this.addEndpoint("endpoints:has", this.hasEndpoint, EndpointModel, PROTO.Boolean); + } + getEndpoints() { + return this.#api.endpoints; + } + hasEndpoint(endpoint) { + return this.getEndpoints().includes(endpoint); + } +}; + +// src/AddonAPI.js var AddonAPI = class { #name; #version; + #allEndpoints; constructor(name, version) { this.#name = name; this.#version = version; + const endpointsController = new EndpointsController(this); + this.setupController(endpointsController); } get name() { return this.#name; @@ -700,6 +736,9 @@ var AddonAPI = class { get endpointBase() { return this.#name + ":"; } + get endpoints() { + return this.#allEndpoints; + } setupController(apiController) { for (const [endpoint, features] of Object.entries(apiController.endpoints)) { const { callback, parameterModel, returnModel } = features; @@ -715,6 +754,7 @@ var AddonAPI = class { const parameters = Object.values(callPacket.parameterMap); return this.#handleCallback(apiVersion, callback, parameters); }); + this.#allEndpoints.push(endpointPath); } #handleCallback(apiVersion, callback, parameters) { try { @@ -760,28 +800,40 @@ var AddonAPI = class { } }; -// APIController.js -var APIController = class _APIController { - #endpoints; - constructor(endpoints) { - if (this.constructor === _APIController) - throw new Error("Cannot instantiate abstract class 'APIController'"); - this.#endpoints = endpoints; - } - get endpoints() { - return this.#endpoints; +// src/AddonAPICaller.js +import { system as system2 } from "@minecraft/server"; + +// src/Errors/APIEndpointNotFoundError.js +var APIEndpointNotFoundError = class extends Error { + constructor(endpoint) { + super(`Endpoint "${endpoint}" was not found.`); + this.name = "APIEndpointNotFoundError"; } }; -// AddonAPICaller.js +// src/AddonAPICaller.js var AddonAPICaller = class { + static #validEndpointCache = []; static async call(endpoint, parameterModel, parameterMap, returnDataModel) { - return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); + if (this.#validEndpointCache.length === 0) { + const endpointBase = endpoint.split(":")[0]; + await this.#populateValidEndpointCache(endpointBase); + } + if (this.#validEndpointCache.includes(endpoint)) + return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); + else + throw new APIEndpointNotFoundError(endpoint); + } + static async #populateValidEndpointCache(endpointBase) { + const endpointsEndpoint = endpointBase + ":endpoints"; + const validEndpoints = await IPC.invoke(endpointsEndpoint, VoidModel, void 0, PROTO.Boolean); + this.#validEndpointCache.push(...validEndpoints); } }; export { APICallerError, APIController, + APIErrorEnum, AddonAPI, AddonAPICaller, PROTO, From 630d5e0950c899595b8b6cd95eb36c38558d9cd6 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 1 Jun 2026 10:59:31 +0200 Subject: [PATCH 42/46] refactor: upgrade AddonAPIKit and propograte changes also fix param input of instance:edit --- packs/BP/scripts/API/ConstructAPI.js | 4 +- ...InstancesModel.js => ConstructAPIModel.js} | 18 ++++- .../API/controllers/BuildersController.js | 2 +- .../API/controllers/InstancesController.js | 4 +- packs/BP/scripts/API/models/BuildersModel.js | 13 ---- packs/BP/scripts/lib/AddonAPIKit.js | 66 +++++++++++++------ 6 files changed, 67 insertions(+), 40 deletions(-) rename packs/BP/scripts/API/{models/InstancesModel.js => ConstructAPIModel.js} (74%) delete mode 100644 packs/BP/scripts/API/models/BuildersModel.js diff --git a/packs/BP/scripts/API/ConstructAPI.js b/packs/BP/scripts/API/ConstructAPI.js index f27d9f9..4b3325d 100644 --- a/packs/BP/scripts/API/ConstructAPI.js +++ b/packs/BP/scripts/API/ConstructAPI.js @@ -1,11 +1,11 @@ -import { AddonAPI } from "../lib/AddonAPIKit"; +import { AddonAPIServer } from "../lib/AddonAPIKit"; import { PACK_IDENTIFIER } from "../consts"; import { InstancesController } from "./controllers/InstancesController"; import { structureCollection } from "../classes/Structure/StructureCollection"; import { BuildersController } from "./controllers/BuildersController"; import { Builders } from "../classes/Builder/Builders"; -class ConstructAPI extends AddonAPI { +class ConstructAPI extends AddonAPIServer { constructor(version) { super(PACK_IDENTIFIER, version); const instancesController = new InstancesController(structureCollection); diff --git a/packs/BP/scripts/API/models/InstancesModel.js b/packs/BP/scripts/API/ConstructAPIModel.js similarity index 74% rename from packs/BP/scripts/API/models/InstancesModel.js rename to packs/BP/scripts/API/ConstructAPIModel.js index cc16bd5..a761777 100644 --- a/packs/BP/scripts/API/models/InstancesModel.js +++ b/packs/BP/scripts/API/ConstructAPIModel.js @@ -1,4 +1,6 @@ -import { PROTO } from '../../lib/AddonAPIKit'; +import { PROTO } from "../../lib/AddonAPIKit"; + +// Instances const LocationModel = PROTO.Object({ x: PROTO.Float64, @@ -42,3 +44,17 @@ export const EditInstanceParameterModel = PROTO.Object({ instanceName: PROTO.String, instance: InstanceModel }); + +// Builders + +export const BuilderModel = PROTO.Object({ + playerId: PROTO.String, + easyPlace: PROTO.Boolean, + fastEasyPlace: PROTO.Boolean, + materialGrabber: PROTO.Boolean, + materialInstanceName: PROTO.String +}); + +export const BuilderIdParameterModel = PROTO.Object({ + playerId: PROTO.String +}); \ No newline at end of file diff --git a/packs/BP/scripts/API/controllers/BuildersController.js b/packs/BP/scripts/API/controllers/BuildersController.js index 20d2032..6f39523 100644 --- a/packs/BP/scripts/API/controllers/BuildersController.js +++ b/packs/BP/scripts/API/controllers/BuildersController.js @@ -1,6 +1,6 @@ import { BuilderNotFoundError } from "../../classes/Errors/BuilderNotFoundError"; import { APICallerError } from "../../lib/AddonAPIKit"; -import { BuilderIdParameterModel } from "../models/BuildersModel"; +import { BuilderIdParameterModel } from "../ConstructAPIModel"; export class BuildersController extends APIController { #context; diff --git a/packs/BP/scripts/API/controllers/InstancesController.js b/packs/BP/scripts/API/controllers/InstancesController.js index 3cdfb4a..ac82e4c 100644 --- a/packs/BP/scripts/API/controllers/InstancesController.js +++ b/packs/BP/scripts/API/controllers/InstancesController.js @@ -2,7 +2,7 @@ import { InstanceExistsError } from "../../classes/Errors/InstanceExistsError"; import { InstanceNotFoundError } from "../../classes/Errors/InstanceNotFoundError"; import { StructureNotFoundError } from "../../classes/Errors/StructureNotFoundError"; import { APICallerError, VoidModel, APIController } from "../../lib/AddonAPIKit"; -import { AddInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../models/InstancesModel"; +import { AddInstanceParameterModel, EditInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../ConstructAPIModel"; export class InstancesController extends APIController { #context; @@ -12,7 +12,7 @@ export class InstancesController extends APIController { this.addEndpoint("instances", this.getInstances, VoidModel, InstancesModel); this.addEndpoint("instance:get", this.getInstance, InstanceNameParameterModel, InstanceModel); this.addEndpoint("instance:add", this.addInstance, AddInstanceParameterModel, InstanceModel); - this.addEndpoint("instance:edit", this.editInstance, InstanceModel, InstanceModel); + this.addEndpoint("instance:edit", this.editInstance, EditInstanceParameterModel, InstanceModel); this.addEndpoint("instance:delete", this.deleteInstance, InstanceNameParameterModel, VoidModel); this.addEndpoint("instance:materials", this.getMaterials, InstanceNameParameterModel, StructureMaterialsModel); this.#context = context; diff --git a/packs/BP/scripts/API/models/BuildersModel.js b/packs/BP/scripts/API/models/BuildersModel.js deleted file mode 100644 index 07bb00c..0000000 --- a/packs/BP/scripts/API/models/BuildersModel.js +++ /dev/null @@ -1,13 +0,0 @@ -import { PROTO } from "../../lib/AddonAPIKit"; - -export const BuilderModel = PROTO.Object({ - playerId: PROTO.String, - easyPlace: PROTO.Boolean, - fastEasyPlace: PROTO.Boolean, - materialGrabber: PROTO.Boolean, - materialInstanceName: PROTO.String -}); - -export const BuilderIdParameterModel = PROTO.Object({ - playerId: PROTO.String -}); \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit.js index bb662ff..a871864 100644 --- a/packs/BP/scripts/lib/AddonAPIKit.js +++ b/packs/BP/scripts/lib/AddonAPIKit.js @@ -640,9 +640,9 @@ var APIErrorEnum = Object.freeze({ // src/Errors/APICallerError.js var APICallerError = class extends Error { constructor(error) { - super(error.message); - this.errorName = error.name; - this.errorMessage = error.message; + const message = error.name + ": " + error.message; + super(message); + this.thrownError = error; this.errorCode = APIErrorEnum.Caller; this.name = "APICallerError"; } @@ -651,7 +651,8 @@ var APICallerError = class extends Error { // src/Errors/APIServerError.js var APIServerError = class extends Error { constructor(error) { - super(error.message); + const message = error.name + ": " + error.message; + super(message); this.thrownError = error; this.errorCode = APIErrorEnum.Server; this.name = "APIServerError"; @@ -686,12 +687,8 @@ var EndpointModel = PROTO.String; var EndpointsModel = PROTO.Array(EndpointModel); // src/APIController.js -var APIController = class _APIController { +var APIController = class { #endpoints = {}; - constructor() { - if (this.constructor === _APIController) - throw new Error("Cannot instantiate abstract class 'APIController'"); - } get endpoints() { return this.#endpoints; } @@ -704,6 +701,7 @@ var APIController = class _APIController { var EndpointsController = class extends APIController { #api; constructor(api) { + super(); this.#api = api; this.addEndpoint("endpoints", this.getEndpoints, VoidModel, EndpointsModel); this.addEndpoint("endpoints:has", this.hasEndpoint, EndpointModel, PROTO.Boolean); @@ -716,8 +714,8 @@ var EndpointsController = class extends APIController { } }; -// src/AddonAPI.js -var AddonAPI = class { +// src/AddonAPIServer.js +var AddonAPIServer = class { #name; #version; #allEndpoints; @@ -812,30 +810,56 @@ var APIEndpointNotFoundError = class extends Error { }; // src/AddonAPICaller.js -var AddonAPICaller = class { +var AddonAPICaller = class _AddonAPICaller { static #validEndpointCache = []; static async call(endpoint, parameterModel, parameterMap, returnDataModel) { - if (this.#validEndpointCache.length === 0) { - const endpointBase = endpoint.split(":")[0]; - await this.#populateValidEndpointCache(endpointBase); - } - if (this.#validEndpointCache.includes(endpoint)) - return await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); - else + await _AddonAPICaller.#tryPopulateEndpointCache(endpoint); + if (_AddonAPICaller.#endpointExists(endpoint)) { + const response = await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); + return _AddonAPICaller.#unwrapPacket(response); + } else { throw new APIEndpointNotFoundError(endpoint); + } + } + static async #tryPopulateEndpointCache(endpoint) { + if (_AddonAPICaller.#validEndpointCache.length === 0) { + const endpointBase = endpoint.split(":")[0]; + await _AddonAPICaller.#populateValidEndpointCache(endpointBase); + } + } + static #endpointExists(endpoint) { + return _AddonAPICaller.#validEndpointCache.includes(endpoint); } static async #populateValidEndpointCache(endpointBase) { const endpointsEndpoint = endpointBase + ":endpoints"; const validEndpoints = await IPC.invoke(endpointsEndpoint, VoidModel, void 0, PROTO.Boolean); - this.#validEndpointCache.push(...validEndpoints); + _AddonAPICaller.#validEndpointCache.push(...validEndpoints); + } + static #unwrapPacket(packet) { + const { data, error } = packet; + if (error.code === APIErrorEnum.Success) + return data; + else + _AddonAPICaller.#throwAPIError(packet.error); + } + static #throwAPIError(errorData) { + switch (errorData.code) { + case APIErrorEnum.Caller: + throw new APICallerError(errorData); + case APIErrorEnum.Server: + throw new APIServerError(errorData); + case APIErrorEnum.Unknown: + default: + throw new Error(errorData.message); + } } }; export { APICallerError, APIController, APIErrorEnum, - AddonAPI, AddonAPICaller, + AddonAPIServer, PROTO, VoidModel }; From 059d7b5c0397097432888d65beb53c2fece593bf Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 3 Jun 2026 23:46:32 +0200 Subject: [PATCH 43/46] feat: API is now functional ingame --- packs/BP/scripts/API/ConstructAPIModel.js | 2 +- .../API/controllers/BuildersController.js | 11 ++- .../API/controllers/InstancesController.js | 1 + packs/BP/scripts/classes/Builder/Builders.js | 1 + .../classes/Instance/InstanceOptions.js | 2 +- .../classes/Instance/StructureInstance.js | 6 +- packs/BP/scripts/lib/AddonAPIKit.js | 87 ++++++++++++------- 7 files changed, 69 insertions(+), 41 deletions(-) diff --git a/packs/BP/scripts/API/ConstructAPIModel.js b/packs/BP/scripts/API/ConstructAPIModel.js index a761777..11db487 100644 --- a/packs/BP/scripts/API/ConstructAPIModel.js +++ b/packs/BP/scripts/API/ConstructAPIModel.js @@ -1,4 +1,4 @@ -import { PROTO } from "../../lib/AddonAPIKit"; +import { PROTO } from "../lib/AddonAPIKit"; // Instances diff --git a/packs/BP/scripts/API/controllers/BuildersController.js b/packs/BP/scripts/API/controllers/BuildersController.js index 6f39523..c211e31 100644 --- a/packs/BP/scripts/API/controllers/BuildersController.js +++ b/packs/BP/scripts/API/controllers/BuildersController.js @@ -1,15 +1,14 @@ import { BuilderNotFoundError } from "../../classes/Errors/BuilderNotFoundError"; -import { APICallerError } from "../../lib/AddonAPIKit"; -import { BuilderIdParameterModel } from "../ConstructAPIModel"; +import { APICallerError, APIController } from "../../lib/AddonAPIKit"; +import { BuilderIdParameterModel, BuilderModel } from "../ConstructAPIModel"; export class BuildersController extends APIController { #context; constructor(context) { - super({ - "builders:get": { callback: this.getBuilder, parameterModel: BuilderIdParameterModel, returnModel: InstanceModel }, - "builders:edit": { callback: this.editBuilder, parameterModel: InstanceModel, returnModel: InstanceModel } - }); + super(); + this.addEndpoint("builder:get", this.getBuilder, BuilderIdParameterModel, BuilderModel); + this.addEndpoint("builder:edit", this.editBuilder, BuilderModel, BuilderModel); this.#context = context; } diff --git a/packs/BP/scripts/API/controllers/InstancesController.js b/packs/BP/scripts/API/controllers/InstancesController.js index ac82e4c..27672e6 100644 --- a/packs/BP/scripts/API/controllers/InstancesController.js +++ b/packs/BP/scripts/API/controllers/InstancesController.js @@ -48,6 +48,7 @@ export class InstancesController extends APIController { try { const instance = this.#context.get(instanceName); instance.setOptions(instanceOptions); + return instance.asPacket(); } catch(error) { if (error instanceof InstanceNotFoundError || error instanceof InstanceExistsError || error instanceof StructureNotFoundError) throw new APICallerError(error); diff --git a/packs/BP/scripts/classes/Builder/Builders.js b/packs/BP/scripts/classes/Builder/Builders.js index 359fdf0..778d57f 100644 --- a/packs/BP/scripts/classes/Builder/Builders.js +++ b/packs/BP/scripts/classes/Builder/Builders.js @@ -19,6 +19,7 @@ export class Builders { const builder = Builders.builders[id]; if (builder === void 0) throw new BuilderNotFoundError(id); + return builder; } static onJoin(playerId) { diff --git a/packs/BP/scripts/classes/Instance/InstanceOptions.js b/packs/BP/scripts/classes/Instance/InstanceOptions.js index de92691..1df2711 100644 --- a/packs/BP/scripts/classes/Instance/InstanceOptions.js +++ b/packs/BP/scripts/classes/Instance/InstanceOptions.js @@ -64,7 +64,7 @@ export class InstanceOptions extends Option { } setLayer(layer) { - this.currentLayer = layer.floor(); + this.currentLayer = Math.floor(layer); this.save(); } diff --git a/packs/BP/scripts/classes/Instance/StructureInstance.js b/packs/BP/scripts/classes/Instance/StructureInstance.js index f474002..6e5028e 100644 --- a/packs/BP/scripts/classes/Instance/StructureInstance.js +++ b/packs/BP/scripts/classes/Instance/StructureInstance.js @@ -308,8 +308,9 @@ export class StructureInstance { setOptions(newOptions) { const newVerifierOptions = newOptions.verifier; - this.options.rename(newOptions.name); - this.options.setStructure(newOptions.structureId); + if (newOptions.name !== this.getName()) + structureCollection.rename(this.getName(), newOptions.name); + this.setStructure(newOptions.structureId); this.options.setEnabled(newOptions.isEnabled); this.options.move(newOptions.dimensionId, newOptions.location); this.options.setLayer(newOptions.currentLayer); @@ -317,5 +318,6 @@ export class StructureInstance { this.options.setVerifierDistance(newVerifierOptions.trackPlayerDistance); this.options.setVerifierParticleLifetime(newVerifierOptions.particleLifetime); this.options.save(); + this.refreshBox(); } } \ No newline at end of file diff --git a/packs/BP/scripts/lib/AddonAPIKit.js b/packs/BP/scripts/lib/AddonAPIKit.js index a871864..e1c0297 100644 --- a/packs/BP/scripts/lib/AddonAPIKit.js +++ b/packs/BP/scripts/lib/AddonAPIKit.js @@ -668,7 +668,7 @@ var APIVersionMismatchError = class extends Error { }; // src/APIModels.js -var VoidModel = PROTO.Void; +var VoidModel = PROTO.Optional(PROTO.Void); var ErrorModel = PROTO.Optional(PROTO.Object({ code: PROTO.Int8, name: PROTO.Optional(PROTO.String), @@ -718,7 +718,7 @@ var EndpointsController = class extends APIController { var AddonAPIServer = class { #name; #version; - #allEndpoints; + #allEndpoints = []; constructor(name, version) { this.#name = name; this.#version = version; @@ -745,12 +745,16 @@ var AddonAPIServer = class { } } #setupEndpoint(endpoint, callback, parameterModel, returnDataModel) { + const callPacketModel = this.#resolveCallModel(parameterModel); const returnPacketModel = this.#resolveReturnModel(returnDataModel); const endpointPath = this.endpointBase + endpoint; - IPC.handle(endpointPath, parameterModel, returnPacketModel, (callPacket) => { + IPC.handle(endpointPath, callPacketModel, returnPacketModel, (callPacket) => { + console.info(`Received at ${endpointPath}: ${JSON.stringify(callPacket)}`); const apiVersion = callPacket.apiVersion; - const parameters = Object.values(callPacket.parameterMap); - return this.#handleCallback(apiVersion, callback, parameters); + const parameters = this.#resolveParameters(callPacket); + const returnPacket = this.#handleCallback(apiVersion, callback, parameters); + console.info(`Replying ${JSON.stringify(returnPacket)}`); + return returnPacket; }); this.#allEndpoints.push(endpointPath); } @@ -764,7 +768,7 @@ var AddonAPIServer = class { const errorPacket2 = this.#resolveErrorPacket(error); return this.#bundleReturnPacket(errorPacket2); } - console.error(error); + console.error(error, error.stack); const apiError = new APIServerError(error); const errorPacket = this.#resolveErrorPacket(apiError); return this.#bundleReturnPacket(errorPacket); @@ -776,11 +780,16 @@ var AddonAPIServer = class { throw new APICallerError(apiVersionMismatchError); } } + #resolveCallModel(parameterModel) { + return PROTO.Object({ ...CallModelShell, parameterMap: parameterModel }); + } #resolveReturnModel(returnDataModel) { - let returnModel = { ...ReturnModelShell }; - returnModel.data = returnDataModel; - returnModel = PROTO.Object(returnModel); - return returnModel; + return PROTO.Object({ ...ReturnModelShell, data: PROTO.Optional(returnDataModel) }); + } + #resolveParameters(callPacket) { + if (callPacket.parameterMap === void 0) + return []; + return Object.values(callPacket.parameterMap); } #bundleReturnPacket(errorPacket, returnValue = void 0) { return { @@ -810,39 +819,54 @@ var APIEndpointNotFoundError = class extends Error { }; // src/AddonAPICaller.js -var AddonAPICaller = class _AddonAPICaller { - static #validEndpointCache = []; - static async call(endpoint, parameterModel, parameterMap, returnDataModel) { - await _AddonAPICaller.#tryPopulateEndpointCache(endpoint); - if (_AddonAPICaller.#endpointExists(endpoint)) { - const response = await IPC.invoke(endpoint, parameterModel, parameterMap, returnDataModel).then((result) => result.value); - return _AddonAPICaller.#unwrapPacket(response); - } else { +var AddonAPICaller = class { + #name; + #version; + #validEndpointCache = []; + constructor(name, version) { + this.#name = name; + this.#version = version; + } + async call(endpoint, parameterMapModel, parameterMap, returnDataModel) { + await this.#tryPopulateEndpointCache(endpoint); + if (this.#endpointExists(endpoint)) + return this.#callDirect(endpoint, parameterMapModel, parameterMap, returnDataModel); + else throw new APIEndpointNotFoundError(endpoint); - } } - static async #tryPopulateEndpointCache(endpoint) { - if (_AddonAPICaller.#validEndpointCache.length === 0) { + async #tryPopulateEndpointCache(endpoint) { + if (this.#validEndpointCache.length === 0) { const endpointBase = endpoint.split(":")[0]; - await _AddonAPICaller.#populateValidEndpointCache(endpointBase); + const validEndpoints = await this.#callDirect(endpointBase + ":endpoints", VoidModel, void 0, EndpointsModel); + this.#validEndpointCache.push(...validEndpoints); } } - static #endpointExists(endpoint) { - return _AddonAPICaller.#validEndpointCache.includes(endpoint); + async #callDirect(endpoint, parameterMapModel, parameterMap, returnDataModel) { + const parameterPacket = { apiVersion: this.#version, parameterMap }; + const parameterModel = this.#resolveParameterModel(parameterMapModel); + const returnModel = this.#resolveReturnModel(returnDataModel); + console.info(`Sending to ${endpoint}: ${JSON.stringify(parameterPacket)}`); + const returnPacket = await IPC.invoke(endpoint, parameterModel, parameterPacket, returnModel); + console.info(`Received from ${endpoint}: ${JSON.stringify(returnPacket)}`); + return this.#unwrapReturnPacket(returnPacket); } - static async #populateValidEndpointCache(endpointBase) { - const endpointsEndpoint = endpointBase + ":endpoints"; - const validEndpoints = await IPC.invoke(endpointsEndpoint, VoidModel, void 0, PROTO.Boolean); - _AddonAPICaller.#validEndpointCache.push(...validEndpoints); + #endpointExists(endpoint) { + return this.#validEndpointCache.includes(endpoint); } - static #unwrapPacket(packet) { + #resolveParameterModel(parameterMapModel) { + return PROTO.Object({ ...CallModelShell, parameterMap: parameterMapModel }); + } + #resolveReturnModel(returnDataModel) { + return PROTO.Object({ ...ReturnModelShell, data: PROTO.Optional(returnDataModel) }); + } + #unwrapReturnPacket(packet) { const { data, error } = packet; if (error.code === APIErrorEnum.Success) return data; else - _AddonAPICaller.#throwAPIError(packet.error); + this.#throwAPIError(packet.error); } - static #throwAPIError(errorData) { + #throwAPIError(errorData) { switch (errorData.code) { case APIErrorEnum.Caller: throw new APICallerError(errorData); @@ -857,6 +881,7 @@ var AddonAPICaller = class _AddonAPICaller { export { APICallerError, APIController, + APIEndpointNotFoundError, APIErrorEnum, AddonAPICaller, AddonAPIServer, From 5256cda6b6bcb40f8a92ae605ae14e5e0eedc115 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 5 Jun 2026 17:52:06 +0200 Subject: [PATCH 44/46] docs: construct CLI & API documentation --- .gitignore | 2 +- docs/API.md | 16 +++++ docs/API/DataModels.md | 56 ++++++++++++++++ docs/API/Endpoints.md | 75 +++++++++++++++++++++ docs/CLI.md | 149 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 docs/API.md create mode 100644 docs/API/DataModels.md create mode 100644 docs/API/Endpoints.md create mode 100644 docs/CLI.md diff --git a/.gitignore b/.gitignore index 0a8c094..bb046ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store /build /.regolith -docs/ +docs/superpowers .claude node_modules/ \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..2a7bb95 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,16 @@ +# API Reference + +This section documents the public API of Construct, including all endpoints that can be accessed by other addons or scripts. This is intended for advanced users who want to extend or integrate with Construct beyond the provided CLI commands and GUI features. + +# API Overview + +Construct's API can be accessed via the [AddonAPIKit](https://github.com/ForestOflight/addonapikit) package. Instructions for importing the API into your project can be found in the AddonAPIKit documentation. + +You'll need to include Construct's Data Model definitions in your project to work with the API effectively. These data models define the structure of the data passed to and from the API endpoints. They can be found at [`packs/BP/scripts/API/ConstructAPIModel.js`](https://github.com/ForestOfLight/Construct/blob/main/packs/BP/scripts/API/ConstructAPIModel.js). Copy the file into your project and import the models as needed. + +Feedback on the Construct API is very welcome! If you have suggestions for new endpoints, improvements to existing ones, or any other feedback, please open a GitHub issue. + +Detailed information about the available API endpoints can be found in the following pages: + +- [Endpoints](./API/Endpoints.md) +- [Data Models](./API/DataModels.md) \ No newline at end of file diff --git a/docs/API/DataModels.md b/docs/API/DataModels.md new file mode 100644 index 0000000..8b5b3fc --- /dev/null +++ b/docs/API/DataModels.md @@ -0,0 +1,56 @@ +# Data Models + +The following data models are used to represent the format of the data passed by the API endpoints. More information about the API endpoints can be found in the [Endpoints](./Endpoints.md) page. + +You'll need to include Construct's Data Model definitions in your project to work with the API effectively. These data models define the structure of the data passed to and from the API endpoints. They can be found at [`packs/BP/scripts/API/ConstructAPIModel.js`](https://github.com/ForestOfLight/Construct/blob/main/packs/BP/scripts/API/ConstructAPIModel.js). Copy the file into your project and import the models as needed. + +## Instance + +An `Instance` represents a single structure instance in the world, along with its properties and state. + +```typescript +interface Instance { + name: PROTO.String, + structureId: PROTO.String, + isEnabled: PROTO.Boolean, + dimensionId: PROTO.Optional(PROTO.String), + location: PROTO.Optional({ + x: PROTO.Float64, + y: PROTO.Float64, + z: PROTO.Float64 + }), + bounds: PROTO.Optional(PROTO.Object({ + min: { + x: PROTO.Float64, + y: PROTO.Float64, + z: PROTO.Float64 + }, + max: { + x: PROTO.Float64, + y: PROTO.Float64, + z: PROTO.Float64 + } + })), + currentLayer: PROTO.Int16, + maxLayer: PROTO.Int16, + verifier: PROTO.Object({ + isEnabled: PROTO.Boolean, + trackPlayerDistance: PROTO.Int8, + particleLifetime: PROTO.Int32 + }) +} +``` + +## Builder + +A `Builder` represents a single builder (Construct's name for Players) in the world, along with its settings and properties. + +```typescript +interface Builder { + playerId: PROTO.String, + easyPlace: PROTO.Boolean, + fastEasyPlace: PROTO.Boolean, + materialGrabber: PROTO.Boolean, + materialInstanceName: PROTO.String +} +``` \ No newline at end of file diff --git a/docs/API/Endpoints.md b/docs/API/Endpoints.md new file mode 100644 index 0000000..db5becd --- /dev/null +++ b/docs/API/Endpoints.md @@ -0,0 +1,75 @@ +# Endpoints + +This page documents all the available API endpoints that can be accessed by other addons or scripts. The API is designed so that entire objects are passed at once, rather than making multiple calls to edit or query individual properties. This allows for fewer API calls and easier access to data. + +## Instances + +### `construct:instances` + +Get a list of all registered instance names. + +- **Parameters**: `void` +- **Returns**: `string[]` + +--- + +### `construct:instance:get` + +Get the full data object for a specific instance. + +- **Parameters**: `instanceName: string` +- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance)) + +--- + +### `construct:instance:add` + +Create a new instance with a given name and structure ID. + +- **Parameters**: `instanceName: string`, `structureId: string` +- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance)) + +--- + +### `construct:instance:edit` + +Edit properties of an existing instance (e.g. enabled state, position). + +- **Parameters**: `instanceName: string`, `properties: Instance` +- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance)) + +--- + +### `construct:instance:delete` + +Permanently delete an instance. + +- **Parameters**: `instanceName: string` +- **Returns**: `void` + +--- + +### `construct:instance:materials` + +Get a list of materials required to build the active section of an instance. Respects the active layer. + +- **Parameters**: `instanceName: string` +- **Returns**: `Map` (material name to quantity) + +## Builders + +### `construct:builder:get` + +Get the full data object for a specific builder (player). + +- **Parameters**: `playerId: string` +- **Returns**: `Builder` (see [Data Model](./DataModels.md#builder)) + +--- + +### `construct:builder:edit` + +Edit properties of an existing builder. + +- **Parameters**: `playerId: string`, `properties: Builder` +- **Returns**: `Builder` (see [Data Model](./DataModels.md#builder)) diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 0000000..4b56b8c --- /dev/null +++ b/docs/CLI.md @@ -0,0 +1,149 @@ +# CLI Reference + +All commands are prefixed with `construct:`. Arguments in angle brackets (``) are required, while those in square brackets (`[arg]`) are optional. Assume commands can be run from any source (player, entity, block, or server) unless otherwise noted. + +## Instance Management + +### `construct:create ` +Create a new instance bound to a structure. + +| Argument | Description | +|---|---| +| `` | Unique name for this instance. | +| `` | ID of a structure saved in the world (without the `mystructure:` prefix). | + +> Corresponds to the "create new instance" flow in the main menu. Errors if `instanceName` is already taken or `structureId` does not exist. + +### `construct:delete ` +Permanently delete an instance. + +| Argument | Description | +|---|---| +| `` | Name of the instance to delete. | + +> Calls `structureCollection.delete()`. Also disables the instance and clears its saved dynamic properties before removal. + +### `construct:rename ` +Rename an existing instance. + +| Argument | Description | +|---|---| +| `` | Current instance name. | +| `` | Desired new name. Errors if already in use. | + +### `construct:list` +List all registered instances and their status. + +> Prints each instance name, its bound structure ID, enabled/disabled state, and placed location (if any). Useful for scripting and quick inspection without opening the GUI. + +## Placement & Movement + +### `construct:place ` +Enable and place an instance at a location. + +| Argument | Description | +|---|---| +| `` | Instance to place. | +| `` | World coordinates. Errors if omitted. Supports tilde (`~`) notation. | + +> Equivalent to the "Place" button in the instance menu — enables the instance and calls `move()` in one step. If the instance already has a location, this is a move, not a fresh place. + +### `construct:move [x y z]` +Reposition a placed instance without toggling its enabled state. + +| Argument | Description | +|---|---| +| `` | Name of a placed instance. | +| `` | Target world coordinates. | + +## Enable / Disable + +### `construct:enable ` +Enable a placed instance. + +> Requires the instance to have a saved location. Refreshes the outliner, verifier, and materials cache. + +### `construct:disable ` +Disable an active instance. + +> Tears down outliner rendering and pauses the verifier. The instance retains its saved location and can be re-enabled. + +## Layer Control + +### `construct:layer ` +Set the active layer of an instance. + +| Argument | Description | +|---|---| +| `` | Name of the instance. | +| `` | Integer layer index. `0` = whole structure (no layer selected). Valid range: `0` to `structure.height`. | + +> Errors if `layer` is out of bounds. Only meaningful for structures with height > 1. + +### `construct:nextlayer ` +Step the layer up by one (wraps from max back to `0`). + +> Mirrors the "Next layer" button. Wrapping from max → `0` restores the whole-structure view. + +### `construct:prevlayer ` +Step the layer down by one (wraps from `0` back to max). + +> Mirrors the "Previous layer" button. + +## Settings + +### `construct:verifier true|false` +Toggle the structure verifier for an instance. + +| Argument | Description | +|---|---| +| `` | Name of the instance. | +| `true\|false` | Whether to run the verifier. Corresponds to the "validation" toggle in Settings. | + +### `construct:option true|false` +Enable or disable a per-player builder option. Must be run as a player source. + +| Argument | Description | +|---|---| +| `` | One of: `easyPlace`, `fastEasyPlace`, `materialGrabber`. | +| `true\|false` | Desired state. Runs the option's enable/disable callback (gives or removes the action item). | + +> Replaces the toggles in the Builder Options form. State is saved per-player via dynamic properties. + +## Information + +### `construct:info ` +Print instance details to chat. + +> Outputs: bound structure ID, enabled state, placed location and dimension, current layer, verifier enabled, structure bounds (min/max). Equivalent to the data shown in the instance menu body. + +### `construct:stats ` +Run the structure verifier and print statistics. + +> Triggers a standalone `StructureVerifier` pass (same as the "Statistics" button) and sends the result to chat. Errors if a verifier is already running on this instance. + +### `construct:materials [missing]` +Print the material list for an instance. + +| Argument | Description | +|---|---| +| `` | Name of the instance. | +| `missing` | When present, show only materials the player does not have in their inventory (mirrors the "missing only" toggle). Can only be used by a player source. | + +> Respects the active layer: if a layer is set, only that layer's material counts are shown. + +## Utility + +### `construct:item` +Give yourself the Construct menu item. + +> Already implemented as a native custom command. Needs to be refactored to fit the new command pipeline. + +### `construct:tag ` +Rename the held Construct item to an instance name for quick-open. Errors if the item is not a construct item or if the instance name is not registered. + +| Argument | Description | +|---|---| +| `` | Instance name to embed in the item's `nameTag`. Using the item in-world will jump straight to that instance's menu. | + +> The item-use handler in `construct.js` already checks `itemStack.nameTag` against known instance names; this command just makes it easy to tag an item without renaming it in an anvil. From 44e8e68c6cb2d371953d1c6800be37edc3a96bfe Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 16 Jun 2026 23:54:18 +0100 Subject: [PATCH 45/46] feat: bump MC & pack version --- README.md | 2 +- package-lock.json | 21 ++++++++++++++++----- package.json | 3 ++- packs/BP/manifest.json | 12 ++++++------ packs/RP/manifest.json | 8 ++++---- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 58f0598..2e1c002 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-v26.20_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) +[![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.30_(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) [![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://buymeacoffee.com/forestoflight) diff --git a/package-lock.json b/package-lock.json index 202f6ac..39856d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@minecraft/server": "^2.8.0-beta.1.26.21-stable" + "@minecraft/server": "^2.9.0-beta.1.26.30-stable", + "@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable" } }, "node_modules/@minecraft/common": { @@ -16,13 +17,23 @@ "peer": true }, "node_modules/@minecraft/server": { - "version": "2.8.0-beta.1.26.21-stable", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.8.0-beta.1.26.21-stable.tgz", - "integrity": "sha512-HdR2EjmleJBBuaXH4iEobdXh7dcuWtV8AIZn2Kd+UkvlBPc4iIPSqXaEt7nqOk0tyhbKfRs/X3aoVtAMHW+kmQ==", + "version": "2.9.0-rc.1.26.40-preview.24", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.9.0-rc.1.26.40-preview.24.tgz", + "integrity": "sha512-/DH82sRkYjADbVh5lEzpQQYKy+oRuQqLLOMGI1Yml1Q/fd3nIHedveoGpU1GkL0hzNFTJGG/pi7D2pPJEhfGzw==", "license": "MIT", "peerDependencies": { "@minecraft/common": "^1.2.0", - "@minecraft/vanilla-data": ">=1.20.70" + "@minecraft/vanilla-data": ">=1.20.70 || 1.26.40-preview.24" + } + }, + "node_modules/@minecraft/server-ui": { + "version": "2.2.0-beta.1.26.30-stable", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.2.0-beta.1.26.30-stable.tgz", + "integrity": "sha512-OMkGdrU5w/g/oIHR6ltpxbTNzEDYcDoHI56jW5FOBK+U6UwBO5wQteAPVvKKcBqD8KsY0R72xw1cKNfVwwJFIg==", + "license": "MIT", + "peerDependencies": { + "@minecraft/common": "^1.0.0", + "@minecraft/server": "^2.0.0 || ^2.9.0-beta.1.26.30-stable" } }, "node_modules/@minecraft/vanilla-data": { diff --git a/package.json b/package.json index 36f01d7..856ad61 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "@minecraft/server": "^2.8.0-beta.1.26.21-stable" + "@minecraft/server": "^2.9.0-beta.1.26.30-stable", + "@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable" } } diff --git a/packs/BP/manifest.json b/packs/BP/manifest.json index 2ecdc13..ffc8fdd 100644 --- a/packs/BP/manifest.json +++ b/packs/BP/manifest.json @@ -1,11 +1,11 @@ { "format_version": 2, "header": { - "name": "Construct [BP] v1.0.9", + "name": "Construct [BP] v1.1.0", "description": "Survival building addon by §aForestOfLight§r.", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", - "min_engine_version": [1, 26, 20], - "version": [1, 0, 9] + "min_engine_version": [1, 26, 30], + "version": [1, 1, 0] }, "modules": [ { @@ -26,15 +26,15 @@ "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.8.0-beta" + "version": "2.9.0-beta" }, { "module_name": "@minecraft/server-ui", - "version": "2.1.0-beta" + "version": "2.2.0-beta" }, { "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", - "version": [1, 0, 9] + "version": [1, 1, 0] } ], "metadata": { diff --git a/packs/RP/manifest.json b/packs/RP/manifest.json index 88bede4..98fffd8 100644 --- a/packs/RP/manifest.json +++ b/packs/RP/manifest.json @@ -1,11 +1,11 @@ { "format_version": 2, "header": { - "name": "Construct [RP] v1.0.9", + "name": "Construct [RP] v1.1.0", "description": "Survival building addon by §aForestOfLight§r.", "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", - "version": [1, 0, 9], - "min_engine_version": [1, 26, 20] + "version": [1, 1, 0], + "min_engine_version": [1, 26, 30] }, "modules": [ { @@ -17,7 +17,7 @@ "dependencies": [ { "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", - "version": [1, 0, 9] + "version": [1, 1, 0] } ], "capabilities": [ From 78ba7382d95184cfc40daedf429b3c9d50ee56a3 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 17 Jun 2026 00:01:14 +0100 Subject: [PATCH 46/46] docs: add donate section to README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e1c002..abdefc8 100644 --- a/README.md +++ b/README.md @@ -74,4 +74,8 @@ If you have any issues or suggestions, please don't hesitate to open an issue on ### Adding Translations -Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out! \ No newline at end of file +Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out! + +### Donate + +If you appreciate my work here and would like to support the future development of my addons, please consider donating to me on [BuyMeACoffee](https://buymeacoffee.com/forestoflight). Your support is greatly appreciated!